Passed
Pull Request — develop_3.0 (#483)
by Adrien
04:19 queued 01:24
created

SheetManager::getSheetDataXMLFilePathForSheetId()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

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