From 7cc6433860bb2df1056f45c7183e856aa8066404 Mon Sep 17 00:00:00 2001 From: madflow Date: Tue, 16 Oct 2018 16:29:04 +0200 Subject: [PATCH] create a reader by file type #569 --- docs/_pages/getting-started.md | 11 +++ .../Common/Creator/ReaderEntityFactory.php | 13 ++++ .../Reader/Common/Creator/ReaderFactory.php | 39 ++++++++++ .../Creator/ReaderEntityFactoryTest.php | 73 ++++++++++++++++++ .../Common/Creator/ReaderFactoryTest.php | 64 +++++++++++++++ .../csv/csv_test_create_from_file.CSV | 1 + .../csv/csv_test_create_from_file.csv | 1 + .../ods/csv_test_create_from_file.ods | Bin 0 -> 6864 bytes .../other/test_unsupported_file_type.other | 0 .../xlsx/csv_test_create_from_file.xlsx | Bin 0 -> 4318 bytes 10 files changed, 202 insertions(+) create mode 100644 tests/Spout/Reader/Common/Creator/ReaderEntityFactoryTest.php create mode 100644 tests/resources/csv/csv_test_create_from_file.CSV create mode 100644 tests/resources/csv/csv_test_create_from_file.csv create mode 100644 tests/resources/ods/csv_test_create_from_file.ods create mode 100644 tests/resources/other/test_unsupported_file_type.other create mode 100644 tests/resources/xlsx/csv_test_create_from_file.xlsx diff --git a/docs/_pages/getting-started.md b/docs/_pages/getting-started.md index ed224d0..1ea17e7 100755 --- a/docs/_pages/getting-started.md +++ b/docs/_pages/getting-started.md @@ -68,6 +68,17 @@ $reader->close(); If there are multiple sheets in the file, the reader will read all of them sequentially. +--- + +In addition to passing a reader type to ```ReaderEntityFactory::createReader```, it is also possible to provide a path to a file and create the reader. + +```php + +use Box\Spout\Reader\Common\Creator\ReaderEntityFactory; + +$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.xlsx'); +``` + ### Writer As with the reader, there is one common interface to write data to a file: diff --git a/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php b/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php index 8638381..e9e8063 100644 --- a/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php +++ b/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php @@ -21,4 +21,17 @@ class ReaderEntityFactory { return (new ReaderFactory())->create($readerType); } + + /** + * Creates a reader by file extension + * + * @param string The path to the spreadsheet file. Supported extensions are .csv,.ods and .xlsx + * @throws \Box\Spout\Common\Exception\IOException + * @throws \Box\Spout\Common\Exception\UnsupportedTypeException + * @return ReaderInterface + */ + public static function createReaderFromFile(string $path) + { + return (new ReaderFactory())->createFromFile($path); + } } diff --git a/src/Spout/Reader/Common/Creator/ReaderFactory.php b/src/Spout/Reader/Common/Creator/ReaderFactory.php index ffc2d6c..0442974 100644 --- a/src/Spout/Reader/Common/Creator/ReaderFactory.php +++ b/src/Spout/Reader/Common/Creator/ReaderFactory.php @@ -3,6 +3,7 @@ namespace Box\Spout\Reader\Common\Creator; use Box\Spout\Common\Creator\HelperFactory; +use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\UnsupportedTypeException; use Box\Spout\Common\Type; use Box\Spout\Reader\CSV\Creator\InternalEntityFactory as CSVInternalEntityFactory; @@ -28,6 +29,16 @@ use Box\Spout\Reader\XLSX\Reader as XLSXReader; */ class ReaderFactory { + /** + * Map file extensions to reader types + * @var array + */ + private static $extensionReaderMap = [ + 'csv' => Type::CSV, + 'ods' => Type::ODS, + 'xlsx' => Type::XLSX, + ]; + /** * This creates an instance of the appropriate reader, given the type of the file to be read * @@ -46,6 +57,34 @@ class ReaderFactory } } + /** + * Creates a reader by file extension + * + * @param string The path to the spreadsheet file. Supported extensions are .csv,.ods and .xlsx + * @throws \Box\Spout\Common\Exception\IOException + * @throws \Box\Spout\Common\Exception\UnsupportedTypeException + * @return ReaderInterface + */ + public static function createFromFile(string $path) + { + if (!\is_file($path)) { + throw new IOException( + sprintf('Could not open "%s" for reading! File does not exist.', $path) + ); + } + + $ext = \pathinfo($path, PATHINFO_EXTENSION); + $ext = \strtolower($ext); + $readerType = self::$extensionReaderMap[$ext] ?? null; + if ($readerType === null) { + throw new UnsupportedTypeException( + sprintf('No readers supporting the file extension "%s".', $ext) + ); + } + + return self::create($readerType); + } + /** * @return CSVReader */ diff --git a/tests/Spout/Reader/Common/Creator/ReaderEntityFactoryTest.php b/tests/Spout/Reader/Common/Creator/ReaderEntityFactoryTest.php new file mode 100644 index 0000000..9b978a3 --- /dev/null +++ b/tests/Spout/Reader/Common/Creator/ReaderEntityFactoryTest.php @@ -0,0 +1,73 @@ +getResourcePath('csv_test_create_from_file.csv'); + $reader = ReaderEntityFactory::createReaderFromFile($validCsv); + $this->assertInstanceOf('Box\Spout\Reader\CSV\Reader', $reader); + } + + /** + * @return void + */ + public function testCreateFromFileCSVAllCaps() + { + $validCsv = $this->getResourcePath('csv_test_create_from_file.CSV'); + $reader = ReaderEntityFactory::createReaderFromFile($validCsv); + $this->assertInstanceOf('Box\Spout\Reader\CSV\Reader', $reader); + } + + /** + * @return void + */ + public function testCreateFromFileODS() + { + $validOds = $this->getResourcePath('csv_test_create_from_file.ods'); + $reader = ReaderEntityFactory::createReaderFromFile($validOds); + $this->assertInstanceOf('Box\Spout\Reader\ODS\Reader', $reader); + } + + /** + * @return void + */ + public function testCreateFromFileXLSX() + { + $validXlsx = $this->getResourcePath('csv_test_create_from_file.xlsx'); + $reader = ReaderEntityFactory::createReaderFromFile($validXlsx); + $this->assertInstanceOf('Box\Spout\Reader\XLSX\Reader', $reader); + } + + /** + * @return void + */ + public function testCreateFromFileUnsupported() + { + $this->expectException(UnsupportedTypeException::class); + $invalid = $this->getResourcePath('test_unsupported_file_type.other'); + $reader = ReaderEntityFactory::createReaderFromFile($invalid); + } + + /** + * @return void + */ + public function testCreateFromFileMissing() + { + $this->expectException(IOException::class); + $invalid = 'thereisnosuchfile.ext'; + $reader = ReaderEntityFactory::createReaderFromFile($invalid); + } +} diff --git a/tests/Spout/Reader/Common/Creator/ReaderFactoryTest.php b/tests/Spout/Reader/Common/Creator/ReaderFactoryTest.php index 994bc3c..1efb292 100644 --- a/tests/Spout/Reader/Common/Creator/ReaderFactoryTest.php +++ b/tests/Spout/Reader/Common/Creator/ReaderFactoryTest.php @@ -2,7 +2,9 @@ namespace Box\Spout\Reader\Common\Creator; +use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\UnsupportedTypeException; +use Box\Spout\TestUsingResource; use PHPUnit\Framework\TestCase; /** @@ -10,6 +12,48 @@ use PHPUnit\Framework\TestCase; */ class ReaderFactoryTest extends TestCase { + use TestUsingResource; + + /** + * @return void + */ + public function testCreateFromFileCSV() + { + $validCsv = $this->getResourcePath('csv_test_create_from_file.csv'); + $reader = ReaderFactory::createFromFile($validCsv); + $this->assertInstanceOf('Box\Spout\Reader\CSV\Reader', $reader); + } + + /** + * @return void + */ + public function testCreateFromFileCSVAllCaps() + { + $validCsv = $this->getResourcePath('csv_test_create_from_file.CSV'); + $reader = ReaderFactory::createFromFile($validCsv); + $this->assertInstanceOf('Box\Spout\Reader\CSV\Reader', $reader); + } + + /** + * @return void + */ + public function testCreateFromFileODS() + { + $validOds = $this->getResourcePath('csv_test_create_from_file.ods'); + $reader = ReaderFactory::createFromFile($validOds); + $this->assertInstanceOf('Box\Spout\Reader\ODS\Reader', $reader); + } + + /** + * @return void + */ + public function testCreateFromFileXLSX() + { + $validXlsx = $this->getResourcePath('csv_test_create_from_file.xlsx'); + $reader = ReaderFactory::createFromFile($validXlsx); + $this->assertInstanceOf('Box\Spout\Reader\XLSX\Reader', $reader); + } + /** * @return void */ @@ -19,4 +63,24 @@ class ReaderFactoryTest extends TestCase ReaderFactory::create('unsupportedType'); } + + /** + * @return void + */ + public function testCreateFromFileUnsupported() + { + $this->expectException(UnsupportedTypeException::class); + $invalid = $this->getResourcePath('test_unsupported_file_type.other'); + $reader = ReaderFactory::createFromFile($invalid); + } + + /** + * @return void + */ + public function testCreateFromFileMissing() + { + $this->expectException(IOException::class); + $invalid = 'thereisnosuchfile.ext'; + $reader = ReaderFactory::createFromFile($invalid); + } } diff --git a/tests/resources/csv/csv_test_create_from_file.CSV b/tests/resources/csv/csv_test_create_from_file.CSV new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/tests/resources/csv/csv_test_create_from_file.CSV @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/tests/resources/csv/csv_test_create_from_file.csv b/tests/resources/csv/csv_test_create_from_file.csv new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/tests/resources/csv/csv_test_create_from_file.csv @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/tests/resources/ods/csv_test_create_from_file.ods b/tests/resources/ods/csv_test_create_from_file.ods new file mode 100644 index 0000000000000000000000000000000000000000..c07641a9f1b46bb21120ecfc52cd6be21a43ea24 GIT binary patch literal 6864 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 literal 0 HcmV?d00001