Skip to content

Commit

Permalink
set adapter from options, fix detect content
Browse files Browse the repository at this point in the history
  • Loading branch information
lekoala committed Apr 22, 2024
1 parent 0f88fdf commit ec56a2b
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 13 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion src/Common/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

namespace LeKoala\SpreadCompat\Common;

class Options
use ArrayAccess;

/**
* @implements ArrayAccess<string,mixed>
*/
class Options implements ArrayAccess
{
use Configure;

Expand Down Expand Up @@ -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;
}
}
82 changes: 71 additions & 11 deletions src/SpreadCompat.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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");
}
Expand All @@ -105,6 +105,31 @@ public static function getAdapterForFile(string $filename, string $ext = null):
return self::getAdapter($ext);
}

/**
* @param array<int,array<string,string>>|array<string,string> $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
*/
Expand All @@ -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;
}
Expand Down Expand Up @@ -269,16 +297,20 @@ 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(
string $filename,
...$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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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);
}
}
56 changes: 56 additions & 0 deletions tests/SpreadCompatCommonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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));
}
}

0 comments on commit ec56a2b

Please sign in to comment.