From 2e43643a26719598ff3302f6a828441fab9cd66e Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Sat, 11 Nov 2017 01:21:04 +0100 Subject: [PATCH] Sheet visibility - ODS writer and reader --- .../Reader/ODS/Creator/EntityFactory.php | 5 +- src/Spout/Reader/ODS/Sheet.php | 15 +++- src/Spout/Reader/ODS/SheetIterator.php | 68 +++++++++++++++++- src/Spout/Writer/Common/Entity/Sheet.php | 10 +-- .../Writer/ODS/Helper/FileSystemHelper.php | 2 +- .../Writer/ODS/Manager/Style/StyleManager.php | 14 ++-- .../Writer/XLSX/Helper/FileSystemHelper.php | 2 +- tests/Spout/Reader/ODS/SheetTest.php | 11 +++ tests/Spout/Writer/ODS/SheetTest.php | 35 +++++++++ tests/Spout/Writer/XLSX/SheetTest.php | 2 +- .../ods/two_sheets_one_hidden_one_not.ods | Bin 0 -> 8258 bytes 11 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 tests/resources/ods/two_sheets_one_hidden_one_not.ods diff --git a/src/Spout/Reader/ODS/Creator/EntityFactory.php b/src/Spout/Reader/ODS/Creator/EntityFactory.php index 3af756a..0324dd8 100644 --- a/src/Spout/Reader/ODS/Creator/EntityFactory.php +++ b/src/Spout/Reader/ODS/Creator/EntityFactory.php @@ -45,14 +45,15 @@ class EntityFactory implements EntityFactoryInterface * @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, $optionsManager) + public function createSheet($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible, $optionsManager) { $rowIterator = $this->createRowIterator($xmlReader, $optionsManager); - return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive); + return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible); } /** diff --git a/src/Spout/Reader/ODS/Sheet.php b/src/Spout/Reader/ODS/Sheet.php index e696a1a..74ec61f 100644 --- a/src/Spout/Reader/ODS/Sheet.php +++ b/src/Spout/Reader/ODS/Sheet.php @@ -25,18 +25,23 @@ 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 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 bool $isSheetVisible Whether the sheet is visible */ - public function __construct($rowIterator, $sheetIndex, $sheetName, $isSheetActive) + public function __construct($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible) { $this->rowIterator = $rowIterator; $this->index = $sheetIndex; $this->name = $sheetName; $this->isActive = $isSheetActive; + $this->isVisible = $isSheetVisible; } /** @@ -70,4 +75,12 @@ class Sheet implements SheetInterface { return $this->isActive; } + + /** + * @return bool Whether the sheet is visible + */ + public function isVisible() + { + return $this->isVisible; + } } diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php index 3c6dd4e..6785b36 100644 --- a/src/Spout/Reader/ODS/SheetIterator.php +++ b/src/Spout/Reader/ODS/SheetIterator.php @@ -17,9 +17,16 @@ 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; @@ -45,6 +52,9 @@ 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\Common\Manager\OptionsManagerInterface $optionsManager @@ -79,6 +89,7 @@ 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()}]"); @@ -87,6 +98,33 @@ class SheetIterator implements IteratorInterface $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 * @see http://php.net/manual/en/iterator.valid.php @@ -123,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 $this->entityFactory->createSheet($this->xmlReader, $this->currentSheetIndex, $sheetName, $isActiveSheet, $this->optionsManager); + $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 + ); } /** @@ -136,7 +185,7 @@ class SheetIterator implements IteratorInterface * @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. @@ -146,6 +195,19 @@ 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 * @see http://php.net/manual/en/iterator.key.php diff --git a/src/Spout/Writer/Common/Entity/Sheet.php b/src/Spout/Writer/Common/Entity/Sheet.php index c2f5366..aabdd91 100644 --- a/src/Spout/Writer/Common/Entity/Sheet.php +++ b/src/Spout/Writer/Common/Entity/Sheet.php @@ -98,11 +98,11 @@ class Sheet return $this->isVisible; } - /** - * @param bool $isVisible Visibility of the sheet - * @return Sheet - */ - public function setIsVisible($isVisible) + /** + * @param bool $isVisible Visibility of the sheet + * @return Sheet + */ + public function setIsVisible($isVisible) { $this->isVisible = $isVisible; diff --git a/src/Spout/Writer/ODS/Helper/FileSystemHelper.php b/src/Spout/Writer/ODS/Helper/FileSystemHelper.php index 65ed815..d0e2e05 100644 --- a/src/Spout/Writer/ODS/Helper/FileSystemHelper.php +++ b/src/Spout/Writer/ODS/Helper/FileSystemHelper.php @@ -202,7 +202,7 @@ EOD; EOD; $contentXmlFileContents .= $styleManager->getContentXmlFontFaceSectionContent(); - $contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent(count($worksheets)); + $contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheets); $contentXmlFileContents .= ''; diff --git a/src/Spout/Writer/ODS/Manager/Style/StyleManager.php b/src/Spout/Writer/ODS/Manager/Style/StyleManager.php index c658b18..38b9eb7 100644 --- a/src/Spout/Writer/ODS/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/ODS/Manager/Style/StyleManager.php @@ -3,6 +3,7 @@ namespace Box\Spout\Writer\ODS\Manager\Style; use Box\Spout\Writer\Common\Entity\Style\BorderPart; +use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\ODS\Helper\BorderHelper; /** @@ -149,10 +150,10 @@ EOD; /** * Returns the contents of the "" section, inside "content.xml" file. * - * @param int $numWorksheets Number of worksheets created + * @param Worksheet[] $worksheets * @return string */ - public function getContentXmlAutomaticStylesSectionContent($numWorksheets) + public function getContentXmlAutomaticStylesSectionContent($worksheets) { $content = ''; @@ -169,10 +170,13 @@ EOD; EOD; - for ($i = 1; $i <= $numWorksheets; $i++) { + foreach ($worksheets as $worksheet) { + $worksheetId = $worksheet->getId(); + $isSheetVisible = $worksheet->getExternalSheet()->isVisible() ? 'true' : 'false'; + $content .= << - + + EOD; } diff --git a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php b/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php index 06d5481..8997b98 100644 --- a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php +++ b/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php @@ -309,7 +309,7 @@ EOD; $worksheetName = $worksheet->getExternalSheet()->getName(); $worksheetVisibility = $worksheet->getExternalSheet()->isVisible() ? 'visible' : 'hidden'; $worksheetId = $worksheet->getId(); - $workbookXmlFileContents .= ''; + $workbookXmlFileContents .= ''; } $workbookXmlFileContents .= <<<'EOD' diff --git a/tests/Spout/Reader/ODS/SheetTest.php b/tests/Spout/Reader/ODS/SheetTest.php index e5dfdf8..d03a3ab 100644 --- a/tests/Spout/Reader/ODS/SheetTest.php +++ b/tests/Spout/Reader/ODS/SheetTest.php @@ -42,6 +42,17 @@ class SheetTest extends \PHPUnit_Framework_TestCase $this->assertFalse($sheets[1]->isActive()); } + /** + * @return void + */ + public function testReaderShouldReturnCorrectSheetVisibility() + { + $sheets = $this->openFileAndReturnSheets('two_sheets_one_hidden_one_not.ods'); + + $this->assertFalse($sheets[0]->isVisible()); + $this->assertTrue($sheets[1]->isVisible()); + } + /** * @param string $fileName * @return Sheet[] diff --git a/tests/Spout/Writer/ODS/SheetTest.php b/tests/Spout/Writer/ODS/SheetTest.php index 1599c8f..52cf7aa 100644 --- a/tests/Spout/Writer/ODS/SheetTest.php +++ b/tests/Spout/Writer/ODS/SheetTest.php @@ -78,6 +78,21 @@ class SheetTest extends \PHPUnit_Framework_TestCase $sheet->setName($customSheetName); } + /** + * @return void + */ + public function testSetSheetVisibilityShouldCreateSheetHidden() + { + $fileName = 'test_set_visibility_should_create_sheet_hidden.xlsx'; + $this->writeDataToHiddenSheet($fileName); + + $resourcePath = $this->getGeneratedResourcePath($fileName); + $pathToContentFile = $resourcePath . '#content.xml'; + $xmlContents = file_get_contents('zip://' . $pathToContentFile); + + $this->assertContains(' table:display="false"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); + } + /** * @param string $fileName * @param string $sheetName @@ -121,6 +136,26 @@ class SheetTest extends \PHPUnit_Framework_TestCase return $writer->getSheets(); } + /** + * @param string $fileName + * @return void + */ + private function writeDataToHiddenSheet($fileName) + { + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = EntityFactory::createWriter(Type::ODS); + $writer->openToFile($resourcePath); + + $sheet = $writer->getCurrentSheet(); + $sheet->setIsVisible(false); + + $writer->addRow($this->createRowFromValues(['ods--11', 'ods--12'])); + $writer->close(); + } + /** * @param string $expectedName * @param string $fileName diff --git a/tests/Spout/Writer/XLSX/SheetTest.php b/tests/Spout/Writer/XLSX/SheetTest.php index 64b276b..9e71563 100644 --- a/tests/Spout/Writer/XLSX/SheetTest.php +++ b/tests/Spout/Writer/XLSX/SheetTest.php @@ -90,7 +90,7 @@ class SheetTest extends \PHPUnit_Framework_TestCase $pathToWorkbookFile = $resourcePath . '#xl/workbook.xml'; $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); - $this->assertContains(" state=\"hidden\"", $xmlContents, 'The sheet visibility should have been changed to "hidden"'); + $this->assertContains(' state="hidden"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); } /** diff --git a/tests/resources/ods/two_sheets_one_hidden_one_not.ods b/tests/resources/ods/two_sheets_one_hidden_one_not.ods new file mode 100644 index 0000000000000000000000000000000000000000..2771f8c95bd87b4897689ef9030f24f4b90ebc41 GIT binary patch literal 8258 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