PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / vendor / openspout / openspout / src / Reader / XLSX / Manager / StyleManager.php

StyleManager.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.1, at vendor/openspout/openspout/src/Reader/XLSX/Manager/StyleManager.php

295 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\OpenSpout\Reader\XLSX\Manager;
4
5 use FluentCart\OpenSpout\Reader\XLSX\Creator\InternalEntityFactory;
6 /**
7 * This class manages XLSX styles.
8 */
9 class StyleManager
10 {
11 /** Nodes used to find relevant information in the styles XML file */
12 public const XML_NODE_NUM_FMTS = 'numFmts';
13 public const XML_NODE_NUM_FMT = 'numFmt';
14 public const XML_NODE_CELL_XFS = 'cellXfs';
15 public const XML_NODE_XF = 'xf';
16 /** Attributes used to find relevant information in the styles XML file */
17 public const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId';
18 public const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode';
19 public const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat';
20 /** By convention, default style ID is 0 */
21 public const DEFAULT_STYLE_ID = 0;
22 public const NUMBER_FORMAT_GENERAL = 'General';
23 /**
24 * @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx
25 *
26 * @var array Mapping between built-in numFmtId and the associated format - for dates only
27 */
28 protected static $builtinNumFmtIdToNumFormatMapping = [
29 14 => 'm/d/yyyy',
30 // @NOTE: ECMA spec is 'mm-dd-yy'
31 15 => 'd-mmm-yy',
32 16 => 'd-mmm',
33 17 => 'mmm-yy',
34 18 => 'h:mm AM/PM',
35 19 => 'h:mm:ss AM/PM',
36 20 => 'h:mm',
37 21 => 'h:mm:ss',
38 22 => 'm/d/yyyy h:mm',
39 // @NOTE: ECMA spec is 'm/d/yy h:mm',
40 45 => 'mm:ss',
41 46 => '[h]:mm:ss',
42 47 => 'mm:ss.0',
43 ];
44 /** @var string Path of the XLSX file being read */
45 protected $filePath;
46 /** @var bool Whether the XLSX file contains a styles XML file */
47 protected $hasStylesXMLFile;
48 /** @var null|string Path of the styles XML file */
49 protected $stylesXMLFilePath;
50 /** @var InternalEntityFactory Factory to create entities */
51 protected $entityFactory;
52 /** @var array Array containing the IDs of built-in number formats indicating a date */
53 protected $builtinNumFmtIdIndicatingDates;
54 /** @var null|array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
55 protected $customNumberFormats;
56 /** @var null|array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
57 protected $stylesAttributes;
58 /** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */
59 protected $numFmtIdToIsDateFormatCache = [];
60 /**
61 * @param string $filePath Path of the XLSX file being read
62 * @param WorkbookRelationshipsManager $workbookRelationshipsManager Helps retrieving workbook relationships
63 * @param InternalEntityFactory $entityFactory Factory to create entities
64 */
65 public function __construct($filePath, $workbookRelationshipsManager, $entityFactory)
66 {
67 $this->filePath = $filePath;
68 $this->entityFactory = $entityFactory;
69 $this->builtinNumFmtIdIndicatingDates = \array_keys(self::$builtinNumFmtIdToNumFormatMapping);
70 $this->hasStylesXMLFile = $workbookRelationshipsManager->hasStylesXMLFile();
71 if ($this->hasStylesXMLFile) {
72 $this->stylesXMLFilePath = $workbookRelationshipsManager->getStylesXMLFilePath();
73 }
74 }
75 /**
76 * Returns whether the style with the given ID should consider
77 * numeric values as timestamps and format the cell as a date.
78 *
79 * @param int $styleId Zero-based style ID
80 *
81 * @return bool Whether the cell with the given cell should display a date instead of a numeric value
82 */
83 public function shouldFormatNumericValueAsDate($styleId)
84 {
85 if (!$this->hasStylesXMLFile) {
86 return \false;
87 }
88 $stylesAttributes = $this->getStylesAttributes();
89 // Default style (0) does not format numeric values as timestamps. Only custom styles do.
90 // Also if the style ID does not exist in the styles.xml file, format as numeric value.
91 // Using isset here because it is way faster than array_key_exists...
92 if (self::DEFAULT_STYLE_ID === $styleId || !isset($stylesAttributes[$styleId])) {
93 return \false;
94 }
95 $styleAttributes = $stylesAttributes[$styleId];
96 return $this->doesStyleIndicateDate($styleAttributes);
97 }
98 /**
99 * Returns the format as defined in "styles.xml" of the given style.
100 * NOTE: It is assumed that the style DOES have a number format associated to it.
101 *
102 * @param int $styleId Zero-based style ID
103 *
104 * @return string The number format code associated with the given style
105 */
106 public function getNumberFormatCode($styleId)
107 {
108 $stylesAttributes = $this->getStylesAttributes();
109 $styleAttributes = $stylesAttributes[$styleId];
110 $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
111 if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) {
112 $numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId];
113 } else {
114 $customNumberFormats = $this->getCustomNumberFormats();
115 $numberFormatCode = $customNumberFormats[$numFmtId];
116 }
117 return $numberFormatCode;
118 }
119 /**
120 * Reads the styles.xml file and extract the relevant information from the file.
121 */
122 protected function extractRelevantInfo()
123 {
124 $this->customNumberFormats = [];
125 $this->stylesAttributes = [];
126 $xmlReader = $this->entityFactory->createXMLReader();
127 if ($xmlReader->openFileInZip($this->filePath, $this->stylesXMLFilePath)) {
128 while ($xmlReader->read()) {
129 if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
130 $this->extractNumberFormats($xmlReader);
131 } elseif ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
132 $this->extractStyleAttributes($xmlReader);
133 }
134 }
135 $xmlReader->close();
136 }
137 }
138 /**
139 * Extracts number formats from the "numFmt" nodes.
140 * For simplicity, the styles attributes are kept in memory. This is possible thanks
141 * to the reuse of formats. So 1 million cells should not use 1 million formats.
142 *
143 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node
144 */
145 protected function extractNumberFormats($xmlReader)
146 {
147 while ($xmlReader->read()) {
148 if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) {
149 $numFmtId = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID);
150 $formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
151 $this->customNumberFormats[$numFmtId] = $formatCode;
152 } elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) {
153 // Once done reading "numFmts" node's children
154 break;
155 }
156 }
157 }
158 /**
159 * Extracts style attributes from the "xf" nodes, inside the "cellXfs" section.
160 * For simplicity, the styles attributes are kept in memory. This is possible thanks
161 * to the reuse of styles. So 1 million cells should not use 1 million styles.
162 *
163 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node
164 */
165 protected function extractStyleAttributes($xmlReader)
166 {
167 while ($xmlReader->read()) {
168 if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) {
169 $numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID);
170 $normalizedNumFmtId = null !== $numFmtId ? (int) $numFmtId : null;
171 $applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT);
172 $normalizedApplyNumberFormat = null !== $applyNumberFormat ? (bool) $applyNumberFormat : null;
173 $this->stylesAttributes[] = [self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId, self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat];
174 } elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
175 // Once done reading "cellXfs" node's children
176 break;
177 }
178 }
179 }
180 /**
181 * @return array The custom number formats
182 */
183 protected function getCustomNumberFormats()
184 {
185 if (!isset($this->customNumberFormats)) {
186 $this->extractRelevantInfo();
187 }
188 return $this->customNumberFormats;
189 }
190 /**
191 * @return array The styles attributes
192 */
193 protected function getStylesAttributes()
194 {
195 if (!isset($this->stylesAttributes)) {
196 $this->extractRelevantInfo();
197 }
198 return $this->stylesAttributes;
199 }
200 /**
201 * @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId")
202 *
203 * @return bool Whether the style with the given attributes indicates that the number is a date
204 */
205 protected function doesStyleIndicateDate($styleAttributes)
206 {
207 $applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
208 $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
209 // A style may apply a date format if it has:
210 // - "applyNumberFormat" attribute not set to "false"
211 // - "numFmtId" attribute set
212 // This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format,
213 // but this is not necessarily a date.
214 if (\false === $applyNumberFormat || null === $numFmtId) {
215 return \false;
216 }
217 return $this->doesNumFmtIdIndicateDate($numFmtId);
218 }
219 /**
220 * Returns whether the number format ID indicates that the number is a date.
221 * The result is cached to avoid recomputing the same thing over and over, as
222 * "numFmtId" attributes can be shared between multiple styles.
223 *
224 * @param int $numFmtId
225 *
226 * @return bool Whether the number format ID indicates that the number is a date
227 */
228 protected function doesNumFmtIdIndicateDate($numFmtId)
229 {
230 if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) {
231 $formatCode = $this->getFormatCodeForNumFmtId($numFmtId);
232 $this->numFmtIdToIsDateFormatCache[$numFmtId] = $this->isNumFmtIdBuiltInDateFormat($numFmtId) || $this->isFormatCodeCustomDateFormat($formatCode);
233 }
234 return $this->numFmtIdToIsDateFormatCache[$numFmtId];
235 }
236 /**
237 * @param int $numFmtId
238 *
239 * @return null|string The custom number format or NULL if none defined for the given numFmtId
240 */
241 protected function getFormatCodeForNumFmtId($numFmtId)
242 {
243 $customNumberFormats = $this->getCustomNumberFormats();
244 // Using isset here because it is way faster than array_key_exists...
245 return isset($customNumberFormats[$numFmtId]) ? $customNumberFormats[$numFmtId] : null;
246 }
247 /**
248 * @param int $numFmtId
249 *
250 * @return bool Whether the number format ID indicates that the number is a date
251 */
252 protected function isNumFmtIdBuiltInDateFormat($numFmtId)
253 {
254 return \in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates, \true);
255 }
256 /**
257 * @param null|string $formatCode
258 *
259 * @return bool Whether the given format code indicates that the number is a date
260 */
261 protected function isFormatCodeCustomDateFormat($formatCode)
262 {
263 // if no associated format code or if using the default "General" format
264 if (null === $formatCode || 0 === \strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL)) {
265 return \false;
266 }
267 return $this->isFormatCodeMatchingDateFormatPattern($formatCode);
268 }
269 /**
270 * @param string $formatCode
271 *
272 * @return bool Whether the given format code matches a date format pattern
273 */
274 protected function isFormatCodeMatchingDateFormatPattern($formatCode)
275 {
276 // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
277 $pattern = '((?<!\\\\)\\[.+?(?<!\\\\)\\])';
278 $formatCode = \preg_replace($pattern, '', $formatCode);
279 // custom date formats contain specific characters to represent the date:
280 // e - yy - m - d - h - s
281 // and all of their variants (yyyy - mm - dd...)
282 $dateFormatCharacters = ['e', 'yy', 'm', 'd', 'h', 's'];
283 $hasFoundDateFormatCharacter = \false;
284 foreach ($dateFormatCharacters as $dateFormatCharacter) {
285 // character not preceded by "\" (case insensitive)
286 $pattern = '/(?<!\\\\)' . $dateFormatCharacter . '/i';
287 if (\preg_match($pattern, $formatCode)) {
288 $hasFoundDateFormatCharacter = \true;
289 break;
290 }
291 }
292 return $hasFoundDateFormatCharacter;
293 }
294 }
295