Completed
Pull Request — develop_3.0 (#460)
by Adrien
02:25
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\Wrapper\XMLReader;
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
 * @package Box\Spout\Reader\XLSX\Manager
14
 */
15
class SheetManager
16
{
17
    /** Paths of XML files relative to the XLSX file root */
18
    const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
19
    const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
20
21
    /** Definition of XML node names used to parse data */
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_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 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
     */
59 34
    public function __construct($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory)
60
    {
61 34
        $this->filePath = $filePath;
62 34
        $this->optionsManager = $optionsManager;
63 34
        $this->sharedStringsManager = $sharedStringsManager;
64 34
        $this->escaper = $escaper;
65 34
        $this->entityFactory = $entityFactory;
66 34
    }
67
68
    /**
69
     * Returns the sheets metadata of the file located at the previously given file path.
70
     * The paths to the sheets' data are read from the [Content_Types].xml file.
71
     *
72
     * @return Sheet[] Sheets within the XLSX file
73
     */
74 34
    public function getSheets()
75
    {
76 34
        $sheets = [];
77 34
        $sheetIndex = 0;
78 34
        $activeSheetIndex = 0; // By default, the first sheet is active
79
80 34
        $xmlReader = $this->entityFactory->createXMLReader();
81
82 34
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
83 34
            while ($xmlReader->read()) {
84 34
                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) {
85
                    // The "workbookView" node is located before "sheet" nodes, ensuring that
86
                    // the active sheet is known before parsing sheets data.
87 12
                    $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
88 34
                } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) {
89 33
                    $isSheetActive = ($sheetIndex === $activeSheetIndex);
90 33
                    $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive);
91 33
                    $sheetIndex++;
92 34
                } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) {
93
                    // stop reading once all sheets have been read
94 34
                    break;
95
                }
96
            }
97
98 34
            $xmlReader->close();
99
        }
100
101 34
        return $sheets;
102
    }
103
104
    /**
105
     * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
106
     * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
107
     * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
108
     *
109
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
110
     * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
111
     * @param bool $isSheetActive Whether this sheet was defined as active
112
     * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
113
     */
114 33
    protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
115
    {
116 33
        $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
117 33
        $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
118 33
        $sheetName = $this->escaper->unescape($escapedSheetName);
119
120 33
        $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
121
122 33
        return $this->entityFactory->createSheet(
123 33
            $this->filePath,
124 33
            $sheetDataXMLFilePath,
125 33
            $sheetIndexZeroBased,
126 33
            $sheetName,
127 33
            $isSheetActive,
128 33
            $this->optionsManager,
129 33
            $this->sharedStringsManager
130
        );
131
    }
132
133
    /**
134
     * @param string $sheetId The sheet ID, as defined in "workbook.xml"
135
     * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
136
     */
137 33
    protected function getSheetDataXMLFilePathForSheetId($sheetId)
138
    {
139 33
        $sheetDataXMLFilePath = '';
140
141
        // find the file path of the sheet, by looking at the "workbook.xml.res" file
142 33
        $xmlReader = $this->entityFactory->createXMLReader();
143 33
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
144 33
            while ($xmlReader->read()) {
145 33
                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
146 33
                    $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
147
148 33
                    if ($relationshipSheetId === $sheetId) {
149
                        // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
150
                        // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
151 33
                        $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
152
153
                        // sometimes, the sheet data file path already contains "/xl/"...
154 33
                        if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
155 32
                            $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
156 32
                            break;
157
                        }
158
                    }
159
                }
160
            }
161
162 33
            $xmlReader->close();
163
        }
164
165 33
        return $sheetDataXMLFilePath;
166
    }
167
}
168