diff --git a/README.md b/README.md index 80070de..951d984 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,15 @@ $sheet->setName('My custom name'); > > 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 diff --git a/src/Spout/Reader/CSV/Sheet.php b/src/Spout/Reader/CSV/Sheet.php index baac559..9a688db 100644 --- a/src/Spout/Reader/CSV/Sheet.php +++ b/src/Spout/Reader/CSV/Sheet.php @@ -32,4 +32,31 @@ class Sheet implements SheetInterface { return $this->rowIterator; } + + /** + * @api + * @return int Index of the sheet + */ + public function getIndex() + { + return 0; + } + + /** + * @api + * @return string Name of the sheet - empty string since CSV does not support that + */ + public function getName() + { + return ''; + } + + /** + * @api + * @return bool Always TRUE as there is only one sheet + */ + public function isActive() + { + return true; + } } diff --git a/src/Spout/Reader/ODS/Helper/SettingsHelper.php b/src/Spout/Reader/ODS/Helper/SettingsHelper.php new file mode 100644 index 0000000..a5388ef --- /dev/null +++ b/src/Spout/Reader/ODS/Helper/SettingsHelper.php @@ -0,0 +1,51 @@ +openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) { + return null; + } + + $activeSheetName = null; + + try { + while ($xmlReader->readUntilNodeFound(self::XML_NODE_CONFIG_ITEM)) { + if ($xmlReader->getAttribute(self::XML_ATTRIBUTE_CONFIG_NAME) === self::XML_ATTRIBUTE_VALUE_ACTIVE_TABLE) { + $activeSheetName = $xmlReader->readString(); + break; + } + } + } catch (XMLProcessingException $exception) { + // do nothing + } + + $xmlReader->close(); + + return $activeSheetName; + } +} diff --git a/src/Spout/Reader/ODS/Sheet.php b/src/Spout/Reader/ODS/Sheet.php index fe48dd2..794ad3a 100644 --- a/src/Spout/Reader/ODS/Sheet.php +++ b/src/Spout/Reader/ODS/Sheet.php @@ -25,17 +25,22 @@ class Sheet implements SheetInterface /** @var string Name of the sheet */ protected $name; + /** @var bool Whether the sheet was the active one */ + protected $isActive; + /** * @param XMLReader $xmlReader XML Reader, positioned on the "" element * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) - * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options * @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 */ - public function __construct($xmlReader, $sheetIndex, $sheetName, $options) + public function __construct($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $options) { $this->rowIterator = new RowIterator($xmlReader, $options); $this->index = $sheetIndex; $this->name = $sheetName; + $this->isActive = $isSheetActive; } /** @@ -64,4 +69,13 @@ class Sheet implements SheetInterface { return $this->name; } + + /** + * @api + * @return bool Whether the sheet was defined as active + */ + public function isActive() + { + return $this->isActive; + } } diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php index 4a311d4..995c136 100644 --- a/src/Spout/Reader/ODS/SheetIterator.php +++ b/src/Spout/Reader/ODS/SheetIterator.php @@ -5,6 +5,7 @@ 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\Helper\SettingsHelper; use Box\Spout\Reader\Wrapper\XMLReader; /** @@ -39,6 +40,9 @@ class SheetIterator implements IteratorInterface /** @var int The index of the sheet being read (zero-based) */ protected $currentSheetIndex; + /** @var string The name of the sheet that was defined as active */ + protected $activeSheetName; + /** * @param string $filePath Path of the file to be read * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options @@ -52,6 +56,9 @@ class SheetIterator implements IteratorInterface /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance(); + + $settingsHelper = new SettingsHelper(); + $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath); } /** @@ -115,8 +122,27 @@ 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, $this->options); + return new Sheet($this->xmlReader, $this->currentSheetIndex, $sheetName, $isActiveSheet, $this->options); + } + + /** + * Returns whether the current sheet was defined as the active one + * + * @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 + * @return bool Whether the current sheet was defined as the active one + */ + private function isActiveSheet($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. + return ( + ($activeSheetName === null && $sheetIndex === 0) || + ($activeSheetName === $sheetName) + ); } /** diff --git a/src/Spout/Reader/Wrapper/XMLReader.php b/src/Spout/Reader/Wrapper/XMLReader.php index c979819..ffe6818 100644 --- a/src/Spout/Reader/Wrapper/XMLReader.php +++ b/src/Spout/Reader/Wrapper/XMLReader.php @@ -28,13 +28,10 @@ class XMLReader extends \XMLReader $wasOpenSuccessful = false; $realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath); - // HHVM does not check if file exists within zip file - // @link https://github.com/facebook/hhvm/issues/5779 - if ($this->isRunningHHVM()) { - if ($this->fileExistsWithinZip($realPathURI)) { - $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); - } - } else { + // We need to check first that the file we are trying to read really exist because: + // - PHP emits a warning when trying to open a file that does not exist. + // - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779) + if ($this->fileExistsWithinZip($realPathURI)) { $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); } @@ -54,16 +51,6 @@ class XMLReader extends \XMLReader return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPath); } - /** - * Returns whether the current environment is HHVM - * - * @return bool TRUE if running on HHVM, FALSE otherwise - */ - protected function isRunningHHVM() - { - return defined('HHVM_VERSION'); - } - /** * Returns whether the file at the given location exists * diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php index abb3b0a..7a36f4a 100644 --- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SheetHelper.php @@ -53,12 +53,18 @@ class SheetHelper { $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('sheet')) { - $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex); + if ($xmlReader->isPositionedOnStartingNode('workbookView')) { + // The "workbookView" node is located before "sheet" nodes, ensuring that + // the active sheet is known before parsing sheets data. + $activeSheetIndex = (int) $xmlReader->getAttribute('activeTab'); + } else if ($xmlReader->isPositionedOnStartingNode('sheet')) { + $isSheetActive = ($sheetIndex === $activeSheetIndex); + $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive); $sheetIndex++; } else if ($xmlReader->isPositionedOnEndingNode('sheets')) { // stop reading once all sheets have been read @@ -79,9 +85,10 @@ class SheetHelper * * @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) + protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive) { $sheetId = $xmlReaderOnSheetNode->getAttribute('r:id'); $escapedSheetName = $xmlReaderOnSheetNode->getAttribute('name'); @@ -92,7 +99,11 @@ class SheetHelper $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId); - return new Sheet($this->filePath, $sheetDataXMLFilePath, $sheetIndexZeroBased, $sheetName, $this->options, $this->sharedStringsHelper); + return new Sheet( + $this->filePath, $sheetDataXMLFilePath, + $sheetIndexZeroBased, $sheetName, $isSheetActive, + $this->options, $this->sharedStringsHelper + ); } /** diff --git a/src/Spout/Reader/XLSX/Sheet.php b/src/Spout/Reader/XLSX/Sheet.php index 32d1ea6..9baaef2 100644 --- a/src/Spout/Reader/XLSX/Sheet.php +++ b/src/Spout/Reader/XLSX/Sheet.php @@ -21,19 +21,24 @@ class Sheet implements SheetInterface /** @var string Name of the sheet */ protected $name; + /** @var bool Whether the sheet was the active one */ + protected $isActive; + /** * @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 \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options * @param Helper\SharedStringsHelper Helper to work with shared strings */ - public function __construct($filePath, $sheetDataXMLFilePath, $sheetIndex, $sheetName, $options, $sharedStringsHelper) + public function __construct($filePath, $sheetDataXMLFilePath, $sheetIndex, $sheetName, $isSheetActive, $options, $sharedStringsHelper) { $this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper); $this->index = $sheetIndex; $this->name = $sheetName; + $this->isActive = $isSheetActive; } /** @@ -62,4 +67,13 @@ class Sheet implements SheetInterface { return $this->name; } + + /** + * @api + * @return bool Whether the sheet was defined as active + */ + public function isActive() + { + return $this->isActive; + } } diff --git a/tests/Spout/Reader/CSV/SheetTest.php b/tests/Spout/Reader/CSV/SheetTest.php new file mode 100644 index 0000000..ae18c2f --- /dev/null +++ b/tests/Spout/Reader/CSV/SheetTest.php @@ -0,0 +1,46 @@ +openFileAndReturnSheet('csv_standard.csv'); + + $this->assertEquals('', $sheet->getName()); + $this->assertEquals(0, $sheet->getIndex()); + $this->assertTrue($sheet->isActive()); + } + + /** + * @param string $fileName + * @return Sheet + */ + private function openFileAndReturnSheet($fileName) + { + $resourcePath = $this->getResourcePath($fileName); + $reader = ReaderFactory::create(Type::CSV); + $reader->open($resourcePath); + + $sheet = $reader->getSheetIterator()->current(); + + $reader->close(); + + return $sheet; + } +} diff --git a/tests/Spout/Reader/ODS/SheetTest.php b/tests/Spout/Reader/ODS/SheetTest.php index 1c98347..cc3bd03 100644 --- a/tests/Spout/Reader/ODS/SheetTest.php +++ b/tests/Spout/Reader/ODS/SheetTest.php @@ -18,15 +18,30 @@ class SheetTest extends \PHPUnit_Framework_TestCase /** * @return void */ - public function testNextSheetShouldReturnCorrectSheetInfos() + public function testReaderShouldReturnCorrectSheetInfos() { + // NOTE: This spreadsheet has its second tab defined as active $sheets = $this->openFileAndReturnSheets('two_sheets_with_custom_names.ods'); $this->assertEquals('Sheet First', $sheets[0]->getName()); $this->assertEquals(0, $sheets[0]->getIndex()); + $this->assertFalse($sheets[0]->isActive()); $this->assertEquals('Sheet Last', $sheets[1]->getName()); $this->assertEquals(1, $sheets[1]->getIndex()); + $this->assertTrue($sheets[1]->isActive()); + } + + /** + * @return void + */ + public function testReaderShouldDefineFirstSheetAsActiveByDefault() + { + // NOTE: This spreadsheet has no information about the active sheet + $sheets = $this->openFileAndReturnSheets('two_sheets_with_no_settings_xml_file.ods'); + + $this->assertTrue($sheets[0]->isActive()); + $this->assertFalse($sheets[1]->isActive()); } /** diff --git a/tests/Spout/Reader/XLSX/SheetTest.php b/tests/Spout/Reader/XLSX/SheetTest.php index 3464819..b8c332b 100644 --- a/tests/Spout/Reader/XLSX/SheetTest.php +++ b/tests/Spout/Reader/XLSX/SheetTest.php @@ -18,15 +18,18 @@ class SheetTest extends \PHPUnit_Framework_TestCase /** * @return void */ - public function testNextSheetShouldReturnCorrectSheetInfos() + public function testReaderShouldReturnCorrectSheetInfos() { - $sheets = $this->openFileAndReturnSheets('two_sheets_with_custom_names.xlsx'); + // NOTE: This spreadsheet has its second tab defined as active + $sheets = $this->openFileAndReturnSheets('two_sheets_with_custom_names_and_custom_active_tab.xlsx'); $this->assertEquals('CustomName1', $sheets[0]->getName()); $this->assertEquals(0, $sheets[0]->getIndex()); + $this->assertFalse($sheets[0]->isActive()); $this->assertEquals('CustomName2', $sheets[1]->getName()); $this->assertEquals(1, $sheets[1]->getIndex()); + $this->assertTrue($sheets[1]->isActive()); } /** diff --git a/tests/resources/ods/two_sheets_with_no_settings_xml_file.ods b/tests/resources/ods/two_sheets_with_no_settings_xml_file.ods new file mode 100644 index 0000000..c2a78e7 Binary files /dev/null and b/tests/resources/ods/two_sheets_with_no_settings_xml_file.ods differ diff --git a/tests/resources/xlsx/two_sheets_with_custom_names_and_custom_active_tab.xlsx b/tests/resources/xlsx/two_sheets_with_custom_names_and_custom_active_tab.xlsx new file mode 100644 index 0000000..8ba5f02 Binary files /dev/null and b/tests/resources/xlsx/two_sheets_with_custom_names_and_custom_active_tab.xlsx differ