Failed Conditions
Push — develop_3.0 ( 80553c...f9d8ad )
by Adrien
31:09
created

SheetHelper::getSheetFromSheetXMLNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1.0008

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 17
ccs 9
cts 10
cp 0.9
rs 9.4285
c 1
b 0
f 0
cc 1
eloc 10
nc 1
nop 3
crap 1.0008
1
<?php
2
3
namespace Box\Spout\Reader\XLSX\Helper;
4
5
use Box\Spout\Reader\Wrapper\XMLReader;
6
use Box\Spout\Reader\XLSX\Sheet;
7
8
/**
9
 * Class SheetHelper
10
 * This class provides helper functions related to XLSX sheets
11
 *
12
 * @package Box\Spout\Reader\XLSX\Helper
13
 */
14
class SheetHelper
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_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_ACTIVE_TAB = 'activeTab';
28
    const XML_ATTRIBUTE_R_ID = 'r:id';
29
    const XML_ATTRIBUTE_NAME = 'name';
30
    const XML_ATTRIBUTE_ID = 'Id';
31
    const XML_ATTRIBUTE_TARGET = 'Target';
32
33
    /** @var string Path of the XLSX file being read */
34
    protected $filePath;
35
36
    /** @var \Box\Spout\Reader\XLSX\ReaderOptions Reader's current options */
37
    protected $options;
38
39
    /** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
40
    protected $sharedStringsHelper;
41
42
    /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
43
    protected $globalFunctionsHelper;
44
45
    /**
46
     * @param string $filePath Path of the XLSX file being read
47
     * @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
48
     * @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
49
     * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
50
     */
51 34
    public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper)
52
    {
53 34
        $this->filePath = $filePath;
54 34
        $this->options = $options;
55 34
        $this->sharedStringsHelper = $sharedStringsHelper;
56 34
        $this->globalFunctionsHelper = $globalFunctionsHelper;
57 34
    }
58
59
    /**
60
     * Returns the sheets metadata of the file located at the previously given file path.
61
     * The paths to the sheets' data are read from the [Content_Types].xml file.
62
     *
63
     * @return Sheet[] Sheets within the XLSX file
64
     */
65 34
    public function getSheets()
66
    {
67 34
        $sheets = [];
68 34
        $sheetIndex = 0;
69 34
        $activeSheetIndex = 0; // By default, the first sheet is active
70
71 34
        $xmlReader = new XMLReader();
72 34
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
73 34
            while ($xmlReader->read()) {
74 34
                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) {
75
                    // The "workbookView" node is located before "sheet" nodes, ensuring that
76
                    // the active sheet is known before parsing sheets data.
77 12
                    $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
78 34
                } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) {
79 33
                    $isSheetActive = ($sheetIndex === $activeSheetIndex);
80 33
                    $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive);
81 33
                    $sheetIndex++;
82 34
                } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) {
83
                    // stop reading once all sheets have been read
84 34
                    break;
85
                }
86
            }
87
88 34
            $xmlReader->close();
89
        }
90
91 34
        return $sheets;
92
    }
93
94
    /**
95
     * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
96
     * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
97
     * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
98
     *
99
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
100
     * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
101
     * @param bool $isSheetActive Whether this sheet was defined as active
102
     * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
103
     */
104 33
    protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
105
    {
106 33
        $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
107 33
        $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
108
109
        /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
110 33
        $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
111 33
        $sheetName = $escaper->unescape($escapedSheetName);
112
113 33
        $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
114
115 33
        return new Sheet(
116 33
            $this->filePath, $sheetDataXMLFilePath,
117
            $sheetIndexZeroBased, $sheetName, $isSheetActive,
118 33
            $this->options, $this->sharedStringsHelper
119
        );
120
    }
121
122
    /**
123
     * @param string $sheetId The sheet ID, as defined in "workbook.xml"
124
     * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
125
     */
126 33
    protected function getSheetDataXMLFilePathForSheetId($sheetId)
127
    {
128 33
        $sheetDataXMLFilePath = '';
129
130
        // find the file path of the sheet, by looking at the "workbook.xml.res" file
131 33
        $xmlReader = new XMLReader();
132 33
        if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
133 33
            while ($xmlReader->read()) {
134 33
                if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
135 33
                    $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
136
137 33
                    if ($relationshipSheetId === $sheetId) {
138
                        // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
139
                        // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
140 33
                        $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
141
142
                        // sometimes, the sheet data file path already contains "/xl/"...
143 33
                        if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
144 32
                            $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
145 32
                            break;
146
                        }
147
                    }
148
                }
149
            }
150
151 33
            $xmlReader->close();
152
        }
153
154 33
        return $sheetDataXMLFilePath;
155
    }
156
}
157