From 606103f7fc7af596924c2ddabc8b92d4ca1a8a59 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Tue, 2 May 2017 14:47:00 +0200 Subject: [PATCH 1/9] Move all documentation to Spout's website (#413) The README became really long. All the documentation (README + Wiki) has been moved to a custom website: http://opensource.box.com/spout/. It is now possible to remove all the duplicate content from the README. --- README.md | 346 +----------------------------------------------------- 1 file changed, 2 insertions(+), 344 deletions(-) diff --git a/README.md b/README.md index a0fb2bf..7725bb1 100644 --- a/README.md +++ b/README.md @@ -12,29 +12,10 @@ Contrary to other file readers or writers, it is capable of processing very larg 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 @@ -44,301 +25,6 @@ require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to c * PHP extension `php_xmlreader` 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'); -``` - -By default, the writer generates CSV files encoded in UTF-8, with a BOM. -It is however possible to not include the BOM: -```php -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Common\Type; - -$writer = WriterFactory::create(Type::CSV); -$writer->setShouldAddBOM(false); -``` - - -### Configuring the XLSX and ODS readers and writers - -#### Row styling - -It is possible to apply some formatting options to a row. Spout supports fonts, background, borders 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() - ->setBackgroundColor(Color::YELLOW) - ->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(); -``` - -Adding borders to a row requires a ```Border``` object. - -```php -use Box\Spout\Common\Type; -use Box\Spout\Writer\Style\Border; -use Box\Spout\Writer\Style\BorderBuilder; -use Box\Spout\Writer\Style\Color; -use Box\Spout\Writer\Style\StyleBuilder; -use Box\Spout\Writer\WriterFactory; - -$border = (new BorderBuilder()) - ->setBorderBottom(Color::GREEN, Border::WIDTH_THIN, Border::STYLE_DASHED) - ->build(); - -$style = (new StyleBuilder()) - ->setBorder($border) - ->build(); - -$writer = WriterFactory::create(Type::XLSX); -$writer->openToFile($filePath); - -$writer->addRowWithStyle(['Border Bottom Green Thin Dashed'], $style); - -$writer->close(); -``` - -Spout will use a default style for all created rows. This style can be overridden this way: - -```php -$defaultStyle = (new StyleBuilder()) - ->setFontName('Arial') - ->setFontSize(11) - ->build(); - -$writer = WriterFactory::create(Type::XLSX); -$writer->setDefaultRowStyle($defaultStyle) - ->openToFile($filePath); -``` - -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)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` -| Alignment | Wrap text | `StyleBuilder::setShouldWrapText(true|false)` - -#### 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! - - -#### Date/Time formatting - -When reading a spreadsheet containing dates or times, Spout returns the values by default as DateTime objects. -It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet. - -```php -use Box\Spout\Reader\ReaderFactory; -use Box\Spout\Common\Type; - -$reader = ReaderFactory::create(Type::XLSX); -$reader->setShouldFormatDates(false); // default value -$reader->setShouldFormatDates(true); // will return formatted dates -``` - -### 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. - -Finally, it is possible to know which sheet was active when the spreadsheet was last saved. This can be useful if you are only interested in processing the one sheet that was last focused. -```php -foreach ($reader->getSheetIterator() as $sheet) { - // only process data for the active sheet - if ($sheet->isActive()) { - // do something... - } -} -``` - -### 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(); -``` - - ## Running tests On the `master` branch, only unit and functional tests are included. The performance tests require very large files and have been excluded. @@ -355,34 +41,6 @@ For information, the performance tests take about 30 minutes to run (processing > 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 3MB 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
*inline strings* | < 1 second | 35-40 seconds | 18-20 minutes | -| | Read
*shared 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. From 99816b0b8ef958d232956e17ec766ce84a536bc1 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Mon, 22 May 2017 14:39:26 +0200 Subject: [PATCH 2/9] Use openFileInZip() only (#421) --- .../Reader/XLSX/Helper/SharedStringsHelper.php | 13 +------------ src/Spout/Reader/XLSX/RowIterator.php | 3 +-- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php index 0e88839..415d5cf 100644 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php @@ -80,8 +80,6 @@ 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. - * Please note that SimpleXML does not provide such a functionality but since it is faster - * and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose. * * @return void * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read @@ -91,8 +89,7 @@ class SharedStringsHelper $xmlReader = new XMLReader(); $sharedStringIndex = 0; - $sharedStringsFilePath = $this->getSharedStringsFilePath(); - if ($xmlReader->open($sharedStringsFilePath) === false) { + if ($xmlReader->openFileInZip($this->filePath, self::SHARED_STRINGS_XML_FILE_PATH) === false) { throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".'); } @@ -119,14 +116,6 @@ class SharedStringsHelper $xmlReader->close(); } - /** - * @return string The path to the shared strings XML file - */ - protected function getSharedStringsFilePath() - { - return 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH; - } - /** * Returns the shared strings unique count, as specified in tag. * diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php index 2440593..45069b3 100644 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ b/src/Spout/Reader/XLSX/RowIterator.php @@ -128,8 +128,7 @@ class RowIterator implements IteratorInterface { $this->xmlReader->close(); - $sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath; - if ($this->xmlReader->open($sheetDataFilePath) === false) { + if ($this->xmlReader->openFileInZip($this->filePath, $this->sheetDataXMLFilePath) === false) { throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\"."); } From 5d4166196ad5bcde89f7316d273ef13eff144666 Mon Sep 17 00:00:00 2001 From: madflow Date: Fri, 2 Jun 2017 08:46:48 +0200 Subject: [PATCH 3/9] HHVM is no longer supported on Ubuntu Precise (#439) --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b23c7fd..631cd13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: php +dist: trusty + php: - 5.4 - 5.5 - 5.6 - 7.0 - - hhvm + - hhvm-3.6 cache: directories: @@ -19,5 +21,5 @@ script: - php vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover after_script: - - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi + - if [[ $TRAVIS_PHP_VERSION != 'hhvm-3.6' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [[ $TRAVIS_PHP_VERSION != 'hhvm-3.6' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi From 40b4a57e6bfb29a7134542a824773495ed165b2f Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Mon, 19 Jun 2017 20:37:30 +0200 Subject: [PATCH 4/9] Update README.md (#442) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7725bb1..b63d95e 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,7 @@ For information, the performance tests take about 30 minutes to run (processing ## 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:
+You can ask questions, submit new features ideas or discuss about Spout in the chat room:
[![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 From ee5dee61c760532bee09166d1bbd57011f97b3d0 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Tue, 25 Jul 2017 14:02:17 +0200 Subject: [PATCH 5/9] Fix HHVM jobs on Travis (#451) --- .scrutinizer.yml | 2 +- .travis.yml | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 42f2492..ef177ea 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -4,7 +4,7 @@ filter: tools: external_code_coverage: timeout: 600 # Wait 10 minutes for results - runs: 3 # Merge results for 5.4, 5.5 and 5.6 jobs + runs: 1 # Results are only coming from the PHP 7.1 job php_mess_detector: true php_code_sniffer: config: diff --git a/.travis.yml b/.travis.yml index 631cd13..3c17614 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ php: - 5.5 - 5.6 - 7.0 + - 7.1 - hhvm-3.6 cache: @@ -21,5 +22,8 @@ script: - php vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover after_script: - - if [[ $TRAVIS_PHP_VERSION != 'hhvm-3.6' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ $TRAVIS_PHP_VERSION != 'hhvm-3.6' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi + - | + if [[ "$TRAVIS_PHP_VERSION" == '7.1' ]]; then + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover + fi From 6d44cd26cc49c0fd59ffc01a4c187223c4b183cc Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Tue, 25 Jul 2017 14:16:22 +0200 Subject: [PATCH 6/9] Fix prefixed shared strings XML file (#450) A prefixed sharedStrings.xml file was not properly read, as we were comparing the un-prefixed name with the possible prefixed name. Also, this commit contains a fix for sheets with rows not starting at column A. --- src/Spout/Reader/Wrapper/XMLReader.php | 8 +++++ .../XLSX/Helper/SharedStringsHelper.php | 4 +-- src/Spout/Reader/XLSX/RowIterator.php | 2 +- tests/Spout/Reader/XLSX/ReaderTest.php | 31 ++++++++++++++++++ ...heet_with_prefixed_shared_strings_xml.xlsx | Bin 0 -> 3683 bytes ...eet_with_row_not_starting_at_column_a.xlsx | Bin 0 -> 3613 bytes 6 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/resources/xlsx/sheet_with_prefixed_shared_strings_xml.xlsx create mode 100644 tests/resources/xlsx/sheet_with_row_not_starting_at_column_a.xlsx diff --git a/src/Spout/Reader/Wrapper/XMLReader.php b/src/Spout/Reader/Wrapper/XMLReader.php index 2e20327..08e99fc 100644 --- a/src/Spout/Reader/Wrapper/XMLReader.php +++ b/src/Spout/Reader/Wrapper/XMLReader.php @@ -164,4 +164,12 @@ class XMLReader extends \XMLReader return ($this->nodeType === $nodeType && $currentNodeName === $nodeName); } + + /** + * @return string The name of the current node, un-prefixed + */ + public function getCurrentNodeName() + { + return $this->localName; + } } diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php index 415d5cf..fc04c79 100644 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php @@ -99,7 +99,7 @@ class SharedStringsHelper $xmlReader->readUntilNodeFound(self::XML_NODE_SI); - while ($xmlReader->name === self::XML_NODE_SI) { + while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SI) { $this->processSharedStringsItem($xmlReader, $sharedStringIndex); $sharedStringIndex++; @@ -128,7 +128,7 @@ class SharedStringsHelper $xmlReader->next(self::XML_NODE_SST); // Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE) - while ($xmlReader->name === self::XML_NODE_SST && $xmlReader->nodeType !== XMLReader::ELEMENT) { + while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SST && $xmlReader->nodeType !== XMLReader::ELEMENT) { $xmlReader->read(); } diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php index 45069b3..e70c617 100644 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ b/src/Spout/Reader/XLSX/RowIterator.php @@ -348,7 +348,7 @@ class RowIterator implements IteratorInterface */ protected function isEmptyRow($rowData) { - return (count($rowData) === 1 && $rowData[0] === ''); + return (count($rowData) === 1 && key($rowData) === ''); } /** diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index b9e032e..2ad6834 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -112,6 +112,22 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadShouldSupportPrefixedSharedStringsXML() + { + // The sharedStrings.xml file of this spreadsheet is prefixed. + // For instance, they use "" instead of "", etc. + $allRows = $this->getAllRowsForFile('sheet_with_prefixed_shared_strings_xml.xlsx'); + + $expectedRows = [ + ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'], + ['s1--A2', 's1--B2', 's1--C2', 's1--D2', 's1--E2'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * @return void */ @@ -169,6 +185,21 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadShouldSupportFilesWithRowsNotStartingAtColumnA() + { + // file where the row starts at column C: + // 0... + $allRows = $this->getAllRowsForFile('sheet_with_row_not_starting_at_column_a.xlsx'); + + $expectedRows = [ + ['', '', 's1--C1', 's1--D1', 's1--E1'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * @return void */ diff --git a/tests/resources/xlsx/sheet_with_prefixed_shared_strings_xml.xlsx b/tests/resources/xlsx/sheet_with_prefixed_shared_strings_xml.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..33f92f73c85e691e465474e1520cffad5dea0fc2 GIT binary patch literal 3683 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 Date: Thu, 27 Jul 2017 23:40:26 +0200 Subject: [PATCH 7/9] Update gitignore (#453) --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 68715b4..3584766 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +/.idea +*.iml + /tests/resources/generated /tests/coverage /vendor -/.idea -*.iml From 3bbff7ea7df9c2fd3437b3ff416e4cddcd6ed739 Mon Sep 17 00:00:00 2001 From: Chris Muthig Date: Tue, 5 Sep 2017 15:32:43 -0700 Subject: [PATCH 8/9] Fix WriterInterface docblock typo --- src/Spout/Writer/WriterInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spout/Writer/WriterInterface.php b/src/Spout/Writer/WriterInterface.php index e2d9f8d..93a6ee2 100644 --- a/src/Spout/Writer/WriterInterface.php +++ b/src/Spout/Writer/WriterInterface.php @@ -35,7 +35,7 @@ interface WriterInterface * @param array $dataRow Array containing data to be streamed. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @return WriterInterface - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yetthe writer + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ public function addRow(array $dataRow); From 3681a3421a868ab9a65da156c554f756541f452b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lum=C3=ADr=20Toman?= Date: Thu, 31 Aug 2017 10:15:47 +0200 Subject: [PATCH 9/9] Change Worksheet function visibility from private to protected in order to class extending. We have created small package on top of the Spout for support cell number format and per cell styles. --- src/Spout/Writer/XLSX/Internal/Worksheet.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Spout/Writer/XLSX/Internal/Worksheet.php b/src/Spout/Writer/XLSX/Internal/Worksheet.php index b5a3dc7..0bd909d 100644 --- a/src/Spout/Writer/XLSX/Internal/Worksheet.php +++ b/src/Spout/Writer/XLSX/Internal/Worksheet.php @@ -159,7 +159,7 @@ EOD; * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @return bool Whether the given row is empty */ - private function isEmptyRow($dataRow) + protected function isEmptyRow($dataRow) { $numCells = count($dataRow); // using "reset()" instead of "$dataRow[0]" because $dataRow can be an associative array @@ -176,7 +176,7 @@ EOD; * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported */ - private function addNonEmptyRow($dataRow, $style) + protected function addNonEmptyRow($dataRow, $style) { $cellNumber = 0; $rowIndex = $this->lastWrittenRowIndex + 1; @@ -207,7 +207,7 @@ EOD; * @return string * @throws InvalidArgumentException If the given value cannot be processed */ - private function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId) + protected function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId) { $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber); $cellXML = 'stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) { throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');