From e2b519d6f9679e07ddc8aace26f38daecb857ea0 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Sat, 11 Nov 2017 15:06:18 +0100 Subject: [PATCH] Fetch XML file paths from Workbook Relationships --- src/Spout/Reader/Wrapper/XMLReader.php | 5 +- .../Reader/XLSX/Creator/ManagerFactory.php | 33 ++++- .../XLSX/Manager/SharedStringsManager.php | 36 ++--- .../Reader/XLSX/Manager/StyleManager.php | 12 +- .../Manager/WorkbookRelationshipsManager.php | 136 ++++++++++++++++++ .../XLSX/Manager/SharedStringsManagerTest.php | 10 +- .../Reader/XLSX/Manager/StyleManagerTest.php | 10 +- tests/Spout/Reader/XLSX/ReaderTest.php | 17 +++ ..._with_capital_shared_strings_filename.xlsx | Bin 0 -> 3789 bytes 9 files changed, 226 insertions(+), 33 deletions(-) create mode 100644 src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php create mode 100644 tests/resources/xlsx/one_sheet_with_capital_shared_strings_filename.xlsx diff --git a/src/Spout/Reader/Wrapper/XMLReader.php b/src/Spout/Reader/Wrapper/XMLReader.php index 1507cc9..bdbaf4d 100644 --- a/src/Spout/Reader/Wrapper/XMLReader.php +++ b/src/Spout/Reader/Wrapper/XMLReader.php @@ -45,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); } /** diff --git a/src/Spout/Reader/XLSX/Creator/ManagerFactory.php b/src/Spout/Reader/XLSX/Creator/ManagerFactory.php index 497b130..58162f3 100644 --- a/src/Spout/Reader/XLSX/Creator/ManagerFactory.php +++ b/src/Spout/Reader/XLSX/Creator/ManagerFactory.php @@ -6,6 +6,7 @@ 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 @@ -19,6 +20,9 @@ class ManagerFactory /** @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 @@ -37,7 +41,30 @@ class ManagerFactory */ public function createSharedStringsManager($filePath, $tempFolder, $entityFactory) { - return new SharedStringsManager($filePath, $tempFolder, $entityFactory, $this->helperFactory, $this->cachingStrategyFactory); + $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 EntityFactory $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; } /** @@ -61,6 +88,8 @@ class ManagerFactory */ public function createStyleManager($filePath, $entityFactory) { - return new StyleManager($filePath, $entityFactory); + $workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory); + + return new StyleManager($filePath, $workbookRelationshipsManager, $entityFactory); } } diff --git a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php index af9279b..2a21862 100644 --- a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php +++ b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php @@ -9,6 +9,7 @@ use Box\Spout\Reader\XLSX\Creator\EntityFactory; use Box\Spout\Reader\XLSX\Creator\HelperFactory; use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory; use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface; +use Box\Spout\Writer\Common\Entity\Workbook; /** * Class SharedStringsManager @@ -16,9 +17,6 @@ use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface; */ 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,6 +38,9 @@ class SharedStringsManager /** @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 EntityFactory Factory to create entities */ protected $entityFactory; @@ -55,14 +56,22 @@ class SharedStringsManager /** * @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 WorkbookRelationshipsManager $workbookRelationshipsManager Helps retrieving workbook relationships * @param EntityFactory $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, $entityFactory, $helperFactory, $cachingStrategyFactory) - { + 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; @@ -75,15 +84,7 @@ class SharedStringsManager */ public function hasSharedStrings() { - $hasSharedStrings = false; - $zip = $this->entityFactory->createZipArchive(); - - if ($zip->open($this->filePath) === true) { - $hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false); - $zip->close(); - } - - return $hasSharedStrings; + return $this->workbookRelationshipsManager->hasSharedStringsXMLFile(); } /** @@ -96,16 +97,17 @@ class SharedStringsManager * 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 sharedStrings.xml can't be read + * @throws \Box\Spout\Common\Exception\IOException If shared strings XML file can't be read * @return void */ public function extractSharedStrings() { + $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 { diff --git a/src/Spout/Reader/XLSX/Manager/StyleManager.php b/src/Spout/Reader/XLSX/Manager/StyleManager.php index b36661b..fa33331 100644 --- a/src/Spout/Reader/XLSX/Manager/StyleManager.php +++ b/src/Spout/Reader/XLSX/Manager/StyleManager.php @@ -10,9 +10,6 @@ use Box\Spout\Reader\XLSX\Creator\EntityFactory; */ 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'; @@ -51,6 +48,9 @@ class StyleManager /** @var string Path of the XLSX file being read */ protected $filePath; + /** @var string Path of the styles XML file */ + protected $stylesXMLFilePath; + /** @var EntityFactory Factory to create entities */ protected $entityFactory; @@ -68,13 +68,15 @@ class StyleManager /** * @param string $filePath Path of the XLSX file being read + * @param WorkbookRelationshipsManager $workbookRelationshipsManager Helps retrieving workbook relationships * @param EntityFactory $entityFactory Factory to create entities */ - public function __construct($filePath, $entityFactory) + public function __construct($filePath, $workbookRelationshipsManager, $entityFactory) { $this->filePath = $filePath; $this->entityFactory = $entityFactory; $this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping); + $this->stylesXMLFilePath = $workbookRelationshipsManager->getStylesXMLFilePath(); } /** @@ -112,7 +114,7 @@ class StyleManager $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); diff --git a/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php b/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php new file mode 100644 index 0000000..17877b7 --- /dev/null +++ b/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php @@ -0,0 +1,136 @@ + [FILE_NAME] */ + private $cachedWorkbookRelationships; + + /** + * @param string $filePath Path of the XLSX file being read + * @param EntityFactory $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; + } +} diff --git a/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php b/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php index 8eb01bf..cecd208 100644 --- a/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php +++ b/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php @@ -51,8 +51,16 @@ class SharedStringsManagerTest extends \PHPUnit_Framework_TestCase $helperFactory = new HelperFactory(); $managerFactory = new ManagerFactory($helperFactory, $cachingStrategyFactory); $entityFactory = new EntityFactory($managerFactory, $helperFactory); + $workbookRelationshipsManager = new WorkbookRelationshipsManager($resourcePath, $entityFactory); - $this->sharedStringsManager = new SharedStringsManager($resourcePath, $tempFolder, $entityFactory, $helperFactory, $cachingStrategyFactory); + $this->sharedStringsManager = new SharedStringsManager( + $resourcePath, + $tempFolder, + $workbookRelationshipsManager, + $entityFactory, + $helperFactory, + $cachingStrategyFactory + ); return $this->sharedStringsManager; } diff --git a/tests/Spout/Reader/XLSX/Manager/StyleManagerTest.php b/tests/Spout/Reader/XLSX/Manager/StyleManagerTest.php index 4127795..b3b6510 100644 --- a/tests/Spout/Reader/XLSX/Manager/StyleManagerTest.php +++ b/tests/Spout/Reader/XLSX/Manager/StyleManagerTest.php @@ -3,9 +3,6 @@ namespace Box\Spout\Reader\XLSX\Manager; use Box\Spout\Reader\XLSX\Creator\EntityFactory; -use Box\Spout\Reader\XLSX\Creator\HelperFactory; -use Box\Spout\Reader\XLSX\Creator\ManagerFactory; -use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory; /** * Class StyleManagerTest @@ -19,13 +16,12 @@ class StyleManagerTest extends \PHPUnit_Framework_TestCase */ private function getStyleManagerMock($styleAttributes = [], $customNumberFormats = []) { - $helperFactory = new HelperFactory(); - $managerFactory = new ManagerFactory($helperFactory, new CachingStrategyFactory()); - $entityFactory = new EntityFactory($managerFactory, $helperFactory); + $entityFactory = $this->createMock(EntityFactory::class); + $workbookRelationshipsManager = $this->createMock(WorkbookRelationshipsManager::class); /** @var StyleManager $styleManager */ $styleManager = $this->getMockBuilder('\Box\Spout\Reader\XLSX\Manager\StyleManager') - ->setConstructorArgs(['/path/to/file.xlsx', $entityFactory]) + ->setConstructorArgs(['/path/to/file.xlsx', $workbookRelationshipsManager, $entityFactory]) ->setMethods(['getCustomNumberFormats', 'getStylesAttributes']) ->getMock(); diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index b16df6b..49deac6 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -169,6 +169,23 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadShouldSupportFilesWithCapitalSharedStringsFileName() + { + $allRows = $this->getAllRowsForFile('one_sheet_with_capital_shared_strings_filename.xlsx'); + + $expectedRows = [ + ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'], + ['s1--A2', 's1--B2', 's1--C2', 's1--D2', 's1--E2'], + ['s1--A3', 's1--B3', 's1--C3', 's1--D3', 's1--E3'], + ['s1--A4', 's1--B4', 's1--C4', 's1--D4', 's1--E4'], + ['s1--A5', 's1--B5', 's1--C5', 's1--D5', 's1--E5'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * @return void */ diff --git a/tests/resources/xlsx/one_sheet_with_capital_shared_strings_filename.xlsx b/tests/resources/xlsx/one_sheet_with_capital_shared_strings_filename.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..93cd7cdbb3c7bfd6cf2e9402ce98984a72f0d50a GIT binary patch literal 3789 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 STFgU05D*G*uKnsq66imOV5r&v literal 0 HcmV?d00001