Test Failed
Pull Request — develop_3.0 (#483)
by Adrien
05:03
created

SheetManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 6
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\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 34
     * @param EntityFactory $entityFactory Factory to create entities
58
     * @param mixed $sharedStringsManager
59 34
     */
60 34
    public function __construct($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory)
61 34
    {
62 34
        $this->filePath = $filePath;
63 34
        $this->optionsManager = $optionsManager;
64 34
        $this->sharedStringsManager = $sharedStringsManager;
65
        $this->escaper = $escaper;
66
        $this->entityFactory = $entityFactory;
67
    }
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 34
     *
73
     * @return Sheet[] Sheets within the XLSX file
74 34
     */
75 34
    public function getSheets()
76 34
    {
77
        $sheets = [];
78 34
        $sheetIndex = 0;
79
        $activeSheetIndex = 0; // By default, the first sheet is active
80 34
81 34
        $xmlReader = $this->entityFactory->createXMLReader();
82 34
83
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
84
            while ($xmlReader->read()) {
85 12
                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_PROPERTIES)) {
86 34
                    $shouldUse1904Dates = (bool) $xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904);
87 33
                    $this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates);
88 33
                } elseif ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) {
89 33
                    // The "workbookView" node is located before "sheet" nodes, ensuring that
90 34
                    // the active sheet is known before parsing sheets data.
91
                    $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
92 34
                } elseif ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) {
93
                    $isSheetActive = ($sheetIndex === $activeSheetIndex);
94
                    $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive);
95
                    $sheetIndex++;
96 34
                } elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) {
97
                    // stop reading once all sheets have been read
98
                    break;
99 34
                }
100
            }
101
102
            $xmlReader->close();
103
        }
104
105
        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 33
     *
113
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
114 33
     * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
115 33
     * @param bool $isSheetActive Whether this sheet was defined as active
116 33
     * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
117
     */
118 33
    protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
119
    {
120 33
        $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
121 33
        $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
122 33
        $sheetName = $this->escaper->unescape($escapedSheetName);
123 33
124 33
        $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
125 33
126 33
        return $this->entityFactory->createSheet(
127 33
            $this->filePath,
128
            $sheetDataXMLFilePath,
129
            $sheetIndexZeroBased,
130
            $sheetName,
131
            $isSheetActive,
132
            $this->optionsManager,
133
            $this->sharedStringsManager
134
        );
135 33
    }
136
137 33
    /**
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 33
     */
141 33
    protected function getSheetDataXMLFilePathForSheetId($sheetId)
142 33
    {
143 33
        $sheetDataXMLFilePath = '';
144 33
145
        // find the file path of the sheet, by looking at the "workbook.xml.res" file
146 33
        $xmlReader = $this->entityFactory->createXMLReader();
147
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
148
            while ($xmlReader->read()) {
149 33
                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
150
                    $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
151
152 33
                    if ($relationshipSheetId === $sheetId) {
153 32
                        // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
154 32
                        // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
155
                        $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
156
157
                        // sometimes, the sheet data file path already contains "/xl/"...
158
                        if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
159
                            $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
160 33
                            break;
161
                        }
162
                    }
163 33
                }
164
            }
165
166
            $xmlReader->close();
167
        }
168
169
        return $sheetDataXMLFilePath;
170
    }
171
}
172