Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.24% covered (warning)
88.24%
45 / 51
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZippedCsvExport
88.24% covered (warning)
88.24%
45 / 51
50.00% covered (danger)
50.00%
1 / 2
14.32
0.00% covered (danger)
0.00%
0 / 1
 streamFromSheets
87.50% covered (warning)
87.50%
42 / 48
0.00% covered (danger)
0.00%
0 / 1
13.33
 getSupportedExtensions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Export;
3
4use Apie\Export\Concerns\FlattensValues;
5use Apie\Export\Lists\FileExtensionList;
6use Apie\Export\ValueObjects\FileExtension;
7use Nyholm\Psr7\Stream;
8use Psr\Http\Message\StreamInterface;
9use UnitEnum;
10use ZipStream\ZipStream;
11
12class 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}