MDL-65762 lib: Upgrade the spout library to 3.0.1
authorAdrian Greeve <abgreeve@gmail.com>
Mon, 17 Jun 2019 00:42:31 +0000 (08:42 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Mon, 17 Jun 2019 00:42:31 +0000 (08:42 +0800)
159 files changed:
lib/spout/README.md
lib/spout/src/Spout/Autoloader/Psr4Autoloader.php
lib/spout/src/Spout/Autoloader/autoload.php
lib/spout/src/Spout/Common/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Entity/Cell.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Entity/Row.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Entity/Style/Border.php [moved from lib/spout/src/Spout/Writer/Style/Border.php with 79% similarity]
lib/spout/src/Spout/Common/Entity/Style/BorderPart.php [moved from lib/spout/src/Spout/Writer/Style/BorderPart.php with 98% similarity]
lib/spout/src/Spout/Common/Entity/Style/Color.php [moved from lib/spout/src/Spout/Writer/Style/Color.php with 92% similarity]
lib/spout/src/Spout/Common/Entity/Style/Style.php [moved from lib/spout/src/Spout/Writer/Style/Style.php with 57% similarity]
lib/spout/src/Spout/Common/Exception/EncodingConversionException.php
lib/spout/src/Spout/Common/Exception/IOException.php
lib/spout/src/Spout/Common/Exception/InvalidArgumentException.php
lib/spout/src/Spout/Common/Exception/InvalidColorException.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Exception/SpoutException.php
lib/spout/src/Spout/Common/Exception/UnsupportedTypeException.php
lib/spout/src/Spout/Common/Helper/CellTypeHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/EncodingHelper.php
lib/spout/src/Spout/Common/Helper/Escaper/CSV.php [moved from lib/spout/src/Spout/Common/Escaper/CSV.php with 90% similarity]
lib/spout/src/Spout/Common/Helper/Escaper/EscaperInterface.php [moved from lib/spout/src/Spout/Common/Escaper/EscaperInterface.php with 87% similarity]
lib/spout/src/Spout/Common/Helper/Escaper/ODS.php [moved from lib/spout/src/Spout/Common/Escaper/ODS.php with 80% similarity]
lib/spout/src/Spout/Common/Helper/Escaper/XLSX.php [moved from lib/spout/src/Spout/Common/Escaper/XLSX.php with 78% similarity]
lib/spout/src/Spout/Common/Helper/FileSystemHelper.php
lib/spout/src/Spout/Common/Helper/FileSystemHelperInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php
lib/spout/src/Spout/Common/Helper/StringHelper.php
lib/spout/src/Spout/Common/Manager/OptionsManagerAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Manager/OptionsManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Singleton.php [deleted file]
lib/spout/src/Spout/Common/Type.php
lib/spout/src/Spout/Reader/CSV/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/Reader.php
lib/spout/src/Spout/Reader/CSV/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/CSV/RowIterator.php
lib/spout/src/Spout/Reader/CSV/Sheet.php
lib/spout/src/Spout/Reader/CSV/SheetIterator.php
lib/spout/src/Spout/Reader/Common/Creator/InternalEntityFactoryInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Creator/ReaderFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Entity/Options.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Manager/RowManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/Common/XMLProcessor.php
lib/spout/src/Spout/Reader/Exception/InvalidValueException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php
lib/spout/src/Spout/Reader/Exception/NoSheetsFoundException.php
lib/spout/src/Spout/Reader/Exception/ReaderException.php
lib/spout/src/Spout/Reader/Exception/ReaderNotOpenedException.php
lib/spout/src/Spout/Reader/Exception/SharedStringNotFoundException.php
lib/spout/src/Spout/Reader/Exception/XMLProcessingException.php
lib/spout/src/Spout/Reader/IteratorInterface.php
lib/spout/src/Spout/Reader/ODS/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/ODS/Helper/SettingsHelper.php
lib/spout/src/Spout/Reader/ODS/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Reader.php
lib/spout/src/Spout/Reader/ODS/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/ODS/RowIterator.php
lib/spout/src/Spout/Reader/ODS/Sheet.php
lib/spout/src/Spout/Reader/ODS/SheetIterator.php
lib/spout/src/Spout/Reader/ReaderAbstract.php [moved from lib/spout/src/Spout/Reader/AbstractReader.php with 82% similarity]
lib/spout/src/Spout/Reader/ReaderFactory.php [deleted file]
lib/spout/src/Spout/Reader/ReaderInterface.php
lib/spout/src/Spout/Reader/SheetInterface.php
lib/spout/src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php
lib/spout/src/Spout/Reader/Wrapper/XMLReader.php
lib/spout/src/Spout/Reader/XLSX/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php [deleted file]
lib/spout/src/Spout/Reader/XLSX/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php with 80% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php with 90% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/FileBasedStrategy.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/FileBasedStrategy.php with 90% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/InMemoryStrategy.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php with 95% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php with 74% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SheetManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Manager/StyleManager.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php with 88% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Reader.php
lib/spout/src/Spout/Reader/XLSX/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/XLSX/RowIterator.php
lib/spout/src/Spout/Reader/XLSX/Sheet.php
lib/spout/src/Spout/Reader/XLSX/SheetIterator.php
lib/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php [deleted file]
lib/spout/src/Spout/Writer/AbstractWriter.php [deleted file]
lib/spout/src/Spout/Writer/CSV/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/CSV/Writer.php
lib/spout/src/Spout/Writer/Common/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Creator/ManagerFactoryInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Creator/Style/BorderBuilder.php [moved from lib/spout/src/Spout/Writer/Style/BorderBuilder.php with 60% similarity]
lib/spout/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php [moved from lib/spout/src/Spout/Writer/Style/StyleBuilder.php with 93% similarity]
lib/spout/src/Spout/Writer/Common/Creator/WriterEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Creator/WriterFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Options.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Sheet.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Workbook.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Worksheet.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php [deleted file]
lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php
lib/spout/src/Spout/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Helper/ZipHelper.php
lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php [deleted file]
lib/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php [deleted file]
lib/spout/src/Spout/Writer/Common/Internal/WorksheetInterface.php [deleted file]
lib/spout/src/Spout/Writer/Common/Manager/CellManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/RowManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/SheetManager.php [moved from lib/spout/src/Spout/Writer/Common/Sheet.php with 57% similarity]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleMerger.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/WorksheetManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/Border/InvalidNameException.php
lib/spout/src/Spout/Writer/Exception/Border/InvalidStyleException.php
lib/spout/src/Spout/Writer/Exception/Border/InvalidWidthException.php
lib/spout/src/Spout/Writer/Exception/InvalidColorException.php [deleted file]
lib/spout/src/Spout/Writer/Exception/InvalidSheetNameException.php
lib/spout/src/Spout/Writer/Exception/SheetNotFoundException.php
lib/spout/src/Spout/Writer/Exception/WriterAlreadyOpenedException.php
lib/spout/src/Spout/Writer/Exception/WriterException.php
lib/spout/src/Spout/Writer/Exception/WriterNotOpenedException.php
lib/spout/src/Spout/Writer/ODS/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Helper/BorderHelper.php
lib/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php [deleted file]
lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php [deleted file]
lib/spout/src/Spout/Writer/ODS/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleManager.php [moved from lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php with 84% similarity]
lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Manager/WorkbookManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Manager/WorksheetManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Writer.php
lib/spout/src/Spout/Writer/WriterAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/WriterFactory.php [deleted file]
lib/spout/src/Spout/Writer/WriterInterface.php
lib/spout/src/Spout/Writer/WriterMultiSheetsAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Helper/BorderHelper.php
lib/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php [deleted file]
lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php [deleted file]
lib/spout/src/Spout/Writer/XLSX/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Manager/SharedStringsManager.php [moved from lib/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php with 86% similarity]
lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php [moved from lib/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php with 53% similarity]
lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Manager/WorkbookManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Manager/WorksheetManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Writer.php

index 6bb73aa..3fbb10b 100644 (file)
 [![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
 [![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges)
 [![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/box/spout/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
 [![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
 [![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
-[![License](https://poser.pugx.org/box/spout/license)](https://packagist.org/packages/box/spout)
 
 Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
-Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
+Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
 
 Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
 
-## Installation
 
-### Composer (recommended)
+## Documentation
 
-Spout can be installed directly from [Composer](https://getcomposer.org/).
-
-Run the following command:
-```
-$ composer require box/spout
-```
-
-### Manual installation
-
-If you can't use Composer, no worries! You can still install Spout manually.
-
-> Before starting, make sure your system meets the [requirements](#requirements).
-
-1. Download the source code from the [Releases page](https://github.com/box/spout/releases)
-2. Extract the downloaded content into your project.
-3. Add this code to the top controller (index.php) or wherever it may be more appropriate:
-```php
-require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path!
-```
+Full documentation can be found at [http://opensource.box.com/spout/](http://opensource.box.com/spout/).
 
 
 ## Requirements
 
-* PHP version 5.4.0 or higher
+* PHP version 7.1 or higher
 * PHP extension `php_zip` enabled
 * PHP extension `php_xmlreader` enabled
-* PHP extension `php_simplexml` enabled
-
-
-## Basic usage
-
-### Reader
-
-Regardless of the file type, the interface to read a file is always the same:
-
-```php
-use Box\Spout\Reader\ReaderFactory;
-use Box\Spout\Common\Type;
-
-$reader = ReaderFactory::create(Type::XLSX); // for XLSX files
-//$reader = ReaderFactory::create(Type::CSV); // for CSV files
-//$reader = ReaderFactory::create(Type::ODS); // for ODS files
-
-$reader->open($filePath);
-
-foreach ($reader->getSheetIterator() as $sheet) {
-    foreach ($sheet->getRowIterator() as $row) {
-        // do stuff with the row
-    }
-}
-
-$reader->close();
-```
-
-If there are multiple sheets in the file, the reader will read all of them sequentially.
-
-### Writer
-
-As with the reader, there is one common interface to write data to a file:
-
-```php
-use Box\Spout\Writer\WriterFactory;
-use Box\Spout\Common\Type;
-
-$writer = WriterFactory::create(Type::XLSX); // for XLSX files
-//$writer = WriterFactory::create(Type::CSV); // for CSV files
-//$writer = WriterFactory::create(Type::ODS); // for ODS files
-
-$writer->openToFile($filePath); // write data to a file or to a PHP stream
-//$writer->openToBrowser($fileName); // stream data directly to the browser
-
-$writer->addRow($singleRow); // add a row at a time
-$writer->addRows($multipleRows); // add multiple rows at a time
-
-$writer->close();
-```
-
-For XLSX and ODS files, the number of rows per sheet is limited to 1,048,576. By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
-
-
-## Advanced usage
-
-If you are looking for  how to perform some common, more advanced tasks with Spout, please take a look at the [Wiki](https://github.com/box/spout/wiki). It contains code snippets, ready to be used.
-
-### Configuring the CSV reader and writer
-
-It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
-```php
-use Box\Spout\Reader\ReaderFactory;
-use Box\Spout\Common\Type;
-
-$reader = ReaderFactory::create(Type::CSV);
-$reader->setFieldDelimiter('|');
-$reader->setFieldEnclosure('@');
-$reader->setEndOfLineCharacter("\r");
-```
-
-Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
-```php
-$reader->setEncoding('UTF-16LE');
-```
-
-The writer always generate CSV files encoded in UTF-8, with a BOM.
-
-
-### Configuring the XLSX and ODS writers
-
-#### Row styling
-
-It is possible to apply some formatting options to a row. Spout supports fonts as well as alignment styles.
-
-```php
-use Box\Spout\Common\Type;
-use Box\Spout\Writer\WriterFactory;
-use Box\Spout\Writer\Style\StyleBuilder;
-use Box\Spout\Writer\Style\Color;
-
-$style = (new StyleBuilder())
-           ->setFontBold()
-           ->setFontSize(15)
-           ->setFontColor(Color::BLUE)
-           ->setShouldWrapText()
-           ->build();
-
-$writer = WriterFactory::create(Type::XLSX);
-$writer->openToFile($filePath);
-
-$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row
-$writer->addRow($otherSingleRow); // no style will be applied
-$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows
-
-$writer->close();
-```
 
-Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
-
-Category  | Property      | API
-----------|---------------|---------------------------------------
-Font      | Bold          | `StyleBuilder::setFontBold()`
-          | Italic        | `StyleBuilder::setFontItalic()`
-          | Underline     | `StyleBuilder::setFontUnderline()`
-          | Strikethrough | `StyleBuilder::setFontStrikethrough()`
-          | Font name     | `StyleBuilder::setFontName('Arial')`
-          | Font size     | `StyleBuilder::setFontSize(14)`
-          | Font color    | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
-Alignment | Wrap text     | `StyleBuilder::setShouldWrapText()`
-
-
-#### New sheet creation
-
-It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
-```php
-use Box\Spout\Writer\WriterFactory;
-use Box\Spout\Common\Type;
-
-$writer = WriterFactory::create(Type::ODS);
-$writer->setShouldCreateNewSheetsAutomatically(true); // default value
-$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
-```
-
-#### Using custom temporary folder
-
-Processing XLSX and ODS files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer:
-```php
-use Box\Spout\Writer\WriterFactory;
-use Box\Spout\Common\Type;
-
-$writer = WriterFactory::create(Type::XLSX);
-$writer->setTempFolder($customTempFolderPath);
-```
-
-#### Strings storage (XLSX writer)
-
-XLSX files support different ways to store the string values:
-* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored)
-* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process
-
-In order to keep the memory usage really low, Spout does not optimize strings when using shared strings. It is nevertheless possible to use this mode.
-```php
-use Box\Spout\Writer\WriterFactory;
-use Box\Spout\Common\Type;
-
-$writer = WriterFactory::create(Type::XLSX);
-$writer->setShouldUseInlineStrings(true); // default (and recommended) value
-$writer->setShouldUseInlineStrings(false); // will use shared strings
-```
-
-> ##### Note on Apple Numbers and iOS support
->
-> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings!
-
-
-### Playing with sheets
-
-When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet:
-```php
-$firstSheet = $writer->getCurrentSheet();
-$writer->addRow($rowForSheet1); // writes the row to the first sheet
-
-$newSheet = $writer->addNewSheetAndMakeItCurrent();
-$writer->addRow($rowForSheet2); // writes the row to the new sheet
-
-$writer->setCurrentSheet($firstSheet);
-$writer->addRow($anotherRowForSheet1); // append the row to the first sheet
-```
-
-It is also possible to retrieve all the sheets currently created:
-```php
-$sheets = $writer->getSheets();
-```
-
-If you rely on the sheet's name in your application, you can access it and customize it this way:
-```php
-// Accessing the sheet name when reading
-foreach ($reader->getSheetIterator() as $sheet) {
-    $sheetName = $sheet->getName();
-}
-
-// Accessing the sheet name when writing
-$sheet = $writer->getCurrentSheet();
-$sheetName = $sheet->getName();
-
-// Customizing the sheet name when writing
-$sheet = $writer->getCurrentSheet();
-$sheet->setName('My custom name');
-``` 
-
-> Please note that Excel has some restrictions on the sheet's name:
-> * it must not be blank
-> * it must not exceed 31 characters
-> * it must not contain these characters: \ / ? * : [ or ]
-> * it must not start or end with a single quote
-> * it must be unique
->
-> Handling these restrictions is the developer's responsibility. Spout does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in.
-
-
-### Fluent interface
-
-Because fluent interfaces are great, you can use them with Spout:
-```php
-use Box\Spout\Writer\WriterFactory;
-use Box\Spout\Common\Type;
-
-$writer = WriterFactory::create(Type::XLSX);
-$writer->setTempFolder($customTempFolderPath)
-       ->setShouldUseInlineStrings(true)
-       ->openToFile($filePath)
-       ->addRow($headerRow)
-       ->addRows($dataRows)
-       ->close();
-```
+## Upgrade guide
 
+Version 3 introduced new functionality but also some breaking changes. If you want to upgrade your Spout codebase from version 2 please consult the [Upgrade guide](UPGRADE-3.0.md). 
 
 ## Running tests
 
-On the `master` branch, only unit and functional tests are included. The performance tests require very large files and have been excluded.
-If you just want to check that everything is working as expected, executing the tests of the `master` branch is enough.
-
-If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output:
+The `master` branch includes unit, functional and performance tests.
+If you just want to check that everything is working as expected, executing the unit and functional tests is enough.
 
-* `phpunit` - runs the whole test suite (unit + functional + performance tests)
-* `phpunit --exclude-group perf-tests` - only runs the unit and functional tests
+* `phpunit` - runs unit and functional tests
 * `phpunit --group perf-tests` - only runs the performance tests
 
-For information, the performance tests take about 30 minutes to run (processing 1 million rows files is not a quick thing).
+For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing).
 
 > Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout)
 
 
-## Frequently Asked Questions
-
-#### How can Spout handle such large data sets and still use less than 10MB of memory?
-
-When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
-
-Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access.
-
-#### How long does it take to generate a file with X rows?
-
-Here are a few numbers regarding the performance of Spout:
-
-| Type | Action                        | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
-|------|-------------------------------|--------------------------|------------------------------|----------------------------------|
-| CSV  | Read                          | < 1 second               | 4 seconds                    | 2-3 minutes                      |
-|      | Write                         | < 1 second               | 2 seconds                    | 2-3 minutes                      |
-| XLSX | Read<br>*inline&nbsp;strings* | < 1 second               | 35-40 seconds                | 18-20 minutes                    |
-|      | Read<br>*shared&nbsp;strings* | 1 second                 | 1-2 minutes                  | 35-40 minutes                    |
-|      | Write                         | 1 second                 | 20-25 seconds                | 8-10 minutes                     |
-| ODS  | Read                          | 1 second                 | 1-2 minutes                  | 5-6 minutes                      |
-|      | Write                         | < 1 second               | 35-40 seconds                | 5-6 minutes                      |
-
-#### Does Spout support charts or formulas?
-
-No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used.
-So the larger the file would be, the more memory would be consumed, preventing your code to scale well.
-
-
 ## Support
 
-Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject.
-
-You can also ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
+You can ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
 [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
 
 ## Copyright and License
 
-Copyright 2015 Box, Inc. All rights reserved.
+Copyright 2017 Box, Inc. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
index 67cccc3..1699293 100644 (file)
@@ -5,8 +5,6 @@ namespace Box\Spout\Autoloader;
 /**
  * Class Psr4Autoloader
  * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
- *
- * @package Box\Spout\Autoloader
  */
 class Psr4Autoloader
 {
@@ -16,7 +14,7 @@ class Psr4Autoloader
      *
      * @var array
      */
-    protected $prefixes = array();
+    protected $prefixes = [];
 
     /**
      * Register loader with SPL autoloader stack.
@@ -25,7 +23,7 @@ class Psr4Autoloader
      */
     public function register()
     {
-        spl_autoload_register(array($this, 'loadClass'));
+        spl_autoload_register([$this, 'loadClass']);
     }
 
     /**
@@ -49,7 +47,7 @@ class Psr4Autoloader
 
         // initialize the namespace prefix array
         if (isset($this->prefixes[$prefix]) === false) {
-            $this->prefixes[$prefix] = array();
+            $this->prefixes[$prefix] = [];
         }
 
         // retain the base directory for the namespace prefix
@@ -74,8 +72,7 @@ class Psr4Autoloader
 
         // work backwards through the namespace names of the fully-qualified
         // class name to find a mapped file name
-        while (false !== $pos = strrpos($prefix, '\\')) {
-
+        while (($pos = strrpos($prefix, '\\')) !== false) {
             // retain the trailing namespace separator in the prefix
             $prefix = substr($class, 0, $pos + 1);
 
@@ -114,7 +111,6 @@ class Psr4Autoloader
 
         // look through base directories for this namespace prefix
         foreach ($this->prefixes[$prefix] as $baseDir) {
-
             // replace the namespace prefix with the base directory,
             // replace namespace separators with directory separators
             // in the relative class name, append with .php
@@ -143,8 +139,10 @@ class Psr4Autoloader
     {
         if (file_exists($file)) {
             require $file;
+
             return true;
         }
+
         return false;
     }
 }
index 73ee519..e08ee34 100644 (file)
@@ -5,7 +5,7 @@ namespace Box\Spout\Autoloader;
 require_once 'Psr4Autoloader.php';
 
 /**
- * @var string $srcBaseDirectory
+ * @var string
  * Full path to "src/Spout" which is what we want "Box\Spout" to map to.
  */
 $srcBaseDirectory = dirname(dirname(__FILE__));
diff --git a/lib/spout/src/Spout/Common/Creator/HelperFactory.php b/lib/spout/src/Spout/Common/Creator/HelperFactory.php
new file mode 100644 (file)
index 0000000..2277f2c
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace Box\Spout\Common\Creator;
+
+use Box\Spout\Common\Helper\EncodingHelper;
+use Box\Spout\Common\Helper\FileSystemHelper;
+use Box\Spout\Common\Helper\GlobalFunctionsHelper;
+use Box\Spout\Common\Helper\StringHelper;
+
+/**
+ * Class HelperFactory
+ * Factory to create helpers
+ */
+class HelperFactory
+{
+    /**
+     * @return GlobalFunctionsHelper
+     */
+    public function createGlobalFunctionsHelper()
+    {
+        return new GlobalFunctionsHelper();
+    }
+
+    /**
+     * @param string $baseFolderPath The path of the base folder where all the I/O can occur
+     * @return FileSystemHelper
+     */
+    public function createFileSystemHelper($baseFolderPath)
+    {
+        return new FileSystemHelper($baseFolderPath);
+    }
+
+    /**
+     * @param GlobalFunctionsHelper $globalFunctionsHelper
+     * @return EncodingHelper
+     */
+    public function createEncodingHelper(GlobalFunctionsHelper $globalFunctionsHelper)
+    {
+        return new EncodingHelper($globalFunctionsHelper);
+    }
+
+    /**
+     * @return StringHelper
+     */
+    public function createStringHelper()
+    {
+        return new StringHelper();
+    }
+}
diff --git a/lib/spout/src/Spout/Common/Entity/Cell.php b/lib/spout/src/Spout/Common/Entity/Cell.php
new file mode 100644 (file)
index 0000000..bd7a730
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+
+namespace Box\Spout\Common\Entity;
+
+use Box\Spout\Common\Entity\Style\Style;
+use Box\Spout\Common\Helper\CellTypeHelper;
+
+/**
+ * Class Cell
+ */
+class Cell
+{
+    /**
+     * Numeric cell type (whole numbers, fractional numbers, dates)
+     */
+    const TYPE_NUMERIC = 0;
+
+    /**
+     * String (text) cell type
+     */
+    const TYPE_STRING = 1;
+
+    /**
+     * Formula cell type
+     * Not used at the moment
+     */
+    const TYPE_FORMULA = 2;
+
+    /**
+     * Empty cell type
+     */
+    const TYPE_EMPTY = 3;
+
+    /**
+     * Boolean cell type
+     */
+    const TYPE_BOOLEAN = 4;
+
+    /**
+     * Date cell type
+     */
+    const TYPE_DATE = 5;
+
+    /**
+     * Error cell type
+     */
+    const TYPE_ERROR = 6;
+
+    /**
+     * The value of this cell
+     * @var mixed|null
+     */
+    protected $value;
+
+    /**
+     * The cell type
+     * @var int|null
+     */
+    protected $type;
+
+    /**
+     * The cell style
+     * @var Style
+     */
+    protected $style;
+
+    /**
+     * @param $value mixed
+     * @param Style|null $style
+     */
+    public function __construct($value, Style $style = null)
+    {
+        $this->setValue($value);
+        $this->setStyle($style);
+    }
+
+    /**
+     * @param mixed|null $value
+     */
+    public function setValue($value)
+    {
+        $this->value = $value;
+        $this->type = $this->detectType($value);
+    }
+
+    /**
+     * @return mixed|null
+     */
+    public function getValue()
+    {
+        return !$this->isError() ? $this->value : null;
+    }
+
+    /**
+     * @param Style|null $style
+     */
+    public function setStyle($style)
+    {
+        $this->style = $style ?: new Style();
+    }
+
+    /**
+     * @return Style
+     */
+    public function getStyle()
+    {
+        return $this->style;
+    }
+
+    /**
+     * @return int|null
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * @param int $type
+     */
+    public function setType($type)
+    {
+        $this->type = $type;
+    }
+
+    /**
+     * Get the current value type
+     *
+     * @param mixed|null $value
+     * @return int
+     */
+    protected function detectType($value)
+    {
+        if (CellTypeHelper::isBoolean($value)) {
+            return self::TYPE_BOOLEAN;
+        }
+        if (CellTypeHelper::isEmpty($value)) {
+            return self::TYPE_EMPTY;
+        }
+        if (CellTypeHelper::isNumeric($value)) {
+            return self::TYPE_NUMERIC;
+        }
+        if (CellTypeHelper::isDateTimeOrDateInterval($value)) {
+            return self::TYPE_DATE;
+        }
+        if (CellTypeHelper::isNonEmptyString($value)) {
+            return self::TYPE_STRING;
+        }
+
+        return self::TYPE_ERROR;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isBoolean()
+    {
+        return $this->type === self::TYPE_BOOLEAN;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return $this->type === self::TYPE_EMPTY;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isNumeric()
+    {
+        return $this->type === self::TYPE_NUMERIC;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isString()
+    {
+        return $this->type === self::TYPE_STRING;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isDate()
+    {
+        return $this->type === self::TYPE_DATE;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isError()
+    {
+        return $this->type === self::TYPE_ERROR;
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string) $this->getValue();
+    }
+}
diff --git a/lib/spout/src/Spout/Common/Entity/Row.php b/lib/spout/src/Spout/Common/Entity/Row.php
new file mode 100644 (file)
index 0000000..ccb6bf5
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+namespace Box\Spout\Common\Entity;
+
+use Box\Spout\Common\Entity\Style\Style;
+
+class Row
+{
+    /**
+     * The cells in this row
+     * @var Cell[]
+     */
+    protected $cells = [];
+
+    /**
+     * The row style
+     * @var Style
+     */
+    protected $style;
+
+    /**
+     * Row constructor.
+     * @param Cell[] $cells
+     * @param Style|null $style
+     */
+    public function __construct(array $cells, $style)
+    {
+        $this
+            ->setCells($cells)
+            ->setStyle($style);
+    }
+
+    /**
+     * @return Cell[] $cells
+     */
+    public function getCells()
+    {
+        return $this->cells;
+    }
+
+    /**
+     * @param Cell[] $cells
+     * @return Row
+     */
+    public function setCells(array $cells)
+    {
+        $this->cells = [];
+        foreach ($cells as $cell) {
+            $this->addCell($cell);
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param Cell $cell
+     * @param int $cellIndex
+     * @return Row
+     */
+    public function setCellAtIndex(Cell $cell, $cellIndex)
+    {
+        $this->cells[$cellIndex] = $cell;
+
+        return $this;
+    }
+
+    /**
+     * @param int $cellIndex
+     * @return Cell|null
+     */
+    public function getCellAtIndex($cellIndex)
+    {
+        return isset($this->cells[$cellIndex]) ? $this->cells[$cellIndex] : null;
+    }
+
+    /**
+     * @param Cell $cell
+     * @return Row
+     */
+    public function addCell(Cell $cell)
+    {
+        $this->cells[] = $cell;
+
+        return $this;
+    }
+
+    /**
+     * @return int
+     */
+    public function getNumCells()
+    {
+        return count($this->cells);
+    }
+
+    /**
+     * @return Style
+     */
+    public function getStyle()
+    {
+        return $this->style;
+    }
+
+    /**
+     * @param Style|null $style
+     * @return Row
+     */
+    public function setStyle($style)
+    {
+        $this->style = $style ?: new Style();
+
+        return $this;
+    }
+
+    /**
+     * @return array The row values, as array
+     */
+    public function toArray()
+    {
+        return array_map(function (Cell $cell) {
+            return $cell->getValue();
+        }, $this->cells);
+    }
+}
@@ -1,6 +1,6 @@
 <?php
 
-namespace Box\Spout\Writer\Style;
+namespace Box\Spout\Common\Entity\Style;
 
 /**
  * Class Border
@@ -22,13 +22,11 @@ class Border
     const WIDTH_MEDIUM = 'medium';
     const WIDTH_THICK = 'thick';
 
-    /**
-     * @var array A list of BorderPart objects for this border.
-     */
-    protected $parts = [];
+    /** @var array A list of BorderPart objects for this border. */
+    private $parts = [];
 
     /**
-     * @param array|void $borderParts
+     * @param array $borderParts
      */
     public function __construct(array $borderParts = [])
     {
@@ -36,8 +34,8 @@ class Border
     }
 
     /**
-     * @param $name The name of the border part
-     * @return null|BorderPart
+     * @param string $name The name of the border part
+     * @return BorderPart|null
      */
     public function getPart($name)
     {
@@ -45,7 +43,7 @@ class Border
     }
 
     /**
-     * @param $name The name of the border part
+     * @param string $name The name of the border part
      * @return bool
      */
     public function hasPart($name)
@@ -64,6 +62,7 @@ class Border
     /**
      * Set BorderParts
      * @param array $parts
+     * @return void
      */
     public function setParts($parts)
     {
@@ -75,11 +74,12 @@ class Border
 
     /**
      * @param BorderPart $borderPart
-     * @return self
+     * @return Border
      */
     public function addPart(BorderPart $borderPart)
     {
         $this->parts[$borderPart->getName()] = $borderPart;
+
         return $this;
     }
 }
@@ -1,6 +1,6 @@
 <?php
 
-namespace Box\Spout\Writer\Style;
+namespace Box\Spout\Common\Entity\Style;
 
 use Box\Spout\Writer\Exception\Border\InvalidNameException;
 use Box\Spout\Writer\Exception\Border\InvalidStyleException;
@@ -39,7 +39,7 @@ class BorderPart
         'solid',
         'dashed',
         'dotted',
-        'double'
+        'double',
     ];
 
     /**
@@ -1,14 +1,12 @@
 <?php
 
-namespace Box\Spout\Writer\Style;
+namespace Box\Spout\Common\Entity\Style;
 
-use Box\Spout\Writer\Exception\InvalidColorException;
+use Box\Spout\Common\Exception\InvalidColorException;
 
 /**
  * Class Color
  * This class provides constants and functions to work with colors
- *
- * @package Box\Spout\Writer\Style
  */
 class Color
 {
@@ -29,7 +27,6 @@ class Color
     /**
      * Returns an RGB color from R, G and B values
      *
-     * @api
      * @param int $red Red component, 0 - 255
      * @param int $green Green component, 0 - 255
      * @param int $blue Blue component, 0 - 255
@@ -52,8 +49,8 @@ class Color
      * Throws an exception is the color component value is outside of bounds (0 - 255)
      *
      * @param int $colorComponent
+     * @throws \Box\Spout\Common\Exception\InvalidColorException
      * @return void
-     * @throws \Box\Spout\Writer\Exception\InvalidColorException
      */
     protected static function throwIfInvalidColorComponentValue($colorComponent)
     {
@@ -1,12 +1,10 @@
 <?php
 
-namespace Box\Spout\Writer\Style;
+namespace Box\Spout\Common\Entity\Style;
 
 /**
  * Class Style
  * Represents a style to be applied to a cell
- *
- * @package Box\Spout\Writer\Style
  */
 class Style
 {
@@ -16,67 +14,62 @@ class Style
     const DEFAULT_FONT_NAME = 'Arial';
 
     /** @var int|null Style ID */
-    protected $id = null;
+    private $id;
 
     /** @var bool Whether the font should be bold */
-    protected $fontBold = false;
+    private $fontBold = false;
     /** @var bool Whether the bold property was set */
-    protected $hasSetFontBold = false;
+    private $hasSetFontBold = false;
 
     /** @var bool Whether the font should be italic */
-    protected $fontItalic = false;
+    private $fontItalic = false;
     /** @var bool Whether the italic property was set */
-    protected $hasSetFontItalic = false;
+    private $hasSetFontItalic = false;
 
     /** @var bool Whether the font should be underlined */
-    protected $fontUnderline = false;
+    private $fontUnderline = false;
     /** @var bool Whether the underline property was set */
-    protected $hasSetFontUnderline = false;
+    private $hasSetFontUnderline = false;
 
     /** @var bool Whether the font should be struck through */
-    protected $fontStrikethrough = false;
+    private $fontStrikethrough = false;
     /** @var bool Whether the strikethrough property was set */
-    protected $hasSetFontStrikethrough = false;
+    private $hasSetFontStrikethrough = false;
 
     /** @var int Font size */
-    protected $fontSize = self::DEFAULT_FONT_SIZE;
+    private $fontSize = self::DEFAULT_FONT_SIZE;
     /** @var bool Whether the font size property was set */
-    protected $hasSetFontSize = false;
+    private $hasSetFontSize = false;
 
     /** @var string Font color */
-    protected $fontColor = self::DEFAULT_FONT_COLOR;
+    private $fontColor = self::DEFAULT_FONT_COLOR;
     /** @var bool Whether the font color property was set */
-    protected $hasSetFontColor = false;
+    private $hasSetFontColor = false;
 
     /** @var string Font name */
-    protected $fontName = self::DEFAULT_FONT_NAME;
+    private $fontName = self::DEFAULT_FONT_NAME;
     /** @var bool Whether the font name property was set */
-    protected $hasSetFontName = false;
+    private $hasSetFontName = false;
 
     /** @var bool Whether specific font properties should be applied */
-    protected $shouldApplyFont = false;
+    private $shouldApplyFont = false;
 
     /** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */
-    protected $shouldWrapText = false;
+    private $shouldWrapText = false;
     /** @var bool Whether the wrap text property was set */
-    protected $hasSetWrapText = false;
+    private $hasSetWrapText = false;
 
-    /**
-     * @var Border
-     */
-    protected $border = null;
+    /** @var Border */
+    private $border;
 
-    /**
-     * @var bool Whether border properties should be applied
-     */
-    protected $shouldApplyBorder = false;
+    /** @var bool Whether border properties should be applied */
+    private $shouldApplyBorder = false;
 
     /** @var string Background color */
-    protected $backgroundColor = null;
+    private $backgroundColor;
 
     /** @var bool */
-    protected $hasSetBackgroundColor = false;
-
+    private $hasSetBackgroundColor = false;
 
     /**
      * @return int|null
@@ -93,6 +86,7 @@ class Style
     public function setId($id)
     {
         $this->id = $id;
+
         return $this;
     }
 
@@ -112,6 +106,7 @@ class Style
     {
         $this->shouldApplyBorder = true;
         $this->border = $border;
+
         return $this;
     }
 
@@ -139,9 +134,18 @@ class Style
         $this->fontBold = true;
         $this->hasSetFontBold = true;
         $this->shouldApplyFont = true;
+
         return $this;
     }
 
+    /**
+     * @return bool
+     */
+    public function hasSetFontBold()
+    {
+        return $this->hasSetFontBold;
+    }
+
     /**
      * @return bool
      */
@@ -158,9 +162,18 @@ class Style
         $this->fontItalic = true;
         $this->hasSetFontItalic = true;
         $this->shouldApplyFont = true;
+
         return $this;
     }
 
+    /**
+     * @return bool
+     */
+    public function hasSetFontItalic()
+    {
+        return $this->hasSetFontItalic;
+    }
+
     /**
      * @return bool
      */
@@ -177,9 +190,18 @@ class Style
         $this->fontUnderline = true;
         $this->hasSetFontUnderline = true;
         $this->shouldApplyFont = true;
+
         return $this;
     }
 
+    /**
+     * @return bool
+     */
+    public function hasSetFontUnderline()
+    {
+        return $this->hasSetFontUnderline;
+    }
+
     /**
      * @return bool
      */
@@ -196,9 +218,18 @@ class Style
         $this->fontStrikethrough = true;
         $this->hasSetFontStrikethrough = true;
         $this->shouldApplyFont = true;
+
         return $this;
     }
 
+    /**
+     * @return bool
+     */
+    public function hasSetFontStrikethrough()
+    {
+        return $this->hasSetFontStrikethrough;
+    }
+
     /**
      * @return int
      */
@@ -216,9 +247,18 @@ class Style
         $this->fontSize = $fontSize;
         $this->hasSetFontSize = true;
         $this->shouldApplyFont = true;
+
         return $this;
     }
 
+    /**
+     * @return bool
+     */
+    public function hasSetFontSize()
+    {
+        return $this->hasSetFontSize;
+    }
+
     /**
      * @return string
      */
@@ -238,9 +278,18 @@ class Style
         $this->fontColor = $fontColor;
         $this->hasSetFontColor = true;
         $this->shouldApplyFont = true;
+
         return $this;
     }
 
+    /**
+     * @return bool
+     */
+    public function hasSetFontColor()
+    {
+        return $this->hasSetFontColor;
+    }
+
     /**
      * @return string
      */
@@ -258,9 +307,18 @@ class Style
         $this->fontName = $fontName;
         $this->hasSetFontName = true;
         $this->shouldApplyFont = true;
+
         return $this;
     }
 
+    /**
+     * @return bool
+     */
+    public function hasSetFontName()
+    {
+        return $this->hasSetFontName;
+    }
+
     /**
      * @return bool
      */
@@ -270,13 +328,14 @@ class Style
     }
 
     /**
-     * @param bool|void $shouldWrap Should the text be wrapped
+     * @param bool $shouldWrap Should the text be wrapped
      * @return Style
      */
     public function setShouldWrapText($shouldWrap = true)
     {
         $this->shouldWrapText = $shouldWrap;
         $this->hasSetWrapText = true;
+
         return $this;
     }
 
@@ -305,6 +364,7 @@ class Style
     {
         $this->hasSetBackgroundColor = true;
         $this->backgroundColor = $color;
+
         return $this;
     }
 
@@ -317,110 +377,10 @@ class Style
     }
 
     /**
-     *
      * @return bool Whether the background color should be applied
      */
     public function shouldApplyBackgroundColor()
     {
         return $this->hasSetBackgroundColor;
     }
-
-    /**
-     * Serializes the style for future comparison with other styles.
-     * The ID is excluded from the comparison, as we only care about
-     * actual style properties.
-     *
-     * @return string The serialized style
-     */
-    public function serialize()
-    {
-        // In order to be able to properly compare style, set static ID value
-        $currentId = $this->id;
-        $this->setId(0);
-
-        $serializedStyle = serialize($this);
-
-        $this->setId($currentId);
-
-        return $serializedStyle;
-    }
-
-    /**
-     * Merges the current style with the given style, using the given style as a base. This means that:
-     *   - if current style and base style both have property A set, use current style property's value
-     *   - if current style has property A set but base style does not, use current style property's value
-     *   - if base style has property A set but current style does not, use base style property's value
-     *
-     * @NOTE: This function returns a new style.
-     *
-     * @param Style $baseStyle
-     * @return Style New style corresponding to the merge of the 2 styles
-     */
-    public function mergeWith($baseStyle)
-    {
-        $mergedStyle = clone $this;
-
-        $this->mergeFontStyles($mergedStyle, $baseStyle);
-        $this->mergeOtherFontProperties($mergedStyle, $baseStyle);
-        $this->mergeCellProperties($mergedStyle, $baseStyle);
-
-        return $mergedStyle;
-    }
-
-    /**
-     * @param Style $styleToUpdate (passed as reference)
-     * @param Style $baseStyle
-     * @return void
-     */
-    private function mergeFontStyles($styleToUpdate, $baseStyle)
-    {
-        if (!$this->hasSetFontBold && $baseStyle->isFontBold()) {
-            $styleToUpdate->setFontBold();
-        }
-        if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) {
-            $styleToUpdate->setFontItalic();
-        }
-        if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) {
-            $styleToUpdate->setFontUnderline();
-        }
-        if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) {
-            $styleToUpdate->setFontStrikethrough();
-        }
-    }
-
-    /**
-     * @param Style $styleToUpdate Style to update (passed as reference)
-     * @param Style $baseStyle
-     * @return void
-     */
-    private function mergeOtherFontProperties($styleToUpdate, $baseStyle)
-    {
-        if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
-            $styleToUpdate->setFontSize($baseStyle->getFontSize());
-        }
-        if (!$this->hasSetFontColor && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) {
-            $styleToUpdate->setFontColor($baseStyle->getFontColor());
-        }
-        if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
-            $styleToUpdate->setFontName($baseStyle->getFontName());
-        }
-    }
-
-    /**
-     * @param Style $styleToUpdate Style to update (passed as reference)
-     * @param Style $baseStyle
-     * @return void
-     */
-    private function mergeCellProperties($styleToUpdate, $baseStyle)
-    {
-        if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) {
-            $styleToUpdate->setShouldWrapText();
-        }
-        if (!$this->getBorder() && $baseStyle->shouldApplyBorder()) {
-            $styleToUpdate->setBorder($baseStyle->getBorder());
-        }
-        if (!$this->hasSetBackgroundColor && $baseStyle->shouldApplyBackgroundColor()) {
-            $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor());
-        }
-    }
 }
index 6dcc0a8..098d064 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
 
 /**
  * Class EncodingConversionException
- *
- * @api
- * @package Box\Spout\Common\Exception
  */
 class EncodingConversionException extends SpoutException
 {
index 4307ca4..3ccae4c 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
 
 /**
  * Class IOException
- *
- * @api
- * @package Box\Spout\Common\Exception
  */
 class IOException extends SpoutException
 {
index b1ec05f..5fadc25 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
 
 /**
  * Class InvalidArgumentException
- *
- * @api
- * @package Box\Spout\Common\Exception
  */
 class InvalidArgumentException extends SpoutException
 {
diff --git a/lib/spout/src/Spout/Common/Exception/InvalidColorException.php b/lib/spout/src/Spout/Common/Exception/InvalidColorException.php
new file mode 100644 (file)
index 0000000..f0c6ec9
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+namespace Box\Spout\Common\Exception;
+
+/**
+ * Class InvalidColorException
+ */
+class InvalidColorException extends SpoutException
+{
+}
index aa762d7..2816fcc 100644 (file)
@@ -5,7 +5,6 @@ namespace Box\Spout\Common\Exception;
 /**
  * Class SpoutException
  *
- * @package Box\Spout\Common\Exception
  * @abstract
  */
 abstract class SpoutException extends \Exception
index 65e8500..1fd59e0 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
 
 /**
  * Class UnsupportedTypeException
- *
- * @api
- * @package Box\Spout\Common\Exception
  */
 class UnsupportedTypeException extends SpoutException
 {
diff --git a/lib/spout/src/Spout/Common/Helper/CellTypeHelper.php b/lib/spout/src/Spout/Common/Helper/CellTypeHelper.php
new file mode 100644 (file)
index 0000000..03d26a3
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace Box\Spout\Common\Helper;
+
+/**
+ * Class CellTypeHelper
+ * This class provides helper functions to determine the type of the cell value
+ */
+class CellTypeHelper
+{
+    /**
+     * @param $value
+     * @return bool Whether the given value is considered "empty"
+     */
+    public static function isEmpty($value)
+    {
+        return ($value === null || $value === '');
+    }
+
+    /**
+     * @param $value
+     * @return bool Whether the given value is a non empty string
+     */
+    public static function isNonEmptyString($value)
+    {
+        return (gettype($value) === 'string' && $value !== '');
+    }
+
+    /**
+     * Returns whether the given value is numeric.
+     * A numeric value is from type "integer" or "double" ("float" is not returned by gettype).
+     *
+     * @param $value
+     * @return bool Whether the given value is numeric
+     */
+    public static function isNumeric($value)
+    {
+        $valueType = gettype($value);
+
+        return ($valueType === 'integer' || $valueType === 'double');
+    }
+
+    /**
+     * Returns whether the given value is boolean.
+     * "true"/"false" and 0/1 are not booleans.
+     *
+     * @param $value
+     * @return bool Whether the given value is boolean
+     */
+    public static function isBoolean($value)
+    {
+        return gettype($value) === 'boolean';
+    }
+
+    /**
+     * Returns whether the given value is a DateTime or DateInterval object.
+     *
+     * @param $value
+     * @return bool Whether the given value is a DateTime or DateInterval object
+     */
+    public static function isDateTimeOrDateInterval($value)
+    {
+        return (
+            $value instanceof \DateTime ||
+            $value instanceof \DateInterval
+        );
+    }
+}
index 3a30aaa..9064274 100644 (file)
@@ -7,8 +7,6 @@ use Box\Spout\Common\Exception\EncodingConversionException;
 /**
  * Class EncodingHelper
  * This class provides helper functions to work with encodings.
- *
- * @package Box\Spout\Common\Helper
  */
 class EncodingHelper
 {
@@ -97,8 +95,8 @@ class EncodingHelper
      *
      * @param string $string Non UTF-8 string to be converted
      * @param string $sourceEncoding The encoding used to encode the source string
-     * @return string The converted, UTF-8 string
      * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
+     * @return string The converted, UTF-8 string
      */
     public function attemptConversionToUTF8($string, $sourceEncoding)
     {
@@ -110,8 +108,8 @@ class EncodingHelper
      *
      * @param string $string UTF-8 string to be converted
      * @param string $targetEncoding The encoding the string should be re-encoded into
-     * @return string The converted string, encoded with the given encoding
      * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
+     * @return string The converted string, encoded with the given encoding
      */
     public function attemptConversionFromUTF8($string, $targetEncoding)
     {
@@ -125,8 +123,8 @@ class EncodingHelper
      * @param string $string string to be converted
      * @param string $sourceEncoding The encoding used to encode the source string
      * @param string $targetEncoding The encoding the string should be re-encoded into
-     * @return string The converted string, encoded with the given encoding
      * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
+     * @return string The converted string, encoded with the given encoding
      */
     protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
     {
@@ -139,7 +137,7 @@ class EncodingHelper
 
         if ($this->canUseIconv()) {
             $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
-        } else if ($this->canUseMbString()) {
+        } elseif ($this->canUseMbString()) {
             $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
         } else {
             throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\".");
@@ -1,12 +1,10 @@
 <?php
 
-namespace Box\Spout\Common\Escaper;
+namespace Box\Spout\Common\Helper\Escaper;
 
 /**
  * Class CSV
  * Provides functions to escape and unescape data for CSV files
- *
- * @package Box\Spout\Common\Escaper
  */
 class CSV implements EscaperInterface
 {
@@ -1,11 +1,9 @@
 <?php
 
-namespace Box\Spout\Common\Escaper;
+namespace Box\Spout\Common\Helper\Escaper;
 
 /**
  * Interface EscaperInterface
- *
- * @package Box\Spout\Common\Escaper
  */
 interface EscaperInterface
 {
@@ -1,19 +1,13 @@
 <?php
 
-namespace Box\Spout\Common\Escaper;
-
-use Box\Spout\Common\Singleton;
+namespace Box\Spout\Common\Helper\Escaper;
 
 /**
  * Class ODS
  * Provides functions to escape and unescape data for ODS files
- *
- * @package Box\Spout\Common\Escaper
  */
 class ODS implements EscaperInterface
 {
-    use Singleton;
-
     /**
      * Escapes the given string to make it compatible with XLSX
      *
@@ -22,17 +16,16 @@ class ODS implements EscaperInterface
      */
     public function escape($string)
     {
+        // @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
+        //        single/double quotes (for XML attributes) need to be encoded.
         if (defined('ENT_DISALLOWED')) {
             // 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced.
             // Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor
             // @link https://github.com/box/spout/issues/329
-            $replacedString = htmlspecialchars($string, ENT_NOQUOTES | ENT_DISALLOWED);
+            $replacedString = htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
         } else {
             // We are on hhvm or any other engine that does not support ENT_DISALLOWED.
-            //
-            // @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded.
-            //        Single and double quotes can be left as is.
-            $escapedString =  htmlspecialchars($string, ENT_NOQUOTES);
+            $escapedString =  htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
 
             // control characters values are from 0 to 1F (hex values) in the ASCII table
             // some characters should not be escaped though: "\t", "\r" and "\n".
@@ -1,36 +1,37 @@
 <?php
 
-namespace Box\Spout\Common\Escaper;
-
-use Box\Spout\Common\Singleton;
+namespace Box\Spout\Common\Helper\Escaper;
 
 /**
  * Class XLSX
  * Provides functions to escape and unescape data for XLSX files
- *
- * @package Box\Spout\Common\Escaper
  */
 class XLSX implements EscaperInterface
 {
-    use Singleton;
+    /** @var bool Whether the escaper has already been initialized */
+    private $isAlreadyInitialized = false;
 
     /** @var string Regex pattern to detect control characters that need to be escaped */
-    protected $escapableControlCharactersPattern;
+    private $escapableControlCharactersPattern;
 
     /** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
-    protected $controlCharactersEscapingMap;
+    private $controlCharactersEscapingMap;
 
     /** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
-    protected $controlCharactersEscapingReverseMap;
+    private $controlCharactersEscapingReverseMap;
 
     /**
-     * Initializes the singleton instance
+     * Initializes the control characters if not already done
      */
-    protected function init()
+    protected function initIfNeeded()
     {
-        $this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
-        $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
-        $this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap);
+        if (!$this->isAlreadyInitialized) {
+            $this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
+            $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
+            $this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap);
+
+            $this->isAlreadyInitialized = true;
+        }
     }
 
     /**
@@ -41,10 +42,12 @@ class XLSX implements EscaperInterface
      */
     public function escape($string)
     {
+        $this->initIfNeeded();
+
         $escapedString = $this->escapeControlCharacters($string);
-        // @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded.
-        //        Single and double quotes can be left as is.
-        $escapedString = htmlspecialchars($escapedString, ENT_NOQUOTES);
+        // @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
+        //        single/double quotes (for XML attributes) need to be encoded.
+        $escapedString = htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8');
 
         return $escapedString;
     }
@@ -57,6 +60,8 @@ class XLSX implements EscaperInterface
      */
     public function unescape($string)
     {
+        $this->initIfNeeded();
+
         // ==============
         // =   WARNING  =
         // ==============
@@ -88,7 +93,7 @@ class XLSX implements EscaperInterface
      * "\t", "\r" and "\n" don't need to be escaped.
      *
      * NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
-     * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
+     * @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
      *
      * @return string[]
      */
@@ -101,7 +106,7 @@ class XLSX implements EscaperInterface
             $character = chr($charValue);
             if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
                 $charHexValue = dechex($charValue);
-                $escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
+                $escapedChar = '_x' . sprintf('%04s', strtoupper($charHexValue)) . '_';
                 $controlCharactersEscapingMap[$escapedChar] = $character;
             }
         }
@@ -117,7 +122,7 @@ class XLSX implements EscaperInterface
      * So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
      *
      * NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
-     * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
+     * @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
      *
      * @param string $string String to escape
      * @return string
@@ -131,7 +136,7 @@ class XLSX implements EscaperInterface
             return $escapedString;
         }
 
-        return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function($matches) {
+        return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function ($matches) {
             return $this->controlCharactersEscapingReverseMap[$matches[0]];
         }, $escapedString);
     }
@@ -155,7 +160,7 @@ class XLSX implements EscaperInterface
      * So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
      *
      * NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
-     * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
+     * @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
      *
      * @param string $string String to unescape
      * @return string
index 3145be7..b1bff8f 100644 (file)
@@ -8,10 +8,8 @@ use Box\Spout\Common\Exception\IOException;
  * Class FileSystemHelper
  * This class provides helper functions to help with the file system operations
  * like files/folders creation & deletion
- *
- * @package Box\Spout\Common\Helper
  */
-class FileSystemHelper
+class FileSystemHelper implements FileSystemHelperInterface
 {
     /** @var string Real path of the base folder where all the I/O can occur */
     protected $baseFolderRealPath;
@@ -29,8 +27,8 @@ class FileSystemHelper
      *
      * @param string $parentFolderPath The parent folder path under which the folder is going to be created
      * @param string $folderName The name of the folder to create
-     * @return string Path of the created folder
      * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
+     * @return string Path of the created folder
      */
     public function createFolder($parentFolderPath, $folderName)
     {
@@ -53,8 +51,8 @@ class FileSystemHelper
      * @param string $parentFolderPath The parent folder path where the file is going to be created
      * @param string $fileName The name of the file to create
      * @param string $fileContents The contents of the file to create
-     * @return string Path of the created file
      * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
+     * @return string Path of the created file
      */
     public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
     {
@@ -74,8 +72,8 @@ class FileSystemHelper
      * Delete the file at the given path
      *
      * @param string $filePath Path of the file to delete
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
+     * @return void
      */
     public function deleteFile($filePath)
     {
@@ -90,8 +88,8 @@ class FileSystemHelper
      * Delete the folder at the given path as well as all its contents
      *
      * @param string $folderPath Path of the folder to delete
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
+     * @return void
      */
     public function deleteFolderRecursively($folderPath)
     {
@@ -119,8 +117,8 @@ class FileSystemHelper
      * should occur is not inside the base folder.
      *
      * @param string $operationFolderPath The path of the folder where the I/O operation should occur
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder
+     * @return void
      */
     protected function throwIfOperationNotInBaseFolder($operationFolderPath)
     {
diff --git a/lib/spout/src/Spout/Common/Helper/FileSystemHelperInterface.php b/lib/spout/src/Spout/Common/Helper/FileSystemHelperInterface.php
new file mode 100644 (file)
index 0000000..53c7dd9
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+namespace Box\Spout\Common\Helper;
+
+/**
+ * Class FileSystemHelperInterface
+ * This interface describes helper functions to help with the file system operations
+ * like files/folders creation & deletion
+ */
+interface FileSystemHelperInterface
+{
+    /**
+     * Creates an empty folder with the given name under the given parent folder.
+     *
+     * @param string $parentFolderPath The parent folder path under which the folder is going to be created
+     * @param string $folderName The name of the folder to create
+     * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
+     * @return string Path of the created folder
+     */
+    public function createFolder($parentFolderPath, $folderName);
+
+    /**
+     * Creates a file with the given name and content in the given folder.
+     * The parent folder must exist.
+     *
+     * @param string $parentFolderPath The parent folder path where the file is going to be created
+     * @param string $fileName The name of the file to create
+     * @param string $fileContents The contents of the file to create
+     * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
+     * @return string Path of the created file
+     */
+    public function createFileWithContents($parentFolderPath, $fileName, $fileContents);
+
+    /**
+     * Delete the file at the given path
+     *
+     * @param string $filePath Path of the file to delete
+     * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
+     * @return void
+     */
+    public function deleteFile($filePath);
+
+    /**
+     * Delete the folder at the given path as well as all its contents
+     *
+     * @param string $folderPath Path of the folder to delete
+     * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
+     * @return void
+     */
+    public function deleteFolderRecursively($folderPath);
+}
index c5d6e31..0b5f6f1 100644 (file)
@@ -7,8 +7,6 @@ namespace Box\Spout\Common\Helper;
  * This class wraps global functions to facilitate testing
  *
  * @codeCoverageIgnore
- *
- * @package Box\Spout\Common\Helper
  */
 class GlobalFunctionsHelper
 {
@@ -30,7 +28,7 @@ class GlobalFunctionsHelper
      * @see fgets()
      *
      * @param resource $handle
-     * @param int|void $length
+     * @param int|null $length
      * @return string
      */
     public function fgets($handle, $length = null)
@@ -81,14 +79,20 @@ class GlobalFunctionsHelper
      * @see fgetcsv()
      *
      * @param resource $handle
-     * @param int|void $length
-     * @param string|void $delimiter
-     * @param string|void $enclosure
+     * @param int|null $length
+     * @param string|null $delimiter
+     * @param string|null $enclosure
      * @return array
      */
     public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null)
     {
-        return fgetcsv($handle, $length, $delimiter, $enclosure);
+        // PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
+        // To fix that, simply disable the escape character.
+        // @see https://bugs.php.net/bug.php?id=43225
+        // @see http://tools.ietf.org/html/rfc4180
+        $escapeCharacter = "\0";
+
+        return fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter);
     }
 
     /**
@@ -97,13 +101,19 @@ class GlobalFunctionsHelper
      *
      * @param resource $handle
      * @param array $fields
-     * @param string|void $delimiter
-     * @param string|void $enclosure
+     * @param string|null $delimiter
+     * @param string|null $enclosure
      * @return int
      */
     public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
     {
-        return fputcsv($handle, $fields, $delimiter, $enclosure);
+        // PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
+        // To fix that, simply disable the escape character.
+        // @see https://bugs.php.net/bug.php?id=43225
+        // @see http://tools.ietf.org/html/rfc4180
+        $escapeCharacter = "\0";
+
+        return fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter);
     }
 
     /**
@@ -165,6 +175,7 @@ class GlobalFunctionsHelper
     public function file_get_contents($filePath)
     {
         $realFilePath = $this->convertToUseRealPath($filePath);
+
         return file_get_contents($realFilePath);
     }
 
@@ -207,7 +218,7 @@ class GlobalFunctionsHelper
      * Wrapper around global function feof()
      * @see feof()
      *
-     * @param resource
+     * @param resource $handle
      * @return bool
      */
     public function feof($handle)
@@ -232,7 +243,7 @@ class GlobalFunctionsHelper
      * @see basename()
      *
      * @param string $path
-     * @param string|void $suffix
+     * @param string|null $suffix
      * @return string
      */
     public function basename($path, $suffix = null)
index 273104e..c698815 100644 (file)
@@ -7,8 +7,6 @@ namespace Box\Spout\Common\Helper;
  * This class provides helper functions to work with strings and multibyte strings.
  *
  * @codeCoverageIgnore
- *
- * @package Box\Spout\Common\Helper
  */
 class StringHelper
 {
@@ -50,6 +48,7 @@ class StringHelper
     public function getCharFirstOccurrencePosition($char, $string)
     {
         $position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char);
+
         return ($position !== false) ? $position : -1;
     }
 
@@ -66,6 +65,7 @@ class StringHelper
     public function getCharLastOccurrencePosition($char, $string)
     {
         $position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char);
+
         return ($position !== false) ? $position : -1;
     }
 }
diff --git a/lib/spout/src/Spout/Common/Manager/OptionsManagerAbstract.php b/lib/spout/src/Spout/Common/Manager/OptionsManagerAbstract.php
new file mode 100644 (file)
index 0000000..20eb14e
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace Box\Spout\Common\Manager;
+
+/**
+ * Class OptionsManager
+ */
+abstract class OptionsManagerAbstract implements OptionsManagerInterface
+{
+    const PREFIX_OPTION = 'OPTION_';
+
+    /** @var string[] List of all supported option names */
+    private $supportedOptions = [];
+
+    /** @var array Associative array [OPTION_NAME => OPTION_VALUE] */
+    private $options = [];
+
+    /**
+     * OptionsManagerAbstract constructor.
+     */
+    public function __construct()
+    {
+        $this->supportedOptions = $this->getSupportedOptions();
+        $this->setDefaultOptions();
+    }
+
+    /**
+     * @return array List of supported options
+     */
+    abstract protected function getSupportedOptions();
+
+    /**
+     * Sets the default options.
+     * To be overriden by child classes
+     *
+     * @return void
+     */
+    abstract protected function setDefaultOptions();
+
+    /**
+     * Sets the given option, if this option is supported.
+     *
+     * @param string $optionName
+     * @param mixed $optionValue
+     * @return void
+     */
+    public function setOption($optionName, $optionValue)
+    {
+        if (in_array($optionName, $this->supportedOptions)) {
+            $this->options[$optionName] = $optionValue;
+        }
+    }
+
+    /**
+     * @param string $optionName
+     * @return mixed|null The set option or NULL if no option with given name found
+     */
+    public function getOption($optionName)
+    {
+        $optionValue = null;
+
+        if (isset($this->options[$optionName])) {
+            $optionValue = $this->options[$optionName];
+        }
+
+        return $optionValue;
+    }
+}
diff --git a/lib/spout/src/Spout/Common/Manager/OptionsManagerInterface.php b/lib/spout/src/Spout/Common/Manager/OptionsManagerInterface.php
new file mode 100644 (file)
index 0000000..21fea0c
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+namespace Box\Spout\Common\Manager;
+
+/**
+ * Interface OptionsManagerInterface
+ */
+interface OptionsManagerInterface
+{
+    /**
+     * @param string $optionName
+     * @param mixed $optionValue
+     * @return void
+     */
+    public function setOption($optionName, $optionValue);
+
+    /**
+     * @param string $optionName
+     * @return mixed|null The set option or NULL if no option with given name found
+     */
+    public function getOption($optionName);
+}
diff --git a/lib/spout/src/Spout/Common/Singleton.php b/lib/spout/src/Spout/Common/Singleton.php
deleted file mode 100644 (file)
index 015ede8..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace Box\Spout\Common;
-
-/**
- * Class Singleton
- * Defines a class as a singleton.
- *
- * @package Box\Spout\Common
- */
-trait Singleton
-{
-    protected static $instance;
-
-    /**
-     * @return static
-     */
-    final public static function getInstance()
-    {
-        return isset(static::$instance)
-            ? static::$instance
-            : static::$instance = new static;
-    }
-
-    /**
-     * Singleton constructor.
-     */
-    final private function __construct()
-    {
-        $this->init();
-    }
-
-    /**
-     * Initializes the singleton
-     * @return void
-     */
-    protected function init() {}
-
-    final private function __wakeup() {}
-    final private function __clone() {}
-}
index 5e9b75e..24c0718 100644 (file)
@@ -5,8 +5,6 @@ namespace Box\Spout\Common;
 /**
  * Class Type
  * This class references the supported types
- *
- * @api
  */
 abstract class Type
 {
diff --git a/lib/spout/src/Spout/Reader/CSV/Creator/InternalEntityFactory.php b/lib/spout/src/Spout/Reader/CSV/Creator/InternalEntityFactory.php
new file mode 100644 (file)
index 0000000..cdb4197
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+
+namespace Box\Spout\Reader\CSV\Creator;
+
+use Box\Spout\Common\Creator\HelperFactory;
+use Box\Spout\Common\Entity\Cell;
+use Box\Spout\Common\Entity\Row;
+use Box\Spout\Common\Helper\GlobalFunctionsHelper;
+use Box\Spout\Common\Manager\OptionsManagerInterface;
+use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
+use Box\Spout\Reader\CSV\RowIterator;
+use Box\Spout\Reader\CSV\Sheet;
+use Box\Spout\Reader\CSV\SheetIterator;
+
+/**
+ * Class EntityFactory
+ * Factory to create entities
+ */
+class InternalEntityFactory implements InternalEntityFactoryInterface
+{
+    /** @var HelperFactory */
+    private $helperFactory;
+
+    /**
+     * @param HelperFactory $helperFactory
+     */
+    public function __construct(HelperFactory $helperFactory)
+    {
+        $this->helperFactory = $helperFactory;
+    }
+
+    /**
+     * @param resource $filePointer Pointer to the CSV file to read
+     * @param OptionsManagerInterface $optionsManager
+     * @param GlobalFunctionsHelper $globalFunctionsHelper
+     * @return SheetIterator
+     */
+    public function createSheetIterator($filePointer, $optionsManager, $globalFunctionsHelper)
+    {
+        $rowIterator = $this->createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper);
+        $sheet = $this->createSheet($rowIterator);
+
+        return new SheetIterator($sheet);
+    }
+
+    /**
+     * @param RowIterator $rowIterator
+     * @return Sheet
+     */
+    private function createSheet($rowIterator)
+    {
+        return new Sheet($rowIterator);
+    }
+
+    /**
+     * @param resource $filePointer Pointer to the CSV file to read
+     * @param OptionsManagerInterface $optionsManager
+     * @param GlobalFunctionsHelper $globalFunctionsHelper
+     * @return RowIterator
+     */
+    private function createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper)
+    {
+        $encodingHelper = $this->helperFactory->createEncodingHelper($globalFunctionsHelper);
+
+        return new RowIterator($filePointer, $optionsManager, $encodingHelper, $this, $globalFunctionsHelper);
+    }
+
+    /**
+     * @param Cell[] $cells
+     * @return Row
+     */
+    public function createRow(array $cells = [])
+    {
+        return new Row($cells, null);
+    }
+
+    /**
+     * @param mixed $cellValue
+     * @return Cell
+     */
+    public function createCell($cellValue)
+    {
+        return new Cell($cellValue);
+    }
+
+    /**
+     * @param array $cellValues
+     * @return Row
+     */
+    public function createRowFromArray(array $cellValues = [])
+    {
+        $cells = array_map(function ($cellValue) {
+            return $this->createCell($cellValue);
+        }, $cellValues);
+
+        return $this->createRow($cells);
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/CSV/Manager/OptionsManager.php b/lib/spout/src/Spout/Reader/CSV/Manager/OptionsManager.php
new file mode 100644 (file)
index 0000000..befefe3
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+namespace Box\Spout\Reader\CSV\Manager;
+
+use Box\Spout\Common\Helper\EncodingHelper;
+use Box\Spout\Common\Manager\OptionsManagerAbstract;
+use Box\Spout\Reader\Common\Entity\Options;
+
+/**
+ * Class OptionsManager
+ * CSV Reader options manager
+ */
+class OptionsManager extends OptionsManagerAbstract
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getSupportedOptions()
+    {
+        return [
+            Options::SHOULD_FORMAT_DATES,
+            Options::SHOULD_PRESERVE_EMPTY_ROWS,
+            Options::FIELD_DELIMITER,
+            Options::FIELD_ENCLOSURE,
+            Options::ENCODING,
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function setDefaultOptions()
+    {
+        $this->setOption(Options::SHOULD_FORMAT_DATES, false);
+        $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
+        $this->setOption(Options::FIELD_DELIMITER, ',');
+        $this->setOption(Options::FIELD_ENCLOSURE, '"');
+        $this->setOption(Options::ENCODING, EncodingHelper::ENCODING_UTF8);
+    }
+}
index 648a12d..77249c1 100644 (file)
@@ -2,16 +2,16 @@
 
 namespace Box\Spout\Reader\CSV;
 
-use Box\Spout\Reader\AbstractReader;
 use Box\Spout\Common\Exception\IOException;
+use Box\Spout\Reader\Common\Entity\Options;
+use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
+use Box\Spout\Reader\ReaderAbstract;
 
 /**
  * Class Reader
  * This class provides support to read data from a CSV file.
- *
- * @package Box\Spout\Reader\CSV
  */
-class Reader extends AbstractReader
+class Reader extends ReaderAbstract
 {
     /** @var resource Pointer to the file to be written */
     protected $filePointer;
@@ -22,19 +22,6 @@ class Reader extends AbstractReader
     /** @var string Original value for the "auto_detect_line_endings" INI value */
     protected $originalAutoDetectLineEndings;
 
-    /**
-     * Returns the reader's current options
-     *
-     * @return ReaderOptions
-     */
-    protected function getOptions()
-    {
-        if (!isset($this->options)) {
-            $this->options = new ReaderOptions();
-        }
-        return $this->options;
-    }
-
     /**
      * Sets the field delimiter for the CSV.
      * Needs to be called before opening the reader.
@@ -44,7 +31,8 @@ class Reader extends AbstractReader
      */
     public function setFieldDelimiter($fieldDelimiter)
     {
-        $this->getOptions()->setFieldDelimiter($fieldDelimiter);
+        $this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
+
         return $this;
     }
 
@@ -57,7 +45,8 @@ class Reader extends AbstractReader
      */
     public function setFieldEnclosure($fieldEnclosure)
     {
-        $this->getOptions()->setFieldEnclosure($fieldEnclosure);
+        $this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
+
         return $this;
     }
 
@@ -70,20 +59,8 @@ class Reader extends AbstractReader
      */
     public function setEncoding($encoding)
     {
-        $this->getOptions()->setEncoding($encoding);
-        return $this;
-    }
+        $this->optionsManager->setOption(Options::ENCODING, $encoding);
 
-    /**
-     * Sets the EOL for the CSV.
-     * Needs to be called before opening the reader.
-     *
-     * @param string $endOfLineCharacter used to properly get lines from the CSV file.
-     * @return Reader
-     */
-    public function setEndOfLineCharacter($endOfLineCharacter)
-    {
-        $this->getOptions()->setEndOfLineCharacter($endOfLineCharacter);
         return $this;
     }
 
@@ -102,8 +79,8 @@ class Reader extends AbstractReader
      * If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
      *
      * @param  string $filePath Path of the CSV file to be read
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException
+     * @return void
      */
     protected function openReader($filePath)
     {
@@ -115,9 +92,12 @@ class Reader extends AbstractReader
             throw new IOException("Could not open file $filePath for reading.");
         }
 
-        $this->sheetIterator = new SheetIterator(
+        /** @var InternalEntityFactory $entityFactory */
+        $entityFactory = $this->entityFactory;
+
+        $this->sheetIterator = $entityFactory->createSheetIterator(
             $this->filePointer,
-            $this->getOptions(),
+            $this->optionsManager,
             $this->globalFunctionsHelper
         );
     }
@@ -132,7 +112,6 @@ class Reader extends AbstractReader
         return $this->sheetIterator;
     }
 
-
     /**
      * Closes the reader. To be used after reading the file.
      *
diff --git a/lib/spout/src/Spout/Reader/CSV/ReaderOptions.php b/lib/spout/src/Spout/Reader/CSV/ReaderOptions.php
deleted file mode 100644 (file)
index 9a1adb8..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-namespace Box\Spout\Reader\CSV;
-
-use Box\Spout\Common\Helper\EncodingHelper;
-
-/**
- * Class ReaderOptions
- * This class is used to customize the reader's behavior
- *
- * @package Box\Spout\Reader\CSV
- */
-class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
-{
-    /** @var string Defines the character used to delimit fields (one character only) */
-    protected $fieldDelimiter = ',';
-
-    /** @var string Defines the character used to enclose fields (one character only) */
-    protected $fieldEnclosure = '"';
-
-    /** @var string Encoding of the CSV file to be read */
-    protected $encoding = EncodingHelper::ENCODING_UTF8;
-
-    /** @var string Defines the End of line */
-    protected $endOfLineCharacter = "\n";
-
-    /**
-     * @return string
-     */
-    public function getFieldDelimiter()
-    {
-        return $this->fieldDelimiter;
-    }
-
-    /**
-     * Sets the field delimiter for the CSV.
-     * Needs to be called before opening the reader.
-     *
-     * @param string $fieldDelimiter Character that delimits fields
-     * @return ReaderOptions
-     */
-    public function setFieldDelimiter($fieldDelimiter)
-    {
-        $this->fieldDelimiter = $fieldDelimiter;
-        return $this;
-    }
-
-    /**
-     * @return string
-     */
-    public function getFieldEnclosure()
-    {
-        return $this->fieldEnclosure;
-    }
-
-    /**
-     * Sets the field enclosure for the CSV.
-     * Needs to be called before opening the reader.
-     *
-     * @param string $fieldEnclosure Character that enclose fields
-     * @return ReaderOptions
-     */
-    public function setFieldEnclosure($fieldEnclosure)
-    {
-        $this->fieldEnclosure = $fieldEnclosure;
-        return $this;
-    }
-
-    /**
-     * @return string
-     */
-    public function getEncoding()
-    {
-        return $this->encoding;
-    }
-
-    /**
-     * Sets the encoding of the CSV file to be read.
-     * Needs to be called before opening the reader.
-     *
-     * @param string $encoding Encoding of the CSV file to be read
-     * @return ReaderOptions
-     */
-    public function setEncoding($encoding)
-    {
-        $this->encoding = $encoding;
-        return $this;
-    }
-
-    /**
-     * @return string EOL for the CSV
-     */
-    public function getEndOfLineCharacter()
-    {
-        return $this->endOfLineCharacter;
-    }
-
-    /**
-     * Sets the EOL for the CSV.
-     * Needs to be called before opening the reader.
-     *
-     * @param string $endOfLineCharacter used to properly get lines from the CSV file.
-     * @return ReaderOptions
-     */
-    public function setEndOfLineCharacter($endOfLineCharacter)
-    {
-        $this->endOfLineCharacter = $endOfLineCharacter;
-        return $this;
-    }
-}
index a2a6672..bec3072 100644 (file)
@@ -2,14 +2,17 @@
 
 namespace Box\Spout\Reader\CSV;
 
-use Box\Spout\Reader\IteratorInterface;
+use Box\Spout\Common\Entity\Row;
 use Box\Spout\Common\Helper\EncodingHelper;
+use Box\Spout\Common\Helper\GlobalFunctionsHelper;
+use Box\Spout\Common\Manager\OptionsManagerInterface;
+use Box\Spout\Reader\Common\Entity\Options;
+use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
+use Box\Spout\Reader\IteratorInterface;
 
 /**
  * Class RowIterator
  * Iterate over CSV rows.
- *
- * @package Box\Spout\Reader\CSV
  */
 class RowIterator implements IteratorInterface
 {
@@ -24,8 +27,8 @@ class RowIterator implements IteratorInterface
     /** @var int Number of read rows */
     protected $numReadRows = 0;
 
-    /** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
-    protected $rowDataBuffer = null;
+    /** @var Row|null Buffer used to store the current row, while checking if there are more rows to read */
+    protected $rowBuffer;
 
     /** @var bool Indicates whether all rows have been read */
     protected $hasReachedEndOfFile = false;
@@ -39,42 +42,45 @@ class RowIterator implements IteratorInterface
     /** @var string Encoding of the CSV file to be read */
     protected $encoding;
 
-    /** @var string End of line delimiter, given by the user as input. */
-    protected $inputEOLDelimiter;
-
     /** @var bool Whether empty rows should be returned or skipped */
     protected $shouldPreserveEmptyRows;
 
-    /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
-    protected $globalFunctionsHelper;
-
     /** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */
     protected $encodingHelper;
 
-    /** @var string End of line delimiter, encoded using the same encoding as the CSV */
-    protected $encodedEOLDelimiter;
+    /** @var \Box\Spout\Reader\CSV\Creator\InternalEntityFactory Factory to create entities */
+    protected $entityFactory;
+
+    /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
+    protected $globalFunctionsHelper;
 
     /**
      * @param resource $filePointer Pointer to the CSV file to read
-     * @param \Box\Spout\Reader\CSV\ReaderOptions $options
-     * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
+     * @param OptionsManagerInterface $optionsManager
+     * @param EncodingHelper $encodingHelper
+     * @param InternalEntityFactory $entityFactory
+     * @param GlobalFunctionsHelper $globalFunctionsHelper
      */
-    public function __construct($filePointer, $options, $globalFunctionsHelper)
-    {
+    public function __construct(
+        $filePointer,
+        OptionsManagerInterface $optionsManager,
+        EncodingHelper $encodingHelper,
+        InternalEntityFactory $entityFactory,
+        GlobalFunctionsHelper $globalFunctionsHelper
+    ) {
         $this->filePointer = $filePointer;
-        $this->fieldDelimiter = $options->getFieldDelimiter();
-        $this->fieldEnclosure = $options->getFieldEnclosure();
-        $this->encoding = $options->getEncoding();
-        $this->inputEOLDelimiter = $options->getEndOfLineCharacter();
-        $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
+        $this->fieldDelimiter = $optionsManager->getOption(Options::FIELD_DELIMITER);
+        $this->fieldEnclosure = $optionsManager->getOption(Options::FIELD_ENCLOSURE);
+        $this->encoding = $optionsManager->getOption(Options::ENCODING);
+        $this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
+        $this->encodingHelper = $encodingHelper;
+        $this->entityFactory = $entityFactory;
         $this->globalFunctionsHelper = $globalFunctionsHelper;
-
-        $this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
     }
 
     /**
      * Rewind the Iterator to the first element
-     * @link http://php.net/manual/en/iterator.rewind.php
+     * @see http://php.net/manual/en/iterator.rewind.php
      *
      * @return void
      */
@@ -83,7 +89,7 @@ class RowIterator implements IteratorInterface
         $this->rewindAndSkipBom();
 
         $this->numReadRows = 0;
-        $this->rowDataBuffer = null;
+        $this->rowBuffer = null;
 
         $this->next();
     }
@@ -104,7 +110,7 @@ class RowIterator implements IteratorInterface
 
     /**
      * Checks if current position is valid
-     * @link http://php.net/manual/en/iterator.valid.php
+     * @see http://php.net/manual/en/iterator.valid.php
      *
      * @return bool
      */
@@ -115,10 +121,10 @@ class RowIterator implements IteratorInterface
 
     /**
      * Move forward to next element. Reads data for the next unprocessed row.
-     * @link http://php.net/manual/en/iterator.next.php
+     * @see http://php.net/manual/en/iterator.next.php
      *
-     * @return void
      * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
+     * @return void
      */
     public function next()
     {
@@ -130,8 +136,8 @@ class RowIterator implements IteratorInterface
     }
 
     /**
-     * @return void
      * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
+     * @return void
      */
     protected function readDataForNextRow()
     {
@@ -141,7 +147,8 @@ class RowIterator implements IteratorInterface
 
         if ($rowData !== false) {
             // str_replace will replace NULL values by empty strings
-            $this->rowDataBuffer = str_replace(null, null, $rowData);
+            $rowDataBufferAsArray = str_replace(null, null, $rowData);
+            $this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
             $this->numReadRows++;
         } else {
             // If we reach this point, it means end of file was reached.
@@ -171,8 +178,8 @@ class RowIterator implements IteratorInterface
      * As fgetcsv() does not manage correctly encoding for non UTF-8 data,
      * we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes)
      *
-     * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
      * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
+     * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
      */
     protected function getNextUTF8EncodedRow()
     {
@@ -182,7 +189,7 @@ class RowIterator implements IteratorInterface
         }
 
         foreach ($encodedRowData as $cellIndex => $cellValue) {
-            switch($this->encoding) {
+            switch ($this->encoding) {
                 case EncodingHelper::ENCODING_UTF16_LE:
                 case EncodingHelper::ENCODING_UTF32_LE:
                     // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
@@ -202,21 +209,6 @@ class RowIterator implements IteratorInterface
         return $encodedRowData;
     }
 
-    /**
-     * Returns the end of line delimiter, encoded using the same encoding as the CSV.
-     * The return value is cached.
-     *
-     * @return string
-     */
-    protected function getEncodedEOLDelimiter()
-    {
-        if (!isset($this->encodedEOLDelimiter)) {
-            $this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding);
-        }
-
-        return $this->encodedEOLDelimiter;
-    }
-
     /**
      * @param array|bool $lineData Array containing the cells value for the line
      * @return bool Whether the given line is empty
@@ -228,18 +220,18 @@ class RowIterator implements IteratorInterface
 
     /**
      * Return the current element from the buffer
-     * @link http://php.net/manual/en/iterator.current.php
+     * @see http://php.net/manual/en/iterator.current.php
      *
-     * @return array|null
+     * @return Row|null
      */
     public function current()
     {
-        return $this->rowDataBuffer;
+        return $this->rowBuffer;
     }
 
     /**
      * Return the key of the current element
-     * @link http://php.net/manual/en/iterator.key.php
+     * @see http://php.net/manual/en/iterator.key.php
      *
      * @return int
      */
index 9a688db..a3055a8 100644 (file)
@@ -6,8 +6,6 @@ use Box\Spout\Reader\SheetInterface;
 
 /**
  * Class Sheet
- *
- * @package Box\Spout\Reader\CSV
  */
 class Sheet implements SheetInterface
 {
@@ -15,17 +13,14 @@ class Sheet implements SheetInterface
     protected $rowIterator;
 
     /**
-     * @param resource $filePointer Pointer to the CSV file to read
-     * @param \Box\Spout\Reader\CSV\ReaderOptions $options
-     * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
+     * @param RowIterator $rowIterator Corresponding row iterator
      */
-    public function __construct($filePointer, $options, $globalFunctionsHelper)
+    public function __construct(RowIterator $rowIterator)
     {
-        $this->rowIterator = new RowIterator($filePointer, $options, $globalFunctionsHelper);
+        $this->rowIterator = $rowIterator;
     }
 
     /**
-     * @api
      * @return \Box\Spout\Reader\CSV\RowIterator
      */
     public function getRowIterator()
@@ -34,7 +29,6 @@ class Sheet implements SheetInterface
     }
 
     /**
-     * @api
      * @return int Index of the sheet
      */
     public function getIndex()
@@ -43,7 +37,6 @@ class Sheet implements SheetInterface
     }
 
     /**
-     * @api
      * @return string Name of the sheet - empty string since CSV does not support that
      */
     public function getName()
@@ -52,11 +45,18 @@ class Sheet implements SheetInterface
     }
 
     /**
-     * @api
      * @return bool Always TRUE as there is only one sheet
      */
     public function isActive()
     {
         return true;
     }
+
+    /**
+     * @return bool Always TRUE as the only sheet is always visible
+     */
+    public function isVisible()
+    {
+        return true;
+    }
 }
index 58a9480..69eb58a 100644 (file)
@@ -7,8 +7,6 @@ use Box\Spout\Reader\IteratorInterface;
 /**
  * Class SheetIterator
  * Iterate over CSV unique "sheet".
- *
- * @package Box\Spout\Reader\CSV
  */
 class SheetIterator implements IteratorInterface
 {
@@ -19,18 +17,16 @@ class SheetIterator implements IteratorInterface
     protected $hasReadUniqueSheet = false;
 
     /**
-     * @param resource $filePointer
-     * @param \Box\Spout\Reader\CSV\ReaderOptions $options
-     * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
+     * @param Sheet $sheet Corresponding unique sheet
      */
-    public function __construct($filePointer, $options, $globalFunctionsHelper)
+    public function __construct($sheet)
     {
-        $this->sheet = new Sheet($filePointer, $options, $globalFunctionsHelper);
+        $this->sheet = $sheet;
     }
 
     /**
      * Rewind the Iterator to the first element
-     * @link http://php.net/manual/en/iterator.rewind.php
+     * @see http://php.net/manual/en/iterator.rewind.php
      *
      * @return void
      */
@@ -41,7 +37,7 @@ class SheetIterator implements IteratorInterface
 
     /**
      * Checks if current position is valid
-     * @link http://php.net/manual/en/iterator.valid.php
+     * @see http://php.net/manual/en/iterator.valid.php
      *
      * @return bool
      */
@@ -52,7 +48,7 @@ class SheetIterator implements IteratorInterface
 
     /**
      * Move forward to next element
-     * @link http://php.net/manual/en/iterator.next.php
+     * @see http://php.net/manual/en/iterator.next.php
      *
      * @return void
      */
@@ -63,7 +59,7 @@ class SheetIterator implements IteratorInterface
 
     /**
      * Return the current element
-     * @link http://php.net/manual/en/iterator.current.php
+     * @see http://php.net/manual/en/iterator.current.php
      *
      * @return \Box\Spout\Reader\CSV\Sheet
      */
@@ -74,7 +70,7 @@ class SheetIterator implements IteratorInterface
 
     /**
      * Return the key of the current element
-     * @link http://php.net/manual/en/iterator.key.php
+     * @see http://php.net/manual/en/iterator.key.php
      *
      * @return int
      */
diff --git a/lib/spout/src/Spout/Reader/Common/Creator/InternalEntityFactoryInterface.php b/lib/spout/src/Spout/Reader/Common/Creator/InternalEntityFactoryInterface.php
new file mode 100644 (file)
index 0000000..c1c78f1
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+namespace Box\Spout\Reader\Common\Creator;
+
+use Box\Spout\Common\Entity\Cell;
+use Box\Spout\Common\Entity\Row;
+
+/**
+ * Interface EntityFactoryInterface
+ */
+interface InternalEntityFactoryInterface
+{
+    /**
+     * @param Cell[] $cells
+     * @return Row
+     */
+    public function createRow(array $cells = []);
+
+    /**
+     * @param mixed $cellValue
+     * @return Cell
+     */
+    public function createCell($cellValue);
+}
diff --git a/lib/spout/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php b/lib/spout/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php
new file mode 100644 (file)
index 0000000..06a9d9f
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace Box\Spout\Reader\Common\Creator;
+
+use Box\Spout\Common\Exception\UnsupportedTypeException;
+use Box\Spout\Common\Type;
+use Box\Spout\Reader\ReaderInterface;
+
+/**
+ * Class ReaderEntityFactory
+ * Factory to create external entities
+ */
+class ReaderEntityFactory
+{
+    /**
+     * Creates a reader by file extension
+     *
+     * @param string $path The path to the spreadsheet file. Supported extensions are .csv, .ods and .xlsx
+     * @throws \Box\Spout\Common\Exception\UnsupportedTypeException
+     * @return ReaderInterface
+     */
+    public static function createReaderFromFile(string $path)
+    {
+        return ReaderFactory::createFromFile($path);
+    }
+
+    /**
+     * This creates an instance of a CSV reader
+     *
+     * @return \Box\Spout\Reader\CSV\Reader
+     */
+    public static function createCSVReader()
+    {
+        try {
+            return ReaderFactory::createFromType(Type::CSV);
+        } catch (UnsupportedTypeException $e) {
+            // should never happen
+        }
+    }
+
+    /**
+     * This creates an instance of a XLSX reader
+     *
+     * @return \Box\Spout\Reader\XLSX\Reader
+     */
+    public static function createXLSXReader()
+    {
+        try {
+            return ReaderFactory::createFromType(Type::XLSX);
+        } catch (UnsupportedTypeException $e) {
+            // should never happen
+        }
+    }
+
+    /**
+     * This creates an instance of a ODS reader
+     *
+     * @return \Box\Spout\Reader\ODS\Reader
+     */
+    public static function createODSReader()
+    {
+        try {
+            return ReaderFactory::createFromType(Type::ODS);
+        } catch (UnsupportedTypeException $e) {
+            // should never happen
+        }
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/Common/Creator/ReaderFactory.php b/lib/spout/src/Spout/Reader/Common/Creator/ReaderFactory.php
new file mode 100644 (file)
index 0000000..245a42a
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+
+namespace Box\Spout\Reader\Common\Creator;
+
+use Box\Spout\Common\Creator\HelperFactory;
+use Box\Spout\Common\Exception\UnsupportedTypeException;
+use Box\Spout\Common\Type;
+use Box\Spout\Reader\CSV\Creator\InternalEntityFactory as CSVInternalEntityFactory;
+use Box\Spout\Reader\CSV\Manager\OptionsManager as CSVOptionsManager;
+use Box\Spout\Reader\CSV\Reader as CSVReader;
+use Box\Spout\Reader\ODS\Creator\HelperFactory as ODSHelperFactory;
+use Box\Spout\Reader\ODS\Creator\InternalEntityFactory as ODSInternalEntityFactory;
+use Box\Spout\Reader\ODS\Creator\ManagerFactory as ODSManagerFactory;
+use Box\Spout\Reader\ODS\Manager\OptionsManager as ODSOptionsManager;
+use Box\Spout\Reader\ODS\Reader as ODSReader;
+use Box\Spout\Reader\ReaderInterface;
+use Box\Spout\Reader\XLSX\Creator\HelperFactory as XLSXHelperFactory;
+use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory as XLSXInternalEntityFactory;
+use Box\Spout\Reader\XLSX\Creator\ManagerFactory as XLSXManagerFactory;
+use Box\Spout\Reader\XLSX\Manager\OptionsManager as XLSXOptionsManager;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory;
+use Box\Spout\Reader\XLSX\Reader as XLSXReader;
+
+/**
+ * Class ReaderFactory
+ * This factory is used to create readers, based on the type of the file to be read.
+ * It supports CSV, XLSX and ODS formats.
+ */
+class ReaderFactory
+{
+    /**
+     * Creates a reader by file extension
+     *
+     * @param string $path The path to the spreadsheet file. Supported extensions are .csv,.ods and .xlsx
+     * @throws \Box\Spout\Common\Exception\UnsupportedTypeException
+     * @return ReaderInterface
+     */
+    public static function createFromFile(string $path)
+    {
+        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
+
+        return self::createFromType($extension);
+    }
+
+    /**
+     * This creates an instance of the appropriate reader, given the type of the file to be read
+     *
+     * @param  string $readerType Type of the reader to instantiate
+     * @throws \Box\Spout\Common\Exception\UnsupportedTypeException
+     * @return ReaderInterface
+     */
+    public static function createFromType($readerType)
+    {
+        switch ($readerType) {
+            case Type::CSV: return self::createCSVReader();
+            case Type::XLSX: return self::createXLSXReader();
+            case Type::ODS: return self::createODSReader();
+            default:
+                throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
+        }
+    }
+
+    /**
+     * @return CSVReader
+     */
+    private static function createCSVReader()
+    {
+        $optionsManager = new CSVOptionsManager();
+        $helperFactory = new HelperFactory();
+        $entityFactory = new CSVInternalEntityFactory($helperFactory);
+        $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
+
+        return new CSVReader($optionsManager, $globalFunctionsHelper, $entityFactory);
+    }
+
+    /**
+     * @return XLSXReader
+     */
+    private static function createXLSXReader()
+    {
+        $optionsManager = new XLSXOptionsManager();
+        $helperFactory = new XLSXHelperFactory();
+        $managerFactory = new XLSXManagerFactory($helperFactory, new CachingStrategyFactory());
+        $entityFactory = new XLSXInternalEntityFactory($managerFactory, $helperFactory);
+        $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
+
+        return new XLSXReader($optionsManager, $globalFunctionsHelper, $entityFactory, $managerFactory);
+    }
+
+    /**
+     * @return ODSReader
+     */
+    private static function createODSReader()
+    {
+        $optionsManager = new ODSOptionsManager();
+        $helperFactory = new ODSHelperFactory();
+        $managerFactory = new ODSManagerFactory();
+        $entityFactory = new ODSInternalEntityFactory($helperFactory, $managerFactory);
+        $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
+
+        return new ODSReader($optionsManager, $globalFunctionsHelper, $entityFactory);
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/Common/Entity/Options.php b/lib/spout/src/Spout/Reader/Common/Entity/Options.php
new file mode 100644 (file)
index 0000000..293d4c0
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+namespace Box\Spout\Reader\Common\Entity;
+
+/**
+ * Class Options
+ * Readers' options holder
+ */
+abstract class Options
+{
+    // Common options
+    const SHOULD_FORMAT_DATES = 'shouldFormatDates';
+    const SHOULD_PRESERVE_EMPTY_ROWS = 'shouldPreserveEmptyRows';
+
+    // CSV specific options
+    const FIELD_DELIMITER = 'fieldDelimiter';
+    const FIELD_ENCLOSURE = 'fieldEnclosure';
+    const ENCODING = 'encoding';
+
+    // XLSX specific options
+    const TEMP_FOLDER = 'tempFolder';
+    const SHOULD_USE_1904_DATES = 'shouldUse1904Dates';
+}
diff --git a/lib/spout/src/Spout/Reader/Common/Manager/RowManager.php b/lib/spout/src/Spout/Reader/Common/Manager/RowManager.php
new file mode 100644 (file)
index 0000000..ea76255
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+namespace Box\Spout\Reader\Common\Manager;
+
+use Box\Spout\Common\Entity\Row;
+use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
+
+/**
+ * Class RowManager
+ */
+class RowManager
+{
+    /** @var InternalEntityFactoryInterface Factory to create entities */
+    private $entityFactory;
+
+    /**
+     * @param InternalEntityFactoryInterface $entityFactory Factory to create entities
+     */
+    public function __construct(InternalEntityFactoryInterface $entityFactory)
+    {
+        $this->entityFactory = $entityFactory;
+    }
+
+    /**
+     * Detect whether a row is considered empty.
+     * An empty row has all of its cells empty.
+     *
+     * @param Row $row
+     * @return bool
+     */
+    public function isEmpty(Row $row)
+    {
+        foreach ($row->getCells() as $cell) {
+            if (!$cell->isEmpty()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Fills the missing indexes of a row with empty cells.
+     *
+     * @param Row $row
+     * @return Row
+     */
+    public function fillMissingIndexesWithEmptyCells(Row $row)
+    {
+        if ($row->getNumCells() === 0) {
+            return $row;
+        }
+
+        $rowCells = $row->getCells();
+        $maxCellIndex = max(array_keys($rowCells));
+
+        for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) {
+            if (!isset($rowCells[$cellIndex])) {
+                $row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
+            }
+        }
+
+        return $row;
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/Common/ReaderOptions.php b/lib/spout/src/Spout/Reader/Common/ReaderOptions.php
deleted file mode 100644 (file)
index 4ab7a07..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-namespace Box\Spout\Reader\Common;
-
-/**
- * Class ReaderOptions
- * Readers' common options
- *
- * @package Box\Spout\Reader\Common
- */
-class ReaderOptions
-{
-    /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
-    protected $shouldFormatDates = false;
-
-    /** @var bool Whether empty rows should be returned or skipped */
-    protected $shouldPreserveEmptyRows = false;
-
-    /**
-     * @return bool Whether date/time values should be returned as PHP objects or be formatted as strings.
-     */
-    public function shouldFormatDates()
-    {
-        return $this->shouldFormatDates;
-    }
-
-    /**
-     * Sets whether date/time values should be returned as PHP objects or be formatted as strings.
-     *
-     * @param bool $shouldFormatDates
-     * @return ReaderOptions
-     */
-    public function setShouldFormatDates($shouldFormatDates)
-    {
-        $this->shouldFormatDates = $shouldFormatDates;
-        return $this;
-    }
-
-    /**
-     * @return bool Whether empty rows should be returned or skipped.
-     */
-    public function shouldPreserveEmptyRows()
-    {
-        return $this->shouldPreserveEmptyRows;
-    }
-
-    /**
-     * Sets whether empty rows should be returned or skipped.
-     *
-     * @param bool $shouldPreserveEmptyRows
-     * @return ReaderOptions
-     */
-    public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
-    {
-        $this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows;
-        return $this;
-    }
-}
index d8a1da8..3b2d848 100644 (file)
@@ -7,8 +7,6 @@ use Box\Spout\Reader\Wrapper\XMLReader;
 /**
  * Class XMLProcessor
  * Helps process XML files
- *
- * @package Box\Spout\Reader\Common
  */
 class XMLProcessor
 {
@@ -24,14 +22,12 @@ class XMLProcessor
     const PROCESSING_CONTINUE = 1;
     const PROCESSING_STOP = 2;
 
-
     /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
     protected $xmlReader;
 
     /** @var array Registered callbacks */
     private $callbacks = [];
 
-
     /**
      * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object
      */
@@ -90,8 +86,8 @@ class XMLProcessor
      * Resumes the reading of the XML file where it was left off.
      * Stops whenever a callback indicates that reading should stop or at the end of the file.
      *
-     * @return void
      * @throws \Box\Spout\Reader\Exception\XMLProcessingException
+     * @return void
      */
     public function readUntilStopped()
     {
diff --git a/lib/spout/src/Spout/Reader/Exception/InvalidValueException.php b/lib/spout/src/Spout/Reader/Exception/InvalidValueException.php
new file mode 100644 (file)
index 0000000..6ed0b6d
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace Box\Spout\Reader\Exception;
+
+use Throwable;
+
+/**
+ * Class InvalidValueException
+ */
+class InvalidValueException extends ReaderException
+{
+    /** @var mixed */
+    private $invalidValue;
+
+    /**
+     * @param mixed $invalidValue
+     * @param string $message
+     * @param int $code
+     * @param Throwable|null $previous
+     */
+    public function __construct($invalidValue, $message = '', $code = 0, Throwable $previous = null)
+    {
+        $this->invalidValue = $invalidValue;
+        parent::__construct($message, $code, $previous);
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getInvalidValue()
+    {
+        return $this->invalidValue;
+    }
+}
index a030c12..ef23e1f 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Reader\Exception;
 
 /**
  * Class IteratorNotRewindableException
- *
- * @api
- * @package Box\Spout\Reader\Exception
  */
 class IteratorNotRewindableException extends ReaderException
 {
index 38d8772..b74a5ae 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Reader\Exception;
 
 /**
  * Class NoSheetsFoundException
- *
- * @api
- * @package Box\Spout\Reader\Exception
  */
 class NoSheetsFoundException extends ReaderException
 {
index ee9ca79..4b38192 100644 (file)
@@ -7,7 +7,6 @@ use Box\Spout\Common\Exception\SpoutException;
 /**
  * Class ReaderException
  *
- * @package Box\Spout\Reader\Exception
  * @abstract
  */
 abstract class ReaderException extends SpoutException
index 8f94de3..aa00ea3 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Reader\Exception;
 
 /**
  * Class ReaderNotOpenedException
- *
- * @api
- * @package Box\Spout\Reader\Exception
  */
 class ReaderNotOpenedException extends ReaderException
 {
index b494612..024b43d 100644 (file)
@@ -4,9 +4,6 @@ namespace Box\Spout\Reader\Exception;
 
 /**
  * Class SharedStringNotFoundException
- *
- * @api
- * @package Box\Spout\Reader\Exception
  */
 class SharedStringNotFoundException extends ReaderException
 {
index 70f630b..ee2602b 100644 (file)
@@ -4,8 +4,6 @@ namespace Box\Spout\Reader\Exception;
 
 /**
  * Class XMLProcessingException
- *
- * @package Box\Spout\Reader\Exception
  */
 class XMLProcessingException extends ReaderException
 {
index 7d58e28..4a9305d 100644 (file)
@@ -4,8 +4,6 @@ namespace Box\Spout\Reader;
 
 /**
  * Interface IteratorInterface
- *
- * @package Box\Spout\Reader
  */
 interface IteratorInterface extends \Iterator
 {
diff --git a/lib/spout/src/Spout/Reader/ODS/Creator/HelperFactory.php b/lib/spout/src/Spout/Reader/ODS/Creator/HelperFactory.php
new file mode 100644 (file)
index 0000000..43ec5e9
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+namespace Box\Spout\Reader\ODS\Creator;
+
+use Box\Spout\Reader\ODS\Helper\CellValueFormatter;
+use Box\Spout\Reader\ODS\Helper\SettingsHelper;
+
+/**
+ * Class HelperFactory
+ * Factory to create helpers
+ */
+class HelperFactory extends \Box\Spout\Common\Creator\HelperFactory
+{
+    /**
+     * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
+     * @return CellValueFormatter
+     */
+    public function createCellValueFormatter($shouldFormatDates)
+    {
+        $escaper = $this->createStringsEscaper();
+
+        return new CellValueFormatter($shouldFormatDates, $escaper);
+    }
+
+    /**
+     * @param InternalEntityFactory $entityFactory
+     * @return SettingsHelper
+     */
+    public function createSettingsHelper($entityFactory)
+    {
+        return new SettingsHelper($entityFactory);
+    }
+
+    /**
+     * @return \Box\Spout\Common\Helper\Escaper\ODS
+     */
+    public function createStringsEscaper()
+    {
+        /* @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+        return new \Box\Spout\Common\Helper\Escaper\ODS();
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/ODS/Creator/InternalEntityFactory.php b/lib/spout/src/Spout/Reader/ODS/Creator/InternalEntityFactory.php
new file mode 100644 (file)
index 0000000..cb06ce2
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+namespace Box\Spout\Reader\ODS\Creator;
+
+use Box\Spout\Common\Entity\Cell;
+use Box\Spout\Common\Entity\Row;
+use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
+use Box\Spout\Reader\Common\Entity\Options;
+use Box\Spout\Reader\Common\XMLProcessor;
+use Box\Spout\Reader\ODS\RowIterator;
+use Box\Spout\Reader\ODS\Sheet;
+use Box\Spout\Reader\ODS\SheetIterator;
+use Box\Spout\Reader\Wrapper\XMLReader;
+
+/**
+ * Class EntityFactory
+ * Factory to create entities
+ */
+class InternalEntityFactory implements InternalEntityFactoryInterface
+{
+    /** @var HelperFactory */
+    private $helperFactory;
+
+    /** @var ManagerFactory */
+    private $managerFactory;
+
+    /**
+     * @param HelperFactory $helperFactory
+     * @param ManagerFactory $managerFactory
+     */
+    public function __construct(HelperFactory $helperFactory, ManagerFactory $managerFactory)
+    {
+        $this->helperFactory = $helperFactory;
+        $this->managerFactory = $managerFactory;
+    }
+
+    /**
+     * @param string $filePath Path of the file to be read
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @return SheetIterator
+     */
+    public function createSheetIterator($filePath, $optionsManager)
+    {
+        $escaper = $this->helperFactory->createStringsEscaper();
+        $settingsHelper = $this->helperFactory->createSettingsHelper($this);
+
+        return new SheetIterator($filePath, $optionsManager, $escaper, $settingsHelper, $this);
+    }
+
+    /**
+     * @param XMLReader $xmlReader XML Reader
+     * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
+     * @param string $sheetName Name of the sheet
+     * @param bool $isSheetActive Whether the sheet was defined as active
+     * @param bool $isSheetVisible Whether the sheet is visible
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @return Sheet
+     */
+    public function createSheet($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible, $optionsManager)
+    {
+        $rowIterator = $this->createRowIterator($xmlReader, $optionsManager);
+
+        return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible);
+    }
+
+    /**
+     * @param XMLReader $xmlReader XML Reader
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @return RowIterator
+     */
+    private function createRowIterator($xmlReader, $optionsManager)
+    {
+        $shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES);
+        $cellValueFormatter = $this->helperFactory->createCellValueFormatter($shouldFormatDates);
+        $xmlProcessor = $this->createXMLProcessor($xmlReader);
+        $rowManager = $this->managerFactory->createRowManager($this);
+
+        return new RowIterator($xmlReader, $optionsManager, $cellValueFormatter, $xmlProcessor, $rowManager, $this);
+    }
+
+    /**
+     * @param Cell[] $cells
+     * @return Row
+     */
+    public function createRow(array $cells = [])
+    {
+        return new Row($cells, null);
+    }
+
+    /**
+     * @param mixed $cellValue
+     * @return Cell
+     */
+    public function createCell($cellValue)
+    {
+        return new Cell($cellValue);
+    }
+
+    /**
+     * @return XMLReader
+     */
+    public function createXMLReader()
+    {
+        return new XMLReader();
+    }
+
+    /**
+     * @param $xmlReader
+     * @return XMLProcessor
+     */
+    private function createXMLProcessor($xmlReader)
+    {
+        return new XMLProcessor($xmlReader);
+    }
+
+    /**
+     * @return \ZipArchive
+     */
+    public function createZipArchive()
+    {
+        return new \ZipArchive();
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/ODS/Creator/ManagerFactory.php b/lib/spout/src/Spout/Reader/ODS/Creator/ManagerFactory.php
new file mode 100644 (file)
index 0000000..3644d15
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace Box\Spout\Reader\ODS\Creator;
+
+use Box\Spout\Reader\Common\Manager\RowManager;
+
+/**
+ * Class ManagerFactory
+ * Factory to create managers
+ */
+class ManagerFactory
+{
+    /**
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @return RowManager
+     */
+    public function createRowManager($entityFactory)
+    {
+        return new RowManager($entityFactory);
+    }
+}
index 0c91410..50209ec 100644 (file)
@@ -2,11 +2,11 @@
 
 namespace Box\Spout\Reader\ODS\Helper;
 
+use Box\Spout\Reader\Exception\InvalidValueException;
+
 /**
  * Class CellValueFormatter
  * This class provides helper functions to format cell values
- *
- * @package Box\Spout\Reader\ODS\Helper
  */
 class CellValueFormatter
 {
@@ -38,18 +38,17 @@ class CellValueFormatter
     /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
     protected $shouldFormatDates;
 
-    /** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */
+    /** @var \Box\Spout\Common\Helper\Escaper\ODS Used to unescape XML data */
     protected $escaper;
 
     /**
      * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
+     * @param \Box\Spout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data
      */
-    public function __construct($shouldFormatDates)
+    public function __construct($shouldFormatDates, $escaper)
     {
         $this->shouldFormatDates = $shouldFormatDates;
-
-        /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
-        $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance();
+        $this->escaper = $escaper;
     }
 
     /**
@@ -57,7 +56,8 @@ class CellValueFormatter
      * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
      *
      * @param \DOMNode $node
-     * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error
+     * @throws InvalidValueException If the node value is not valid
+     * @return string|int|float|bool|\DateTime|\DateInterval The value associated with the cell, empty string if cell's type is void/undefined
      */
     public function extractAndFormatNodeValue($node)
     {
@@ -101,11 +101,11 @@ class CellValueFormatter
             foreach ($pNode->childNodes as $childNode) {
                 if ($childNode instanceof \DOMText) {
                     $currentPValue .= $childNode->nodeValue;
-                } else if ($childNode->nodeName === self::XML_NODE_S) {
+                } elseif ($childNode->nodeName === self::XML_NODE_S) {
                     $spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C);
-                    $numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1;
+                    $numSpaces = (!empty($spaceAttribute)) ? (int) $spaceAttribute : 1;
                     $currentPValue .= str_repeat(' ', $numSpaces);
-                } else if ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) {
+                } elseif ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) {
                     $currentPValue .= $childNode->nodeValue;
                 }
             }
@@ -115,6 +115,7 @@ class CellValueFormatter
 
         $escapedCellValue = implode("\n", $pNodeValues);
         $cellValue = $this->escaper->unescape($escapedCellValue);
+
         return $cellValue;
     }
 
@@ -127,9 +128,11 @@ class CellValueFormatter
     protected function formatFloatCellValue($node)
     {
         $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
-        $nodeIntValue = intval($nodeValue);
-        // The "==" is intentionally not a "===" because only the value matters, not the type
-        $cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
+
+        $nodeIntValue = (int) $nodeValue;
+        $nodeFloatValue = (float) $nodeValue;
+        $cellValue = ((float) $nodeIntValue === $nodeFloatValue) ? $nodeIntValue : $nodeFloatValue;
+
         return $cellValue;
     }
 
@@ -142,16 +145,16 @@ class CellValueFormatter
     protected function formatBooleanCellValue($node)
     {
         $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE);
-        // !! is similar to boolval()
-        $cellValue = !!$nodeValue;
-        return $cellValue;
+
+        return (bool) $nodeValue;
     }
 
     /**
      * Returns the cell Date value from the given node.
      *
      * @param \DOMNode $node
-     * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
+     * @throws InvalidValueException If the value is not a valid date
+     * @return \DateTime|string The value associated with the cell
      */
     protected function formatDateCellValue($node)
     {
@@ -163,23 +166,26 @@ class CellValueFormatter
         if ($this->shouldFormatDates) {
             // The date is already formatted in the "p" tag
             $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
-            return $nodeWithValueAlreadyFormatted->nodeValue;
+            $cellValue = $nodeWithValueAlreadyFormatted->nodeValue;
         } else {
             // otherwise, get it from the "date-value" attribute
+            $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
             try {
-                $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
-                return new \DateTime($nodeValue);
+                $cellValue = new \DateTime($nodeValue);
             } catch (\Exception $e) {
-                return null;
+                throw new InvalidValueException($nodeValue);
             }
         }
+
+        return $cellValue;
     }
 
     /**
      * Returns the cell Time value from the given node.
      *
      * @param \DOMNode $node
-     * @return \DateInterval|string|null The value associated with the cell or NULL if invalid time value
+     * @throws InvalidValueException If the value is not a valid time
+     * @return \DateInterval|string The value associated with the cell
      */
     protected function formatTimeCellValue($node)
     {
@@ -191,16 +197,18 @@ class CellValueFormatter
         if ($this->shouldFormatDates) {
             // The date is already formatted in the "p" tag
             $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
-            return $nodeWithValueAlreadyFormatted->nodeValue;
+            $cellValue = $nodeWithValueAlreadyFormatted->nodeValue;
         } else {
             // otherwise, get it from the "time-value" attribute
+            $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
             try {
-                $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
-                return new \DateInterval($nodeValue);
+                $cellValue = new \DateInterval($nodeValue);
             } catch (\Exception $e) {
-                return null;
+                throw new InvalidValueException($nodeValue);
             }
         }
+
+        return $cellValue;
     }
 
     /**
index a5388ef..7d47821 100644 (file)
@@ -3,13 +3,11 @@
 namespace Box\Spout\Reader\ODS\Helper;
 
 use Box\Spout\Reader\Exception\XMLProcessingException;
-use Box\Spout\Reader\Wrapper\XMLReader;
+use Box\Spout\Reader\ODS\Creator\InternalEntityFactory;
 
 /**
  * Class SettingsHelper
  * This class provides helper functions to extract data from the "settings.xml" file.
- *
- * @package Box\Spout\Reader\ODS\Helper
  */
 class SettingsHelper
 {
@@ -20,13 +18,24 @@ class SettingsHelper
     const XML_ATTRIBUTE_CONFIG_NAME = 'config:name';
     const XML_ATTRIBUTE_VALUE_ACTIVE_TABLE = 'ActiveTable';
 
+    /** @var InternalEntityFactory Factory to create entities */
+    private $entityFactory;
+
+    /**
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     */
+    public function __construct($entityFactory)
+    {
+        $this->entityFactory = $entityFactory;
+    }
+
     /**
      * @param string $filePath Path of the file to be read
      * @return string|null Name of the sheet that was defined as active or NULL if none found
      */
     public function getActiveSheetName($filePath)
     {
-        $xmlReader = new XMLReader();
+        $xmlReader = $this->entityFactory->createXMLReader();
         if ($xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) {
             return null;
         }
diff --git a/lib/spout/src/Spout/Reader/ODS/Manager/OptionsManager.php b/lib/spout/src/Spout/Reader/ODS/Manager/OptionsManager.php
new file mode 100644 (file)
index 0000000..102eccb
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+namespace Box\Spout\Reader\ODS\Manager;
+
+use Box\Spout\Common\Manager\OptionsManagerAbstract;
+use Box\Spout\Reader\Common\Entity\Options;
+
+/**
+ * Class OptionsManager
+ * ODS Reader options manager
+ */
+class OptionsManager extends OptionsManagerAbstract
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getSupportedOptions()
+    {
+        return [
+            Options::SHOULD_FORMAT_DATES,
+            Options::SHOULD_PRESERVE_EMPTY_ROWS,
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function setDefaultOptions()
+    {
+        $this->setOption(Options::SHOULD_FORMAT_DATES, false);
+        $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
+    }
+}
index dbdc47b..d07dbb4 100644 (file)
@@ -3,15 +3,14 @@
 namespace Box\Spout\Reader\ODS;
 
 use Box\Spout\Common\Exception\IOException;
-use Box\Spout\Reader\AbstractReader;
+use Box\Spout\Reader\ODS\Creator\InternalEntityFactory;
+use Box\Spout\Reader\ReaderAbstract;
 
 /**
  * Class Reader
  * This class provides support to read data from a ODS file
- *
- * @package Box\Spout\Reader\ODS
  */
-class Reader extends AbstractReader
+class Reader extends ReaderAbstract
 {
     /** @var \ZipArchive */
     protected $zip;
@@ -19,19 +18,6 @@ class Reader extends AbstractReader
     /** @var SheetIterator To iterator over the ODS sheets */
     protected $sheetIterator;
 
-    /**
-     * Returns the reader's current options
-     *
-     * @return ReaderOptions
-     */
-    protected function getOptions()
-    {
-        if (!isset($this->options)) {
-            $this->options = new ReaderOptions();
-        }
-        return $this->options;
-    }
-
     /**
      * Returns whether stream wrappers are supported
      *
@@ -46,16 +32,21 @@ class Reader extends AbstractReader
      * Opens the file at the given file path to make it ready to be read.
      *
      * @param  string $filePath Path of the file to be read
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
      * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
+     * @return void
      */
     protected function openReader($filePath)
     {
-        $this->zip = new \ZipArchive();
+        /** @var InternalEntityFactory $entityFactory */
+        $entityFactory = $this->entityFactory;
+
+        $this->zip = $entityFactory->createZipArchive();
 
         if ($this->zip->open($filePath) === true) {
-            $this->sheetIterator = new SheetIterator($filePath, $this->getOptions());
+            /** @var InternalEntityFactory $entityFactory */
+            $entityFactory = $this->entityFactory;
+            $this->sheetIterator = $entityFactory->createSheetIterator($filePath, $this->optionsManager);
         } else {
             throw new IOException("Could not open $filePath for reading.");
         }
diff --git a/lib/spout/src/Spout/Reader/ODS/ReaderOptions.php b/lib/spout/src/Spout/Reader/ODS/ReaderOptions.php
deleted file mode 100644 (file)
index 2d29640..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Box\Spout\Reader\ODS;
-
-/**
- * Class ReaderOptions
- * This class is used to customize the reader's behavior
- *
- * @package Box\Spout\Reader\ODS
- */
-class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
-{
-    // No extra options
-}
index 46ff33e..81f0953 100644 (file)
@@ -2,18 +2,23 @@
 
 namespace Box\Spout\Reader\ODS;
 
+use Box\Spout\Common\Entity\Cell;
+use Box\Spout\Common\Entity\Row;
 use Box\Spout\Common\Exception\IOException;
+use Box\Spout\Common\Manager\OptionsManagerInterface;
+use Box\Spout\Reader\Common\Entity\Options;
+use Box\Spout\Reader\Common\Manager\RowManager;
+use Box\Spout\Reader\Common\XMLProcessor;
+use Box\Spout\Reader\Exception\InvalidValueException;
 use Box\Spout\Reader\Exception\IteratorNotRewindableException;
 use Box\Spout\Reader\Exception\XMLProcessingException;
 use Box\Spout\Reader\IteratorInterface;
+use Box\Spout\Reader\ODS\Creator\InternalEntityFactory;
 use Box\Spout\Reader\ODS\Helper\CellValueFormatter;
 use Box\Spout\Reader\Wrapper\XMLReader;
-use Box\Spout\Reader\Common\XMLProcessor;
 
 /**
  * Class RowIterator
- *
- * @package Box\Spout\Reader\ODS
  */
 class RowIterator implements IteratorInterface
 {
@@ -39,14 +44,20 @@ class RowIterator implements IteratorInterface
     /** @var Helper\CellValueFormatter Helper to format cell values */
     protected $cellValueFormatter;
 
+    /** @var RowManager Manages rows */
+    protected $rowManager;
+
+    /** @var InternalEntityFactory Factory to create entities */
+    protected $entityFactory;
+
     /** @var bool Whether the iterator has already been rewound once */
     protected $hasAlreadyBeenRewound = false;
 
-    /** @var array Contains the data for the currently processed row (key = cell index, value = cell value) */
-    protected $currentlyProcessedRowData = [];
+    /** @var Row The currently processed row */
+    protected $currentlyProcessedRow;
 
-    /** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
-    protected $rowDataBuffer = null;
+    /** @var Row Buffer used to store the current row, while checking if there are more rows to read */
+    protected $rowBuffer;
 
     /** @var bool Indicates whether all rows have been read */
     protected $hasReachedEndOfFile = false;
@@ -57,8 +68,8 @@ class RowIterator implements IteratorInterface
     /** @var int Row index to be processed next (one-based) */
     protected $nextRowIndexToBeProcessed = 1;
 
-    /** @var mixed|null Value of the last processed cell (because when reading cell at column N+1, cell N is processed) */
-    protected $lastProcessedCellValue = null;
+    /** @var Cell Last processed cell (because when reading cell at column N+1, cell N is processed) */
+    protected $lastProcessedCell;
 
     /** @var int Number of times the last processed row should be repeated */
     protected $numRowsRepeated = 1;
@@ -69,19 +80,30 @@ class RowIterator implements IteratorInterface
     /** @var bool Whether at least one cell has been read for the row currently being processed */
     protected $hasAlreadyReadOneCellInCurrentRow = false;
 
-
     /**
      * @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
-     * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
+     * @param OptionsManagerInterface $optionsManager Reader's options manager
+     * @param CellValueFormatter $cellValueFormatter Helper to format cell values
+     * @param XMLProcessor $xmlProcessor Helper to process XML files
+     * @param RowManager $rowManager Manages rows
+     * @param InternalEntityFactory $entityFactory Factory to create entities
      */
-    public function __construct($xmlReader, $options)
-    {
+    public function __construct(
+        XMLReader $xmlReader,
+        OptionsManagerInterface $optionsManager,
+        CellValueFormatter $cellValueFormatter,
+        XMLProcessor $xmlProcessor,
+        RowManager $rowManager,
+        InternalEntityFactory $entityFactory
+    ) {
         $this->xmlReader = $xmlReader;
-        $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
-        $this->cellValueFormatter = new CellValueFormatter($options->shouldFormatDates());
+        $this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
+        $this->cellValueFormatter = $cellValueFormatter;
+        $this->entityFactory = $entityFactory;
+        $this->rowManager = $rowManager;
 
         // Register all callbacks to process different nodes when reading the XML file
-        $this->xmlProcessor = new XMLProcessor($this->xmlReader);
+        $this->xmlProcessor = $xmlProcessor;
         $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
         $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
         $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
@@ -91,10 +113,10 @@ class RowIterator implements IteratorInterface
     /**
      * Rewind the Iterator to the first element.
      * NOTE: It can only be done once, as it is not possible to read an XML file backwards.
-     * @link http://php.net/manual/en/iterator.rewind.php
+     * @see http://php.net/manual/en/iterator.rewind.php
      *
-     * @return void
      * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
+     * @return void
      */
     public function rewind()
     {
@@ -108,7 +130,7 @@ class RowIterator implements IteratorInterface
         $this->hasAlreadyBeenRewound = true;
         $this->lastRowIndexProcessed = 0;
         $this->nextRowIndexToBeProcessed = 1;
-        $this->rowDataBuffer = null;
+        $this->rowBuffer = null;
         $this->hasReachedEndOfFile = false;
 
         $this->next();
@@ -116,7 +138,7 @@ class RowIterator implements IteratorInterface
 
     /**
      * Checks if current position is valid
-     * @link http://php.net/manual/en/iterator.valid.php
+     * @see http://php.net/manual/en/iterator.valid.php
      *
      * @return bool
      */
@@ -127,11 +149,11 @@ class RowIterator implements IteratorInterface
 
     /**
      * Move forward to next element. Empty rows will be skipped.
-     * @link http://php.net/manual/en/iterator.next.php
+     * @see http://php.net/manual/en/iterator.next.php
      *
-     * @return void
      * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
      * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
+     * @return void
      */
     public function next()
     {
@@ -162,13 +184,13 @@ class RowIterator implements IteratorInterface
     }
 
     /**
-     * @return void
      * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
      * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
+     * @return void
      */
     protected function readDataForNextRow()
     {
-        $this->currentlyProcessedRowData = [];
+        $this->currentlyProcessedRow = $this->entityFactory->createRow();
 
         try {
             $this->xmlProcessor->readUntilStopped();
@@ -176,7 +198,7 @@ class RowIterator implements IteratorInterface
             throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
         }
 
-        $this->rowDataBuffer = $this->currentlyProcessedRowData;
+        $this->rowBuffer = $this->currentlyProcessedRow;
     }
 
     /**
@@ -187,7 +209,7 @@ class RowIterator implements IteratorInterface
     {
         // Reset data from current row
         $this->hasAlreadyReadOneCellInCurrentRow = false;
-        $this->lastProcessedCellValue = null;
+        $this->lastProcessedCell = null;
         $this->numColumnsRepeated = 1;
         $this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader);
 
@@ -204,17 +226,17 @@ class RowIterator implements IteratorInterface
 
         // NOTE: expand() will automatically decode all XML entities of the child nodes
         $node = $xmlReader->expand();
-        $currentCellValue = $this->getCellValue($node);
+        $currentCell = $this->getCell($node);
 
         // process cell N only after having read cell N+1 (see below why)
         if ($this->hasAlreadyReadOneCellInCurrentRow) {
             for ($i = 0; $i < $this->numColumnsRepeated; $i++) {
-                $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
+                $this->currentlyProcessedRow->addCell($this->lastProcessedCell);
             }
         }
 
         $this->hasAlreadyReadOneCellInCurrentRow = true;
-        $this->lastProcessedCellValue = $currentCellValue;
+        $this->lastProcessedCell = $currentCell;
         $this->numColumnsRepeated = $currentNumColumnsRepeated;
 
         return XMLProcessor::PROCESSING_CONTINUE;
@@ -225,7 +247,7 @@ class RowIterator implements IteratorInterface
      */
     protected function processRowEndingNode()
     {
-        $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRowData, $this->lastProcessedCellValue);
+        $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRow, $this->lastProcessedCell);
 
         // if the fetched row is empty and we don't want to preserve it...
         if (!$this->shouldPreserveEmptyRows && $isEmptyRow) {
@@ -235,6 +257,7 @@ class RowIterator implements IteratorInterface
 
         // if the row is empty, we don't want to return more than one cell
         $actualNumColumnsRepeated = (!$isEmptyRow) ? $this->numColumnsRepeated : 1;
+        $numCellsInCurrentlyProcessedRow = $this->currentlyProcessedRow->getNumCells();
 
         // Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
         // The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData".
@@ -242,9 +265,9 @@ class RowIterator implements IteratorInterface
         // with a number-columns-repeated value equals to the number of (supported columns - used columns).
         // In Excel, the number of supported columns is 16384, but we don't want to returns rows with
         // always 16384 cells.
-        if ((count($this->currentlyProcessedRowData) + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
+        if (($numCellsInCurrentlyProcessedRow + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
             for ($i = 0; $i < $actualNumColumnsRepeated; $i++) {
-                $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
+                $this->currentlyProcessedRow->addCell($this->lastProcessedCell);
             }
         }
 
@@ -275,7 +298,8 @@ class RowIterator implements IteratorInterface
     protected function getNumRowsRepeatedForCurrentNode($xmlReader)
     {
         $numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
-        return ($numRowsRepeated !== null) ? intval($numRowsRepeated) : 1;
+
+        return ($numRowsRepeated !== null) ? (int) $numRowsRepeated : 1;
     }
 
     /**
@@ -285,52 +309,61 @@ class RowIterator implements IteratorInterface
     protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
     {
         $numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
-        return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1;
+
+        return ($numColumnsRepeated !== null) ? (int) $numColumnsRepeated : 1;
     }
 
     /**
-     * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
+     * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
      *
      * @param \DOMNode $node
-     * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error
+     * @return Cell The cell set with the associated with the cell
      */
-    protected function getCellValue($node)
+    protected function getCell($node)
     {
-        return $this->cellValueFormatter->extractAndFormatNodeValue($node);
+        try {
+            $cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node);
+            $cell = $this->entityFactory->createCell($cellValue);
+        } catch (InvalidValueException $exception) {
+            $cell = $this->entityFactory->createCell($exception->getInvalidValue());
+            $cell->setType(Cell::TYPE_ERROR);
+        }
+
+        return $cell;
     }
 
     /**
      * After finishing processing each cell, a row is considered empty if it contains
-     * no cells or if the value of the last read cell is an empty string.
+     * no cells or if the last read cell is empty.
      * After finishing processing each cell, the last read cell is not part of the
      * row data yet (as we still need to apply the "num-columns-repeated" attribute).
      *
-     * @param array $rowData
-     * @param string|int|float|bool|\DateTime|\DateInterval|null The value of the last read cell
+     * @param Row $currentRow
+     * @param Cell $lastReadCell The last read cell
      * @return bool Whether the row is empty
      */
-    protected function isEmptyRow($rowData, $lastReadCellValue)
+    protected function isEmptyRow($currentRow, $lastReadCell)
     {
         return (
-            count($rowData) === 0 &&
-            (!isset($lastReadCellValue) || trim($lastReadCellValue) === '')
+            $this->rowManager->isEmpty($currentRow) &&
+            (!isset($lastReadCell) || $lastReadCell->isEmpty())
         );
     }
 
     /**
      * Return the current element, from the buffer.
-     * @link http://php.net/manual/en/iterator.current.php
+     * @see http://php.net/manual/en/iterator.current.php
      *
-     * @return array|null
+     * @return Row
      */
     public function current()
     {
-        return $this->rowDataBuffer;
+        return $this->rowBuffer;
     }
 
     /**
      * Return the key of the current element
-     * @link http://php.net/manual/en/iterator.key.php
+     * @see http://php.net/manual/en/iterator.key.php
      *
      * @return int
      */
@@ -339,7 +372,6 @@ class RowIterator implements IteratorInterface
         return $this->lastRowIndexProcessed;
     }
 
-
     /**
      * Cleans up what was created to iterate over the object.
      *
index 794ad3a..74ec61f 100644 (file)
@@ -3,13 +3,10 @@
 namespace Box\Spout\Reader\ODS;
 
 use Box\Spout\Reader\SheetInterface;
-use Box\Spout\Reader\Wrapper\XMLReader;
 
 /**
  * Class Sheet
  * Represents a sheet within a ODS file
- *
- * @package Box\Spout\Reader\ODS
  */
 class Sheet implements SheetInterface
 {
@@ -28,23 +25,26 @@ class Sheet implements SheetInterface
     /** @var bool Whether the sheet was the active one */
     protected $isActive;
 
+    /** @var bool Whether the sheet is visible */
+    protected $isVisible;
+
     /**
-     * @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
+     * @param RowIterator $rowIterator The corresponding row iterator
      * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
      * @param string $sheetName Name of the sheet
      * @param bool $isSheetActive Whether the sheet was defined as active
-     * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
+     * @param bool $isSheetVisible Whether the sheet is visible
      */
-    public function __construct($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $options)
+    public function __construct($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible)
     {
-        $this->rowIterator = new RowIterator($xmlReader, $options);
+        $this->rowIterator = $rowIterator;
         $this->index = $sheetIndex;
         $this->name = $sheetName;
         $this->isActive = $isSheetActive;
+        $this->isVisible = $isSheetVisible;
     }
 
     /**
-     * @api
      * @return \Box\Spout\Reader\ODS\RowIterator
      */
     public function getRowIterator()
@@ -53,7 +53,6 @@ class Sheet implements SheetInterface
     }
 
     /**
-     * @api
      * @return int Index of the sheet, based on order in the workbook (zero-based)
      */
     public function getIndex()
@@ -62,7 +61,6 @@ class Sheet implements SheetInterface
     }
 
     /**
-     * @api
      * @return string Name of the sheet
      */
     public function getName()
@@ -71,11 +69,18 @@ class Sheet implements SheetInterface
     }
 
     /**
-     * @api
      * @return bool Whether the sheet was defined as active
      */
     public function isActive()
     {
         return $this->isActive;
     }
+
+    /**
+     * @return bool Whether the sheet is visible
+     */
+    public function isVisible()
+    {
+        return $this->isVisible;
+    }
 }
index 995c136..f35b852 100644 (file)
@@ -5,33 +5,42 @@ namespace Box\Spout\Reader\ODS;
 use Box\Spout\Common\Exception\IOException;
 use Box\Spout\Reader\Exception\XMLProcessingException;
 use Box\Spout\Reader\IteratorInterface;
+use Box\Spout\Reader\ODS\Creator\InternalEntityFactory;
 use Box\Spout\Reader\ODS\Helper\SettingsHelper;
 use Box\Spout\Reader\Wrapper\XMLReader;
 
 /**
  * Class SheetIterator
  * Iterate over ODS sheet.
- *
- * @package Box\Spout\Reader\ODS
  */
 class SheetIterator implements IteratorInterface
 {
     const CONTENT_XML_FILE_PATH = 'content.xml';
 
+    const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0';
+
     /** Definition of XML nodes name and attribute used to parse sheet data */
+    const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles';
+    const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties';
     const XML_NODE_TABLE = 'table:table';
+    const XML_ATTRIBUTE_STYLE_NAME = 'style:name';
     const XML_ATTRIBUTE_TABLE_NAME = 'table:name';
+    const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name';
+    const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display';
 
     /** @var string $filePath Path of the file to be read */
     protected $filePath;
 
-    /** @var \Box\Spout\Reader\ODS\ReaderOptions Reader's current options */
-    protected $options;
+    /** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */
+    protected $optionsManager;
+
+    /** @var InternalEntityFactory $entityFactory Factory to create entities */
+    protected $entityFactory;
 
     /** @var XMLReader The XMLReader object that will help read sheet's XML data */
     protected $xmlReader;
 
-    /** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */
+    /** @var \Box\Spout\Common\Helper\Escaper\ODS Used to unescape XML data */
     protected $escaper;
 
     /** @var bool Whether there are still at least a sheet to be read */
@@ -43,30 +52,32 @@ class SheetIterator implements IteratorInterface
     /** @var string The name of the sheet that was defined as active */
     protected $activeSheetName;
 
+    /** @var array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */
+    protected $sheetsVisibility;
+
     /**
      * @param string $filePath Path of the file to be read
-     * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
-     * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager
+     * @param \Box\Spout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data
+     * @param SettingsHelper $settingsHelper Helper to get data from "settings.xml"
+     * @param InternalEntityFactory $entityFactory Factory to create entities
      */
-    public function __construct($filePath, $options)
+    public function __construct($filePath, $optionsManager, $escaper, $settingsHelper, $entityFactory)
     {
         $this->filePath = $filePath;
-        $this->options = $options;
-        $this->xmlReader = new XMLReader();
-
-        /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
-        $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance();
-
-        $settingsHelper = new SettingsHelper();
+        $this->optionsManager = $optionsManager;
+        $this->entityFactory = $entityFactory;
+        $this->xmlReader = $entityFactory->createXMLReader();
+        $this->escaper = $escaper;
         $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
     }
 
     /**
      * Rewind the Iterator to the first element
-     * @link http://php.net/manual/en/iterator.rewind.php
+     * @see http://php.net/manual/en/iterator.rewind.php
      *
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data
+     * @return void
      */
     public function rewind()
     {
@@ -78,17 +89,45 @@ class SheetIterator implements IteratorInterface
         }
 
         try {
+            $this->sheetsVisibility = $this->readSheetsVisibility();
             $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
         } catch (XMLProcessingException $exception) {
-           throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
-       }
+            throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
+        }
 
         $this->currentSheetIndex = 0;
     }
 
+    /**
+     * Extracts the visibility of the sheets
+     *
+     * @return array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE]
+     */
+    private function readSheetsVisibility()
+    {
+        $sheetsVisibility = [];
+
+        $this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES);
+        $automaticStylesNode = $this->xmlReader->expand();
+
+        $tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES);
+
+        /** @var \DOMElement $tableStyleNode */
+        foreach ($tableStyleNodes as $tableStyleNode) {
+            $isSheetVisible = ($tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY) !== 'false');
+
+            $parentStyleNode = $tableStyleNode->parentNode;
+            $styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME);
+
+            $sheetsVisibility[$styleName] = $isSheetVisible;
+        }
+
+        return $sheetsVisibility;
+    }
+
     /**
      * Checks if current position is valid
-     * @link http://php.net/manual/en/iterator.valid.php
+     * @see http://php.net/manual/en/iterator.valid.php
      *
      * @return bool
      */
@@ -99,7 +138,7 @@ class SheetIterator implements IteratorInterface
 
     /**
      * Move forward to next element
-     * @link http://php.net/manual/en/iterator.next.php
+     * @see http://php.net/manual/en/iterator.next.php
      *
      * @return void
      */
@@ -114,7 +153,7 @@ class SheetIterator implements IteratorInterface
 
     /**
      * Return the current element
-     * @link http://php.net/manual/en/iterator.current.php
+     * @see http://php.net/manual/en/iterator.current.php
      *
      * @return \Box\Spout\Reader\ODS\Sheet
      */
@@ -122,9 +161,20 @@ class SheetIterator implements IteratorInterface
     {
         $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
         $sheetName = $this->escaper->unescape($escapedSheetName);
-        $isActiveSheet = $this->isActiveSheet($sheetName, $this->currentSheetIndex, $this->activeSheetName);
 
-        return new Sheet($this->xmlReader, $this->currentSheetIndex, $sheetName, $isActiveSheet, $this->options);
+        $isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName);
+
+        $sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME);
+        $isSheetVisible = $this->isSheetVisible($sheetStyleName);
+
+        return $this->entityFactory->createSheet(
+            $this->xmlReader,
+            $this->currentSheetIndex,
+            $sheetName,
+            $isSheetActive,
+            $isSheetVisible,
+            $this->optionsManager
+        );
     }
 
     /**
@@ -132,10 +182,10 @@ class SheetIterator implements IteratorInterface
      *
      * @param string $sheetName Name of the current sheet
      * @param int $sheetIndex Index of the current sheet
-     * @param string|null Name of the sheet that was defined as active or NULL if none defined
+     * @param string|null $activeSheetName Name of the sheet that was defined as active or NULL if none defined
      * @return bool Whether the current sheet was defined as the active one
      */
-    private function isActiveSheet($sheetName, $sheetIndex, $activeSheetName)
+    private function isSheetActive($sheetName, $sheetIndex, $activeSheetName)
     {
         // The given sheet is active if its name matches the defined active sheet's name
         // or if no information about the active sheet was found, it defaults to the first sheet.
@@ -145,9 +195,22 @@ class SheetIterator implements IteratorInterface
         );
     }
 
+    /**
+     * Returns whether the current sheet is visible
+     *
+     * @param string $sheetStyleName Name of the sheet style
+     * @return bool Whether the current sheet is visible
+     */
+    private function isSheetVisible($sheetStyleName)
+    {
+        return isset($this->sheetsVisibility[$sheetStyleName]) ?
+            $this->sheetsVisibility[$sheetStyleName] :
+            true;
+    }
+
     /**
      * Return the key of the current element
-     * @link http://php.net/manual/en/iterator.key.php
+     * @see http://php.net/manual/en/iterator.key.php
      *
      * @return int
      */
@@ -3,31 +3,30 @@
 namespace Box\Spout\Reader;
 
 use Box\Spout\Common\Exception\IOException;
+use Box\Spout\Common\Helper\GlobalFunctionsHelper;
+use Box\Spout\Common\Manager\OptionsManagerInterface;
+use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
+use Box\Spout\Reader\Common\Entity\Options;
 use Box\Spout\Reader\Exception\ReaderNotOpenedException;
 
 /**
- * Class AbstractReader
+ * Class ReaderAbstract
  *
- * @package Box\Spout\Reader
  * @abstract
  */
-abstract class AbstractReader implements ReaderInterface
+abstract class ReaderAbstract implements ReaderInterface
 {
     /** @var bool Indicates whether the stream is currently open */
     protected $isStreamOpened = false;
 
+    /** @var InternalEntityFactoryInterface Factory to create entities */
+    protected $entityFactory;
+
     /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
     protected $globalFunctionsHelper;
 
-    /** @var \Box\Spout\Reader\Common\ReaderOptions Reader's customized options */
-    protected $options;
-
-    /**
-     * Returns the reader's current options
-     *
-     * @return \Box\Spout\Reader\Common\ReaderOptions
-     */
-    abstract protected function getOptions();
+    /** @var OptionsManagerInterface Writer options manager */
+    protected $optionsManager;
 
     /**
      * Returns whether stream wrappers are supported
@@ -47,50 +46,55 @@ abstract class AbstractReader implements ReaderInterface
     /**
      * Returns an iterator to iterate over sheets.
      *
-     * @return \Iterator To iterate over sheets
+     * @return IteratorInterface To iterate over sheets
      */
     abstract protected function getConcreteSheetIterator();
 
     /**
      * Closes the reader. To be used after reading the file.
      *
-     * @return AbstractReader
+     * @return ReaderAbstract
      */
     abstract protected function closeReader();
 
     /**
-     * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
-     * @return AbstractReader
+     * @param OptionsManagerInterface $optionsManager
+     * @param GlobalFunctionsHelper $globalFunctionsHelper
+     * @param InternalEntityFactoryInterface $entityFactory
      */
-    public function setGlobalFunctionsHelper($globalFunctionsHelper)
-    {
+    public function __construct(
+        OptionsManagerInterface $optionsManager,
+        GlobalFunctionsHelper $globalFunctionsHelper,
+        InternalEntityFactoryInterface $entityFactory
+    ) {
+        $this->optionsManager = $optionsManager;
         $this->globalFunctionsHelper = $globalFunctionsHelper;
-        return $this;
+        $this->entityFactory = $entityFactory;
     }
 
     /**
      * Sets whether date/time values should be returned as PHP objects or be formatted as strings.
      *
-     * @api
      * @param bool $shouldFormatDates
-     * @return AbstractReader
+     * @return ReaderAbstract
      */
     public function setShouldFormatDates($shouldFormatDates)
     {
-        $this->getOptions()->setShouldFormatDates($shouldFormatDates);
+        $this->optionsManager->setOption(Options::SHOULD_FORMAT_DATES, $shouldFormatDates);
+
         return $this;
     }
 
     /**
      * Sets whether empty rows should be returned or skipped.
      *
-     * @api
      * @param bool $shouldPreserveEmptyRows
-     * @return AbstractReader
+     * @return ReaderAbstract
      */
     public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
     {
-        $this->getOptions()->setShouldPreserveEmptyRows($shouldPreserveEmptyRows);
+        $this->optionsManager->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, $shouldPreserveEmptyRows);
+
         return $this;
     }
 
@@ -98,10 +102,9 @@ abstract class AbstractReader implements ReaderInterface
      * Prepares the reader to read the given file. It also makes sure
      * that the file exists and is readable.
      *
-     * @api
      * @param  string $filePath Path of the file to be read
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
+     * @return void
      */
     public function open($filePath)
     {
@@ -113,7 +116,8 @@ abstract class AbstractReader implements ReaderInterface
             // we skip the checks if the provided file path points to a PHP stream
             if (!$this->globalFunctionsHelper->file_exists($filePath)) {
                 throw new IOException("Could not open $filePath for reading! File does not exist.");
-            } else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
+            }
+            if (!$this->globalFunctionsHelper->is_readable($filePath)) {
                 throw new IOException("Could not open $filePath for reading! File is not readable.");
             }
         }
@@ -157,6 +161,7 @@ abstract class AbstractReader implements ReaderInterface
         if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
             $streamScheme = $matches[1];
         }
+
         return $streamScheme;
     }
 
@@ -183,6 +188,7 @@ abstract class AbstractReader implements ReaderInterface
     protected function isSupportedStreamWrapper($filePath)
     {
         $streamScheme = $this->getStreamWrapperScheme($filePath);
+
         return ($streamScheme !== null) ?
             in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) :
             true;
@@ -197,15 +203,15 @@ abstract class AbstractReader implements ReaderInterface
     protected function isPhpStream($filePath)
     {
         $streamScheme = $this->getStreamWrapperScheme($filePath);
+
         return ($streamScheme === 'php');
     }
 
     /**
      * Returns an iterator to iterate over sheets.
      *
-     * @api
-     * @return \Iterator To iterate over sheets
      * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
+     * @return \Iterator To iterate over sheets
      */
     public function getSheetIterator()
     {
@@ -219,7 +225,6 @@ abstract class AbstractReader implements ReaderInterface
     /**
      * Closes the reader, preventing any additional reading
      *
-     * @api
      * @return void
      */
     public function close()
diff --git a/lib/spout/src/Spout/Reader/ReaderFactory.php b/lib/spout/src/Spout/Reader/ReaderFactory.php
deleted file mode 100644 (file)
index 93a52cb..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-namespace Box\Spout\Reader;
-
-use Box\Spout\Common\Exception\UnsupportedTypeException;
-use Box\Spout\Common\Helper\GlobalFunctionsHelper;
-use Box\Spout\Common\Type;
-
-/**
- * Class ReaderFactory
- * This factory is used to create readers, based on the type of the file to be read.
- * It supports CSV and XLSX formats.
- *
- * @package Box\Spout\Reader
- */
-class ReaderFactory
-{
-    /**
-     * This creates an instance of the appropriate reader, given the type of the file to be read
-     *
-     * @api
-     * @param  string $readerType Type of the reader to instantiate
-     * @return ReaderInterface
-     * @throws \Box\Spout\Common\Exception\UnsupportedTypeException
-     */
-    public static function create($readerType)
-    {
-        $reader = null;
-
-        switch ($readerType) {
-            case Type::CSV:
-                $reader = new CSV\Reader();
-                break;
-            case Type::XLSX:
-                $reader = new XLSX\Reader();
-                break;
-            case Type::ODS:
-                $reader = new ODS\Reader();
-                break;
-            default:
-                throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
-        }
-
-        $reader->setGlobalFunctionsHelper(new GlobalFunctionsHelper());
-
-        return $reader;
-    }
-}
index 8ecde30..74bcc4a 100644 (file)
@@ -4,8 +4,6 @@ namespace Box\Spout\Reader;
 
 /**
  * Interface ReaderInterface
- *
- * @package Box\Spout\Reader
  */
 interface ReaderInterface
 {
@@ -14,16 +12,16 @@ interface ReaderInterface
      * that the file exists and is readable.
      *
      * @param  string $filePath Path of the file to be read
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException
+     * @return void
      */
     public function open($filePath);
 
     /**
      * Returns an iterator to iterate over sheets.
      *
-     * @return \Iterator To iterate over sheets
      * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
+     * @return \Iterator To iterate over sheets
      */
     public function getSheetIterator();
 
index bc19078..f77ecdf 100644 (file)
@@ -4,15 +4,31 @@ namespace Box\Spout\Reader;
 
 /**
  * Interface SheetInterface
- *
- * @package Box\Spout\Reader
  */
 interface SheetInterface
 {
     /**
-     * Returns an iterator to iterate over the sheet's rows.
-     *
-     * @return \Iterator
+     * @return IteratorInterface Iterator to iterate over the sheet's rows.
      */
     public function getRowIterator();
+
+    /**
+     * @return int Index of the sheet
+     */
+    public function getIndex();
+
+    /**
+     * @return string Name of the sheet
+     */
+    public function getName();
+
+    /**
+     * @return bool Whether the sheet was defined as active
+     */
+    public function isActive();
+
+    /**
+     * @return bool Whether the sheet is visible
+     */
+    public function isVisible();
 }
index 8b4464a..7047a94 100644 (file)
@@ -6,8 +6,6 @@ use Box\Spout\Reader\Exception\XMLProcessingException;
 
 /**
  * Trait XMLInternalErrorsHelper
- *
- * @package Box\Spout\Reader\Wrapper
  */
 trait XMLInternalErrorsHelper
 {
@@ -30,8 +28,8 @@ trait XMLInternalErrorsHelper
      * Throws an XMLProcessingException if an error occured.
      * It also always resets the "libxml_use_internal_errors" setting back to its initial value.
      *
-     * @return void
      * @throws \Box\Spout\Reader\Exception\XMLProcessingException
+     * @return void
      */
     protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured()
     {
@@ -57,7 +55,7 @@ trait XMLInternalErrorsHelper
      * Returns the error message for the last XML error that occured.
      * @see libxml_get_last_error
      *
-     * @return String|null Last XML error message or null if no error
+     * @return string|null Last XML error message or null if no error
      */
     private function getLastXMLErrorMessage()
     {
@@ -78,5 +76,4 @@ trait XMLInternalErrorsHelper
     {
         libxml_use_internal_errors($this->initialUseInternalErrorsValue);
     }
-
 }
index 08e99fc..bdbaf4d 100644 (file)
@@ -1,15 +1,11 @@
 <?php
 
 namespace Box\Spout\Reader\Wrapper;
-use DOMNode;
-
 
 /**
  * Class XMLReader
  * Wrapper around the built-in XMLReader
  * @see \XMLReader
- *
- * @package Box\Spout\Reader\Wrapper
  */
 class XMLReader extends \XMLReader
 {
@@ -49,7 +45,10 @@ class XMLReader extends \XMLReader
      */
     public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath)
     {
-        return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPath);
+        // The file path should not start with a '/', otherwise it won't be found
+        $fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/');
+
+        return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPathWithoutLeadingSlash);
     }
 
     /**
@@ -81,8 +80,8 @@ class XMLReader extends \XMLReader
      * Move to next node in document
      * @see \XMLReader::read
      *
-     * @return bool TRUE on success or FALSE on failure
      * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
+     * @return bool TRUE on success or FALSE on failure
      */
     public function read()
     {
@@ -99,8 +98,8 @@ class XMLReader extends \XMLReader
      * Read until the element with the given name is found, or the end of the file.
      *
      * @param string $nodeName Name of the node to find
-     * @return bool TRUE on success or FALSE on failure
      * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
+     * @return bool TRUE on success or FALSE on failure
      */
     public function readUntilNodeFound($nodeName)
     {
@@ -116,9 +115,9 @@ class XMLReader extends \XMLReader
      * Move cursor to next node skipping all subtrees
      * @see \XMLReader::next
      *
-     * @param string|void $localName The name of the next node to move to
-     * @return bool TRUE on success or FALSE on failure
+     * @param string|null $localName The name of the next node to move to
      * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
+     * @return bool TRUE on success or FALSE on failure
      */
     public function next($localName = null)
     {
@@ -137,7 +136,7 @@ class XMLReader extends \XMLReader
      */
     public function isPositionedOnStartingNode($nodeName)
     {
-        return $this->isPositionedOnNode($nodeName, XMLReader::ELEMENT);
+        return $this->isPositionedOnNode($nodeName, self::ELEMENT);
     }
 
     /**
@@ -146,7 +145,7 @@ class XMLReader extends \XMLReader
      */
     public function isPositionedOnEndingNode($nodeName)
     {
-        return $this->isPositionedOnNode($nodeName, XMLReader::END_ELEMENT);
+        return $this->isPositionedOnNode($nodeName, self::END_ELEMENT);
     }
 
     /**
diff --git a/lib/spout/src/Spout/Reader/XLSX/Creator/HelperFactory.php b/lib/spout/src/Spout/Reader/XLSX/Creator/HelperFactory.php
new file mode 100644 (file)
index 0000000..80ee692
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+namespace Box\Spout\Reader\XLSX\Creator;
+
+use Box\Spout\Common\Helper\Escaper;
+use Box\Spout\Reader\XLSX\Helper\CellValueFormatter;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsManager;
+use Box\Spout\Reader\XLSX\Manager\StyleManager;
+
+/**
+ * Class HelperFactory
+ * Factory to create helpers
+ */
+class HelperFactory extends \Box\Spout\Common\Creator\HelperFactory
+{
+    /**
+     * @param SharedStringsManager $sharedStringsManager Manages shared strings
+     * @param StyleManager $styleManager Manages styles
+     * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
+     * @param bool $shouldUse1904Dates Whether date/time values should use a calendar starting in 1904 instead of 1900
+     * @return CellValueFormatter
+     */
+    public function createCellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates)
+    {
+        $escaper = $this->createStringsEscaper();
+
+        return new CellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper);
+    }
+
+    /**
+     * @return Escaper\XLSX
+     */
+    public function createStringsEscaper()
+    {
+        /* @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+        return new Escaper\XLSX();
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/XLSX/Creator/InternalEntityFactory.php b/lib/spout/src/Spout/Reader/XLSX/Creator/InternalEntityFactory.php
new file mode 100644 (file)
index 0000000..4dc2dc7
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+
+namespace Box\Spout\Reader\XLSX\Creator;
+
+use Box\Spout\Common\Entity\Cell;
+use Box\Spout\Common\Entity\Row;
+use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
+use Box\Spout\Reader\Common\Entity\Options;
+use Box\Spout\Reader\Common\XMLProcessor;
+use Box\Spout\Reader\Wrapper\XMLReader;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsManager;
+use Box\Spout\Reader\XLSX\RowIterator;
+use Box\Spout\Reader\XLSX\Sheet;
+use Box\Spout\Reader\XLSX\SheetIterator;
+
+/**
+ * Class InternalEntityFactory
+ * Factory to create entities
+ */
+class InternalEntityFactory implements InternalEntityFactoryInterface
+{
+    /** @var HelperFactory */
+    private $helperFactory;
+
+    /** @var ManagerFactory */
+    private $managerFactory;
+
+    /**
+     * @param ManagerFactory $managerFactory
+     * @param HelperFactory $helperFactory
+     */
+    public function __construct(ManagerFactory $managerFactory, HelperFactory $helperFactory)
+    {
+        $this->managerFactory = $managerFactory;
+        $this->helperFactory = $helperFactory;
+    }
+
+    /**
+     * @param string $filePath Path of the file to be read
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @param SharedStringsManager $sharedStringsManager Manages shared strings
+     * @return SheetIterator
+     */
+    public function createSheetIterator($filePath, $optionsManager, $sharedStringsManager)
+    {
+        $sheetManager = $this->managerFactory->createSheetManager(
+            $filePath,
+            $optionsManager,
+            $sharedStringsManager,
+            $this
+        );
+
+        return new SheetIterator($sheetManager);
+    }
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
+     * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
+     * @param string $sheetName Name of the sheet
+     * @param bool $isSheetActive Whether the sheet was defined as active
+     * @param bool $isSheetVisible Whether the sheet is visible
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @param SharedStringsManager $sharedStringsManager Manages shared strings
+     * @return Sheet
+     */
+    public function createSheet(
+        $filePath,
+        $sheetDataXMLFilePath,
+        $sheetIndex,
+        $sheetName,
+        $isSheetActive,
+        $isSheetVisible,
+        $optionsManager,
+        $sharedStringsManager
+    ) {
+        $rowIterator = $this->createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager);
+
+        return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible);
+    }
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @param SharedStringsManager $sharedStringsManager Manages shared strings
+     * @return RowIterator
+     */
+    private function createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager)
+    {
+        $xmlReader = $this->createXMLReader();
+        $xmlProcessor = $this->createXMLProcessor($xmlReader);
+
+        $styleManager = $this->managerFactory->createStyleManager($filePath, $this);
+        $rowManager = $this->managerFactory->createRowManager($this);
+        $shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES);
+        $shouldUse1904Dates = $optionsManager->getOption(Options::SHOULD_USE_1904_DATES);
+
+        $cellValueFormatter = $this->helperFactory->createCellValueFormatter(
+            $sharedStringsManager,
+            $styleManager,
+            $shouldFormatDates,
+            $shouldUse1904Dates
+        );
+
+        $shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
+
+        return new RowIterator(
+            $filePath,
+            $sheetDataXMLFilePath,
+            $shouldPreserveEmptyRows,
+            $xmlReader,
+            $xmlProcessor,
+            $cellValueFormatter,
+            $rowManager,
+            $this
+        );
+    }
+
+    /**
+     * @param Cell[] $cells
+     * @return Row
+     */
+    public function createRow(array $cells = [])
+    {
+        return new Row($cells, null);
+    }
+
+    /**
+     * @param mixed $cellValue
+     * @return Cell
+     */
+    public function createCell($cellValue)
+    {
+        return new Cell($cellValue);
+    }
+
+    /**
+     * @return \ZipArchive
+     */
+    public function createZipArchive()
+    {
+        return new \ZipArchive();
+    }
+
+    /**
+     * @return XMLReader
+     */
+    public function createXMLReader()
+    {
+        return new XMLReader();
+    }
+
+    /**
+     * @param $xmlReader
+     * @return XMLProcessor
+     */
+    public function createXMLProcessor($xmlReader)
+    {
+        return new XMLProcessor($xmlReader);
+    }
+}
diff --git a/lib/spout/src/Spout/Reader/XLSX/Creator/ManagerFactory.php b/lib/spout/src/Spout/Reader/XLSX/Creator/ManagerFactory.php
new file mode 100644 (file)
index 0000000..d23a53f
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+
+namespace Box\Spout\Reader\XLSX\Creator;
+
+use Box\Spout\Reader\Common\Manager\RowManager;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsManager;
+use Box\Spout\Reader\XLSX\Manager\SheetManager;
+use Box\Spout\Reader\XLSX\Manager\StyleManager;
+use Box\Spout\Reader\XLSX\Manager\WorkbookRelationshipsManager;
+
+/**
+ * Class ManagerFactory
+ * Factory to create managers
+ */
+class ManagerFactory
+{
+    /** @var HelperFactory */
+    private $helperFactory;
+
+    /** @var CachingStrategyFactory */
+    private $cachingStrategyFactory;
+
+    /** @var WorkbookRelationshipsManager */
+    private $cachedWorkbookRelationshipsManager;
+
+    /**
+     * @param HelperFactory $helperFactory Factory to create helpers
+     * @param CachingStrategyFactory $cachingStrategyFactory Factory to create shared strings caching strategies
+     */
+    public function __construct(HelperFactory $helperFactory, CachingStrategyFactory $cachingStrategyFactory)
+    {
+        $this->helperFactory = $helperFactory;
+        $this->cachingStrategyFactory = $cachingStrategyFactory;
+    }
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @return SharedStringsManager
+     */
+    public function createSharedStringsManager($filePath, $tempFolder, $entityFactory)
+    {
+        $workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory);
+
+        return new SharedStringsManager(
+            $filePath,
+            $tempFolder,
+            $workbookRelationshipsManager,
+            $entityFactory,
+            $this->helperFactory,
+            $this->cachingStrategyFactory
+        );
+    }
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @return WorkbookRelationshipsManager
+     */
+    private function createWorkbookRelationshipsManager($filePath, $entityFactory)
+    {
+        if (!isset($this->cachedWorkbookRelationshipsManager)) {
+            $this->cachedWorkbookRelationshipsManager = new WorkbookRelationshipsManager($filePath, $entityFactory);
+        }
+
+        return $this->cachedWorkbookRelationshipsManager;
+    }
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @param \Box\Spout\Reader\XLSX\Manager\SharedStringsManager $sharedStringsManager Manages shared strings
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @return SheetManager
+     */
+    public function createSheetManager($filePath, $optionsManager, $sharedStringsManager, $entityFactory)
+    {
+        $escaper = $this->helperFactory->createStringsEscaper();
+
+        return new SheetManager($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory);
+    }
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @return StyleManager
+     */
+    public function createStyleManager($filePath, $entityFactory)
+    {
+        $workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory);
+
+        return new StyleManager($filePath, $workbookRelationshipsManager, $entityFactory);
+    }
+
+    /**
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @return RowManager
+     */
+    public function createRowManager($entityFactory)
+    {
+        return new RowManager($entityFactory);
+    }
+}
index 6077839..27b5abf 100644 (file)
@@ -7,8 +7,6 @@ use Box\Spout\Common\Exception\InvalidArgumentException;
 /**
  * Class CellHelper
  * This class provides helper functions when working with cells
- *
- * @package Box\Spout\Reader\XLSX\Helper
  */
 class CellHelper
 {
@@ -20,30 +18,6 @@ class CellHelper
         'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25,
     ];
 
-    /**
-     * Fills the missing indexes of an array with a given value.
-     * For instance, $dataArray = []; $a[1] = 1; $a[3] = 3;
-     * Calling fillMissingArrayIndexes($dataArray, 'FILL') will return this array: ['FILL', 1, 'FILL', 3]
-     *
-     * @param array $dataArray The array to fill
-     * @param string|void $fillValue optional
-     * @return array
-     */
-    public static function fillMissingArrayIndexes($dataArray, $fillValue = '')
-    {
-        if (empty($dataArray)) {
-            return [];
-        }
-        $existingIndexes = array_keys($dataArray);
-
-        $newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue);
-        $dataArray += $newIndexes;
-
-        ksort($dataArray);
-
-        return $dataArray;
-    }
-
     /**
      * Returns the base 10 column index associated to the cell index (base 26).
      * Excel uses A to Z letters for column indexing, where A is the 1st column,
@@ -51,8 +25,8 @@ class CellHelper
      * The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26.
      *
      * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
-     * @return int
      * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
+     * @return int
      */
     public static function getColumnIndexFromCellIndex($cellIndex)
     {
index b4c6256..fba36b1 100644 (file)
@@ -2,11 +2,13 @@
 
 namespace Box\Spout\Reader\XLSX\Helper;
 
+use Box\Spout\Reader\Exception\InvalidValueException;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsManager;
+use Box\Spout\Reader\XLSX\Manager\StyleManager;
+
 /**
  * Class CellValueFormatter
  * This class provides helper functions to format cell values
- *
- * @package Box\Spout\Reader\XLSX\Helper
  */
 class CellValueFormatter
 {
@@ -38,44 +40,49 @@ class CellValueFormatter
      */
     const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
 
-    /** @var SharedStringsHelper Helper to work with shared strings */
-    protected $sharedStringsHelper;
+    /** @var SharedStringsManager Manages shared strings */
+    protected $sharedStringsManager;
 
-    /** @var StyleHelper Helper to work with styles */
-    protected $styleHelper;
+    /** @var StyleManager Manages styles */
+    protected $styleManager;
 
     /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
     protected $shouldFormatDates;
 
-    /** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
+    /** @var bool Whether date/time values should use a calendar starting in 1904 instead of 1900 */
+    protected $shouldUse1904Dates;
+
+    /** @var \Box\Spout\Common\Helper\Escaper\XLSX Used to unescape XML data */
     protected $escaper;
 
     /**
-     * @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
-     * @param StyleHelper $styleHelper Helper to work with styles
+     * @param SharedStringsManager $sharedStringsManager Manages shared strings
+     * @param StyleManager $styleManager Manages styles
      * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
+     * @param bool $shouldUse1904Dates Whether date/time values should use a calendar starting in 1904 instead of 1900
+     * @param \Box\Spout\Common\Helper\Escaper\XLSX $escaper Used to unescape XML data
      */
-    public function __construct($sharedStringsHelper, $styleHelper, $shouldFormatDates)
+    public function __construct($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper)
     {
-        $this->sharedStringsHelper = $sharedStringsHelper;
-        $this->styleHelper = $styleHelper;
+        $this->sharedStringsManager = $sharedStringsManager;
+        $this->styleManager = $styleManager;
         $this->shouldFormatDates = $shouldFormatDates;
-
-        /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
-        $this->escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
+        $this->shouldUse1904Dates = $shouldUse1904Dates;
+        $this->escaper = $escaper;
     }
 
     /**
      * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
      *
      * @param \DOMNode $node
-     * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
+     * @throws InvalidValueException If the value is not valid
+     * @return string|int|float|bool|\DateTime The value associated with the cell
      */
     public function extractAndFormatNodeValue($node)
     {
         // Default cell type is "n"
         $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
-        $cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID));
+        $cellStyleId = (int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID);
         $vNodeValue = $this->getVNodeValue($node);
 
         if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
@@ -96,7 +103,7 @@ class CellValueFormatter
             case self::CELL_TYPE_DATE:
                 return $this->formatDateCellValue($vNodeValue);
             default:
-                return null;
+                throw new InvalidValueException($vNodeValue);
         }
     }
 
@@ -111,6 +118,7 @@ class CellValueFormatter
         // for cell types having a "v" tag containing the value.
         // if not, the returned value should be empty string.
         $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0);
+
         return ($vNode !== null) ? $vNode->nodeValue : '';
     }
 
@@ -118,7 +126,7 @@ class CellValueFormatter
      * Returns the cell String value where string is inline.
      *
      * @param \DOMNode $node
-     * @return string The value associated with the cell (null when the cell has an error)
+     * @return string The value associated with the cell
      */
     protected function formatInlineStringCellValue($node)
     {
@@ -126,6 +134,7 @@ class CellValueFormatter
         // <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
         $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
         $cellValue = $this->escaper->unescape($tNode->nodeValue);
+
         return $cellValue;
     }
 
@@ -133,15 +142,16 @@ class CellValueFormatter
      * Returns the cell String value from shared-strings file using nodeValue index.
      *
      * @param string $nodeValue
-     * @return string The value associated with the cell (null when the cell has an error)
+     * @return string The value associated with the cell
      */
     protected function formatSharedStringCellValue($nodeValue)
     {
         // shared strings are formatted this way:
         // <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
-        $sharedStringIndex = intval($nodeValue);
-        $escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
+        $sharedStringIndex = (int) $nodeValue;
+        $escapedCellValue = $this->sharedStringsManager->getStringAtIndex($sharedStringIndex);
         $cellValue = $this->escaper->unescape($escapedCellValue);
+
         return $cellValue;
     }
 
@@ -149,12 +159,13 @@ class CellValueFormatter
      * Returns the cell String value, where string is stored in value node.
      *
      * @param string $nodeValue
-     * @return string The value associated with the cell (null when the cell has an error)
+     * @return string The value associated with the cell
      */
     protected function formatStrCellValue($nodeValue)
     {
         $escapedCellValue = trim($nodeValue);
         $cellValue = $this->escaper->unescape($escapedCellValue);
+
         return $cellValue;
     }
 
@@ -164,108 +175,95 @@ class CellValueFormatter
      *
      * @param string $nodeValue
      * @param int $cellStyleId 0 being the default style
-     * @return int|float|\DateTime|null The value associated with the cell
+     * @return int|float|\DateTime The value associated with the cell
      */
     protected function formatNumericCellValue($nodeValue, $cellStyleId)
     {
         // Numeric values can represent numbers as well as timestamps.
         // We need to look at the style of the cell to determine whether it is one or the other.
-        $shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId);
+        $shouldFormatAsDate = $this->styleManager->shouldFormatNumericValueAsDate($cellStyleId);
 
         if ($shouldFormatAsDate) {
-            return $this->formatExcelTimestampValue(floatval($nodeValue), $cellStyleId);
+            $cellValue = $this->formatExcelTimestampValue((float) $nodeValue, $cellStyleId);
         } else {
-            $nodeIntValue = intval($nodeValue);
-            return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
+            $nodeIntValue = (int) $nodeValue;
+            $nodeFloatValue = (float) $nodeValue;
+            $cellValue = ((float) $nodeIntValue === $nodeFloatValue) ? $nodeIntValue : $nodeFloatValue;
         }
+
+        return $cellValue;
     }
 
     /**
      * Returns a cell's PHP Date value, associated to the given timestamp.
-     * NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
+     * NOTE: The timestamp is a float representing the number of days since the base Excel date:
+     *       Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting.
      * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1.
      *
+     * @see ECMA-376 Part 1 - §18.17.4
+     *
      * @param float $nodeValue
      * @param int $cellStyleId 0 being the default style
-     * @return \DateTime|null The value associated with the cell or NULL if invalid date value
+     * @throws InvalidValueException If the value is not a valid timestamp
+     * @return \DateTime The value associated with the cell
      */
     protected function formatExcelTimestampValue($nodeValue, $cellStyleId)
     {
-        // Fix for the erroneous leap year in Excel
-        if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
-            --$nodeValue;
-        }
-
-        if ($nodeValue >= 1) {
-            // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01.
-            return $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId);
-        } else if ($nodeValue >= 0) {
-            // Values between 0 and 1 represent "times".
-            return $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId);
+        if ($this->isValidTimestampValue($nodeValue)) {
+            $cellValue = $this->formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId);
         } else {
-            // invalid date
-            return null;
+            throw new InvalidValueException($nodeValue);
         }
+
+        return $cellValue;
+    }
+
+    /**
+     * Returns whether the given timestamp is supported by SpreadsheetML
+     * @see ECMA-376 Part 1 - §18.17.4 - this specifies the timestamp boundaries.
+     *
+     * @param float $timestampValue
+     * @return bool
+     */
+    protected function isValidTimestampValue($timestampValue)
+    {
+        // @NOTE: some versions of Excel don't support negative dates (e.g. Excel for Mac 2011)
+        return (
+            $this->shouldUse1904Dates && $timestampValue >= -695055 && $timestampValue <= 2957003.9999884 ||
+            !$this->shouldUse1904Dates && $timestampValue >= -693593 && $timestampValue <= 2958465.9999884
+        );
     }
 
     /**
      * Returns a cell's PHP DateTime value, associated to the given timestamp.
-     * Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date).
+     * Only the time value matters. The date part is set to the base Excel date:
+     * Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting.
      *
      * @param float $nodeValue
      * @param int $cellStyleId 0 being the default style
      * @return \DateTime|string The value associated with the cell
      */
-    protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId)
+    protected function formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId)
     {
-        $time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY);
-        $hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR);
-        $minutes = floor($time / self::NUM_SECONDS_IN_ONE_MINUTE) - ($hours * self::NUM_SECONDS_IN_ONE_MINUTE);
-        $seconds = $time - ($hours * self::NUM_SECONDS_IN_ONE_HOUR) - ($minutes * self::NUM_SECONDS_IN_ONE_MINUTE);
+        $baseDate = $this->shouldUse1904Dates ? '1904-01-01' : '1899-12-30';
 
-        // using the base Excel date (Jan 1st, 1900) - not relevant here
-        $dateObj = new \DateTime('1900-01-01');
-        $dateObj->setTime($hours, $minutes, $seconds);
+        $daysSinceBaseDate = (int) $nodeValue;
+        $timeRemainder = fmod($nodeValue, 1);
+        $secondsRemainder = round($timeRemainder * self::NUM_SECONDS_IN_ONE_DAY, 0);
+
+        $dateObj = \DateTime::createFromFormat('|Y-m-d', $baseDate);
+        $dateObj->modify('+' . $daysSinceBaseDate . 'days');
+        $dateObj->modify('+' . $secondsRemainder . 'seconds');
 
         if ($this->shouldFormatDates) {
-            $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
+            $styleNumberFormatCode = $this->styleManager->getNumberFormatCode($cellStyleId);
             $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
-            return $dateObj->format($phpDateFormat);
+            $cellValue = $dateObj->format($phpDateFormat);
         } else {
-            return $dateObj;
+            $cellValue = $dateObj;
         }
-    }
 
-    /**
-     * Returns a cell's PHP Date value, associated to the given timestamp.
-     * NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
-     *
-     * @param float $nodeValue
-     * @param int $cellStyleId 0 being the default style
-     * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
-     */
-    protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId)
-    {
-        // Do not use any unix timestamps for calculation to prevent
-        // issues with numbers exceeding 2^31.
-        $secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY;
-        $secondsRemainder = round($secondsRemainder, 0);
-
-        try {
-            $dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31');
-            $dateObj->modify('+' . intval($nodeValue) . 'days');
-            $dateObj->modify('+' . $secondsRemainder . 'seconds');
-
-            if ($this->shouldFormatDates) {
-                $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
-                $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
-                return $dateObj->format($phpDateFormat);
-            } else {
-                return $dateObj;
-            }
-        } catch (\Exception $e) {
-            return null;
-        }
+        return $cellValue;
     }
 
     /**
@@ -276,9 +274,7 @@ class CellValueFormatter
      */
     protected function formatBooleanCellValue($nodeValue)
     {
-        // !! is similar to boolval()
-        $cellValue = !!$nodeValue;
-        return $cellValue;
+        return (bool) $nodeValue;
     }
 
     /**
@@ -286,15 +282,18 @@ class CellValueFormatter
      * @see ECMA-376 Part 1 - §18.17.4
      *
      * @param string $nodeValue ISO 8601 Date string
-     * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
+     * @throws InvalidValueException If the value is not a valid date
+     * @return \DateTime|string The value associated with the cell
      */
     protected function formatDateCellValue($nodeValue)
     {
         // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
         try {
-            return ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue);
+            $cellValue = ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue);
         } catch (\Exception $e) {
-            return null;
+            throw new InvalidValueException($nodeValue);
         }
+
+        return $cellValue;
     }
 }
index 9dba4c6..1618af7 100644 (file)
@@ -5,8 +5,6 @@ namespace Box\Spout\Reader\XLSX\Helper;
 /**
  * Class DateFormatHelper
  * This class provides helper functions to format Excel dates
- *
- * @package Box\Spout\Reader\XLSX\Helper
  */
 class DateFormatHelper
 {
@@ -104,9 +102,10 @@ class DateFormatHelper
         // Finally, to have the date format compatible with the DateTime::format() function, we need to escape
         // all characters that are inside double quotes (and double quotes must be removed).
         // For instance, ["Day " dd] should become [\D\a\y\ dd]
-        $phpDateFormat = preg_replace_callback('/"(.+?)"/', function($matches) {
+        $phpDateFormat = preg_replace_callback('/"(.+?)"/', function ($matches) {
             $stringToEscape = $matches[1];
             $letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY);
+
             return '\\' . implode('\\', $letters);
         }, $phpDateFormat);
 
diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php
deleted file mode 100644 (file)
index b74ba01..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-<?php
-
-namespace Box\Spout\Reader\XLSX\Helper;
-
-use Box\Spout\Reader\Wrapper\XMLReader;
-use Box\Spout\Reader\XLSX\Sheet;
-
-/**
- * Class SheetHelper
- * This class provides helper functions related to XLSX sheets
- *
- * @package Box\Spout\Reader\XLSX\Helper
- */
-class SheetHelper
-{
-    /** Paths of XML files relative to the XLSX file root */
-    const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
-    const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
-
-    /** Definition of XML node names used to parse data */
-    const XML_NODE_WORKBOOK_VIEW = 'workbookView';
-    const XML_NODE_SHEET = 'sheet';
-    const XML_NODE_SHEETS = 'sheets';
-    const XML_NODE_RELATIONSHIP = 'Relationship';
-
-    /** Definition of XML attributes used to parse data */
-    const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab';
-    const XML_ATTRIBUTE_R_ID = 'r:id';
-    const XML_ATTRIBUTE_NAME = 'name';
-    const XML_ATTRIBUTE_ID = 'Id';
-    const XML_ATTRIBUTE_TARGET = 'Target';
-
-    /** @var string Path of the XLSX file being read */
-    protected $filePath;
-
-    /** @var \Box\Spout\Reader\XLSX\ReaderOptions Reader's current options */
-    protected $options;
-
-    /** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
-    protected $sharedStringsHelper;
-
-    /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
-    protected $globalFunctionsHelper;
-
-    /**
-     * @param string $filePath Path of the XLSX file being read
-     * @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
-     * @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
-     * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
-     */
-    public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper)
-    {
-        $this->filePath = $filePath;
-        $this->options = $options;
-        $this->sharedStringsHelper = $sharedStringsHelper;
-        $this->globalFunctionsHelper = $globalFunctionsHelper;
-    }
-
-    /**
-     * Returns the sheets metadata of the file located at the previously given file path.
-     * The paths to the sheets' data are read from the [Content_Types].xml file.
-     *
-     * @return Sheet[] Sheets within the XLSX file
-     */
-    public function getSheets()
-    {
-        $sheets = [];
-        $sheetIndex = 0;
-        $activeSheetIndex = 0; // By default, the first sheet is active
-
-        $xmlReader = new XMLReader();
-        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
-            while ($xmlReader->read()) {
-                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) {
-                    // The "workbookView" node is located before "sheet" nodes, ensuring that
-                    // the active sheet is known before parsing sheets data.
-                    $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
-                } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) {
-                    $isSheetActive = ($sheetIndex === $activeSheetIndex);
-                    $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive);
-                    $sheetIndex++;
-                } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) {
-                    // stop reading once all sheets have been read
-                    break;
-                }
-            }
-
-            $xmlReader->close();
-        }
-
-        return $sheets;
-    }
-
-    /**
-     * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
-     * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
-     * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
-     *
-     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
-     * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
-     * @param bool $isSheetActive Whether this sheet was defined as active
-     * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
-     */
-    protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
-    {
-        $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
-        $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
-
-        /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
-        $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
-        $sheetName = $escaper->unescape($escapedSheetName);
-
-        $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
-
-        return new Sheet(
-            $this->filePath, $sheetDataXMLFilePath,
-            $sheetIndexZeroBased, $sheetName, $isSheetActive,
-            $this->options, $this->sharedStringsHelper
-        );
-    }
-
-    /**
-     * @param string $sheetId The sheet ID, as defined in "workbook.xml"
-     * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
-     */
-    protected function getSheetDataXMLFilePathForSheetId($sheetId)
-    {
-        $sheetDataXMLFilePath = '';
-
-        // find the file path of the sheet, by looking at the "workbook.xml.res" file
-        $xmlReader = new XMLReader();
-        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
-            while ($xmlReader->read()) {
-                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
-                    $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
-
-                    if ($relationshipSheetId === $sheetId) {
-                        // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
-                        // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
-                        $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
-
-                        // sometimes, the sheet data file path already contains "/xl/"...
-                        if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
-                            $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
-                            break;
-                        }
-                    }
-                }
-            }
-
-            $xmlReader->close();
-        }
-
-        return $sheetDataXMLFilePath;
-    }
-}
diff --git a/lib/spout/src/Spout/Reader/XLSX/Manager/OptionsManager.php b/lib/spout/src/Spout/Reader/XLSX/Manager/OptionsManager.php
new file mode 100644 (file)
index 0000000..253e009
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace Box\Spout\Reader\XLSX\Manager;
+
+use Box\Spout\Common\Manager\OptionsManagerAbstract;
+use Box\Spout\Reader\Common\Entity\Options;
+
+/**
+ * Class OptionsManager
+ * XLSX Reader options manager
+ */
+class OptionsManager extends OptionsManagerAbstract
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getSupportedOptions()
+    {
+        return [
+            Options::TEMP_FOLDER,
+            Options::SHOULD_FORMAT_DATES,
+            Options::SHOULD_PRESERVE_EMPTY_ROWS,
+            Options::SHOULD_USE_1904_DATES,
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function setDefaultOptions()
+    {
+        $this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir());
+        $this->setOption(Options::SHOULD_FORMAT_DATES, false);
+        $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
+        $this->setOption(Options::SHOULD_USE_1904_DATES, false);
+    }
+}
@@ -1,11 +1,11 @@
 <?php
 
-namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
+namespace Box\Spout\Reader\XLSX\Manager\SharedStringsCaching;
+
+use Box\Spout\Reader\XLSX\Creator\HelperFactory;
 
 /**
  * Class CachingStrategyFactory
- *
- * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
  */
 class CachingStrategyFactory
 {
@@ -50,45 +50,22 @@ class CachingStrategyFactory
      */
     const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
 
-    /** @var CachingStrategyFactory|null Singleton instance */
-    protected static $instance = null;
-
-    /**
-     * Private constructor for singleton
-     */
-    private function __construct()
-    {
-    }
-
-    /**
-     * Returns the singleton instance of the factory
-     *
-     * @return CachingStrategyFactory
-     */
-    public static function getInstance()
-    {
-        if (self::$instance === null) {
-            self::$instance = new CachingStrategyFactory();
-        }
-
-        return self::$instance;
-    }
-
     /**
      * Returns the best caching strategy, given the number of unique shared strings
      * and the amount of memory available.
      *
      * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
-     * @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
+     * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
+     * @param HelperFactory $helperFactory Factory to create helpers
      * @return CachingStrategyInterface The best caching strategy
      */
-    public function getBestCachingStrategy($sharedStringsUniqueCount, $tempFolder = null)
+    public function createBestCachingStrategy($sharedStringsUniqueCount, $tempFolder, $helperFactory)
     {
         if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) {
             return new InMemoryStrategy($sharedStringsUniqueCount);
-        } else {
-            return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE);
         }
+
+        return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE, $helperFactory);
     }
 
     /**
@@ -109,11 +86,13 @@ class CachingStrategyFactory
 
         if ($memoryAvailable === -1) {
             // if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe
-            return ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE);
+            $isInMemoryStrategyUsageSafe = ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE);
         } else {
             $memoryNeeded = $sharedStringsUniqueCount * self::AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB;
-            return ($memoryAvailable > $memoryNeeded);
+            $isInMemoryStrategyUsageSafe = ($memoryAvailable > $memoryNeeded);
         }
+
+        return $isInMemoryStrategyUsageSafe;
     }
 
     /**
@@ -132,7 +111,7 @@ class CachingStrategyFactory
         }
 
         if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) {
-            $amount = intval($matches[1]);
+            $amount = (int) ($matches[1]);
             $unit = $matches[2];
 
             switch ($unit) {
@@ -1,11 +1,9 @@
 <?php
 
-namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
+namespace Box\Spout\Reader\XLSX\Manager\SharedStringsCaching;
 
 /**
  * Interface CachingStrategyInterface
- *
- * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
  */
 interface CachingStrategyInterface
 {
@@ -30,8 +28,8 @@ interface CachingStrategyInterface
      * Returns the string located at the given index from the cache.
      *
      * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
-     * @return string The shared string at the given index
      * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
+     * @return string The shared string at the given index
      */
     public function getStringAtIndex($sharedStringIndex);
 
@@ -1,10 +1,9 @@
 <?php
 
-namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
+namespace Box\Spout\Reader\XLSX\Manager\SharedStringsCaching;
 
-use Box\Spout\Common\Helper\FileSystemHelper;
-use Box\Spout\Common\Helper\GlobalFunctionsHelper;
 use Box\Spout\Reader\Exception\SharedStringNotFoundException;
+use Box\Spout\Reader\XLSX\Creator\HelperFactory;
 
 /**
  * Class FileBasedStrategy
@@ -12,8 +11,6 @@ use Box\Spout\Reader\Exception\SharedStringNotFoundException;
  * This class implements the file-based caching strategy for shared strings.
  * Shared strings are stored in small files (with a max number of strings per file).
  * This strategy is slower than an in-memory strategy but is used to avoid out of memory crashes.
- *
- * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
  */
 class FileBasedStrategy implements CachingStrategyInterface
 {
@@ -51,18 +48,18 @@ class FileBasedStrategy implements CachingStrategyInterface
     protected $inMemoryTempFileContents;
 
     /**
-     * @param string|null $tempFolder Temporary folder where the temporary files to store shared strings will be stored
+     * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
      * @param int $maxNumStringsPerTempFile Maximum number of strings that can be stored in one temp file
+     * @param HelperFactory $helperFactory Factory to create helpers
      */
-    public function __construct($tempFolder, $maxNumStringsPerTempFile)
+    public function __construct($tempFolder, $maxNumStringsPerTempFile, $helperFactory)
     {
-        $rootTempFolder = ($tempFolder) ?: sys_get_temp_dir();
-        $this->fileSystemHelper = new FileSystemHelper($rootTempFolder);
-        $this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
+        $this->fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder);
+        $this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, uniqid('sharedstrings'));
 
         $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
 
-        $this->globalFunctionsHelper = new GlobalFunctionsHelper();
+        $this->globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
         $this->tempFilePointer = null;
     }
 
@@ -99,7 +96,8 @@ class FileBasedStrategy implements CachingStrategyInterface
      */
     protected function getSharedStringTempFilePath($sharedStringIndex)
     {
-        $numTempFile = intval($sharedStringIndex / $this->maxNumStringsPerTempFile);
+        $numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile);
+
         return $this->tempFolder . '/sharedstrings' . $numTempFile;
     }
 
@@ -117,13 +115,12 @@ class FileBasedStrategy implements CachingStrategyInterface
         }
     }
 
-
     /**
      * Returns the string located at the given index from the cache.
      *
      * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
-     * @return string The shared string at the given index
      * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
+     * @return string The shared string at the given index
      */
     public function getStringAtIndex($sharedStringIndex)
     {
@@ -1,6 +1,6 @@
 <?php
 
-namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
+namespace Box\Spout\Reader\XLSX\Manager\SharedStringsCaching;
 
 use Box\Spout\Reader\Exception\SharedStringNotFoundException;
 
@@ -9,8 +9,6 @@ use Box\Spout\Reader\Exception\SharedStringNotFoundException;
  *
  * This class implements the in-memory caching strategy for shared strings.
  * This strategy is used when the number of unique strings is low, compared to the memory available.
- *
- * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
  */
 class InMemoryStrategy implements CachingStrategyInterface
 {
@@ -58,8 +56,8 @@ class InMemoryStrategy implements CachingStrategyInterface
      * Returns the string located at the given index from the cache.
      *
      * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
-     * @return string The shared string at the given index
      * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
+     * @return string The shared string at the given index
      */
     public function getStringAtIndex($sharedStringIndex)
     {
@@ -1,24 +1,21 @@
 <?php
 
-namespace Box\Spout\Reader\XLSX\Helper;
+namespace Box\Spout\Reader\XLSX\Manager;
 
 use Box\Spout\Common\Exception\IOException;
 use Box\Spout\Reader\Exception\XMLProcessingException;
 use Box\Spout\Reader\Wrapper\XMLReader;
-use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory;
-use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyInterface;
+use Box\Spout\Reader\XLSX\Creator\HelperFactory;
+use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory;
+use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface;
 
 /**
- * Class SharedStringsHelper
- * This class provides helper functions for reading sharedStrings XML file
- *
- * @package Box\Spout\Reader\XLSX\Helper
+ * Class SharedStringsManager
+ * This class manages the shared strings defined in the associated XML file
  */
-class SharedStringsHelper
+class SharedStringsManager
 {
-    /** Path of sharedStrings XML file inside the XLSX file */
-    const SHARED_STRINGS_XML_FILE_PATH = 'xl/sharedStrings.xml';
-
     /** Main namespace for the sharedStrings.xml file */
     const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
 
@@ -40,17 +37,43 @@ class SharedStringsHelper
     /** @var string Temporary folder where the temporary files to store shared strings will be stored */
     protected $tempFolder;
 
+    /** @var WorkbookRelationshipsManager Helps retrieving workbook relationships */
+    protected $workbookRelationshipsManager;
+
+    /** @var InternalEntityFactory Factory to create entities */
+    protected $entityFactory;
+
+    /** @var HelperFactory $helperFactory Factory to create helpers */
+    protected $helperFactory;
+
+    /** @var CachingStrategyFactory Factory to create shared strings caching strategies */
+    protected $cachingStrategyFactory;
+
     /** @var CachingStrategyInterface The best caching strategy for storing shared strings */
     protected $cachingStrategy;
 
     /**
      * @param string $filePath Path of the XLSX file being read
-     * @param string|null|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
+     * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
+     * @param WorkbookRelationshipsManager $workbookRelationshipsManager Helps retrieving workbook relationships
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @param HelperFactory $helperFactory Factory to create helpers
+     * @param CachingStrategyFactory $cachingStrategyFactory Factory to create shared strings caching strategies
      */
-    public function __construct($filePath, $tempFolder = null)
-    {
+    public function __construct(
+        $filePath,
+        $tempFolder,
+        $workbookRelationshipsManager,
+        $entityFactory,
+        $helperFactory,
+        $cachingStrategyFactory
+    ) {
         $this->filePath = $filePath;
         $this->tempFolder = $tempFolder;
+        $this->workbookRelationshipsManager = $workbookRelationshipsManager;
+        $this->entityFactory = $entityFactory;
+        $this->helperFactory = $helperFactory;
+        $this->cachingStrategyFactory = $cachingStrategyFactory;
     }
 
     /**
@@ -60,15 +83,7 @@ class SharedStringsHelper
      */
     public function hasSharedStrings()
     {
-        $hasSharedStrings = false;
-        $zip = new \ZipArchive();
-
-        if ($zip->open($this->filePath) === true) {
-            $hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false);
-            $zip->close();
-        }
-
-        return $hasSharedStrings;
+        return $this->workbookRelationshipsManager->hasSharedStringsXMLFile();
     }
 
     /**
@@ -81,16 +96,17 @@ class SharedStringsHelper
      * The XML file can be really big with sheets containing a lot of data. That is why
      * we need to use a XML reader that provides streaming like the XMLReader library.
      *
+     * @throws \Box\Spout\Common\Exception\IOException If shared strings XML file can't be read
      * @return void
-     * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read
      */
     public function extractSharedStrings()
     {
-        $xmlReader = new XMLReader();
+        $sharedStringsXMLFilePath = $this->workbookRelationshipsManager->getSharedStringsXMLFilePath();
+        $xmlReader = $this->entityFactory->createXMLReader();
         $sharedStringIndex = 0;
 
-        if ($xmlReader->openFileInZip($this->filePath, self::SHARED_STRINGS_XML_FILE_PATH) === false) {
-            throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
+        if ($xmlReader->openFileInZip($this->filePath, $sharedStringsXMLFilePath) === false) {
+            throw new IOException('Could not open "' . $sharedStringsXMLFilePath . '".');
         }
 
         try {
@@ -108,7 +124,6 @@ class SharedStringsHelper
             }
 
             $this->cachingStrategy->closeCache();
-
         } catch (XMLProcessingException $exception) {
             throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
         }
@@ -120,8 +135,8 @@ class SharedStringsHelper
      * Returns the shared strings unique count, as specified in <sst> tag.
      *
      * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance
-     * @return int|null Number of unique shared strings in the sharedStrings.xml file
      * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read
+     * @return int|null Number of unique shared strings in the sharedStrings.xml file
      */
     protected function getSharedStringsUniqueCount($xmlReader)
     {
@@ -140,7 +155,7 @@ class SharedStringsHelper
             $uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_COUNT);
         }
 
-        return ($uniqueCount !== null) ? intval($uniqueCount) : null;
+        return ($uniqueCount !== null) ? (int) $uniqueCount : null;
     }
 
     /**
@@ -151,8 +166,8 @@ class SharedStringsHelper
      */
     protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount)
     {
-        return CachingStrategyFactory::getInstance()
-                ->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder);
+        return $this->cachingStrategyFactory
+                ->createBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder, $this->helperFactory);
     }
 
     /**
@@ -193,6 +208,7 @@ class SharedStringsHelper
     protected function shouldExtractTextNodeValue($textNode)
     {
         $parentTagName = $textNode->parentNode->localName;
+
         return ($parentTagName === self::XML_NODE_SI || $parentTagName === self::XML_NODE_R);
     }
 
@@ -205,6 +221,7 @@ class SharedStringsHelper
     protected function shouldPreserveWhitespace($textNode)
     {
         $spaceValue = $textNode->getAttribute(self::XML_ATTRIBUTE_XML_SPACE);
+
         return ($spaceValue === self::XML_ATTRIBUTE_VALUE_PRESERVE);
     }
 
@@ -212,8 +229,8 @@ class SharedStringsHelper
      * Returns the shared string at the given index, using the previously chosen caching strategy.
      *
      * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
-     * @return string The shared string at the given index
      * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
+     * @return string The shared string at the given index
      */
     public function getStringAtIndex($sharedStringIndex)
     {
diff --git a/lib/spout/src/Spout/Reader/XLSX/Manager/SheetManager.php b/lib/spout/src/Spout/Reader/XLSX/Manager/SheetManager.php
new file mode 100644 (file)
index 0000000..42f287d
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+
+namespace Box\Spout\Reader\XLSX\Manager;
+
+use Box\Spout\Reader\Common\Entity\Options;
+use Box\Spout\Reader\Common\XMLProcessor;
+use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory;
+use Box\Spout\Reader\XLSX\Sheet;
+
+/**
+ * Class SheetManager
+ * This class manages XLSX sheets
+ */
+class SheetManager
+{
+    /** Paths of XML files relative to the XLSX file root */
+    const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
+    const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
+
+    /** Definition of XML node names used to parse data */
+    const XML_NODE_WORKBOOK_PROPERTIES = 'workbookPr';
+    const XML_NODE_WORKBOOK_VIEW = 'workbookView';
+    const XML_NODE_SHEET = 'sheet';
+    const XML_NODE_SHEETS = 'sheets';
+    const XML_NODE_RELATIONSHIP = 'Relationship';
+
+    /** Definition of XML attributes used to parse data */
+    const XML_ATTRIBUTE_DATE_1904 = 'date1904';
+    const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab';
+    const XML_ATTRIBUTE_R_ID = 'r:id';
+    const XML_ATTRIBUTE_NAME = 'name';
+    const XML_ATTRIBUTE_STATE = 'state';
+    const XML_ATTRIBUTE_ID = 'Id';
+    const XML_ATTRIBUTE_TARGET = 'Target';
+
+    /** State value to represent a hidden sheet */
+    const SHEET_STATE_HIDDEN = 'hidden';
+
+    /** @var string Path of the XLSX file being read */
+    protected $filePath;
+
+    /** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */
+    protected $optionsManager;
+
+    /** @var \Box\Spout\Reader\XLSX\Manager\SharedStringsManager Manages shared strings */
+    protected $sharedStringsManager;
+
+    /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
+    protected $globalFunctionsHelper;
+
+    /** @var InternalEntityFactory Factory to create entities */
+    protected $entityFactory;
+
+    /** @var \Box\Spout\Common\Helper\Escaper\XLSX Used to unescape XML data */
+    protected $escaper;
+
+    /** @var array List of sheets */
+    protected $sheets;
+
+    /** @var int Index of the sheet currently read */
+    protected $currentSheetIndex;
+
+    /** @var int Index of the active sheet (0 by default) */
+    protected $activeSheetIndex;
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
+     * @param \Box\Spout\Reader\XLSX\Manager\SharedStringsManager $sharedStringsManager Manages shared strings
+     * @param \Box\Spout\Common\Helper\Escaper\XLSX $escaper Used to unescape XML data
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     * @param mixed $sharedStringsManager
+     */
+    public function __construct($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory)
+    {
+        $this->filePath = $filePath;
+        $this->optionsManager = $optionsManager;
+        $this->sharedStringsManager = $sharedStringsManager;
+        $this->escaper = $escaper;
+        $this->entityFactory = $entityFactory;
+    }
+
+    /**
+     * Returns the sheets metadata of the file located at the previously given file path.
+     * The paths to the sheets' data are read from the [Content_Types].xml file.
+     *
+     * @return Sheet[] Sheets within the XLSX file
+     */
+    public function getSheets()
+    {
+        $this->sheets = [];
+        $this->currentSheetIndex = 0;
+        $this->activeSheetIndex = 0; // By default, the first sheet is active
+
+        $xmlReader = $this->entityFactory->createXMLReader();
+        $xmlProcessor = $this->entityFactory->createXMLProcessor($xmlReader);
+
+        $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_PROPERTIES, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookPropertiesStartingNode']);
+        $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_VIEW, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookViewStartingNode']);
+        $xmlProcessor->registerCallback(self::XML_NODE_SHEET, XMLProcessor::NODE_TYPE_START, [$this, 'processSheetStartingNode']);
+        $xmlProcessor->registerCallback(self::XML_NODE_SHEETS, XMLProcessor::NODE_TYPE_END, [$this, 'processSheetsEndingNode']);
+
+        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
+            $xmlProcessor->readUntilStopped();
+            $xmlReader->close();
+        }
+
+        return $this->sheets;
+    }
+
+    /**
+     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<workbookPr>" starting node
+     * @return int A return code that indicates what action should the processor take next
+     */
+    protected function processWorkbookPropertiesStartingNode($xmlReader)
+    {
+        // Using "filter_var($x, FILTER_VALIDATE_BOOLEAN)" here because the value of the "date1904" attribute
+        // may be the string "false", that is not mapped to the boolean "false" by default...
+        $shouldUse1904Dates = filter_var($xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904), FILTER_VALIDATE_BOOLEAN);
+        $this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates);
+
+        return XMLProcessor::PROCESSING_CONTINUE;
+    }
+
+    /**
+     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<workbookView>" starting node
+     * @return int A return code that indicates what action should the processor take next
+     */
+    protected function processWorkbookViewStartingNode($xmlReader)
+    {
+        // The "workbookView" node is located before "sheet" nodes, ensuring that
+        // the active sheet is known before parsing sheets data.
+        $this->activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
+
+        return XMLProcessor::PROCESSING_CONTINUE;
+    }
+
+    /**
+     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<sheet>" starting node
+     * @return int A return code that indicates what action should the processor take next
+     */
+    protected function processSheetStartingNode($xmlReader)
+    {
+        $isSheetActive = ($this->currentSheetIndex === $this->activeSheetIndex);
+        $this->sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $this->currentSheetIndex, $isSheetActive);
+        $this->currentSheetIndex++;
+
+        return XMLProcessor::PROCESSING_CONTINUE;
+    }
+
+    /**
+     * @return int A return code that indicates what action should the processor take next
+     */
+    protected function processSheetsEndingNode()
+    {
+        return XMLProcessor::PROCESSING_STOP;
+    }
+
+    /**
+     * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
+     * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
+     * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
+     *
+     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
+     * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
+     * @param bool $isSheetActive Whether this sheet was defined as active
+     * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
+     */
+    protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
+    {
+        $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
+
+        $sheetState = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_STATE);
+        $isSheetVisible = ($sheetState !== self::SHEET_STATE_HIDDEN);
+
+        $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
+        $sheetName = $this->escaper->unescape($escapedSheetName);
+
+        $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
+
+        return $this->entityFactory->createSheet(
+            $this->filePath,
+            $sheetDataXMLFilePath,
+            $sheetIndexZeroBased,
+            $sheetName,
+            $isSheetActive,
+            $isSheetVisible,
+            $this->optionsManager,
+            $this->sharedStringsManager
+        );
+    }
+
+    /**
+     * @param string $sheetId The sheet ID, as defined in "workbook.xml"
+     * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
+     */
+    protected function getSheetDataXMLFilePathForSheetId($sheetId)
+    {
+        $sheetDataXMLFilePath = '';
+
+        // find the file path of the sheet, by looking at the "workbook.xml.res" file
+        $xmlReader = $this->entityFactory->createXMLReader();
+        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
+            while ($xmlReader->read()) {
+                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
+                    $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
+
+                    if ($relationshipSheetId === $sheetId) {
+                        // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
+                        // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
+                        $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
+
+                        // sometimes, the sheet data file path already contains "/xl/"...
+                        if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
+                            $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            $xmlReader->close();
+        }
+
+        return $sheetDataXMLFilePath;
+    }
+}
@@ -1,20 +1,15 @@
 <?php
 
-namespace Box\Spout\Reader\XLSX\Helper;
+namespace Box\Spout\Reader\XLSX\Manager;
 
-use Box\Spout\Reader\Wrapper\XMLReader;
+use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory;
 
 /**
- * Class StyleHelper
- * This class provides helper functions related to XLSX styles
- *
- * @package Box\Spout\Reader\XLSX\Helper
+ * Class StyleManager
+ * This class manages XLSX styles
  */
-class StyleHelper
+class StyleManager
 {
-    /** Paths of XML files relative to the XLSX file root */
-    const STYLES_XML_FILE_PATH = 'xl/styles.xml';
-
     /** Nodes used to find relevant information in the styles XML file */
     const XML_NODE_NUM_FMTS = 'numFmts';
     const XML_NODE_NUM_FMT = 'numFmt';
@@ -53,6 +48,12 @@ class StyleHelper
     /** @var string Path of the XLSX file being read */
     protected $filePath;
 
+    /** @var string Path of the styles XML file */
+    protected $stylesXMLFilePath;
+
+    /** @var InternalEntityFactory Factory to create entities */
+    protected $entityFactory;
+
     /** @var array Array containing the IDs of built-in number formats indicating a date */
     protected $builtinNumFmtIdIndicatingDates;
 
@@ -67,11 +68,15 @@ class StyleHelper
 
     /**
      * @param string $filePath Path of the XLSX file being read
+     * @param WorkbookRelationshipsManager $workbookRelationshipsManager Helps retrieving workbook relationships
+     * @param InternalEntityFactory $entityFactory Factory to create entities
      */
-    public function __construct($filePath)
+    public function __construct($filePath, $workbookRelationshipsManager, $entityFactory)
     {
         $this->filePath = $filePath;
+        $this->entityFactory = $entityFactory;
         $this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping);
+        $this->stylesXMLFilePath = $workbookRelationshipsManager->getStylesXMLFilePath();
     }
 
     /**
@@ -107,14 +112,13 @@ class StyleHelper
         $this->customNumberFormats = [];
         $this->stylesAttributes = [];
 
-        $xmlReader = new XMLReader();
+        $xmlReader = $this->entityFactory->createXMLReader();
 
-        if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) {
+        if ($xmlReader->openFileInZip($this->filePath, $this->stylesXMLFilePath)) {
             while ($xmlReader->read()) {
                 if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
                     $this->extractNumberFormats($xmlReader);
-
-                } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
+                } elseif ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
                     $this->extractStyleAttributes($xmlReader);
                 }
             }
@@ -135,10 +139,10 @@ class StyleHelper
     {
         while ($xmlReader->read()) {
             if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) {
-                $numFmtId = intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
+                $numFmtId = (int) ($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
                 $formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
                 $this->customNumberFormats[$numFmtId] = $formatCode;
-            } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) {
+            } elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) {
                 // Once done reading "numFmts" node's children
                 break;
             }
@@ -158,16 +162,16 @@ class StyleHelper
         while ($xmlReader->read()) {
             if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) {
                 $numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID);
-                $normalizedNumFmtId = ($numFmtId !== null) ? intval($numFmtId) : null;
+                $normalizedNumFmtId = ($numFmtId !== null) ? (int) $numFmtId : null;
 
                 $applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT);
-                $normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? !!$applyNumberFormat : null;
+                $normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? (bool) $applyNumberFormat : null;
 
                 $this->stylesAttributes[] = [
-                    self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId,
+                    self::XML_ATTRIBUTE_NUM_FMT_ID          => $normalizedNumFmtId,
                     self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat,
                 ];
-            } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
+            } elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
                 // Once done reading "cellXfs" node's children
                 break;
             }
diff --git a/lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php b/lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php
new file mode 100644 (file)
index 0000000..2a9b4e0
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+
+namespace Box\Spout\Reader\XLSX\Manager;
+
+use Box\Spout\Common\Exception\IOException;
+use Box\Spout\Reader\Wrapper\XMLReader;
+use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory;
+
+/**
+ * Class WorkbookRelationshipsManager
+ * This class manages the workbook relationships defined in the associated XML file
+ */
+class WorkbookRelationshipsManager
+{
+    const BASE_PATH = 'xl/';
+
+    /** Path of workbook relationships XML file inside the XLSX file */
+    const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels';
+
+    /** Relationships types */
+    const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
+    const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
+    const RELATIONSHIP_TYPE_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
+
+    /** Nodes and attributes used to find relevant information in the workbook relationships XML file */
+    const XML_NODE_RELATIONSHIP = 'Relationship';
+    const XML_ATTRIBUTE_TYPE = 'Type';
+    const XML_ATTRIBUTE_TARGET = 'Target';
+
+    /** @var string Path of the XLSX file being read */
+    private $filePath;
+
+    /** @var InternalEntityFactory Factory to create entities */
+    private $entityFactory;
+
+    /** @var array Cache of the already read workbook relationships: [TYPE] => [FILE_NAME] */
+    private $cachedWorkbookRelationships;
+
+    /**
+     * @param string $filePath Path of the XLSX file being read
+     * @param InternalEntityFactory $entityFactory Factory to create entities
+     */
+    public function __construct($filePath, $entityFactory)
+    {
+        $this->filePath = $filePath;
+        $this->entityFactory = $entityFactory;
+    }
+
+    /**
+     * @return string The path of the shared string XML file
+     */
+    public function getSharedStringsXMLFilePath()
+    {
+        $workbookRelationships = $this->getWorkbookRelationships();
+        $sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS];
+
+        // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
+        $doesContainBasePath = (strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false);
+        if (!$doesContainBasePath) {
+            // make sure we return an absolute file path
+            $sharedStringsXMLFilePath = self::BASE_PATH . $sharedStringsXMLFilePath;
+        }
+
+        return $sharedStringsXMLFilePath;
+    }
+
+    /**
+     * @return bool Whether the XLSX file contains a shared string XML file
+     */
+    public function hasSharedStringsXMLFile()
+    {
+        $workbookRelationships = $this->getWorkbookRelationships();
+
+        return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]);
+    }
+
+    /**
+     * @return string|null The path of the styles XML file
+     */
+    public function getStylesXMLFilePath()
+    {
+        $workbookRelationships = $this->getWorkbookRelationships();
+        $stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES];
+
+        // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
+        $doesContainBasePath = (strpos($stylesXMLFilePath, self::BASE_PATH) !== false);
+        if (!$doesContainBasePath) {
+            // make sure we return a full path
+            $stylesXMLFilePath = self::BASE_PATH . $stylesXMLFilePath;
+        }
+
+        return $stylesXMLFilePath;
+    }
+
+    /**
+     * Reads the workbook.xml.rels and extracts the filename associated to the different types.
+     * It caches the result so that the file is read only once.
+     *
+     * @throws \Box\Spout\Common\Exception\IOException If workbook.xml.rels can't be read
+     * @return array
+     */
+    private function getWorkbookRelationships()
+    {
+        if (!isset($this->cachedWorkbookRelationships)) {
+            $xmlReader = $this->entityFactory->createXMLReader();
+
+            if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_RELS_XML_FILE_PATH) === false) {
+                throw new IOException('Could not open "' . self::WORKBOOK_RELS_XML_FILE_PATH . '".');
+            }
+
+            $this->cachedWorkbookRelationships = [];
+
+            while ($xmlReader->readUntilNodeFound(self::XML_NODE_RELATIONSHIP)) {
+                $this->processWorkbookRelationship($xmlReader);
+            }
+        }
+
+        return $this->cachedWorkbookRelationships;
+    }
+
+    /**
+     * Extracts and store the data of the current workbook relationship.
+     *
+     * @param XMLReader $xmlReader
+     * @return void
+     */
+    private function processWorkbookRelationship($xmlReader)
+    {
+        $type = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TYPE);
+        $target = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
+
+        // @NOTE: if a type is defined more than once, we overwrite the previous value
+        // To be changed if we want to get the file paths of sheet XML files for instance.
+        $this->cachedWorkbookRelationships[$type] = $target;
+    }
+}
index 76e8e32..689f6e2 100644 (file)
@@ -3,38 +3,46 @@
 namespace Box\Spout\Reader\XLSX;
 
 use Box\Spout\Common\Exception\IOException;
-use Box\Spout\Reader\AbstractReader;
-use Box\Spout\Reader\XLSX\Helper\SharedStringsHelper;
+use Box\Spout\Common\Helper\GlobalFunctionsHelper;
+use Box\Spout\Common\Manager\OptionsManagerInterface;
+use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
+use Box\Spout\Reader\Common\Entity\Options;
+use Box\Spout\Reader\ReaderAbstract;
+use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory;
+use Box\Spout\Reader\XLSX\Creator\ManagerFactory;
 
 /**
  * Class Reader
  * This class provides support to read data from a XLSX file
- *
- * @package Box\Spout\Reader\XLSX
  */
-class Reader extends AbstractReader
+class Reader extends ReaderAbstract
 {
+    /** @var ManagerFactory */
+    protected $managerFactory;
+
     /** @var \ZipArchive */
     protected $zip;
 
-    /** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
-    protected $sharedStringsHelper;
+    /** @var \Box\Spout\Reader\XLSX\Manager\SharedStringsManager Manages shared strings */
+    protected $sharedStringsManager;
 
     /** @var SheetIterator To iterator over the XLSX sheets */
     protected $sheetIterator;
 
-
     /**
-     * Returns the reader's current options
-     *
-     * @return ReaderOptions
+     * @param OptionsManagerInterface $optionsManager
+     * @param GlobalFunctionsHelper $globalFunctionsHelper
+     * @param InternalEntityFactoryInterface $entityFactory
+     * @param ManagerFactory $managerFactory
      */
-    protected function getOptions()
-    {
-        if (!isset($this->options)) {
-            $this->options = new ReaderOptions();
-        }
-        return $this->options;
+    public function __construct(
+        OptionsManagerInterface $optionsManager,
+        GlobalFunctionsHelper $globalFunctionsHelper,
+        InternalEntityFactoryInterface $entityFactory,
+        ManagerFactory $managerFactory
+    ) {
+        parent::__construct($optionsManager, $globalFunctionsHelper, $entityFactory);
+        $this->managerFactory = $managerFactory;
     }
 
     /**
@@ -43,7 +51,8 @@ class Reader extends AbstractReader
      */
     public function setTempFolder($tempFolder)
     {
-        $this->getOptions()->setTempFolder($tempFolder);
+        $this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder);
+
         return $this;
     }
 
@@ -63,23 +72,31 @@ class Reader extends AbstractReader
      * and fetches all the available sheets.
      *
      * @param  string $filePath Path of the file to be read
-     * @return void
      * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
      * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
+     * @return void
      */
     protected function openReader($filePath)
     {
-        $this->zip = new \ZipArchive();
+        /** @var InternalEntityFactory $entityFactory */
+        $entityFactory = $this->entityFactory;
+
+        $this->zip = $entityFactory->createZipArchive();
 
         if ($this->zip->open($filePath) === true) {
-            $this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->getOptions()->getTempFolder());
+            $tempFolder = $this->optionsManager->getOption(Options::TEMP_FOLDER);
+            $this->sharedStringsManager = $this->managerFactory->createSharedStringsManager($filePath, $tempFolder, $entityFactory);
 
-            if ($this->sharedStringsHelper->hasSharedStrings()) {
+            if ($this->sharedStringsManager->hasSharedStrings()) {
                 // Extracts all the strings from the sheets for easy access in the future
-                $this->sharedStringsHelper->extractSharedStrings();
+                $this->sharedStringsManager->extractSharedStrings();
             }
 
-            $this->sheetIterator = new SheetIterator($filePath, $this->getOptions(), $this->sharedStringsHelper, $this->globalFunctionsHelper);
+            $this->sheetIterator = $entityFactory->createSheetIterator(
+                $filePath,
+                $this->optionsManager,
+                $this->sharedStringsManager
+            );
         } else {
             throw new IOException("Could not open $filePath for reading.");
         }
@@ -106,8 +123,8 @@ class Reader extends AbstractReader
             $this->zip->close();
         }
 
-        if ($this->sharedStringsHelper) {
-            $this->sharedStringsHelper->cleanup();
+        if ($this->sharedStringsManager) {
+            $this->sharedStringsManager->cleanup();
         }
     }
 }
diff --git a/lib/spout/src/Spout/Reader/XLSX/ReaderOptions.php b/lib/spout/src/Spout/Reader/XLSX/ReaderOptions.php
deleted file mode 100644 (file)
index 5f78c5d..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-namespace Box\Spout\Reader\XLSX;
-
-/**
- * Class ReaderOptions
- * This class is used to customize the reader's behavior
- *
- * @package Box\Spout\Reader\XLSX
- */
-class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
-{
-    /** @var string|null Temporary folder where the temporary files will be created */
-    protected $tempFolder = null;
-
-    /**
-     * @return string|null Temporary folder where the temporary files will be created
-     */
-    public function getTempFolder()
-    {
-        return $this->tempFolder;
-    }
-
-    /**
-     * @param string|null $tempFolder Temporary folder where the temporary files will be created
-     * @return ReaderOptions
-     */
-    public function setTempFolder($tempFolder)
-