1 |
efrain |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
declare(strict_types=1);
|
|
|
4 |
|
|
|
5 |
namespace OpenSpout\Reader\ODS;
|
|
|
6 |
|
|
|
7 |
use DOMElement;
|
|
|
8 |
use OpenSpout\Common\Exception\IOException;
|
|
|
9 |
use OpenSpout\Common\Helper\Escaper\ODS;
|
|
|
10 |
use OpenSpout\Reader\Common\XMLProcessor;
|
|
|
11 |
use OpenSpout\Reader\Exception\XMLProcessingException;
|
|
|
12 |
use OpenSpout\Reader\ODS\Helper\CellValueFormatter;
|
|
|
13 |
use OpenSpout\Reader\ODS\Helper\SettingsHelper;
|
|
|
14 |
use OpenSpout\Reader\SheetIteratorInterface;
|
|
|
15 |
use OpenSpout\Reader\Wrapper\XMLReader;
|
|
|
16 |
|
|
|
17 |
/**
|
|
|
18 |
* @implements SheetIteratorInterface<Sheet>
|
|
|
19 |
*/
|
|
|
20 |
final class SheetIterator implements SheetIteratorInterface
|
|
|
21 |
{
|
|
|
22 |
public const CONTENT_XML_FILE_PATH = 'content.xml';
|
|
|
23 |
|
|
|
24 |
public const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0';
|
|
|
25 |
|
|
|
26 |
/**
|
|
|
27 |
* Definition of XML nodes name and attribute used to parse sheet data.
|
|
|
28 |
*/
|
|
|
29 |
public const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles';
|
|
|
30 |
public const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties';
|
|
|
31 |
public const XML_NODE_TABLE = 'table:table';
|
|
|
32 |
public const XML_ATTRIBUTE_STYLE_NAME = 'style:name';
|
|
|
33 |
public const XML_ATTRIBUTE_TABLE_NAME = 'table:name';
|
|
|
34 |
public const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name';
|
|
|
35 |
public const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display';
|
|
|
36 |
|
|
|
37 |
/** @var string Path of the file to be read */
|
|
|
38 |
private readonly string $filePath;
|
|
|
39 |
|
|
|
40 |
private readonly Options $options;
|
|
|
41 |
|
|
|
42 |
/** @var XMLReader The XMLReader object that will help read sheet's XML data */
|
|
|
43 |
private readonly XMLReader $xmlReader;
|
|
|
44 |
|
|
|
45 |
/** @var ODS Used to unescape XML data */
|
|
|
46 |
private readonly ODS $escaper;
|
|
|
47 |
|
|
|
48 |
/** @var bool Whether there are still at least a sheet to be read */
|
|
|
49 |
private bool $hasFoundSheet;
|
|
|
50 |
|
|
|
51 |
/** @var int The index of the sheet being read (zero-based) */
|
|
|
52 |
private int $currentSheetIndex;
|
|
|
53 |
|
|
|
54 |
/** @var string The name of the sheet that was defined as active */
|
|
|
55 |
private readonly ?string $activeSheetName;
|
|
|
56 |
|
|
|
57 |
/** @var array<string, bool> Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */
|
|
|
58 |
private array $sheetsVisibility;
|
|
|
59 |
|
|
|
60 |
public function __construct(
|
|
|
61 |
string $filePath,
|
|
|
62 |
Options $options,
|
|
|
63 |
ODS $escaper,
|
|
|
64 |
SettingsHelper $settingsHelper
|
|
|
65 |
) {
|
|
|
66 |
$this->filePath = $filePath;
|
|
|
67 |
$this->options = $options;
|
|
|
68 |
$this->xmlReader = new XMLReader();
|
|
|
69 |
$this->escaper = $escaper;
|
|
|
70 |
$this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
|
|
|
71 |
}
|
|
|
72 |
|
|
|
73 |
/**
|
|
|
74 |
* Rewind the Iterator to the first element.
|
|
|
75 |
*
|
|
|
76 |
* @see http://php.net/manual/en/iterator.rewind.php
|
|
|
77 |
*
|
|
|
78 |
* @throws IOException If unable to open the XML file containing sheets' data
|
|
|
79 |
*/
|
|
|
80 |
public function rewind(): void
|
|
|
81 |
{
|
|
|
82 |
$this->xmlReader->close();
|
|
|
83 |
|
|
|
84 |
if (false === $this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH)) {
|
|
|
85 |
$contentXmlFilePath = $this->filePath.'#'.self::CONTENT_XML_FILE_PATH;
|
|
|
86 |
|
|
|
87 |
throw new IOException("Could not open \"{$contentXmlFilePath}\".");
|
|
|
88 |
}
|
|
|
89 |
|
|
|
90 |
try {
|
|
|
91 |
$this->sheetsVisibility = $this->readSheetsVisibility();
|
|
|
92 |
$this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
|
|
|
93 |
} catch (XMLProcessingException $exception) {
|
|
|
94 |
throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
|
|
|
95 |
}
|
|
|
96 |
|
|
|
97 |
$this->currentSheetIndex = 0;
|
|
|
98 |
}
|
|
|
99 |
|
|
|
100 |
/**
|
|
|
101 |
* Checks if current position is valid.
|
|
|
102 |
*
|
|
|
103 |
* @see http://php.net/manual/en/iterator.valid.php
|
|
|
104 |
*/
|
|
|
105 |
public function valid(): bool
|
|
|
106 |
{
|
|
|
107 |
$valid = $this->hasFoundSheet;
|
|
|
108 |
if (!$valid) {
|
|
|
109 |
$this->xmlReader->close();
|
|
|
110 |
}
|
|
|
111 |
|
|
|
112 |
return $valid;
|
|
|
113 |
}
|
|
|
114 |
|
|
|
115 |
/**
|
|
|
116 |
* Move forward to next element.
|
|
|
117 |
*
|
|
|
118 |
* @see http://php.net/manual/en/iterator.next.php
|
|
|
119 |
*/
|
|
|
120 |
public function next(): void
|
|
|
121 |
{
|
|
|
122 |
$this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
|
|
|
123 |
|
|
|
124 |
if ($this->hasFoundSheet) {
|
|
|
125 |
++$this->currentSheetIndex;
|
|
|
126 |
}
|
|
|
127 |
}
|
|
|
128 |
|
|
|
129 |
/**
|
|
|
130 |
* Return the current element.
|
|
|
131 |
*
|
|
|
132 |
* @see http://php.net/manual/en/iterator.current.php
|
|
|
133 |
*/
|
|
|
134 |
public function current(): Sheet
|
|
|
135 |
{
|
|
|
136 |
$escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
|
|
|
137 |
\assert(null !== $escapedSheetName);
|
|
|
138 |
$sheetName = $this->escaper->unescape($escapedSheetName);
|
|
|
139 |
|
|
|
140 |
$isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName);
|
|
|
141 |
|
|
|
142 |
$sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME);
|
|
|
143 |
\assert(null !== $sheetStyleName);
|
|
|
144 |
$isSheetVisible = $this->isSheetVisible($sheetStyleName);
|
|
|
145 |
|
|
|
146 |
return new Sheet(
|
|
|
147 |
new RowIterator(
|
|
|
148 |
$this->options,
|
|
|
149 |
new CellValueFormatter($this->options->SHOULD_FORMAT_DATES, new ODS()),
|
|
|
150 |
new XMLProcessor($this->xmlReader)
|
|
|
151 |
),
|
|
|
152 |
$this->currentSheetIndex,
|
|
|
153 |
$sheetName,
|
|
|
154 |
$isSheetActive,
|
|
|
155 |
$isSheetVisible
|
|
|
156 |
);
|
|
|
157 |
}
|
|
|
158 |
|
|
|
159 |
/**
|
|
|
160 |
* Return the key of the current element.
|
|
|
161 |
*
|
|
|
162 |
* @see http://php.net/manual/en/iterator.key.php
|
|
|
163 |
*/
|
|
|
164 |
public function key(): int
|
|
|
165 |
{
|
|
|
166 |
return $this->currentSheetIndex + 1;
|
|
|
167 |
}
|
|
|
168 |
|
|
|
169 |
/**
|
|
|
170 |
* Extracts the visibility of the sheets.
|
|
|
171 |
*
|
|
|
172 |
* @return array<string, bool> Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE]
|
|
|
173 |
*/
|
|
|
174 |
private function readSheetsVisibility(): array
|
|
|
175 |
{
|
|
|
176 |
$sheetsVisibility = [];
|
|
|
177 |
|
|
|
178 |
$this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES);
|
|
|
179 |
|
|
|
180 |
$automaticStylesNode = $this->xmlReader->expand();
|
|
|
181 |
\assert($automaticStylesNode instanceof DOMElement);
|
|
|
182 |
|
|
|
183 |
$tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES);
|
|
|
184 |
|
|
|
185 |
foreach ($tableStyleNodes as $tableStyleNode) {
|
|
|
186 |
$isSheetVisible = ('false' !== $tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY));
|
|
|
187 |
|
|
|
188 |
$parentStyleNode = $tableStyleNode->parentNode;
|
|
|
189 |
\assert($parentStyleNode instanceof DOMElement);
|
|
|
190 |
$styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME);
|
|
|
191 |
|
|
|
192 |
$sheetsVisibility[$styleName] = $isSheetVisible;
|
|
|
193 |
}
|
|
|
194 |
|
|
|
195 |
return $sheetsVisibility;
|
|
|
196 |
}
|
|
|
197 |
|
|
|
198 |
/**
|
|
|
199 |
* Returns whether the current sheet was defined as the active one.
|
|
|
200 |
*
|
|
|
201 |
* @param string $sheetName Name of the current sheet
|
|
|
202 |
* @param int $sheetIndex Index of the current sheet
|
|
|
203 |
* @param null|string $activeSheetName Name of the sheet that was defined as active or NULL if none defined
|
|
|
204 |
*
|
|
|
205 |
* @return bool Whether the current sheet was defined as the active one
|
|
|
206 |
*/
|
|
|
207 |
private function isSheetActive(string $sheetName, int $sheetIndex, ?string $activeSheetName): bool
|
|
|
208 |
{
|
|
|
209 |
// The given sheet is active if its name matches the defined active sheet's name
|
|
|
210 |
// or if no information about the active sheet was found, it defaults to the first sheet.
|
|
|
211 |
return
|
|
|
212 |
(null === $activeSheetName && 0 === $sheetIndex)
|
|
|
213 |
|| ($activeSheetName === $sheetName);
|
|
|
214 |
}
|
|
|
215 |
|
|
|
216 |
/**
|
|
|
217 |
* Returns whether the current sheet is visible.
|
|
|
218 |
*
|
|
|
219 |
* @param string $sheetStyleName Name of the sheet style
|
|
|
220 |
*
|
|
|
221 |
* @return bool Whether the current sheet is visible
|
|
|
222 |
*/
|
|
|
223 |
private function isSheetVisible(string $sheetStyleName): bool
|
|
|
224 |
{
|
|
|
225 |
return $this->sheetsVisibility[$sheetStyleName] ??
|
|
|
226 |
true;
|
|
|
227 |
}
|
|
|
228 |
}
|