192 lines
6.9 KiB
PHP
192 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use ZipArchive;
|
|
|
|
/**
|
|
* Penjana .xlsx ringan tanpa kebergantungan luar. Setiap nilai sel ditulis
|
|
* sebagai "inline string" supaya nilai seperti No. KP kekal sebagai teks
|
|
* (mengekalkan sengkang / sifar di hadapan). Menyokong berbilang worksheet.
|
|
*/
|
|
class SimpleXlsx
|
|
{
|
|
/** @var array<int,array{name:string,rows:array<int,array<int,scalar|null>>}> */
|
|
private array $sheets = [];
|
|
|
|
/**
|
|
* Tambah satu worksheet. Baris pertama dianggap pengepala (huruf tebal).
|
|
*
|
|
* @param array<int,array<int,scalar|null>> $rows
|
|
*/
|
|
public function addSheet(string $name, array $rows): static
|
|
{
|
|
$this->sheets[] = ['name' => $this->namaSah($name), 'rows' => $rows];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Hasilkan kandungan binari fail .xlsx.
|
|
*/
|
|
public function toString(): string
|
|
{
|
|
$tmp = tempnam(sys_get_temp_dir(), 'xlsx');
|
|
|
|
$zip = new ZipArchive;
|
|
$zip->open($tmp, ZipArchive::OVERWRITE);
|
|
|
|
$zip->addFromString('[Content_Types].xml', $this->contentTypes());
|
|
$zip->addFromString('_rels/.rels', $this->rootRels());
|
|
$zip->addFromString('xl/workbook.xml', $this->workbook());
|
|
$zip->addFromString('xl/_rels/workbook.xml.rels', $this->workbookRels());
|
|
$zip->addFromString('xl/styles.xml', $this->styles());
|
|
|
|
foreach ($this->sheets as $i => $sheet) {
|
|
$zip->addFromString('xl/worksheets/sheet'.($i + 1).'.xml', $this->sheetXml($sheet['rows']));
|
|
}
|
|
|
|
$zip->close();
|
|
|
|
$data = file_get_contents($tmp);
|
|
@unlink($tmp);
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function namaSah(string $name): string
|
|
{
|
|
$name = preg_replace('/[\\\\\/\*\?\[\]:]/', ' ', $name);
|
|
|
|
return mb_substr(trim($name), 0, 31) ?: 'Sheet';
|
|
}
|
|
|
|
/**
|
|
* Tukar indeks lajur (0-asas) kepada huruf A, B, … AA.
|
|
*/
|
|
private function col(int $index): string
|
|
{
|
|
$letters = '';
|
|
$index++;
|
|
|
|
while ($index > 0) {
|
|
$mod = ($index - 1) % 26;
|
|
$letters = chr(65 + $mod).$letters;
|
|
$index = intdiv($index - 1, 26);
|
|
}
|
|
|
|
return $letters;
|
|
}
|
|
|
|
private function cell(string $ref, string $value, int $style): string
|
|
{
|
|
$v = htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
|
$s = $style > 0 ? ' s="'.$style.'"' : '';
|
|
|
|
return '<c r="'.$ref.'"'.$s.' t="inlineStr"><is><t xml:space="preserve">'.$v.'</t></is></c>';
|
|
}
|
|
|
|
/**
|
|
* @param array<int,array<int,scalar|null>> $rows
|
|
*/
|
|
private function sheetXml(array $rows): string
|
|
{
|
|
$xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
.'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>';
|
|
|
|
foreach ($rows as $r => $cells) {
|
|
$rowNum = $r + 1;
|
|
$style = $r === 0 ? 1 : 0;
|
|
$xml .= '<row r="'.$rowNum.'">';
|
|
|
|
foreach (array_values($cells) as $c => $val) {
|
|
$xml .= $this->cell($this->col($c).$rowNum, (string) $val, $style);
|
|
}
|
|
|
|
$xml .= '</row>';
|
|
}
|
|
|
|
return $xml.'</sheetData></worksheet>';
|
|
}
|
|
|
|
private function contentTypes(): string
|
|
{
|
|
$overrides = '';
|
|
|
|
foreach ($this->sheets as $i => $s) {
|
|
$overrides .= '<Override PartName="/xl/worksheets/sheet'.($i + 1).'.xml" '
|
|
.'ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
|
|
}
|
|
|
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
.'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
|
.'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
.'<Default Extension="xml" ContentType="application/xml"/>'
|
|
.'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
|
|
.'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
|
|
.$overrides
|
|
.'</Types>';
|
|
}
|
|
|
|
private function rootRels(): string
|
|
{
|
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
.'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
.'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
|
|
.'</Relationships>';
|
|
}
|
|
|
|
private function workbook(): string
|
|
{
|
|
$sheetsXml = '';
|
|
|
|
foreach ($this->sheets as $i => $s) {
|
|
$name = htmlspecialchars($s['name'], ENT_QUOTES | ENT_XML1, 'UTF-8');
|
|
$sheetsXml .= '<sheet name="'.$name.'" sheetId="'.($i + 1).'" r:id="rId'.($i + 1).'"/>';
|
|
}
|
|
|
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
.'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
|
|
.'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
|
.'<sheets>'.$sheetsXml.'</sheets></workbook>';
|
|
}
|
|
|
|
private function workbookRels(): string
|
|
{
|
|
$rels = '';
|
|
$count = count($this->sheets);
|
|
|
|
foreach ($this->sheets as $i => $s) {
|
|
$rels .= '<Relationship Id="rId'.($i + 1).'" '
|
|
.'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" '
|
|
.'Target="worksheets/sheet'.($i + 1).'.xml"/>';
|
|
}
|
|
|
|
$rels .= '<Relationship Id="rId'.($count + 1).'" '
|
|
.'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" '
|
|
.'Target="styles.xml"/>';
|
|
|
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
.'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
.$rels.'</Relationships>';
|
|
}
|
|
|
|
private function styles(): string
|
|
{
|
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
.'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
|
.'<fonts count="2">'
|
|
.'<font><sz val="11"/><name val="Calibri"/></font>'
|
|
.'<font><b/><sz val="11"/><name val="Calibri"/></font>'
|
|
.'</fonts>'
|
|
.'<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>'
|
|
.'<borders count="1"><border/></borders>'
|
|
.'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
|
|
.'<cellXfs count="2">'
|
|
.'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
|
|
.'<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/>'
|
|
.'</cellXfs>'
|
|
.'</styleSheet>';
|
|
}
|
|
}
|