diff --git a/README.md b/README.md index 735fbac..44c34bd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Ideally, importing single sheets of csv or excel should be just a matter of chan ## Supported packages +Native php: very fast csv import/export, but limited features. Can read/output streams. + OpenSpout: fast csv and excel import/export https://github.com/openspout/openspout @@ -18,8 +20,6 @@ https://github.com/thephpleague/csv PhpSpreadsheet: slow excel (xls and xlsx) and csv import/export, but more features https://github.com/PHPOffice/PhpSpreadsheet -Native php: very fast csv import/export, but limited features. Can read/output streams. - SimpleXLSX: very fast excel import/export https://github.com/shuchkin/simplexlsx https://github.com/shuchkin/simplexlsxgen @@ -100,45 +100,13 @@ This package supports only 1 worksheet, as it is meant to be able to replace csv ## Benchmarks -Since we can compare our solutions, there is a built in bench.php script that give the following results on my machine -These results are run with: - - openspout/openspout 4.24.4 - phpoffice/phpspreadsheet 2.1.0 - league/csv 9.16.0 - shuchkin/simplexlsx 1.1.11 - shuchkin/simplexlsxgen 1.4.11 - -Reading a file with 5000 rows: - - Results for csv - LeKoala\SpreadCompat\Csv\Native : 0.0084 - LeKoala\SpreadCompat\Csv\League : 0.0317 - LeKoala\SpreadCompat\Csv\OpenSpout : 0.0993 - LeKoala\SpreadCompat\Csv\PhpSpreadsheet : 0.6713 - - Results for xlsx - LeKoala\SpreadCompat\Xlsx\Native : 0.0639 - LeKoala\SpreadCompat\Xlsx\Simple : 0.1476 - LeKoala\SpreadCompat\Xlsx\PhpSpreadsheet : 0.7436 - LeKoala\SpreadCompat\Xlsx\OpenSpout : 0.8979 - - -Write a file with 2500 rows: +Since we can compare our solutions, there is a built in bench script. You can check the results here - Results for csv - LeKoala\SpreadCompat\Csv\Native : 0.0065 - LeKoala\SpreadCompat\Csv\League : 0.0129 - LeKoala\SpreadCompat\Csv\OpenSpout : 0.0443 - LeKoala\SpreadCompat\Csv\PhpSpreadsheet : 0.3668 +- [read benchmark](docs/bench-read.md) +- [write benchmark](docs/bench-write.md) - Results for xlsx - LeKoala\SpreadCompat\Xlsx\Native : 0.0401 - LeKoala\SpreadCompat\Xlsx\Simple : 0.0861 - LeKoala\SpreadCompat\Xlsx\OpenSpout : 0.2555 - LeKoala\SpreadCompat\Xlsx\PhpSpreadsheet : 0.674 +For simple imports/exports, it's very clear that using the `Native` adapter is the fastest. -For simple imports/exports, it's very clear that using the Native adapter is the fastest. -These are not enabled by default since they might lack some compatibility feature you may require. +Otherwise, `league/csv` and `shuchkin/simplexlsx` are great choices. Stop wasting cpu cycles right now and please use the most efficient adapter :-) diff --git a/bench-read.php b/bin/bench-read.php similarity index 89% rename from bench-read.php rename to bin/bench-read.php index 3276d0f..095faff 100644 --- a/bench-read.php +++ b/bin/bench-read.php @@ -9,10 +9,10 @@ use LeKoala\SpreadCompat\Xlsx\OpenSpout as XlsxOpenSpout; use LeKoala\SpreadCompat\Xlsx\Simple; -require './vendor/autoload.php'; +require dirname(__DIR__) . '/vendor/autoload.php'; -$largeCsv = __DIR__ . '/tests/data/large.csv'; -$largeXlsx = __DIR__ . '/tests/data/large.xlsx'; +$largeCsv = dirname(__DIR__) . '/tests/data/large.csv'; +$largeXlsx = dirname(__DIR__) . '/tests/data/large.xlsx'; $csv = [ League::class, @@ -67,7 +67,7 @@ $results[$class] = $averageTime; } - uasort($results, fn ($a, $b) => $a <=> $b); + uasort($results, fn($a, $b) => $a <=> $b); foreach ($results as $class => $averageTime) { echo "$class : " . $averageTime . PHP_EOL; } diff --git a/bench-write.php b/bin/bench-write.php similarity index 95% rename from bench-write.php rename to bin/bench-write.php index c8899ad..6d80510 100644 --- a/bench-write.php +++ b/bin/bench-write.php @@ -10,7 +10,7 @@ use LeKoala\SpreadCompat\Xlsx\OpenSpout as XlsxOpenSpout; use LeKoala\SpreadCompat\Xlsx\Simple; -require './vendor/autoload.php'; +require dirname(__DIR__) . '/vendor/autoload.php'; $largeCsv = SpreadCompat::getTempFilename(); $largeXlsx = SpreadCompat::getTempFilename(); @@ -75,7 +75,7 @@ $results[$class] = $averageTime; } - uasort($results, fn ($a, $b) => $a <=> $b); + uasort($results, fn($a, $b) => $a <=> $b); foreach ($results as $class => $averageTime) { echo "$class : " . $averageTime . PHP_EOL; } diff --git a/composer.json b/composer.json index 3b543b0..4e30db8 100644 --- a/composer.json +++ b/composer.json @@ -25,8 +25,8 @@ "maennchen/zipstream-php": "^3.1", "openspout/openspout": "^4", "phpoffice/phpspreadsheet": "^1.26|^2", - "phpstan/phpstan": "^1", - "phpunit/phpunit": "^10", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10|^11", "shuchkin/simplexlsx": "^1", "shuchkin/simplexlsxgen": "^1.3", "squizlabs/php_codesniffer": "^3.6" @@ -58,6 +58,10 @@ "phpunit-only": "phpunit --group=only", "phpcs": "phpcs", "phpstan": "phpstan analyse src/ --memory-limit=-1", - "serve": "php -S localhost:8001 -t ./" + "serve": "php -S localhost:8001 -t ./", + "bench": [ + "php ./bin/bench-read.php > ./docs/bench-read.md", + "php ./bin/bench-write.php > ./docs/bench-write.md" + ] } } diff --git a/docs/bench-read.md b/docs/bench-read.md new file mode 100644 index 0000000..94dce43 --- /dev/null +++ b/docs/bench-read.md @@ -0,0 +1,12 @@ +Results for csv +LeKoala\SpreadCompat\Csv\Native : 0.008 +LeKoala\SpreadCompat\Csv\League : 0.0312 +LeKoala\SpreadCompat\Csv\OpenSpout : 0.0979 +LeKoala\SpreadCompat\Csv\PhpSpreadsheet : 1.3744 + +Results for xlsx +LeKoala\SpreadCompat\Xlsx\Native : 0.0617 +LeKoala\SpreadCompat\Xlsx\Simple : 0.1396 +LeKoala\SpreadCompat\Xlsx\OpenSpout : 0.9332 +LeKoala\SpreadCompat\Xlsx\PhpSpreadsheet : 1.4632 + diff --git a/docs/bench-write.md b/docs/bench-write.md new file mode 100644 index 0000000..3396fcf --- /dev/null +++ b/docs/bench-write.md @@ -0,0 +1,12 @@ +Results for csv +LeKoala\SpreadCompat\Csv\Native : 0.0074 +LeKoala\SpreadCompat\Csv\League : 0.0125 +LeKoala\SpreadCompat\Csv\OpenSpout : 0.0433 +LeKoala\SpreadCompat\Csv\PhpSpreadsheet : 0.7075 + +Results for xlsx +LeKoala\SpreadCompat\Xlsx\Native : 0.0402 +LeKoala\SpreadCompat\Xlsx\Simple : 0.0845 +LeKoala\SpreadCompat\Xlsx\OpenSpout : 0.2622 +LeKoala\SpreadCompat\Xlsx\PhpSpreadsheet : 1.017 + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d753395..5df5e33 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,12 +4,13 @@ parameters: - src reportUnmatchedIgnoredErrors: true ignoreErrors: - - "#(.*)return type has no value type specified in iterable type array#" - "#(.*)with no value type specified in iterable type iterable#" - "#(.*)has parameter \\$opts with no type specified.#" + - "#Parameter (.*) \\$opts#" - "#(.*)array_combine expects(.*)#" - "#(.*)SpreadInterface but returns object.#" - "#but does not specify its types: TValue#" + - "#method_exists(.*) OpenSpout#" # https://backendtea.com/post/use-phpstan-bleeding-edge/ includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon diff --git a/src/Common/ZipUtils.php b/src/Common/ZipUtils.php new file mode 100644 index 0000000..c0b7198 --- /dev/null +++ b/src/Common/ZipUtils.php @@ -0,0 +1,38 @@ + 'File already exists.', + ZipArchive::ER_INCONS => 'Zip archive inconsistent.', + ZipArchive::ER_INVAL => 'Invalid argument.', + ZipArchive::ER_MEMORY => 'Malloc failure.', + ZipArchive::ER_NOENT => 'No such file.', + ZipArchive::ER_NOZIP => 'Not a zip archive.', + ZipArchive::ER_OPEN => 'Can\'t open file.', + ZipArchive::ER_READ => 'Read error.', + ZipArchive::ER_SEEK => 'Seek error.', + default => 'Unknown error code ' . $code . '.', + }; + } + + public static function getData(ZipArchive $zip, string $name): ?string + { + $idx = $zip->locateName($name); + if ($idx) { + $result = $zip->getFromIndex($idx); + if ($result) { + return $result; + } + } + return null; + } +} diff --git a/src/Csv/League.php b/src/Csv/League.php index 45b0f82..e989b72 100644 --- a/src/Csv/League.php +++ b/src/Csv/League.php @@ -41,6 +41,11 @@ public function readFile(string $filename, ...$opts): Generator return $this->read(Reader::createFromPath($filename)); } + /** + * @param iterable> $data + * @param mixed ...$opts + * @return string + */ public function writeString(iterable $data, ...$opts): string { $this->configure(...$opts); @@ -50,6 +55,12 @@ public function writeString(iterable $data, ...$opts): string return $csv->toString(); } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return bool + */ public function writeFile(iterable $data, string $filename, ...$opts): bool { try { @@ -62,6 +73,12 @@ public function writeFile(iterable $data, string $filename, ...$opts): bool } } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return void + */ public function output(iterable $data, string $filename, ...$opts): void { $this->configure(...$opts); @@ -83,6 +100,11 @@ protected function read(Reader $csv): Generator } } + /** + * @param iterable> $data + * @param Writer $csv + * @return void + */ protected function write(iterable $data, Writer $csv): void { $this->initialize($csv); @@ -127,8 +149,8 @@ protected function initialize(Reader|Writer $csv): void $csv->addFormatter( (new CharsetConverter()) - ->inputEncoding($inputEncoding) - ->outputEncoding($outputEncoding) + ->inputEncoding($inputEncoding) + ->outputEncoding($outputEncoding) ); } } diff --git a/src/Csv/Native.php b/src/Csv/Native.php index a98e679..8a57053 100644 --- a/src/Csv/Native.php +++ b/src/Csv/Native.php @@ -70,7 +70,7 @@ public function readFile( /** * @param resource $stream - * @param iterable $data + * @param iterable> $data * @return void */ protected function write($stream, iterable $data): void @@ -88,6 +88,11 @@ protected function write($stream, iterable $data): void } } + /** + * @param iterable> $data + * @param mixed ...$opts + * @return string + */ public function writeString( iterable $data, ...$opts @@ -101,6 +106,12 @@ public function writeString( return $contents; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return bool + */ public function writeFile( iterable $data, string $filename, @@ -113,6 +124,12 @@ public function writeFile( return true; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return void + */ public function output( iterable $data, string $filename, diff --git a/src/Csv/OpenSpout.php b/src/Csv/OpenSpout.php index 2241aa0..688b768 100644 --- a/src/Csv/OpenSpout.php +++ b/src/Csv/OpenSpout.php @@ -76,6 +76,11 @@ protected function getWriter(): Writer return $writer; } + /** + * @param iterable> $data + * @param mixed ...$opts + * @return string + */ public function writeString(iterable $data, ...$opts): string { $this->configure(...$opts); @@ -89,6 +94,12 @@ public function writeString(iterable $data, ...$opts): string return $contents; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return bool + */ public function writeFile(iterable $data, string $filename, ...$opts): bool { $this->configure(...$opts); @@ -104,6 +115,12 @@ public function writeFile(iterable $data, string $filename, ...$opts): bool return true; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return void + */ public function output(iterable $data, string $filename, ...$opts): void { $this->configure(...$opts); diff --git a/src/SpreadCompat.php b/src/SpreadCompat.php index 458e3e6..7542af9 100644 --- a/src/SpreadCompat.php +++ b/src/SpreadCompat.php @@ -6,7 +6,10 @@ use Exception; use Generator; +use LeKoala\SpreadCompat\Common\ZipUtils; use RuntimeException; +use ZipArchive; +use SimpleXMLElement; /** * This class provides a static facade for adapters @@ -142,6 +145,13 @@ public static function getTempFilename(): string return $result; } + public static function stringToTempFile(string $data): string + { + $filename = self::getTempFilename(); + file_put_contents($filename, $data); + return $filename; + } + public static function isTempFile(string $file): bool { return str_starts_with(basename($file), 'S_C'); @@ -255,11 +265,50 @@ public static function outputHeaders(string $contentType, string $filename): voi public static function excelColumnRange(string $lower = 'A', string $upper = 'ZZ'): Generator { ++$upper; + //@phpstan-ignore-next-line for ($i = $lower; $i !== $upper; ++$i) { + //@phpstan-ignore-next-line yield $i; } } + /** + * Read properties from an excel file + * @param string $filename + * @return array{title:string,subject:string,creator:string,description:string,language:string,lastModifiedBy:string,keywords:string,category:string,revision:string} + */ + public static function excelProperties(string $filename) + { + $zip = new ZipArchive(); + $zip->open($filename); + + $arr = [ + 'title' => '', + 'subject' => '', + 'creator' => '', + 'description' => '', + 'language' => '', + 'lastModifiedBy' => '', + 'keywords' => '', + 'category' => '', + 'revision' => '', + ]; + $props = ZipUtils::getData($zip, 'docProps/core.xml'); + if (!$props) { + return $arr; + } + $matches = []; + preg_match_all("/<(?:dc|cp):([\w]*)>(.*)<\/(?:dc|cp):([\w]*)>/", $props, $matches); + $combine = array_combine($matches[1], $matches[2]); + if ($combine) { + $arr = array_merge($arr, $combine); + } + + $zip->close(); + //@phpstan-ignore-next-line + return $arr; + } + /** * String from column index. * diff --git a/src/Xlsx/Native.php b/src/Xlsx/Native.php index 58ea033..b05ddaf 100644 --- a/src/Xlsx/Native.php +++ b/src/Xlsx/Native.php @@ -6,6 +6,7 @@ use Exception; use Generator; +use LeKoala\SpreadCompat\Common\ZipUtils; use ZipArchive; use SimpleXMLElement; use ZipStream\ZipStream; @@ -29,37 +30,21 @@ public function readFile( $zip = new ZipArchive(); $zip->open($filename); - $ssData = null; - $wsData = null; - for ($i = 0; $i < $zip->numFiles; $i++) { - $name = $zip->getNameIndex($i); - if (!$name) { - continue; - } - if (str_contains($name, 'xl/sharedStrings.xml')) { - $ssData = $zip->getFromName($name); - } elseif (str_contains($name, 'xl/worksheets/sheet1.xml')) { - $wsData = $zip->getFromName($name); - } + // shared strings + $ssXml = null; + $ssData = ZipUtils::getData($zip, 'xl/sharedStrings.xml'); + if ($ssData) { + $ssXml = new SimpleXMLElement($ssData); } + + // worksheet + $wsData = ZipUtils::getData($zip, 'xl/worksheets/sheet1.xml'); $zip->close(); if (!$wsData) { throw new Exception("No data"); } - // Process shared strings. Obviously this doesn't work for super large documents - $ss = []; - if ($ssData) { - $i = 0; - - $ssXml = new SimpleXMLElement($ssData); - foreach ($ssXml->children() as $node) { - $ss[$i] = (string)$node->t; - $i++; - } - } - // Process data $wsXml = new SimpleXMLElement($wsData); $headers = null; @@ -67,11 +52,14 @@ public function readFile( $r = []; foreach ($row->children() as $c) { $t = (string)$c->attributes()->t; + $v = (string)$c->v; + + // it's a shared string + if ($t === 's' && $ssXml) { + $v = $ssXml->si[(int)$c->v]->t ?? ''; + } - $r[] = match ($t) { - 's' => $ss[(int)$c->v] ?? '', - default => (string)$c->v, - }; + $r[] = $v; } if (empty($r) || $r[0] === "") { continue; @@ -89,7 +77,7 @@ public function readFile( /** * @param ZipStream|ZipArchive $zip - * @param iterable $data + * @param iterable> $data * @return void */ protected function write($zip, iterable $data): void @@ -243,6 +231,7 @@ protected function genWorkbook(): string } /** + * @param iterable> $data * @return resource */ protected function genWorksheet(iterable $data, bool $memory = true) @@ -348,6 +337,12 @@ protected static function esc(string $str): string return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&', '<', '>', '', '', ''], $str); } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return bool + */ public function writeFile( iterable $data, string $filename, @@ -373,7 +368,7 @@ public function writeFile( $zip = new ZipArchive(); $result = $zip->open($filename, $mode); if ($result !== true) { - throw new Exception("Failed to open zip archive, code: " . self::zipError($result)); + throw new Exception("Failed to open zip archive, code: " . ZipUtils::zipError($result)); } $this->write($zip, $data); if (!SpreadCompat::isTempFile($filename)) { @@ -384,22 +379,12 @@ public function writeFile( return fclose($stream); } - protected static function zipError(int $code): string - { - return match ($code) { - ZipArchive::ER_EXISTS => 'File already exists.', - ZipArchive::ER_INCONS => 'Zip archive inconsistent.', - ZipArchive::ER_INVAL => 'Invalid argument.', - ZipArchive::ER_MEMORY => 'Malloc failure.', - ZipArchive::ER_NOENT => 'No such file.', - ZipArchive::ER_NOZIP => 'Not a zip archive.', - ZipArchive::ER_OPEN => 'Can\'t open file.', - ZipArchive::ER_READ => 'Read error.', - ZipArchive::ER_SEEK => 'Seek error.', - default => 'Unknown error code ' . $code . '.', - }; - } - + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return void + */ public function output( iterable $data, string $filename, diff --git a/src/Xlsx/OpenSpout.php b/src/Xlsx/OpenSpout.php index 75d0170..97ffa48 100644 --- a/src/Xlsx/OpenSpout.php +++ b/src/Xlsx/OpenSpout.php @@ -11,6 +11,7 @@ use OpenSpout\Writer\XLSX\Writer; use LeKoala\SpreadCompat\SpreadCompat; use OpenSpout\Writer\XLSX\Entity\SheetView; +use OpenSpout\Writer\XLSX\Properties; class OpenSpout extends XlsxAdapter { @@ -45,12 +46,17 @@ public function readFile( protected function getWriter(): Writer { $options = new \OpenSpout\Writer\XLSX\Options(); + + if (method_exists($options, 'getProperties')) { + $options->setProperties(new Properties( + creator: $this->creator + )); + } $writer = new Writer($options); - if ($this->creator) { + // @link https://github.com/openspout/openspout/issues/286 + if ($this->creator && method_exists($writer, 'setCreator')) { $writer->setCreator($this->creator); } - - $writer = new Writer(); return $writer; } @@ -78,6 +84,9 @@ protected function setSheetView(Writer $writer) } } + /** + * @return array{int<0,25>,positive-int,int<0,25>,positive-int} + */ public function autofilterCoords(): array { $parts = explode(":", $this->autofilter ?? ""); @@ -86,11 +95,13 @@ public function autofilterCoords(): array $letters = range('A', 'Z'); - $fromColumnIndex = array_search(substr($from, 0, 1), $letters, true); + $fromColumnIndex = (int)array_search(substr($from, 0, 1), $letters, true); $fromRow = (int)substr($from, 1, 1); - $toColumnIndex = array_search(substr($to, 0, 1), $letters, true); + $toColumnIndex = (int)array_search(substr($to, 0, 1), $letters, true); $toRow = (int)substr($to, 1, 1); + assert($fromRow > 0 && $toRow > 0); + return [ $fromColumnIndex, $fromRow, @@ -99,6 +110,12 @@ public function autofilterCoords(): array ]; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return bool + */ public function writeFile(iterable $data, string $filename, ...$opts): bool { $this->configure(...$opts); @@ -115,6 +132,12 @@ public function writeFile(iterable $data, string $filename, ...$opts): bool return true; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return void + */ public function output(iterable $data, string $filename, ...$opts): void { $this->configure(...$opts); diff --git a/src/Xlsx/Simple.php b/src/Xlsx/Simple.php index c4f6e80..8ea7646 100644 --- a/src/Xlsx/Simple.php +++ b/src/Xlsx/Simple.php @@ -21,12 +21,21 @@ public function readFile( ...$opts ): Generator { $this->configure(...$opts); + + /** @var SimpleXLSX|null $xlsx */ $xlsx = SimpleXLSX::parse($filename); if (!$xlsx) { - throw new RuntimeException("Parse error: " . (string)SimpleXLSX::parseError()); + $err = SimpleXLSX::parseError(); + if (!is_string($err)) { + $err = "unknown error"; + } + throw new RuntimeException("Parse error: $err"); } $headers = null; - foreach ($xlsx->readRows() as $r) { + + /** @var iterable> $rows */ + $rows = $xlsx->readRows(); + foreach ($rows as $r) { if (empty($r) || $r[0] === "") { continue; } @@ -46,7 +55,12 @@ protected function getWriter(iterable $data): SimpleXLSXGen if (!is_array($data)) { $data = iterator_to_array($data); } + + /** @var SimpleXLSXGen|null $xlsx */ $xlsx = SimpleXLSXGen::fromArray($data); + if (!$xlsx) { + throw new RuntimeException("Read from array error"); + } if ($this->creator) { $xlsx->setAuthor($this->creator); } @@ -59,6 +73,12 @@ protected function getWriter(iterable $data): SimpleXLSXGen return $xlsx; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return bool + */ public function writeFile( iterable $data, string $filename, @@ -70,6 +90,12 @@ public function writeFile( return true; } + /** + * @param iterable> $data + * @param string $filename + * @param mixed ...$opts + * @return void + */ public function output( iterable $data, string $filename, diff --git a/tests/SpreadCompatXlsxTest.php b/tests/SpreadCompatXlsxTest.php index 1411be0..c0d9519 100644 --- a/tests/SpreadCompatXlsxTest.php +++ b/tests/SpreadCompatXlsxTest.php @@ -42,7 +42,9 @@ public function testOpenSpoutCanWriteXlsx() $openSpout = new OpenSpout(); $string = $openSpout->writeString([ [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ]); $this->assertStringContainsString('[Content_Types].xml', $string); @@ -50,10 +52,14 @@ public function testOpenSpoutCanWriteXlsx() $openSpout = new OpenSpout(); $string2 = $openSpout->writeString([ [ - "firstname", "surname", "email" + "firstname", + "surname", + "email" ], [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ], ...[ 'autofilter' => 'A1:C1', @@ -65,6 +71,21 @@ public function testOpenSpoutCanWriteXlsx() $coordsOpenSpout = new OpenSpout(); $coordsOpenSpout->autofilter = 'A1:C1'; $this->assertEquals([0, 1, 2, 1], $coordsOpenSpout->autofilterCoords()); + + $openSpout = new OpenSpout(); + $openSpout->creator = "test"; + $string = $openSpout->writeString([ + [ + "john", + "doe", + "john.doe@example.com" + ] + ]); + $this->assertStringContainsString('[Content_Types].xml', $string); + $tmpFile = SpreadCompat::stringToTempFile($string); + $props = SpreadCompat::excelProperties($tmpFile); + $this->assertEquals("test", $props['creator']); + $this->assertNotEquals("OpenSpout", $props['creator']); } public function testSpreadsheetCanReadXlsx() @@ -89,7 +110,9 @@ public function testSpreadsheetCanWriteXlsx() $openSpout = new PhpSpreadsheet(); $string = $openSpout->writeString([ [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ]); $this->assertStringContainsString('[Content_Types].xml', $string); @@ -97,10 +120,14 @@ public function testSpreadsheetCanWriteXlsx() $openSpout = new PhpSpreadsheet(); $string2 = $openSpout->writeString([ [ - "fname", "sname", "email" + "fname", + "sname", + "email" ], [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ], ...[ 'autofilter' => 'A1:C1', @@ -132,7 +159,9 @@ public function testSimpleCanWriteXlsx() $openSpout = new Simple(); $string = $openSpout->writeString([ [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ]); $this->assertStringContainsString('[Content_Types].xml', $string); @@ -140,10 +169,14 @@ public function testSimpleCanWriteXlsx() $openSpout = new Simple(); $string2 = $openSpout->writeString([ [ - "fname", "sname", "email" + "fname", + "sname", + "email" ], [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ], ...[ 'autofilter' => 'A1:C1', @@ -175,7 +208,9 @@ public function testNativeCanWriteXlsx() $Native = new Native(); $string = $Native->writeString([ [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ]); $this->assertStringContainsString('[Content_Types].xml', $string); @@ -183,10 +218,14 @@ public function testNativeCanWriteXlsx() $Native = new Native(); $string2 = $Native->writeString([ [ - "fname", "sname", "email" + "fname", + "sname", + "email" ], [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ], ...[ 'autofilter' => 'A1:C1', @@ -200,7 +239,9 @@ public function testNativeCanWriteXlsx() $Native->stream = true; $string = $Native->writeString([ [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ]); $this->assertStringContainsString('[Content_Types].xml', $string); @@ -208,10 +249,14 @@ public function testNativeCanWriteXlsx() $Native = new Native(); $string2 = $Native->writeString([ [ - "fname", "sname", "email" + "fname", + "sname", + "email" ], [ - "john", "doe", "john.doe@example.com" + "john", + "doe", + "john.doe@example.com" ] ], ...[ 'autofilter' => 'A1:C1',