Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
88.24% |
45 / 51 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
| ZippedCsvExport | |
88.24% |
45 / 51 |
|
50.00% |
1 / 2 |
14.32 | |
0.00% |
0 / 1 |
| streamFromSheets | |
87.50% |
42 / 48 |
|
0.00% |
0 / 1 |
13.33 | |||
| getSupportedExtensions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | namespace Apie\Export; |
| 3 | |
| 4 | use Apie\Export\Concerns\FlattensValues; |
| 5 | use Apie\Export\Lists\FileExtensionList; |
| 6 | use Apie\Export\ValueObjects\FileExtension; |
| 7 | use Nyholm\Psr7\Stream; |
| 8 | use Psr\Http\Message\StreamInterface; |
| 9 | use UnitEnum; |
| 10 | use ZipStream\ZipStream; |
| 11 | |
| 12 | class ZippedCsvExport implements ExportInterface |
| 13 | { |
| 14 | use FlattensValues; |
| 15 | |
| 16 | public function streamFromSheets(array $sheets, string $outputFilename = 'export.zip'): StreamInterface |
| 17 | { |
| 18 | // Sanitize and reindex sheet names |
| 19 | $sheetNames = []; |
| 20 | $i = 1; |
| 21 | foreach ($sheets as $name => $gen) { |
| 22 | // remove path separators and other chars that are problematic in filenames |
| 23 | $clean = preg_replace('/[\\\\\/\?\*\[\]:]/', '', (string)$name); |
| 24 | $clean = $clean === '' ? "Sheet{$i}" : $clean; |
| 25 | // limit filename length to 200 chars to be safe |
| 26 | $sheetNames[] = substr($clean, 0, 200); |
| 27 | $i++; |
| 28 | } |
| 29 | |
| 30 | // Initialize ZIP stream |
| 31 | $stream = fopen('php://temp', 'r+'); |
| 32 | $outputStream = new Stream($stream); |
| 33 | |
| 34 | $zip = new ZipStream( |
| 35 | outputName: $outputFilename, |
| 36 | outputStream: $outputStream, |
| 37 | sendHttpHeaders: false, |
| 38 | ); |
| 39 | |
| 40 | // Add each sheet as an individual CSV file |
| 41 | $index = 1; |
| 42 | $usedFilenames = []; |
| 43 | foreach ($sheets as $name => $rowsGenerator) { |
| 44 | $filenameBase = $sheetNames[$index - 1] . '.csv'; |
| 45 | $filename = $filenameBase; |
| 46 | $occ = 1; |
| 47 | while (in_array($filename, $usedFilenames, true)) { |
| 48 | $filename = pathinfo($filenameBase, PATHINFO_FILENAME) . "({$occ}).csv"; |
| 49 | $occ++; |
| 50 | } |
| 51 | $usedFilenames[] = $filename; |
| 52 | |
| 53 | $zip->addFileFromCallback($filename, function () use ($rowsGenerator) { |
| 54 | $stream = fopen('php://temp', 'r+'); |
| 55 | // Add UTF-8 BOM to help Excel detect UTF-8 |
| 56 | fwrite($stream, chr(0xEF) . chr(0xBB) . chr(0xBF)); |
| 57 | |
| 58 | foreach ($rowsGenerator as $row) { |
| 59 | // If row is Traversable or object, try to convert to array |
| 60 | if (is_object($row) && !$row instanceof \Stringable && !$row instanceof UnitEnum) { |
| 61 | if ($row instanceof \Traversable) { |
| 62 | $row = iterator_to_array($row); |
| 63 | } else { |
| 64 | // cast to array will extract properties; prefer that to avoid errors |
| 65 | $row = (array)$row; |
| 66 | } |
| 67 | } |
| 68 | if (!is_array($row)) { |
| 69 | // single value row -> wrap |
| 70 | $row = [$row]; |
| 71 | } |
| 72 | |
| 73 | $converted = []; |
| 74 | foreach ($row as $cell) { |
| 75 | $cell = $this->toSingleValue($cell); |
| 76 | // fputcsv expects strings/numbers/null |
| 77 | if ($cell === null) { |
| 78 | $converted[] = ''; |
| 79 | } else { |
| 80 | $converted[] = (string)$cell; |
| 81 | } |
| 82 | } |
| 83 | // Use fputcsv for proper escaping |
| 84 | fputcsv($stream, $converted, ',', '"'); |
| 85 | } |
| 86 | |
| 87 | rewind($stream); |
| 88 | return $stream; |
| 89 | }); |
| 90 | |
| 91 | $index++; |
| 92 | } |
| 93 | |
| 94 | $zip->finish(); |
| 95 | rewind($stream); |
| 96 | return $outputStream; |
| 97 | } |
| 98 | |
| 99 | |
| 100 | |
| 101 | public function getSupportedExtensions(): FileExtensionList |
| 102 | { |
| 103 | return new FileExtensionList([ |
| 104 | new FileExtension('zip'), |
| 105 | ]); |
| 106 | } |
| 107 | } |