diff --git a/.github/labeler.yml b/.github/labeler.yml index c416b3c1..2d3e1e22 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -11,6 +11,8 @@ - 'src/batch-doctrine-persistence/**' 'yokai/batch-league-flysystem': - 'src/batch-league-flysystem/**' +'yokai/batch-openspout': + - 'src/batch-openspout/**' 'yokai/batch-symfony-console': - 'src/batch-symfony-console/**' 'yokai/batch-symfony-framework': diff --git a/.github/release.yml b/.github/release.yml index 12be75b3..973d1422 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -13,6 +13,8 @@ changelog: labels: ['yokai/batch-doctrine-persistence'] - title: 'Changes made to the `league/flysystem` bridge: `yokai/batch-league-flysystem`' labels: ['yokai/batch-league-flysystem'] + - title: 'Changes made to the `openspout/openspout` bridge: `yokai/batch-openspout`' + labels: ['yokai/batch-openspout'] - title: 'Changes made to the `symfony/console` bridge: `yokai/batch-symfony-console`' labels: ['yokai/batch-symfony-console'] - title: 'Changes made to the `symfony/framework-bundle` bridge: `yokai/batch-symfony-framework`' diff --git a/MAINTAINER.md b/MAINTAINER.md index 8e8cb8d9..e0d40f1f 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -36,6 +36,7 @@ see https://github.com/yokai-php/batch-src/releases/tag/{created tag} - https://github.com/yokai-php/batch-doctrine-orm/releases/new - https://github.com/yokai-php/batch-doctrine-persistence/releases/new - https://github.com/yokai-php/batch-league-flysystem/releases/new + - https://github.com/yokai-php/batch-openspout/releases/new - https://github.com/yokai-php/batch-symfony-console/releases/new - https://github.com/yokai-php/batch-symfony-framework/releases/new - https://github.com/yokai-php/batch-symfony-messenger/releases/new diff --git a/README.md b/README.md index d1445324..7b9009c9 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,12 @@ Some bridges to popular packages : | Bridge with | | |------------------------------------------------------------------------------------|----------------------------------------------------------------| -| [`box/spout`](https://github.com/yokai-php/batch-box-spout) | Read/Write from/to CSV/ODS/XLSX | +| `DEPRECATED` [`box/spout`](https://github.com/yokai-php/batch-box-spout) | Read/Write from/to CSV/ODS/XLSX | | [`doctrine/dbal`](https://github.com/yokai-php/batch-doctrine-dbal) | Read/Write from/to SQL databases | | [`doctrine/orm`](https://github.com/yokai-php/batch-doctrine-orm) | Read from Doctrine ORM entities | | [`doctrine/persistence`](https://github.com/yokai-php/batch-doctrine-persistence) | Write to Doctrine ORM/ODM objects | | [`league/flysystem`](https://github.com/yokai-php/batch-league-flysystem) | Copy/Move files in a job / Trigger job when file found | +| [`openspout/openspout`](https://github.com/yokai-php/batch-openspout) | Read/Write from/to CSV/ODS/XLSX | | [`symfony/console`](https://github.com/yokai-php/batch-symfony-console) | Add command to trigger jobs and async job launcher via command | | [`symfony/framework-bundle`](https://github.com/yokai-php/batch-symfony-framework) | Bundle to integrate with Symfony framework | | [`symfony/messenger`](https://github.com/yokai-php/batch-symfony-messenger) | Trigger jobs using message dispatch | diff --git a/composer.json b/composer.json index c168bf36..45984ece 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "doctrine/orm": "^2.8", "doctrine/persistence": "^2.0|^3.0", "league/flysystem": "^3.0", + "openspout/openspout": "^4.0", "psr/container": "^1.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0|^2.0|^3.0", @@ -50,6 +51,7 @@ "yokai/batch-doctrine-orm": "self.version", "yokai/batch-doctrine-persistence": "self.version", "yokai/batch-league-flysystem": "self.version", + "yokai/batch-openspout": "self.version", "yokai/batch-symfony-console": "self.version", "yokai/batch-symfony-framework": "self.version", "yokai/batch-symfony-messenger": "self.version", @@ -64,6 +66,7 @@ "Yokai\\Batch\\Bridge\\Doctrine\\ORM\\": "src/batch-doctrine-orm/src/", "Yokai\\Batch\\Bridge\\Doctrine\\Persistence\\": "src/batch-doctrine-persistence/src/", "Yokai\\Batch\\Bridge\\League\\Flysystem\\": "src/batch-league-flysystem/src/", + "Yokai\\Batch\\Bridge\\OpenSpout\\": "src/batch-openspout/src/", "Yokai\\Batch\\Bridge\\Symfony\\Console\\": "src/batch-symfony-console/src/", "Yokai\\Batch\\Bridge\\Symfony\\Framework\\": "src/batch-symfony-framework/src/", "Yokai\\Batch\\Bridge\\Symfony\\Messenger\\": "src/batch-symfony-messenger/src/", @@ -83,6 +86,7 @@ "Yokai\\Batch\\Tests\\Bridge\\Doctrine\\ORM\\": "src/batch-doctrine-orm/tests/", "Yokai\\Batch\\Tests\\Bridge\\Doctrine\\Persistence\\": "src/batch-doctrine-persistence/tests/", "Yokai\\Batch\\Tests\\Bridge\\League\\Flysystem\\": "src/batch-league-flysystem/tests/", + "Yokai\\Batch\\Tests\\Bridge\\OpenSpout\\": "src/batch-openspout/tests/", "Yokai\\Batch\\Tests\\Bridge\\Symfony\\Console\\": "src/batch-symfony-console/tests/", "Yokai\\Batch\\Tests\\Bridge\\Symfony\\Framework\\": "src/batch-symfony-framework/tests/", "Yokai\\Batch\\Tests\\Bridge\\Symfony\\Messenger\\": "src/batch-symfony-messenger/tests/", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 13a73f6b..8812407e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -100,6 +100,66 @@ parameters: count: 1 path: src/batch-doctrine-dbal/src/JobExecutionRowNormalizer.php + - + message: "#^Method Yokai\\\\Batch\\\\Bridge\\\\OpenSpout\\\\Reader\\\\FlatFileReader\\:\\:rows\\(\\) has parameter \\$reader with generic interface OpenSpout\\\\Reader\\\\ReaderInterface but does not specify its types\\: T$#" + count: 1 + path: src/batch-openspout/src/Reader/FlatFileReader.php + + - + message: "#^Parameter \\#1 \\$headers of method Yokai\\\\Batch\\\\Bridge\\\\OpenSpout\\\\Reader\\\\HeaderStrategy\\:\\:setHeaders\\(\\) expects array\\, array\\ given\\.$#" + count: 1 + path: src/batch-openspout/src/Reader/FlatFileReader.php + + - + message: "#^Parameter \\#1 \\$options of class OpenSpout\\\\Reader\\\\CSV\\\\Reader constructor expects OpenSpout\\\\Reader\\\\CSV\\\\Options\\|null, OpenSpout\\\\Reader\\\\CSV\\\\Options\\|OpenSpout\\\\Reader\\\\ODS\\\\Options\\|OpenSpout\\\\Reader\\\\XLSX\\\\Options\\|null given\\.$#" + count: 1 + path: src/batch-openspout/src/Reader/FlatFileReader.php + + - + message: "#^Parameter \\#1 \\$options of class OpenSpout\\\\Reader\\\\ODS\\\\Reader constructor expects OpenSpout\\\\Reader\\\\ODS\\\\Options\\|null, OpenSpout\\\\Reader\\\\CSV\\\\Options\\|OpenSpout\\\\Reader\\\\ODS\\\\Options\\|OpenSpout\\\\Reader\\\\XLSX\\\\Options\\|null given\\.$#" + count: 1 + path: src/batch-openspout/src/Reader/FlatFileReader.php + + - + message: "#^Parameter \\#1 \\$options of class OpenSpout\\\\Reader\\\\XLSX\\\\Reader constructor expects OpenSpout\\\\Reader\\\\XLSX\\\\Options\\|null, OpenSpout\\\\Reader\\\\CSV\\\\Options\\|OpenSpout\\\\Reader\\\\ODS\\\\Options\\|OpenSpout\\\\Reader\\\\XLSX\\\\Options\\|null given\\.$#" + count: 1 + path: src/batch-openspout/src/Reader/FlatFileReader.php + + - + message: "#^Parameter \\#1 \\$row of method Yokai\\\\Batch\\\\Bridge\\\\OpenSpout\\\\Reader\\\\HeaderStrategy\\:\\:getItem\\(\\) expects array\\, array\\ given\\.$#" + count: 1 + path: src/batch-openspout/src/Reader/FlatFileReader.php + + - + message: "#^Method Yokai\\\\Batch\\\\Bridge\\\\OpenSpout\\\\Reader\\\\SheetFilter\\:\\:list\\(\\) has parameter \\$reader with generic interface OpenSpout\\\\Reader\\\\ReaderInterface but does not specify its types\\: T$#" + count: 1 + path: src/batch-openspout/src/Reader/SheetFilter.php + + - + message: "#^Method Yokai\\\\Batch\\\\Bridge\\\\OpenSpout\\\\Reader\\\\SheetFilter\\:\\:list\\(\\) return type with generic interface OpenSpout\\\\Reader\\\\SheetInterface does not specify its types\\: T$#" + count: 1 + path: src/batch-openspout/src/Reader/SheetFilter.php + + - + message: "#^PHPDoc tag @var for variable \\$sheet contains generic interface OpenSpout\\\\Reader\\\\SheetInterface but does not specify its types\\: T$#" + count: 1 + path: src/batch-openspout/src/Reader/SheetFilter.php + + - + message: "#^Parameter \\#1 \\$options of class OpenSpout\\\\Writer\\\\CSV\\\\Writer constructor expects OpenSpout\\\\Writer\\\\CSV\\\\Options\\|null, OpenSpout\\\\Writer\\\\CSV\\\\Options\\|OpenSpout\\\\Writer\\\\ODS\\\\Options\\|OpenSpout\\\\Writer\\\\XLSX\\\\Options\\|null given\\.$#" + count: 1 + path: src/batch-openspout/src/Writer/FlatFileWriter.php + + - + message: "#^Parameter \\#1 \\$options of class OpenSpout\\\\Writer\\\\ODS\\\\Writer constructor expects OpenSpout\\\\Writer\\\\ODS\\\\Options\\|null, OpenSpout\\\\Writer\\\\CSV\\\\Options\\|OpenSpout\\\\Writer\\\\ODS\\\\Options\\|OpenSpout\\\\Writer\\\\XLSX\\\\Options\\|null given\\.$#" + count: 1 + path: src/batch-openspout/src/Writer/FlatFileWriter.php + + - + message: "#^Parameter \\#1 \\$options of class OpenSpout\\\\Writer\\\\XLSX\\\\Writer constructor expects OpenSpout\\\\Writer\\\\XLSX\\\\Options\\|null, OpenSpout\\\\Writer\\\\CSV\\\\Options\\|OpenSpout\\\\Writer\\\\ODS\\\\Options\\|OpenSpout\\\\Writer\\\\XLSX\\\\Options\\|null given\\.$#" + count: 1 + path: src/batch-openspout/src/Writer/FlatFileWriter.php + - message: "#^Cannot access offset 'connection' on mixed\\.$#" count: 1 diff --git a/phpstan.neon b/phpstan.neon index c035bb0b..898616e1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,6 +10,7 @@ parameters: - src/batch-doctrine-orm/src/ - src/batch-doctrine-persistence/src/ - src/batch-league-flysystem/src/ + - src/batch-openspout/src/ - src/batch-symfony-console/src/ - src/batch-symfony-framework/src/ - src/batch-symfony-messenger/src/ diff --git a/scripts/split-branch b/scripts/split-branch index ba491e54..1fb450d7 100755 --- a/scripts/split-branch +++ b/scripts/split-branch @@ -30,6 +30,7 @@ remote batch-doctrine-dbal git@github.com:yokai-php/batch-doctrine-dbal.g remote batch-doctrine-orm git@github.com:yokai-php/batch-doctrine-orm.git remote batch-doctrine-persistence git@github.com:yokai-php/batch-doctrine-persistence.git remote batch-league-flysystem git@github.com:yokai-php/batch-league-flysystem.git +remote batch-openspout git@github.com:yokai-php/batch-openspout.git remote batch-symfony-console git@github.com:yokai-php/batch-symfony-console.git remote batch-symfony-framework git@github.com:yokai-php/batch-symfony-framework.git remote batch-symfony-messenger git@github.com:yokai-php/batch-symfony-messenger.git @@ -43,6 +44,7 @@ split 'src/batch-doctrine-dbal' batch-doctrine-dbal split 'src/batch-doctrine-orm' batch-doctrine-orm split 'src/batch-doctrine-persistence' batch-doctrine-persistence split 'src/batch-league-flysystem' batch-league-flysystem +split 'src/batch-openspout' batch-openspout split 'src/batch-symfony-console' batch-symfony-console split 'src/batch-symfony-framework' batch-symfony-framework split 'src/batch-symfony-messenger' batch-symfony-messenger diff --git a/scripts/split-tag b/scripts/split-tag index deb87215..fb3a2659 100755 --- a/scripts/split-tag +++ b/scripts/split-tag @@ -30,6 +30,7 @@ remote batch-doctrine-dbal git@github.com:yokai-php/batch-doctrine-dbal.g remote batch-doctrine-orm git@github.com:yokai-php/batch-doctrine-orm.git remote batch-doctrine-persistence git@github.com:yokai-php/batch-doctrine-persistence.git remote batch-league-flysystem git@github.com:yokai-php/batch-league-flysystem.git +remote batch-openspout git@github.com:yokai-php/batch-openspout.git remote batch-symfony-console git@github.com:yokai-php/batch-symfony-console.git remote batch-symfony-framework git@github.com:yokai-php/batch-symfony-framework.git remote batch-symfony-messenger git@github.com:yokai-php/batch-symfony-messenger.git @@ -43,6 +44,7 @@ split 'src/batch-doctrine-dbal' batch-doctrine-dbal split 'src/batch-doctrine-orm' batch-doctrine-orm split 'src/batch-doctrine-persistence' batch-doctrine-persistence split 'src/batch-league-flysystem' batch-league-flysystem +split 'src/batch-openspout' batch-openspout split 'src/batch-symfony-console' batch-symfony-console split 'src/batch-symfony-framework' batch-symfony-framework split 'src/batch-symfony-messenger' batch-symfony-messenger diff --git a/src/batch-box-spout/README.md b/src/batch-box-spout/README.md index 42d45f7f..839d11b6 100644 --- a/src/batch-box-spout/README.md +++ b/src/batch-box-spout/README.md @@ -6,6 +6,13 @@ [`box/spout`](https://github.com/box/spout) bridge for [Batch](https://github.com/yokai-php/batch) processing library. +## :warning: DEPRECATED + +This library is deprecated because the package it relies on was also deprecated. +The library has been replaced with [`openspout/openspout`](https://github.com/openspout/openspout). +And this bridge was replaced with [`yokai/batch-openspout`](https://github.com/yokai-php/batch-openspout). + + ## :warning: BETA This library is following [semver](https://semver.org/). diff --git a/src/batch-box-spout/tests/Writer/FlatFileWriterTest.php b/src/batch-box-spout/tests/Writer/FlatFileWriterTest.php index ce7fb0bd..0ad596e6 100644 --- a/src/batch-box-spout/tests/Writer/FlatFileWriterTest.php +++ b/src/batch-box-spout/tests/Writer/FlatFileWriterTest.php @@ -22,7 +22,7 @@ class FlatFileWriterTest extends TestCase { - private const WRITE_DIR = ARTIFACT_DIR . '/flat-file-writer'; + private const WRITE_DIR = ARTIFACT_DIR . '/box-spout-flat-file-writer'; /** * @dataProvider sets diff --git a/src/batch-openspout/.gitattributes b/src/batch-openspout/.gitattributes new file mode 100644 index 00000000..62fd109e --- /dev/null +++ b/src/batch-openspout/.gitattributes @@ -0,0 +1,6 @@ +.gitattributes export-ignore +.gitignore export-ignore +docs/ export-ignore +tests/ export-ignore +LICENSE export-ignore +*.md export-ignore diff --git a/src/batch-openspout/.github/workflows/lockdown.yml b/src/batch-openspout/.github/workflows/lockdown.yml new file mode 100644 index 00000000..aee0811b --- /dev/null +++ b/src/batch-openspout/.github/workflows/lockdown.yml @@ -0,0 +1,27 @@ +name: 'Lock down Pull Requests' + +on: + pull_request: + types: opened + +jobs: + lockdown: + runs-on: ubuntu-latest + steps: + - uses: dessant/repo-lockdown@v2 + with: + github-token: ${{ github.token }} + close-pr: true + lock-pr: true + pr-comment: > + Thanks for your pull request! + + However, this repository does not accept pull requests, + see the README for details. + + If you want to contribute, + you should instead open a pull request on the main repository: + + https://github.com/yokai-php/batch-src + + Thank you diff --git a/src/batch-openspout/.gitignore b/src/batch-openspout/.gitignore new file mode 100644 index 00000000..88259615 --- /dev/null +++ b/src/batch-openspout/.gitignore @@ -0,0 +1,4 @@ +/.phpunit.result.cache +/tests/.artifacts/ +/vendor/ +/composer.lock diff --git a/src/batch-openspout/LICENSE b/src/batch-openspout/LICENSE new file mode 100644 index 00000000..83096655 --- /dev/null +++ b/src/batch-openspout/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Yann Eugoné + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/batch-openspout/README.md b/src/batch-openspout/README.md new file mode 100644 index 00000000..2933f758 --- /dev/null +++ b/src/batch-openspout/README.md @@ -0,0 +1,46 @@ +# box/spout bridge for Batch processing library + +[![Latest Stable Version](https://img.shields.io/packagist/v/yokai/batch-openspout?style=flat-square)](https://packagist.org/packages/yokai/batch-openspout) +[![Downloads Monthly](https://img.shields.io/packagist/dm/yokai/batch-openspout?style=flat-square)](https://packagist.org/packages/yokai/batch-openspout) + +[`openspout/openspout`](https://github.com/openspout/openspout) bridge for [Batch](https://github.com/yokai-php/batch) processing library. + + +## :warning: BETA + +This library is following [semver](https://semver.org/). +However before we reach the first stable version (`v1.0.0`), we may decide to introduce **API changes in minor versions**. +This is why you should stick to a `v0.[minor].*` requirement ! + + +# Installation + +``` +composer require yokai/batch-openspout +``` + + +## Documentation + +This package provides: + +- a [item reader](docs/flat-file-item-reader.md) that read from CSV/XLSX/ODS files +- a [item writer](docs/flat-file-item-writer.md) that write to CSV/XLSX/ODS files + + +## Contribution + +This package is a readonly split of a [larger repository](https://github.com/yokai-php/batch-src), +containing all tests and sources for all librairies of the batch universe. + +Please feel free to open an [issue](https://github.com/yokai-php/batch-src/issues) +or a [pull request](https://github.com/yokai-php/batch-src/pulls) +in the [main repository](https://github.com/yokai-php/batch-src). + +The library was originally created by [Yann Eugoné](https://github.com/yann-eugone). +See the list of [contributors](https://github.com/yokai-php/batch-src/contributors). + + +## License + +This library is under MIT [LICENSE](LICENSE). diff --git a/src/batch-openspout/composer.json b/src/batch-openspout/composer.json new file mode 100644 index 00000000..294860ff --- /dev/null +++ b/src/batch-openspout/composer.json @@ -0,0 +1,32 @@ +{ + "name": "yokai/batch-openspout", + "description": "openspout/openspout bridge for yokai/batch", + "keywords": ["batch", "job", "reader", "writer", "flat", "csv", "xlsx", "ods"], + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Yann Eugoné", + "email": "eugone.yann@gmail.com" + } + ], + "require": { + "php": "^8.0", + "openspout/openspout": "^4.0", + "yokai/batch": "^0.5.0" + }, + "autoload": { + "psr-4": { + "Yokai\\Batch\\Bridge\\OpenSpout\\": "src/" + } + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/filesystem": "^5.0|^6.0" + }, + "autoload-dev": { + "psr-4": { + "Yokai\\Batch\\Tests\\Bridge\\OpenSpout\\": "tests/" + } + } +} diff --git a/src/batch-openspout/docs/flat-file-item-reader.md b/src/batch-openspout/docs/flat-file-item-reader.md new file mode 100644 index 00000000..ed16ebe4 --- /dev/null +++ b/src/batch-openspout/docs/flat-file-item-reader.md @@ -0,0 +1,48 @@ +# Item reader with CSV/ODS/XLSX files + +The [FlatFileReader](../src/Reader/FlatFileReader.php) is a reader +that will read from CSV/ODS/XLSX file and return each line as an array. + +```php +FIELD_DELIMITER = ';'; +$options->FIELD_ENCLOSURE = '|'; +new FlatFileReader( + new StaticValueParameterAccessor('/path/to/file.csv'), + $options, + null, + HeaderStrategy::none(), +); + +// Read .ods file +// Only sheet named "Sheet name to read" will be read +// Each item will be an array_combine of first line as key and line as values +new FlatFileReader( + new StaticValueParameterAccessor('/path/to/file.ods'), + null, + SheetFilter::nameIs('Sheet name to read'), + HeaderStrategy::combine(), +); +``` + +## On the same subject + +- [What is an item reader ?](https://github.com/yokai-php/batch/blob/0.x/docs/domain/item-job/item-reader.md) diff --git a/src/batch-openspout/docs/flat-file-item-writer.md b/src/batch-openspout/docs/flat-file-item-writer.md new file mode 100644 index 00000000..92d057ce --- /dev/null +++ b/src/batch-openspout/docs/flat-file-item-writer.md @@ -0,0 +1,50 @@ +# Item writer with CSV/ODS/XLSX files + +The [FlatFileWriter](../src/Writer/FlatFileWriter.php) is a writer that will write to CSV/ODS/XLSX file and each item will +written its own line. + +```php +FIELD_DELIMITER = ';'; +$options->FIELD_ENCLOSURE = '|'; +new FlatFileWriter( + new StaticValueParameterAccessor('/path/to/file.csv'), + $options, +); + +// Write items to .ods file +// That file will contain a header line with : static | header | keys +// Change the sheet name data will be written +// Change the default style of each cell +$options = new ODSOptions(); +$options->DEFAULT_ROW_STYLE = (new Style())->setFontBold(); +new FlatFileWriter( + new StaticValueParameterAccessor('/path/to/file.ods'), + $options, + 'The sheet name', + ['static', 'header', 'keys'], +); +``` + +## On the same subject + +- [What is an item writer ?](https://github.com/yokai-php/batch/blob/0.x/docs/domain/item-job/item-writer.md) diff --git a/src/batch-openspout/phpunit.xml b/src/batch-openspout/phpunit.xml new file mode 100644 index 00000000..f2bbace6 --- /dev/null +++ b/src/batch-openspout/phpunit.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + ./tests + + + + + + ./src + + + diff --git a/src/batch-openspout/src/Exception/InvalidRowSizeException.php b/src/batch-openspout/src/Exception/InvalidRowSizeException.php new file mode 100644 index 00000000..ed952cdd --- /dev/null +++ b/src/batch-openspout/src/Exception/InvalidRowSizeException.php @@ -0,0 +1,39 @@ + + */ + private array $headers, + /** + * @var array + */ + private array $row, + ) { + parent::__construct('Invalid row size'); + } + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * @return array + */ + public function getRow(): array + { + return $this->row; + } +} diff --git a/src/batch-openspout/src/Reader/FlatFileReader.php b/src/batch-openspout/src/Reader/FlatFileReader.php new file mode 100644 index 00000000..8eb905db --- /dev/null +++ b/src/batch-openspout/src/Reader/FlatFileReader.php @@ -0,0 +1,102 @@ +sheetFilter = $sheetFilter ?? SheetFilter::all(); + $this->headerStrategy = $headerStrategy ?? HeaderStrategy::none(); + } + + public function read(): iterable + { + /** @var string $path */ + $path = $this->filePath->get($this->jobExecution); + $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $reader = match ($extension) { + 'csv' => new CSVReader($this->options), + 'xlsx' => new XLSXReader($this->options), + 'ods' => new ODSReader($this->options), + default => throw new UnsupportedTypeException('No readers supporting the given type: ' . $extension), + }; + + $reader->open($path); + + foreach ($this->rows($reader) as $rowIndex => $row) { + if ($rowIndex === 1) { + if (!$this->headerStrategy->setHeaders($row)) { + continue; + } + } + + try { + yield $this->headerStrategy->getItem($row); + } catch (InvalidRowSizeException $exception) { + $this->jobExecution->addWarning( + new Warning( + sprintf( + 'Expecting row %s to have exactly %d columns(s), but got %d.', + $rowIndex, + count($exception->getHeaders()), + count($exception->getRow()), + ), + [], + ['headers' => $exception->getHeaders(), 'row' => $exception->getRow()] + ) + ); + } + } + + $reader->close(); + } + + /** + * @return Generator> + */ + private function rows(ReaderInterface $reader): Generator + { + foreach ($this->sheetFilter->list($reader) as $sheet) { + /** @var int $rowIndex */ + /** @var Row $row */ + foreach ($sheet->getRowIterator() as $rowIndex => $row) { + yield $rowIndex => $row->toArray(); + } + } + } +} diff --git a/src/batch-openspout/src/Reader/HeaderStrategy.php b/src/batch-openspout/src/Reader/HeaderStrategy.php new file mode 100644 index 00000000..df164f6d --- /dev/null +++ b/src/batch-openspout/src/Reader/HeaderStrategy.php @@ -0,0 +1,99 @@ +|null + */ + private ?array $headers + ) { + } + + /** + * Read file has headers but should be skipped. + * + * @param list|null $headers + */ + public static function skip(array $headers = null): self + { + return new self(self::SKIP, $headers); + } + + /** + * Read file has headers and should be used to array_combine each row. + */ + public static function combine(): self + { + return new self(self::COMBINE, null); + } + + /** + * Read file has no headers. + * + * @param list|null $headers + */ + public static function none(array $headers = null): self + { + return new self(self::NONE, $headers); + } + + /** + * @param list $headers + * @internal + */ + public function setHeaders(array $headers): bool + { + if ($this->mode === self::NONE) { + return true; // row should be read, will be considered as an item + } + if ($this->mode === self::COMBINE) { + $this->headers = $headers; + } + + return false; // row should be skipped, will not be considered as an item + } + + /** + * Build the associative item, a combination of headers and values. + * + * @throws InvalidRowSizeException + * + * @param array $row + * + * @return array + * @internal + */ + public function getItem(array $row): array + { + if ($this->headers === null) { + return $row; // headers were not set, read row as is + } + + try { + /** @var array $combined */ + $combined = @array_combine($this->headers, $row); + } catch (\ValueError) { + throw new InvalidRowSizeException($this->headers, $row); + } + + return $combined; + } +} diff --git a/src/batch-openspout/src/Reader/SheetFilter.php b/src/batch-openspout/src/Reader/SheetFilter.php new file mode 100644 index 00000000..331cb206 --- /dev/null +++ b/src/batch-openspout/src/Reader/SheetFilter.php @@ -0,0 +1,72 @@ +accept = $accept; + } + + /** + * Will read every sheets in file. + */ + public static function all(): self + { + return new self(fn() => true); + } + + /** + * Will read sheets that are at specified indexes. + */ + public static function indexIs(int $index, int ...$indexes): self + { + $indexes[] = $index; + + return new self(fn(SheetInterface $sheet) => \in_array($sheet->getIndex(), $indexes, true)); + } + + /** + * Will read sheets that are named as specified. + */ + public static function nameIs(string $name, string ...$names): self + { + $names[] = $name; + + return new self(fn(SheetInterface $sheet) => \in_array($sheet->getName(), $names, true)); + } + + /** + * Iterate over valid sheets for the provided filter. + * + * @return Generator + * @internal + */ + public function list(ReaderInterface $reader): Generator + { + /** @var SheetInterface $sheet */ + foreach ($reader->getSheetIterator() as $sheet) { + if (($this->accept)($sheet)) { + yield $sheet; + } + } + } +} diff --git a/src/batch-openspout/src/Writer/FlatFileWriter.php b/src/batch-openspout/src/Writer/FlatFileWriter.php new file mode 100644 index 00000000..1e84f9e1 --- /dev/null +++ b/src/batch-openspout/src/Writer/FlatFileWriter.php @@ -0,0 +1,139 @@ +|null + */ + private ?array $headers = null, + ) { + } + + public function initialize(): void + { + /** @var string $path */ + $path = $this->filePath->get($this->jobExecution); + $dir = \dirname($path); + if (!@\is_dir($dir) && !@\mkdir($dir, 0777, true)) { + throw new RuntimeException(\sprintf('Cannot create dir "%s".', $dir)); + } + + $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $this->writer = match ($extension) { + 'csv' => new CSVWriter($this->options), + 'xlsx' => new XLSXWriter($this->options), + 'ods' => new ODSWriter($this->options), + default => throw new UnsupportedTypeException('No writers supporting the given type: ' . $extension), + }; + $this->writer->openToFile($path); + + if ($this->writer instanceof AbstractWriterMultiSheets) { + if ($this->defaultSheet !== null) { + $this->writer->getCurrentSheet()->setName($this->defaultSheet); + } else { + $this->defaultSheet = $this->writer->getCurrentSheet()->getName(); + } + } + } + + public function write(iterable $items): void + { + $writer = $this->writer; + if ($writer === null) { + throw BadMethodCallException::itemComponentNotInitialized($this); + } + + if (!$this->headersAdded) { + $this->headersAdded = true; + if ($this->headers !== null) { + $writer->addRow(Row::fromValues($this->headers)); + } + } + + foreach ($items as $row) { + if ($row instanceof WriteToSheetItem) { + $this->changeSheet($row->getSheet()); + $row = $row->getItem(); + } elseif ($this->defaultSheet !== null) { + $this->changeSheet($this->defaultSheet); + } + if (\is_array($row)) { + $row = Row::fromValues($row); + } + if (!$row instanceof Row) { + throw UnexpectedValueException::type('array|' . Row::class, $row); + } + + $writer->addRow($row); + } + } + + public function flush(): void + { + if ($this->writer === null) { + throw BadMethodCallException::itemComponentNotInitialized($this); + } + + $this->writer->close(); + $this->writer = null; + $this->headersAdded = false; + } + + private function changeSheet(string $name): void + { + if (!$this->writer instanceof AbstractWriterMultiSheets) { + return; + } + + foreach ($this->writer->getSheets() as $sheet) { + if ($sheet->getName() === $name) { + $this->writer->setCurrentSheet($sheet); + return; + } + } + + $sheet = $this->writer->addNewSheetAndMakeItCurrent(); + $sheet->setName($name); + } +} diff --git a/src/batch-openspout/src/Writer/WriteToSheetItem.php b/src/batch-openspout/src/Writer/WriteToSheetItem.php new file mode 100644 index 00000000..a9086dc2 --- /dev/null +++ b/src/batch-openspout/src/Writer/WriteToSheetItem.php @@ -0,0 +1,49 @@ + $item + */ + public static function array(string $sheet, array $item, Style $style = null): self + { + return new self($sheet, Row::fromValues($item, $style)); + } + + /** + * Static constructor from {@see Row} object. + */ + public static function row(string $sheet, Row $item): self + { + return new self($sheet, $item); + } + + public function getSheet(): string + { + return $this->sheet; + } + + public function getItem(): Row + { + return $this->item; + } +} diff --git a/src/batch-openspout/tests/Reader/FlatFileReaderTest.php b/src/batch-openspout/tests/Reader/FlatFileReaderTest.php new file mode 100644 index 00000000..32626b4f --- /dev/null +++ b/src/batch-openspout/tests/Reader/FlatFileReaderTest.php @@ -0,0 +1,250 @@ +setJobExecution($jobExecution); + + /** @var \Iterator $got */ + $got = $reader->read(); + self::assertInstanceOf(\Iterator::class, $got); + self::assertSame($expected, iterator_to_array($got)); + } + + public function sets(): Generator + { + $csv = __DIR__ . '/fixtures/sample.csv'; + $ods = __DIR__ . '/fixtures/sample.ods'; + $xlsx = __DIR__ . '/fixtures/sample.xlsx'; + + // first line is not header + $expected = [ + ['firstName', 'lastName'], + ['John', 'Doe'], + ['Jane', 'Doe'], + ['Jack', 'Doe'], + ]; + foreach ([$csv, $ods, $xlsx] as $file) { + yield [ + $file, + null, + null, + fn() => HeaderStrategy::none(), + $expected, + ]; + } + + // first line is header and should be skipped + $expected = [ + ['John', 'Doe'], + ['Jane', 'Doe'], + ['Jack', 'Doe'], + ]; + foreach ([$csv, $ods, $xlsx] as $file) { + yield [ + $file, + null, + null, + fn() => HeaderStrategy::skip(), + $expected, + ]; + } + + // first line is header and should be skipped, but headers is provided with static value + $expected = [ + ['prenom' => 'John', 'nom' => 'Doe'], + ['prenom' => 'Jane', 'nom' => 'Doe'], + ['prenom' => 'Jack', 'nom' => 'Doe'], + ]; + foreach ([$csv, $ods, $xlsx] as $file) { + yield [ + $file, + null, + null, + fn() => HeaderStrategy::skip(['prenom', 'nom']), + $expected, + ]; + } + + // first line is header and should be skipped + $expected = [ + ['firstName' => 'John', 'lastName' => 'Doe'], + ['firstName' => 'Jane', 'lastName' => 'Doe'], + ['firstName' => 'Jack', 'lastName' => 'Doe'], + ]; + foreach ([$csv, $ods, $xlsx] as $file) { + yield [ + $file, + null, + null, + fn() => HeaderStrategy::combine(), + $expected, + ]; + } + + // non-standard CSV (delimiter and enclosure changed) encoded in ISO-8859 + $options = new CSVOptions(); + $options->FIELD_DELIMITER = ';'; + $options->FIELD_ENCLOSURE = '|'; + $options->ENCODING = 'ISO-8859-1'; + yield [ + __DIR__ . '/fixtures/iso-8859-1.csv', + $options, + null, + null, + [ + ['Gérard', 'À peu près'], + ['Benoît', 'Bien-être'], + ['Gaëlle', 'Ça va'], + ], + ]; + + // change files to multi tab + $ods = __DIR__ . '/fixtures/multi-tabs.ods'; + $xlsx = __DIR__ . '/fixtures/multi-tabs.xlsx'; + + // multi-tab files, 1st tab + $expected = [ + ['firstName' => 'John', 'lastName' => 'Doe'], + ['firstName' => 'Jane', 'lastName' => 'Doe'], + ['firstName' => 'Jack', 'lastName' => 'Doe'], + ]; + foreach ([$ods, $xlsx] as $file) { + yield [ + $file, + null, + fn() => SheetFilter::indexIs(0), + fn() => HeaderStrategy::combine(), + $expected, + ]; + } + + // multi-tab files, tab "Français" + $expected = [ + ['prénom' => 'Jean', 'nom' => 'Bon'], + ['prénom' => 'Jeanne', 'nom' => 'Aimar'], + ['prénom' => 'Jacques', 'nom' => 'Ouzi'], + ]; + foreach ([$ods, $xlsx] as $file) { + yield [ + $file, + null, + fn() => SheetFilter::nameIs('Français'), + fn() => HeaderStrategy::combine(), + $expected, + ]; + } + + // multi-tab files, all tabs + $expected = [ + ['firstName' => 'John', 'lastName' => 'Doe'], + ['firstName' => 'Jane', 'lastName' => 'Doe'], + ['firstName' => 'Jack', 'lastName' => 'Doe'], + ['prénom' => 'Jean', 'nom' => 'Bon'], + ['prénom' => 'Jeanne', 'nom' => 'Aimar'], + ['prénom' => 'Jacques', 'nom' => 'Ouzi'], + ]; + foreach ([$ods, $xlsx] as $file) { + yield [ + $file, + null, + fn() => SheetFilter::all(), + fn() => HeaderStrategy::combine(), + $expected, + ]; + } + } + + public function testReadWrongLineSize(): void + { + $file = __DIR__ . '/fixtures/wrong-line-size.csv'; + $jobExecution = JobExecution::createRoot('123456789', 'parent'); + $reader = new FlatFileReader( + new StaticValueParameterAccessor($file), + null, + null, + HeaderStrategy::combine(), + ); + $reader->setJobExecution($jobExecution); + + /** @var \Iterator $result */ + $result = $reader->read(); + self::assertInstanceOf(\Iterator::class, $result); + self::assertSame( + [ + ['firstName' => 'John', 'lastName' => 'Doe'], + ['firstName' => 'Jack', 'lastName' => 'Doe'], + ], + iterator_to_array($result) + ); + + self::assertSame( + 'Expecting row 3 to have exactly 2 columns(s), but got 3.', + $jobExecution->getWarnings()[0]->getMessage() + ); + self::assertSame( + ['headers' => ['firstName', 'lastName'], 'row' => ['Jane', 'Doe', 'too much data']], + $jobExecution->getWarnings()[0]->getContext() + ); + } + + /** + * @dataProvider wrongOptions + */ + public function testWrongOptions(string $file, object $options): void + { + $this->expectException(\TypeError::class); + + $jobExecution = JobExecution::createRoot('123456789', 'parent'); + $reader = new FlatFileReader(new StaticValueParameterAccessor($file), $options); + $reader->setJobExecution($jobExecution); + + iterator_to_array($reader->read()); + } + + public function wrongOptions(): \Generator + { + // with CSV file, CSVOptions is expected + yield [__DIR__ . '/fixtures/sample.csv', new XLSXOptions()]; + yield [__DIR__ . '/fixtures/sample.csv', new ODSOptions()]; + + // with ODS file, ODSOptions is expected + yield [__DIR__ . '/fixtures/sample.ods', new CSVOptions()]; + yield [__DIR__ . '/fixtures/sample.ods', new XLSXOptions()]; + + // with XLSX file, XLSXOptions is expected + yield [__DIR__ . '/fixtures/sample.xlsx', new CSVOptions()]; + yield [__DIR__ . '/fixtures/sample.xlsx', new ODSOptions()]; + } +} diff --git a/src/batch-openspout/tests/Reader/fixtures/iso-8859-1.csv b/src/batch-openspout/tests/Reader/fixtures/iso-8859-1.csv new file mode 100644 index 00000000..df0df07c --- /dev/null +++ b/src/batch-openspout/tests/Reader/fixtures/iso-8859-1.csv @@ -0,0 +1,3 @@ +|Gérard|;|À peu près| +|Benoît|;|Bien-être| +|Gaëlle|;|Ça va| diff --git a/src/batch-openspout/tests/Reader/fixtures/multi-tabs.ods b/src/batch-openspout/tests/Reader/fixtures/multi-tabs.ods new file mode 100644 index 00000000..1ba774d4 Binary files /dev/null and b/src/batch-openspout/tests/Reader/fixtures/multi-tabs.ods differ diff --git a/src/batch-openspout/tests/Reader/fixtures/multi-tabs.xlsx b/src/batch-openspout/tests/Reader/fixtures/multi-tabs.xlsx new file mode 100644 index 00000000..6e2d3e8b Binary files /dev/null and b/src/batch-openspout/tests/Reader/fixtures/multi-tabs.xlsx differ diff --git a/src/batch-openspout/tests/Reader/fixtures/sample.csv b/src/batch-openspout/tests/Reader/fixtures/sample.csv new file mode 100644 index 00000000..08eff973 --- /dev/null +++ b/src/batch-openspout/tests/Reader/fixtures/sample.csv @@ -0,0 +1,4 @@ +firstName,lastName +John,Doe +Jane,Doe +Jack,Doe diff --git a/src/batch-openspout/tests/Reader/fixtures/sample.ods b/src/batch-openspout/tests/Reader/fixtures/sample.ods new file mode 100644 index 00000000..80ae1907 Binary files /dev/null and b/src/batch-openspout/tests/Reader/fixtures/sample.ods differ diff --git a/src/batch-openspout/tests/Reader/fixtures/sample.xlsx b/src/batch-openspout/tests/Reader/fixtures/sample.xlsx new file mode 100644 index 00000000..122f9ece Binary files /dev/null and b/src/batch-openspout/tests/Reader/fixtures/sample.xlsx differ diff --git a/src/batch-openspout/tests/Reader/fixtures/wrong-line-size.csv b/src/batch-openspout/tests/Reader/fixtures/wrong-line-size.csv new file mode 100644 index 00000000..c5985ddc --- /dev/null +++ b/src/batch-openspout/tests/Reader/fixtures/wrong-line-size.csv @@ -0,0 +1,4 @@ +firstName,lastName +John,Doe +Jane,Doe,too much data +Jack,Doe diff --git a/src/batch-openspout/tests/Writer/FlatFileWriterTest.php b/src/batch-openspout/tests/Writer/FlatFileWriterTest.php new file mode 100644 index 00000000..0ff3aa15 --- /dev/null +++ b/src/batch-openspout/tests/Writer/FlatFileWriterTest.php @@ -0,0 +1,375 @@ +setJobExecution(JobExecution::createRoot('123456789', 'export')); + + $writer->initialize(); + $writer->write($itemsToWrite); + $writer->flush(); + + self::assertFileContents($file, $expectedContent); + } + + public function sets(): \Generator + { + $headers = ['firstName', 'lastName']; + $items = [ + ['John', 'Doe'], + ['Jane', 'Doe'], + ['Jack', 'Doe'], + ]; + $contentWithoutHeader = <<types() as [$type]) { + yield [ + "no-header.$type", + null, + null, + null, + $items, + $contentWithoutHeader, + ]; + yield [ + "with-header.$type", + null, + null, + $headers, + $items, + $contentWithHeader, + ]; + } + + $options = new CSVOptions(); + $options->FIELD_DELIMITER = ';'; + $options->FIELD_ENCLOSURE = '|'; + $content = <<setFontBold() + ->setFontSize(15) + ->setFontColor(Color::BLUE) + ->setShouldWrapText() + ->setCellAlignment(CellAlignment::RIGHT) + ->setBackgroundColor(Color::YELLOW); + + $options = new XLSXOptions(); + $options->DEFAULT_ROW_STYLE = $style; + yield [ + "total-style.xlsx", + $options, + 'Sheet1 with styles', + null, + $items, + $contentWithoutHeader, + ]; + $options = new ODSOptions(); + $options->DEFAULT_ROW_STYLE = $style; + yield [ + "total-style.ods", + $options, + 'Sheet1 with styles', + null, + $items, + $contentWithoutHeader, + ]; + + $blue = (new Style()) + ->setFontBold() + ->setFontColor(Color::BLUE); + $red = (new Style()) + ->setFontBold() + ->setFontColor(Color::RED); + $green = (new Style()) + ->setFontBold() + ->setFontColor(Color::GREEN); + $styledItems = [ + Row::fromValues(['John', 'Doe'], $blue), + Row::fromValues(['Jane', 'Doe'], $red), + Row::fromValues(['Jack', 'Doe'], $green), + ]; + yield [ + "partial-style.xlsx", + null, + null, + null, + $styledItems, + $contentWithoutHeader, + ]; + yield [ + "partial-style.ods", + null, + null, + null, + $styledItems, + $contentWithoutHeader, + ]; + } + + /** + * @dataProvider types + */ + public function testWriteInvalidItem(string $type): void + { + $this->expectException(UnexpectedValueException::class); + + $file = self::WRITE_DIR . '/invalid-item.' . $type; + $writer = new FlatFileWriter(new StaticValueParameterAccessor($file)); + $writer->setJobExecution(JobExecution::createRoot('123456789', 'export')); + + $writer->initialize(); + $writer->write([true]); // writer accept collection of array or \OpenSpout\Common\Entity\Row + } + + /** + * @dataProvider types + */ + public function testCannotCreateFile(string $type): void + { + $this->expectException(RuntimeException::class); + + $file = '/path/to/a/dir/that/do/not/exists/and/not/creatable/file.' . $type; + $writer = new FlatFileWriter(new StaticValueParameterAccessor($file)); + $writer->setJobExecution(JobExecution::createRoot('123456789', 'export')); + + $writer->initialize(); + } + + /** + * @dataProvider types + */ + public function testShouldInitializeBeforeWrite(string $type): void + { + $this->expectException(BadMethodCallException::class); + + $file = self::WRITE_DIR . '/should-initialize-before-write.' . $type; + $writer = new FlatFileWriter(new StaticValueParameterAccessor($file)); + $writer->write([true]); + } + + /** + * @dataProvider types + */ + public function testShouldInitializeBeforeFlush(string $type): void + { + $this->expectException(BadMethodCallException::class); + + $file = self::WRITE_DIR . '/should-initialize-before-flush.' . $type; + $writer = new FlatFileWriter(new StaticValueParameterAccessor($file)); + $writer->flush(); + } + + public function types(): \Generator + { + yield ['csv']; + yield ['ods']; + yield ['xlsx']; + } + + /** + * @dataProvider multipleSheets + */ + public function testWriteMultipleSheets(string $type, ?string $defaultSheet): void + { + $file = self::WRITE_DIR . '/multiple-sheets.' . $type; + self::assertFileDoesNotExist($file); + + $writer = new FlatFileWriter(new StaticValueParameterAccessor($file), null, $defaultSheet); + $writer->setJobExecution(JobExecution::createRoot('123456789', 'export')); + + $writer->initialize(); + $writer->write([ + WriteToSheetItem::array('English', ['John', 'Doe']), + WriteToSheetItem::array('Français', ['Jean', 'Aimar']), + WriteToSheetItem::row('English', Row::fromValues(['Jack', 'Doe'])), + WriteToSheetItem::row('Français', Row::fromValues(['Jacques', 'Ouzi'])), + ]); + $writer->flush(); + + if ($type === 'csv') { + self::assertFileContents($file, <<expectException(\TypeError::class); + + $file = self::WRITE_DIR . '/should-initialize-before-flush.' . $type; + $jobExecution = JobExecution::createRoot('123456789', 'parent'); + $reader = new FlatFileWriter(new StaticValueParameterAccessor($file), $options); + $reader->setJobExecution($jobExecution); + $reader->initialize(); + } + + public function wrongOptions(): \Generator + { + // with CSV file, CSVOptions is expected + yield ['csv', new XLSXOptions()]; + yield ['csv', new ODSOptions()]; + + // with ODS file, ODSOptions is expected + yield ['ods', new CSVOptions()]; + yield ['ods', new XLSXOptions()]; + + // with XLSX file, XLSXOptions is expected + yield ['xlsx', new CSVOptions()]; + yield ['xlsx', new ODSOptions()]; + } + + private static function assertFileContents(string $filePath, string $inlineData): void + { + $type = \strtolower(\pathinfo($filePath, PATHINFO_EXTENSION)); + $strings = array_merge(...array_map('str_getcsv', explode(PHP_EOL, $inlineData))); + + switch ($type) { + case 'csv': + $fileContents = file_get_contents($filePath); + foreach ($strings as $string) { + self::assertStringContainsString($string, $fileContents); + } + break; + + case 'xlsx': + $pathToSheetFile = $filePath . '#xl/worksheets/sheet1.xml'; + $xmlContents = file_get_contents('zip://' . $pathToSheetFile); + foreach ($strings as $string) { + self::assertStringContainsString("$string", $xmlContents); + } + break; + + case 'ods': + $sheetContent = file_get_contents('zip://' . $filePath . '#content.xml'); + if (!preg_match('#]+>[\s\S]*?<\/table:table>#', $sheetContent, $matches)) { + self::fail('No sheet found in file "' . $filePath . '".'); + } + $sheetXmlAsString = $matches[0]; + foreach ($strings as $string) { + self::assertStringContainsString("$string", $sheetXmlAsString); + } + break; + } + } + + private static function assertSheetContents(string $filePath, string $sheet, string $inlineData): void + { + $type = \strtolower(\pathinfo($filePath, PATHINFO_EXTENSION)); + $strings = array_merge(...array_map('str_getcsv', explode(PHP_EOL, $inlineData))); + + switch ($type) { + case 'csv': + $fileContents = file_get_contents($filePath); + foreach ($strings as $string) { + self::assertStringContainsString($string, $fileContents); + } + break; + + case 'xlsx': + $workbookContent = file_get_contents('zip://' . $filePath . '#xl/workbook.xml'); + if (!preg_match('#$string", $sheetContent); + } + break; + + case 'ods': + $sheetContent = file_get_contents('zip://' . $filePath . '#content.xml'); + $regex = '#[\s\S]*?<\/table:table>#'; + if (!preg_match($regex, $sheetContent, $matches)) { + self::fail('Sheet ' . $sheet . ' was not found in file "' . $filePath . '".'); + } + $sheetXmlAsString = $matches[0]; + foreach ($strings as $string) { + self::assertStringContainsString("$string", $sheetXmlAsString); + } + break; + } + } +} diff --git a/src/batch-openspout/tests/bootstrap.php b/src/batch-openspout/tests/bootstrap.php new file mode 100644 index 00000000..6dc2ab0c --- /dev/null +++ b/src/batch-openspout/tests/bootstrap.php @@ -0,0 +1,22 @@ +remove($artifactDir); +} + +(new Filesystem())->mkdir($artifactDir); + +define('ARTIFACT_DIR', $artifactDir); diff --git a/src/batch/README.md b/src/batch/README.md index bb358a12..1f13ddff 100644 --- a/src/batch/README.md +++ b/src/batch/README.md @@ -39,10 +39,11 @@ Looking for something in particular ? Looking for something more specific ? -- [Read/Write from/to CSV/ODS/XLSX](https://github.com/yokai-php/batch-box-spout) - [Store job executions in relational database](https://github.com/yokai-php/batch-doctrine-dbal) - [Read from Doctrine ORM entities](https://github.com/yokai-php/batch-doctrine-orm) - [Write to Doctrine ORM/ODM... objects](https://github.com/yokai-php/batch-doctrine-persistence) +- [Copy/Move files in a job / Trigger job when file found](https://github.com/yokai-php/batch-league-flysystem) +- [Read/Write from/to CSV/ODS/XLSX](https://github.com/yokai-php/batch-box-spout) - [Trigger async jobs using CLI command](https://github.com/yokai-php/batch-symfony-console): - [Integration with Symfony framework](https://github.com/yokai-php/batch-symfony-framework) - [Trigger async jobs using using queue](https://github.com/yokai-php/batch-symfony-messenger): diff --git a/src/batch/docs/domain/item-job/item-reader.md b/src/batch/docs/domain/item-job/item-reader.md index b0a4210c..fe20c720 100644 --- a/src/batch/docs/domain/item-job/item-reader.md +++ b/src/batch/docs/domain/item-job/item-reader.md @@ -23,7 +23,9 @@ It can be any class implementing [ItemReaderInterface](../../../src/Job/Item/Ite read from an iterable you provide during construction. **Item readers from bridges:** -- [FlatFileReader (`box/spout`)](https://github.com/yokai-php/batch-box-spout/blob/0.x/src/Reader/FlatFileReader.php): +- `DEPRECATED` [FlatFileReader (`box/spout`)](https://github.com/yokai-php/batch-box-spout/blob/0.x/src/Reader/FlatFileReader.php): + read from any CSV/ODS/XLSX file. +- [FlatFileReader (`openspout/openspout`)](https://github.com/yokai-php/batch-openspout/blob/0.x/src/Reader/FlatFileReader.php): read from any CSV/ODS/XLSX file. - [DoctrineDBALQueryOffsetReader (`doctrine/dbal`)](https://github.com/yokai-php/batch-doctrine-dbal/blob/0.x/src/DoctrineDBALQueryOffsetReader.php): read execute an SQL query and iterate over results, using a limit + offset pagination strategy. diff --git a/src/batch/docs/domain/item-job/item-writer.md b/src/batch/docs/domain/item-job/item-writer.md index 61d0dc9c..58ff04ce 100644 --- a/src/batch/docs/domain/item-job/item-writer.md +++ b/src/batch/docs/domain/item-job/item-writer.md @@ -35,7 +35,9 @@ It can be any class implementing [ItemWriterInterface](../../../src/Job/Item/Ite write items by inserting/updating in a table via a Doctrine `Connection`. - [ObjectWriter (`doctrine/persistence`)](https://github.com/yokai-php/batch-doctrine-persistence/blob/0.x/src/ObjectWriter.php): write items to any Doctrine `ObjectManager`. -- [FlatFileWriter (`box/spout`)](https://github.com/yokai-php/batch-box-spout/blob/0.x/src/Writer/FlatFileWriter.php): +- `DEPRECATED` [FlatFileWriter (`box/spout`)](https://github.com/yokai-php/batch-box-spout/blob/0.x/src/Writer/FlatFileWriter.php): + write items to any CSV/ODS/XLSX file. +- [FlatFileWriter (`openspout/openspout`)](https://github.com/yokai-php/batch-openspout/blob/0.x/src/Writer/FlatFileWriter.php): write items to any CSV/ODS/XLSX file. **Item writers for testing purpose:** diff --git a/tests/integration/ImportDevelopersXlsxToORMTest.php b/tests/integration/ImportDevelopersXlsxToORMTest.php index f3879175..4167ef86 100644 --- a/tests/integration/ImportDevelopersXlsxToORMTest.php +++ b/tests/integration/ImportDevelopersXlsxToORMTest.php @@ -12,10 +12,9 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Yokai\Batch\Bridge\Box\Spout\Reader\FlatFileReader; -use Yokai\Batch\Bridge\Box\Spout\Reader\HeaderStrategy; -use Yokai\Batch\Bridge\Box\Spout\Reader\Options\CSVOptions; use Yokai\Batch\Bridge\Doctrine\Persistence\ObjectWriter; +use Yokai\Batch\Bridge\OpenSpout\Reader\FlatFileReader; +use Yokai\Batch\Bridge\OpenSpout\Reader\HeaderStrategy; use Yokai\Batch\Job\Item\ItemJob; use Yokai\Batch\Job\JobInterface; use Yokai\Batch\Job\JobWithChildJobs; @@ -85,9 +84,8 @@ protected function createJob(JobExecutionStorageInterface $executionStorage): Jo $csvReader = function (string $file): FlatFileReader { return new FlatFileReader( - new StaticValueParameterAccessor($file), - new CSVOptions(), - HeaderStrategy::combine() + filePath: new StaticValueParameterAccessor($file), + headerStrategy: HeaderStrategy::combine(), ); }; diff --git a/tests/integration/Job/SplitDeveloperXlsxJob.php b/tests/integration/Job/SplitDeveloperXlsxJob.php index 58be587a..4c8845c2 100644 --- a/tests/integration/Job/SplitDeveloperXlsxJob.php +++ b/tests/integration/Job/SplitDeveloperXlsxJob.php @@ -4,12 +4,10 @@ namespace Yokai\Batch\Sources\Tests\Integration\Job; -use Box\Spout\Common\Entity\Row; -use Box\Spout\Common\Type; -use Box\Spout\Reader\Common\Creator\ReaderFactory; -use Box\Spout\Reader\SheetInterface; -use Yokai\Batch\Bridge\Box\Spout\Writer\FlatFileWriter; -use Yokai\Batch\Bridge\Box\Spout\Writer\Options\CSVOptions; +use OpenSpout\Common\Entity\Row; +use OpenSpout\Reader\SheetInterface; +use OpenSpout\Reader\XLSX\Reader; +use Yokai\Batch\Bridge\OpenSpout\Writer\FlatFileWriter; use Yokai\Batch\Job\JobInterface; use Yokai\Batch\Job\Parameters\StaticValueParameterAccessor; use Yokai\Batch\JobExecution; @@ -30,7 +28,7 @@ public function execute(JobExecution $jobExecution): void $repositories = []; $developers = []; - $reader = ReaderFactory::createFromType(Type::XLSX); + $reader = new Reader(); $reader->open($this->inputFile); $sheets = iterator_to_array($reader->getSheetIterator(), false); [$badgeSheet, $repositorySheet] = $sheets; @@ -81,7 +79,7 @@ public function execute(JobExecution $jobExecution): void private function writeToCsv(string $filename, array $data, array $headers): void { - $writer = new FlatFileWriter(new StaticValueParameterAccessor($filename), new CSVOptions(), $headers); + $writer = new FlatFileWriter(new StaticValueParameterAccessor($filename), null, null, $headers); $writer->setJobExecution(JobExecution::createRoot('fake', 'fake')); $writer->initialize(); $writer->write($data); diff --git a/tests/symfony/src/Job/Country/CountryJob.php b/tests/symfony/src/Job/Country/CountryJob.php index d476d116..71870b65 100644 --- a/tests/symfony/src/Job/Country/CountryJob.php +++ b/tests/symfony/src/Job/Country/CountryJob.php @@ -5,8 +5,7 @@ namespace Yokai\Batch\Sources\Tests\Symfony\App\Job\Country; use Symfony\Component\HttpKernel\KernelInterface; -use Yokai\Batch\Bridge\Box\Spout\Writer\FlatFileWriter; -use Yokai\Batch\Bridge\Box\Spout\Writer\Options\CSVOptions; +use Yokai\Batch\Bridge\OpenSpout\Writer\FlatFileWriter; use Yokai\Batch\Bridge\Symfony\Framework\JobWithStaticNameInterface; use Yokai\Batch\Job\AbstractDecoratedJob; use Yokai\Batch\Job\Item\ElementConfiguratorTrait; @@ -79,7 +78,7 @@ public function __construct(JobExecutionStorageInterface $executionStorage, Kern $headers = \array_merge(['iso2'], $fragments); $this->writer = new ChainWriter([ new SummaryWriter(new StaticValueParameterAccessor('countries')), - new FlatFileWriter($writePath('csv'), new CSVOptions(), $headers), + new FlatFileWriter($writePath('csv'), null, null, $headers), new JsonLinesWriter($writePath('jsonl')), ]); diff --git a/tests/symfony/src/Job/StarWars/AbstractImportStartWarsEntityJob.php b/tests/symfony/src/Job/StarWars/AbstractImportStartWarsEntityJob.php index 60803d10..105cb603 100644 --- a/tests/symfony/src/Job/StarWars/AbstractImportStartWarsEntityJob.php +++ b/tests/symfony/src/Job/StarWars/AbstractImportStartWarsEntityJob.php @@ -7,10 +7,9 @@ use Closure; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Validator\Validator\ValidatorInterface; -use Yokai\Batch\Bridge\Box\Spout\Reader\FlatFileReader; -use Yokai\Batch\Bridge\Box\Spout\Reader\HeaderStrategy; -use Yokai\Batch\Bridge\Box\Spout\Reader\Options\CSVOptions; use Yokai\Batch\Bridge\Doctrine\Persistence\ObjectWriter; +use Yokai\Batch\Bridge\OpenSpout\Reader\FlatFileReader; +use Yokai\Batch\Bridge\OpenSpout\Reader\HeaderStrategy; use Yokai\Batch\Bridge\Symfony\Framework\JobWithStaticNameInterface; use Yokai\Batch\Bridge\Symfony\Validator\SkipInvalidItemProcessor; use Yokai\Batch\Job\AbstractDecoratedJob; @@ -50,7 +49,8 @@ public function __construct( 50, // could be much higher, but set this way for demo purpose new FlatFileReader( new StaticValueParameterAccessor($file), - new CSVOptions(), + null, + null, HeaderStrategy::combine() ), new ChainProcessor([