diff --git a/README.md b/README.md index 8eeddf2..8486c60 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ SimpleXLSX: very fast excel import/export https://github.com/shuchkin/simplexlsx https://github.com/shuchkin/simplexlsxgen -This package will prioritize installed library, by order of performance. You can also pick your preferred adapter for each format like this: +This package will prioritize installed library, by order of performance. You can also pick your preferred default adapter for each format like this: ```php SpreadCompat::$preferredCsvAdapter = SpreadCompat::NATIVE; // our native csv adapter is the fastest @@ -82,6 +82,18 @@ $options->separator = ";"; $data = iterator_to_array(SpreadCompat::read('myfile.csv', $options)); ``` +## Setting the adapter + +Instead of relying on the static variables, you can choose which adapter to use: + +```php +$csvData = SpreadCompat::readString($csv, adapter: SpreadCompat::NATIVE); +// or +$options = new Options(); +$options->adapter = SpreadCompat::NATIVE; +$csvData = SpreadCompat::readString($csv, $options); +``` + ## Worksheets This package supports only 1 worksheet, as it is meant to be able to replace csv by xlsx or vice versa diff --git a/src/Common/Options.php b/src/Common/Options.php index 2d61d6f..3594b1a 100644 --- a/src/Common/Options.php +++ b/src/Common/Options.php @@ -4,7 +4,12 @@ namespace LeKoala\SpreadCompat\Common; -class Options +use ArrayAccess; + +/** + * @implements ArrayAccess + */ +class Options implements ArrayAccess { use Configure; @@ -45,4 +50,24 @@ public function __construct(...$opts) $this->configure(...$opts); } } + + public function offsetExists(mixed $offset): bool + { + return property_exists($this, $offset); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->$offset ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->$offset = $value; + } + + public function offsetUnset(mixed $offset): void + { + $this->$offset = null; + } } diff --git a/src/SpreadCompat.php b/src/SpreadCompat.php index 7b77a23..458e3e6 100644 --- a/src/SpreadCompat.php +++ b/src/SpreadCompat.php @@ -80,7 +80,7 @@ public static function getAdapter(string $ext): SpreadInterface { $name = self::getAdapterName($ext); $ext = ucfirst($ext); - $class = 'LeKoala\\SpreadCompat\\' . $ext . '\\' . $name; + $class = '\\LeKoala\\SpreadCompat\\' . $ext . '\\' . $name; if (!class_exists($class)) { throw new Exception("Invalid adapter $class"); } @@ -90,7 +90,7 @@ public static function getAdapter(string $ext): SpreadInterface public static function getAdapterByName(string $ext, string $name): SpreadInterface { $ext = ucfirst($ext); - $class = 'LeKoala\\SpreadCompat\\' . $ext . '\\' . $name; + $class = '\\LeKoala\\SpreadCompat\\' . $ext . '\\' . $name; if (!class_exists($class)) { throw new Exception("Invalid adapter $class"); } @@ -105,6 +105,31 @@ public static function getAdapterForFile(string $filename, string $ext = null): return self::getAdapter($ext); } + /** + * @param array>|array $opts + * @param ?string $ext + * @return ?SpreadInterface + */ + public static function getAdapterFromOpts(array $opts, ?string $ext = null): ?SpreadInterface + { + $name = $opts[0]['adapter'] ?? $opts['adapter'] ?? null; + if ($name === null || !is_string($name)) { + return null; + } + // It's a full class name + if (is_a($name, SpreadInterface::class, true)) { + return new ($name); + } + if (!$ext) { + $ext = self::getExtensionFromOpts($opts); + } + // It's a partial name, we need the extension for this + if ($ext) { + return self::getAdapterByName($ext, $name); + } + return null; + } + /** * @return string */ @@ -128,10 +153,13 @@ public static function isTempFile(string $file): bool */ public static function getExtensionForContent(string $contents): string { - if (ctype_print($contents)) { - $ext = self::EXT_CSV; - } else { + //@link https://gist.github.com/leommoore/f9e57ba2aa4bf197ebc5 + //50 4b 03 04 + $header = strtoupper(substr(bin2hex($contents), 0, 8)); + if ($header === '504B0304') { $ext = self::EXT_XLSX; + } else { + $ext = self::EXT_CSV; } return $ext; } @@ -269,8 +297,8 @@ public static function excelCell(int $row = 0, int $column = 0, bool $absolute = */ protected static function getExtensionFromOpts(array $opts, ?string $fallback = null): ?string { - //@phpstan-ignore-next-line PHPStan doesn't detect properly our return type - return $opts[0]['extension'] ?? $opts['extension'] ?? $fallback; + $ext = $opts[0]['extension'] ?? $opts['extension'] ?? $fallback; + return is_string($ext) ? $ext : null; } public static function read( @@ -278,7 +306,11 @@ public static function read( ...$opts ): Generator { $ext = self::getExtensionFromOpts($opts); - return static::getAdapterForFile($filename, $ext)->readFile($filename, ...$opts); + $adapter = self::getAdapterFromOpts($opts, $ext); + if (!$adapter) { + $adapter = static::getAdapterForFile($filename, $ext); + } + return $adapter->readFile($filename, ...$opts); } public static function readString( @@ -290,7 +322,11 @@ public static function readString( if ($ext === null) { $ext = self::getExtensionForContent($contents); } - return static::getAdapter($ext)->readString($contents, ...$opts); + $adapter = self::getAdapterFromOpts($opts, $ext); + if (!$adapter) { + $adapter = static::getAdapter($ext); + } + return $adapter->readString($contents, ...$opts); } public static function write( @@ -299,7 +335,27 @@ public static function write( ...$opts ): bool { $ext = self::getExtensionFromOpts($opts); - return static::getAdapterForFile($filename, $ext)->writeFile($data, $filename, ...$opts); + $adapter = self::getAdapterFromOpts($opts, $ext); + if (!$adapter) { + $adapter = static::getAdapterForFile($filename, $ext); + } + return $adapter->writeFile($data, $filename, ...$opts); + } + + public static function writeString( + iterable $data, + string $ext = null, + ...$opts + ): string { + $ext = self::getExtensionFromOpts($opts); + $adapter = self::getAdapterFromOpts($opts, $ext); + if (!$adapter && !$ext) { + throw new Exception("No adapter or extension specified for string"); + } + if (!$adapter) { + $adapter = static::getAdapter($ext); + } + return $adapter->writeString($data, ...$opts); } public static function output( @@ -311,6 +367,10 @@ public static function output( if ($ext) { $filename = self::ensureExtension($filename, $ext); } - static::getAdapterForFile($filename, $ext)->output($data, $filename, ...$opts); + $adapter = self::getAdapterFromOpts($opts, $ext); + if (!$adapter) { + $adapter = static::getAdapterForFile($filename, $ext); + } + $adapter->output($data, $filename, ...$opts); } } diff --git a/tests/SpreadCompatCommonTest.php b/tests/SpreadCompatCommonTest.php index c6cb994..3c8560a 100644 --- a/tests/SpreadCompatCommonTest.php +++ b/tests/SpreadCompatCommonTest.php @@ -15,9 +15,15 @@ public function testCanUseOptions() { $options = new Options(); $options->separator = ";"; + + // Can use configure $csv = new Native(); $csv->configure($options); $this->assertEquals(";", $csv->separator); + + // Or use directly + $csvData = SpreadCompat::read(__DIR__ . '/data/separator.csv', $options); + $this->assertNotEmpty(iterator_to_array($csvData)); } public function testCanUseNamedArguments() @@ -61,4 +67,54 @@ public function testCanReadTemp() $this->expectException(\Exception::class); $csvData = SpreadCompat::read($filename); } + + public function testCanDetectContentType() + { + $csv = file_get_contents(__DIR__ . '/data/basic.csv'); + $this->assertTrue('csv' == SpreadCompat::getExtensionForContent($csv), "Content is: $csv"); + $xlsx = file_get_contents(__DIR__ . '/data/basic.xlsx'); + $this->assertTrue('xlsx' == SpreadCompat::getExtensionForContent($xlsx), "Content is: $xlsx"); + } + + public function testCanSpecifyAdapter() + { + // Csv, with extension in opts or as param + $adapter = SpreadCompat::getAdapterFromOpts([ + 'adapter' => SpreadCompat::NATIVE, + 'extension' => 'csv' + ]); + $this->assertInstanceOf(\LeKoala\SpreadCompat\Csv\Native::class, $adapter); + $adapter = SpreadCompat::getAdapterFromOpts([ + 'adapter' => SpreadCompat::PHP_SPREADSHEET, + 'extension' => 'csv' + ]); + $this->assertInstanceOf(\LeKoala\SpreadCompat\Csv\PhpSpreadsheet::class, $adapter); + $adapter = SpreadCompat::getAdapterFromOpts([ + 'adapter' => SpreadCompat::PHP_SPREADSHEET, + ], 'csv'); + $this->assertInstanceOf(\LeKoala\SpreadCompat\Csv\PhpSpreadsheet::class, $adapter); + // Xlsx + $adapter = SpreadCompat::getAdapterFromOpts([ + 'adapter' => SpreadCompat::PHP_SPREADSHEET, + ], 'xlsx'); + $this->assertInstanceOf(\LeKoala\SpreadCompat\Xlsx\PhpSpreadsheet::class, $adapter); + $adapter = SpreadCompat::getAdapterFromOpts([ + 'adapter' => SpreadCompat::NATIVE, + ], 'xlsx'); + $this->assertInstanceOf(\LeKoala\SpreadCompat\Xlsx\Native::class, $adapter); + // Can specify full class + $adapter = SpreadCompat::getAdapterFromOpts([ + 'adapter' => \LeKoala\SpreadCompat\Xlsx\Native::class, + ], 'xlsx'); + $this->assertInstanceOf(\LeKoala\SpreadCompat\Xlsx\Native::class, $adapter); + + // Make sure it actually works + $csv = file_get_contents(__DIR__ . '/data/basic.csv'); + $csvData = SpreadCompat::readString($csv, null, adapter: SpreadCompat::NATIVE); + $this->assertNotEmpty(iterator_to_array($csvData)); + $options = new Options(); + $options->adapter = SpreadCompat::NATIVE; + $csvData = SpreadCompat::readString($csv, null, $options); + $this->assertNotEmpty(iterator_to_array($csvData)); + } }