Passed
Push — develop_3.0 ( e1ae3c...c74c0d )
by Adrien
02:37
created

SheetManager::getSheetFromSheetXMLNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 13
cts 13
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 13
nc 1
nop 3
crap 1
1
<?php
2
3
namespace Box\Spout\Reader\XLSX\Manager;
4
5
use Box\Spout\Reader\Common\Entity\Options;
6
use Box\Spout\Reader\Common\XMLProcessor;
7
use Box\Spout\Reader\XLSX\Creator\EntityFactory;
8
use Box\Spout\Reader\XLSX\Sheet;
9
10
/**
11
 * Class SheetManager
12
 * This class manages XLSX sheets
13
 */
14
class SheetManager
15
{
16
    /** Paths of XML files relative to the XLSX file root */
17
    const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
18
    const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
19
20
    /** Definition of XML node names used to parse data */
21
    const XML_NODE_WORKBOOK_PROPERTIES = 'workbookPr';
22
    const XML_NODE_WORKBOOK_VIEW = 'workbookView';
23
    const XML_NODE_SHEET = 'sheet';
24
    const XML_NODE_SHEETS = 'sheets';
25
    const XML_NODE_RELATIONSHIP = 'Relationship';
26
27
    /** Definition of XML attributes used to parse data */
28
    const XML_ATTRIBUTE_DATE_1904 = 'date1904';
29
    const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab';
30
    const XML_ATTRIBUTE_R_ID = 'r:id';
31
    const XML_ATTRIBUTE_NAME = 'name';
32
    const XML_ATTRIBUTE_ID = 'Id';
33
    const XML_ATTRIBUTE_TARGET = 'Target';
34
35
    /** @var string Path of the XLSX file being read */
36
    protected $filePath;
37
38
    /** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */
39
    protected $optionsManager;
40
41
    /** @var \Box\Spout\Reader\XLSX\Manager\SharedStringsManager Manages shared strings */
42
    protected $sharedStringsManager;
43
44
    /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
45
    protected $globalFunctionsHelper;
46
47
    /** @var EntityFactory Factory to create entities */
48
    protected $entityFactory;
49
50
    /** @var \Box\Spout\Common\Helper\Escaper\XLSX Used to unescape XML data */
51
    protected $escaper;
52
53
    /** @var array List of sheets */
54
    protected $sheets;
55
56
    /** @var int Index of the sheet currently read */
57
    protected $currentSheetIndex;
58
59
    /** @var int Index of the active sheet (0 by default) */
60
    protected $activeSheetIndex;
61
62
    /**
63
     * @param string $filePath Path of the XLSX file being read
64
     * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
65
     * @param \Box\Spout\Reader\XLSX\Manager\SharedStringsManager $sharedStringsManager Manages shared strings
66
     * @param \Box\Spout\Common\Helper\Escaper\XLSX $escaper Used to unescape XML data
67
     * @param EntityFactory $entityFactory Factory to create entities
68
     * @param mixed $sharedStringsManager
69
     */
70 35
    public function __construct($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory)
71
    {
72 35
        $this->filePath = $filePath;
73 35
        $this->optionsManager = $optionsManager;
74 35
        $this->sharedStringsManager = $sharedStringsManager;
75 35
        $this->escaper = $escaper;
76 35
        $this->entityFactory = $entityFactory;
77 35
    }
78
79
    /**
80
     * Returns the sheets metadata of the file located at the previously given file path.
81
     * The paths to the sheets' data are read from the [Content_Types].xml file.
82
     *
83
     * @return Sheet[] Sheets within the XLSX file
84
     */
85 35
    public function getSheets()
86
    {
87 35
        $this->sheets = [];
88 35
        $this->currentSheetIndex = 0;
89 35
        $this->activeSheetIndex = 0; // By default, the first sheet is active
90
91 35
        $xmlReader = $this->entityFactory->createXMLReader();
92 35
        $xmlProcessor = $this->entityFactory->createXMLProcessor($xmlReader);
93
94 35
        $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_PROPERTIES, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookPropertiesStartingNode']);
95 35
        $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_VIEW, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookViewStartingNode']);
96 35
        $xmlProcessor->registerCallback(self::XML_NODE_SHEET, XMLProcessor::NODE_TYPE_START, [$this, 'processSheetStartingNode']);
97 35
        $xmlProcessor->registerCallback(self::XML_NODE_SHEETS, XMLProcessor::NODE_TYPE_END, [$this, 'processSheetsEndingNode']);
98
99 35
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
100 35
            $xmlProcessor->readUntilStopped();
101 35
            $xmlReader->close();
102
        }
103
104 35
        return $this->sheets;
105
    }
106
107
    /**
108
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<workbookPr>" starting node
109
     * @return int A return code that indicates what action should the processor take next
110
     */
111 12
    protected function processWorkbookPropertiesStartingNode($xmlReader)
112
    {
113 12
        $shouldUse1904Dates = (bool) $xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904);
114 12
        $this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates);
115
116 12
        return XMLProcessor::PROCESSING_CONTINUE;
117
    }
118
119
    /**
120
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<workbookView>" starting node
121
     * @return int A return code that indicates what action should the processor take next
122
     */
123 13
    protected function processWorkbookViewStartingNode($xmlReader)
124
    {
125
        // The "workbookView" node is located before "sheet" nodes, ensuring that
126
        // the active sheet is known before parsing sheets data.
127 13
        $this->activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
128
129 13
        return XMLProcessor::PROCESSING_CONTINUE;
130
    }
131
132
    /**
133
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<sheet>" starting node
134
     * @return int A return code that indicates what action should the processor take next
135
     */
136 34
    protected function processSheetStartingNode($xmlReader)
137
    {
138 34
        $isSheetActive = ($this->currentSheetIndex === $this->activeSheetIndex);
139 34
        $this->sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $this->currentSheetIndex, $isSheetActive);
140 34
        $this->currentSheetIndex++;
141
142 34
        return XMLProcessor::PROCESSING_CONTINUE;
143
    }
144
145
    /**
146
     * @return int A return code that indicates what action should the processor take next
147
     */
148 35
    protected function processSheetsEndingNode()
149
    {
150 35
        return XMLProcessor::PROCESSING_STOP;
151
    }
152
153
    /**
154
     * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
155
     * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
156
     * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
157
     *
158
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
159
     * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
160
     * @param bool $isSheetActive Whether this sheet was defined as active
161
     * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
162
     */
163 34
    protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
164
    {
165 34
        $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
166 34
        $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
167 34
        $sheetName = $this->escaper->unescape($escapedSheetName);
168
169 34
        $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
170
171 34
        return $this->entityFactory->createSheet(
172 34
            $this->filePath,
173 34
            $sheetDataXMLFilePath,
174 34
            $sheetIndexZeroBased,
175 34
            $sheetName,
176 34
            $isSheetActive,
177 34
            $this->optionsManager,
178 34
            $this->sharedStringsManager
179
        );
180
    }
181
182
    /**
183
     * @param string $sheetId The sheet ID, as defined in "workbook.xml"
184
     * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
185
     */
186 34
    protected function getSheetDataXMLFilePathForSheetId($sheetId)
187
    {
188 34
        $sheetDataXMLFilePath = '';
189
190
        // find the file path of the sheet, by looking at the "workbook.xml.res" file
191 34
        $xmlReader = $this->entityFactory->createXMLReader();
192 34
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
193 34
            while ($xmlReader->read()) {
194 34
                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
195 34
                    $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
196
197 34
                    if ($relationshipSheetId === $sheetId) {
198
                        // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
199
                        // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
200 34
                        $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
201
202
                        // sometimes, the sheet data file path already contains "/xl/"...
203 34
                        if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
204 33
                            $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
205 33
                            break;
206
                        }
207
                    }
208
                }
209
            }
210
211 34
            $xmlReader->close();
212
        }
213
214 34
        return $sheetDataXMLFilePath;
215
    }
216
}
217