See Release Notes
Long Term Support Release
<?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);> $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) {> if (\strpos($sheetDataXMLFilePath, '/xl/') !== 0) {$sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath; break; } } } } $xmlReader->close(); } return $sheetDataXMLFilePath; } }