Completed
Pull Request — master (#649)
by Adrien
03:03 queued 33s
created

SheetManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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