Total Complexity | 149 |
Total Lines | 856 |
Duplicated Lines | 0 % |
Coverage | 65.55% |
Changes | 0 |
Complex classes like Xml often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Xml, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
23 | class Xml extends BaseReader |
||
24 | { |
||
25 | /** |
||
26 | * Formats. |
||
27 | * |
||
28 | * @var array |
||
29 | */ |
||
30 | protected $styles = []; |
||
31 | |||
32 | /** |
||
33 | * Character set used in the file. |
||
34 | * |
||
35 | * @var string |
||
36 | */ |
||
37 | protected $charSet = 'UTF-8'; |
||
38 | |||
39 | /** |
||
40 | * Create a new Excel2003XML Reader instance. |
||
41 | */ |
||
42 | 16 | public function __construct() |
|
43 | { |
||
44 | 16 | $this->readFilter = new DefaultReadFilter(); |
|
45 | 16 | $this->securityScanner = XmlScanner::getInstance($this); |
|
46 | 16 | } |
|
47 | |||
48 | /** |
||
49 | * Can the current IReader read the file? |
||
50 | * |
||
51 | * @param string $pFilename |
||
52 | * |
||
53 | * @throws Exception |
||
54 | * |
||
55 | * @return bool |
||
56 | */ |
||
57 | 5 | public function canRead($pFilename) |
|
58 | { |
||
59 | // Office xmlns:o="urn:schemas-microsoft-com:office:office" |
||
60 | // Excel xmlns:x="urn:schemas-microsoft-com:office:excel" |
||
61 | // XML Spreadsheet xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" |
||
62 | // Spreadsheet component xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet" |
||
63 | // XML schema xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" |
||
64 | // XML data type xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" |
||
65 | // MS-persist recordset xmlns:rs="urn:schemas-microsoft-com:rowset" |
||
66 | // Rowset xmlns:z="#RowsetSchema" |
||
67 | // |
||
68 | |||
69 | $signature = [ |
||
70 | 5 | '<?xml version="1.0"', |
|
71 | '<?mso-application progid="Excel.Sheet"?>', |
||
72 | ]; |
||
73 | |||
74 | // Open file |
||
75 | 5 | $this->openFile($pFilename); |
|
76 | 5 | $fileHandle = $this->fileHandle; |
|
77 | |||
78 | // Read sample data (first 2 KB will do) |
||
79 | 5 | $data = fread($fileHandle, 2048); |
|
1 ignored issue
–
show
|
|||
80 | 5 | fclose($fileHandle); |
|
1 ignored issue
–
show
|
|||
81 | 5 | $data = str_replace("'", '"', $data); // fix headers with single quote |
|
82 | |||
83 | 5 | $valid = true; |
|
84 | 5 | foreach ($signature as $match) { |
|
85 | // every part of the signature must be present |
||
86 | 5 | if (strpos($data, $match) === false) { |
|
87 | $valid = false; |
||
88 | |||
89 | 5 | break; |
|
90 | } |
||
91 | } |
||
92 | |||
93 | // Retrieve charset encoding |
||
94 | 5 | if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/um', $data, $matches)) { |
|
95 | 5 | $this->charSet = strtoupper($matches[1]); |
|
96 | } |
||
97 | |||
98 | 5 | return $valid; |
|
99 | } |
||
100 | |||
101 | /** |
||
102 | * Check if the file is a valid SimpleXML. |
||
103 | * |
||
104 | * @param string $pFilename |
||
105 | * |
||
106 | * @throws Exception |
||
107 | * |
||
108 | * @return false|\SimpleXMLElement |
||
109 | */ |
||
110 | 4 | public function trySimpleXMLLoadString($pFilename) |
|
111 | { |
||
112 | try { |
||
113 | 4 | $xml = simplexml_load_string( |
|
114 | 4 | $this->securityScanner->scan(file_get_contents($pFilename)), |
|
115 | 4 | 'SimpleXMLElement', |
|
116 | 4 | Settings::getLibXmlLoaderOptions() |
|
117 | ); |
||
118 | 1 | } catch (\Exception $e) { |
|
119 | 1 | throw new Exception('Cannot load invalid XML file: ' . $pFilename, 0, $e); |
|
120 | } |
||
121 | |||
122 | 3 | return $xml; |
|
123 | } |
||
124 | |||
125 | /** |
||
126 | * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. |
||
127 | * |
||
128 | * @param string $pFilename |
||
129 | * |
||
130 | * @throws Exception |
||
131 | * |
||
132 | * @return array |
||
133 | */ |
||
134 | public function listWorksheetNames($pFilename) |
||
135 | { |
||
136 | File::assertFile($pFilename); |
||
137 | if (!$this->canRead($pFilename)) { |
||
138 | throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); |
||
139 | } |
||
140 | |||
141 | $worksheetNames = []; |
||
142 | |||
143 | $xml = $this->trySimpleXMLLoadString($pFilename); |
||
144 | |||
145 | $namespaces = $xml->getNamespaces(true); |
||
146 | |||
147 | $xml_ss = $xml->children($namespaces['ss']); |
||
148 | foreach ($xml_ss->Worksheet as $worksheet) { |
||
149 | $worksheet_ss = $worksheet->attributes($namespaces['ss']); |
||
150 | $worksheetNames[] = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet); |
||
151 | } |
||
152 | |||
153 | return $worksheetNames; |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). |
||
158 | * |
||
159 | * @param string $pFilename |
||
160 | * |
||
161 | * @throws Exception |
||
162 | * |
||
163 | * @return array |
||
164 | */ |
||
165 | public function listWorksheetInfo($pFilename) |
||
166 | { |
||
167 | File::assertFile($pFilename); |
||
168 | |||
169 | $worksheetInfo = []; |
||
170 | |||
171 | $xml = $this->trySimpleXMLLoadString($pFilename); |
||
172 | |||
173 | $namespaces = $xml->getNamespaces(true); |
||
174 | |||
175 | $worksheetID = 1; |
||
176 | $xml_ss = $xml->children($namespaces['ss']); |
||
177 | foreach ($xml_ss->Worksheet as $worksheet) { |
||
178 | $worksheet_ss = $worksheet->attributes($namespaces['ss']); |
||
179 | |||
180 | $tmpInfo = []; |
||
181 | $tmpInfo['worksheetName'] = ''; |
||
182 | $tmpInfo['lastColumnLetter'] = 'A'; |
||
183 | $tmpInfo['lastColumnIndex'] = 0; |
||
184 | $tmpInfo['totalRows'] = 0; |
||
185 | $tmpInfo['totalColumns'] = 0; |
||
186 | |||
187 | if (isset($worksheet_ss['Name'])) { |
||
188 | $tmpInfo['worksheetName'] = (string) $worksheet_ss['Name']; |
||
189 | } else { |
||
190 | $tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}"; |
||
191 | } |
||
192 | |||
193 | if (isset($worksheet->Table->Row)) { |
||
194 | $rowIndex = 0; |
||
195 | |||
196 | foreach ($worksheet->Table->Row as $rowData) { |
||
197 | $columnIndex = 0; |
||
198 | $rowHasData = false; |
||
199 | |||
200 | foreach ($rowData->Cell as $cell) { |
||
201 | if (isset($cell->Data)) { |
||
202 | $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); |
||
203 | $rowHasData = true; |
||
204 | } |
||
205 | |||
206 | ++$columnIndex; |
||
207 | } |
||
208 | |||
209 | ++$rowIndex; |
||
210 | |||
211 | if ($rowHasData) { |
||
212 | $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); |
||
213 | } |
||
214 | } |
||
215 | } |
||
216 | |||
217 | $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); |
||
218 | $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; |
||
219 | |||
220 | $worksheetInfo[] = $tmpInfo; |
||
221 | ++$worksheetID; |
||
222 | } |
||
223 | |||
224 | return $worksheetInfo; |
||
225 | } |
||
226 | |||
227 | /** |
||
228 | * Loads Spreadsheet from file. |
||
229 | * |
||
230 | * @param string $pFilename |
||
231 | * |
||
232 | * @throws Exception |
||
233 | * |
||
234 | * @return Spreadsheet |
||
235 | */ |
||
236 | 3 | public function load($pFilename) |
|
237 | { |
||
238 | // Create new Spreadsheet |
||
239 | 3 | $spreadsheet = new Spreadsheet(); |
|
240 | 3 | $spreadsheet->removeSheetByIndex(0); |
|
241 | |||
242 | // Load into this instance |
||
243 | 3 | return $this->loadIntoExisting($pFilename, $spreadsheet); |
|
244 | } |
||
245 | |||
246 | 2 | private static function identifyFixedStyleValue($styleList, &$styleAttributeValue) |
|
247 | { |
||
248 | 2 | $styleAttributeValue = strtolower($styleAttributeValue); |
|
249 | 2 | foreach ($styleList as $style) { |
|
250 | 2 | if ($styleAttributeValue == strtolower($style)) { |
|
251 | 2 | $styleAttributeValue = $style; |
|
252 | |||
253 | 2 | return true; |
|
254 | } |
||
255 | } |
||
256 | |||
257 | return false; |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * pixel units to excel width units(units of 1/256th of a character width). |
||
262 | * |
||
263 | * @param float $pxs |
||
264 | * |
||
265 | * @return float |
||
266 | */ |
||
267 | protected static function pixel2WidthUnits($pxs) |
||
268 | { |
||
269 | $UNIT_OFFSET_MAP = [0, 36, 73, 109, 146, 182, 219]; |
||
270 | |||
271 | $widthUnits = 256 * ($pxs / 7); |
||
272 | $widthUnits += $UNIT_OFFSET_MAP[($pxs % 7)]; |
||
273 | |||
274 | return $widthUnits; |
||
275 | } |
||
276 | |||
277 | /** |
||
278 | * excel width units(units of 1/256th of a character width) to pixel units. |
||
279 | * |
||
280 | * @param float $widthUnits |
||
281 | * |
||
282 | * @return float |
||
283 | */ |
||
284 | protected static function widthUnits2Pixel($widthUnits) |
||
285 | { |
||
286 | $pixels = ($widthUnits / 256) * 7; |
||
287 | $offsetWidthUnits = $widthUnits % 256; |
||
288 | $pixels += round($offsetWidthUnits / (256 / 7)); |
||
289 | |||
290 | return $pixels; |
||
291 | } |
||
292 | |||
293 | protected static function hex2str($hex) |
||
294 | { |
||
295 | return chr(hexdec($hex[1])); |
||
1 ignored issue
–
show
|
|||
296 | } |
||
297 | |||
298 | /** |
||
299 | * Loads from file into Spreadsheet instance. |
||
300 | * |
||
301 | * @param string $pFilename |
||
302 | * @param Spreadsheet $spreadsheet |
||
303 | * |
||
304 | * @throws Exception |
||
305 | * |
||
306 | * @return Spreadsheet |
||
307 | */ |
||
308 | 3 | public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) |
|
309 | { |
||
310 | 3 | File::assertFile($pFilename); |
|
311 | 3 | if (!$this->canRead($pFilename)) { |
|
312 | throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); |
||
313 | } |
||
314 | |||
315 | 3 | $xml = $this->trySimpleXMLLoadString($pFilename); |
|
316 | |||
317 | 3 | $namespaces = $xml->getNamespaces(true); |
|
318 | |||
319 | 3 | $docProps = $spreadsheet->getProperties(); |
|
320 | 3 | if (isset($xml->DocumentProperties[0])) { |
|
321 | foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) { |
||
322 | switch ($propertyName) { |
||
323 | case 'Title': |
||
324 | $docProps->setTitle(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
325 | |||
326 | break; |
||
327 | case 'Subject': |
||
328 | $docProps->setSubject(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
329 | |||
330 | break; |
||
331 | case 'Author': |
||
332 | $docProps->setCreator(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
333 | |||
334 | break; |
||
335 | case 'Created': |
||
336 | $creationDate = strtotime($propertyValue); |
||
337 | $docProps->setCreated($creationDate); |
||
338 | |||
339 | break; |
||
340 | case 'LastAuthor': |
||
341 | $docProps->setLastModifiedBy(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
342 | |||
343 | break; |
||
344 | case 'LastSaved': |
||
345 | $lastSaveDate = strtotime($propertyValue); |
||
346 | $docProps->setModified($lastSaveDate); |
||
347 | |||
348 | break; |
||
349 | case 'Company': |
||
350 | $docProps->setCompany(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
351 | |||
352 | break; |
||
353 | case 'Category': |
||
354 | $docProps->setCategory(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
355 | |||
356 | break; |
||
357 | case 'Manager': |
||
358 | $docProps->setManager(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
359 | |||
360 | break; |
||
361 | case 'Keywords': |
||
362 | $docProps->setKeywords(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
363 | |||
364 | break; |
||
365 | case 'Description': |
||
366 | $docProps->setDescription(self::convertStringEncoding($propertyValue, $this->charSet)); |
||
367 | |||
368 | break; |
||
369 | } |
||
370 | } |
||
371 | } |
||
372 | 3 | if (isset($xml->CustomDocumentProperties)) { |
|
373 | foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { |
||
374 | $propertyAttributes = $propertyValue->attributes($namespaces['dt']); |
||
375 | $propertyName = preg_replace_callback('/_x([0-9a-z]{4})_/', ['self', 'hex2str'], $propertyName); |
||
376 | $propertyType = Properties::PROPERTY_TYPE_UNKNOWN; |
||
377 | switch ((string) $propertyAttributes) { |
||
378 | case 'string': |
||
379 | $propertyType = Properties::PROPERTY_TYPE_STRING; |
||
380 | $propertyValue = trim($propertyValue); |
||
381 | |||
382 | break; |
||
383 | case 'boolean': |
||
384 | $propertyType = Properties::PROPERTY_TYPE_BOOLEAN; |
||
385 | $propertyValue = (bool) $propertyValue; |
||
386 | |||
387 | break; |
||
388 | case 'integer': |
||
389 | $propertyType = Properties::PROPERTY_TYPE_INTEGER; |
||
390 | $propertyValue = (int) $propertyValue; |
||
391 | |||
392 | break; |
||
393 | case 'float': |
||
394 | $propertyType = Properties::PROPERTY_TYPE_FLOAT; |
||
395 | $propertyValue = (float) $propertyValue; |
||
396 | |||
397 | break; |
||
398 | case 'dateTime.tz': |
||
399 | $propertyType = Properties::PROPERTY_TYPE_DATE; |
||
400 | $propertyValue = strtotime(trim($propertyValue)); |
||
401 | |||
402 | break; |
||
403 | } |
||
404 | $docProps->setCustomProperty($propertyName, $propertyValue, $propertyType); |
||
405 | } |
||
406 | } |
||
407 | |||
408 | 3 | $this->parseStyles($xml, $namespaces); |
|
1 ignored issue
–
show
|
|||
409 | |||
410 | 3 | $worksheetID = 0; |
|
411 | 3 | $xml_ss = $xml->children($namespaces['ss']); |
|
412 | |||
413 | 3 | foreach ($xml_ss->Worksheet as $worksheet) { |
|
414 | 3 | $worksheet_ss = $worksheet->attributes($namespaces['ss']); |
|
415 | |||
416 | 3 | if ((isset($this->loadSheetsOnly)) && (isset($worksheet_ss['Name'])) && |
|
417 | 3 | (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))) { |
|
418 | continue; |
||
419 | } |
||
420 | |||
421 | // Create new Worksheet |
||
422 | 3 | $spreadsheet->createSheet(); |
|
423 | 3 | $spreadsheet->setActiveSheetIndex($worksheetID); |
|
424 | 3 | if (isset($worksheet_ss['Name'])) { |
|
425 | 3 | $worksheetName = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet); |
|
426 | // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in |
||
427 | // formula cells... during the load, all formulae should be correct, and we're simply bringing |
||
428 | // the worksheet name in line with the formula, not the reverse |
||
429 | 3 | $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); |
|
430 | } |
||
431 | |||
432 | 3 | $columnID = 'A'; |
|
433 | 3 | if (isset($worksheet->Table->Column)) { |
|
434 | 3 | foreach ($worksheet->Table->Column as $columnData) { |
|
435 | 3 | $columnData_ss = $columnData->attributes($namespaces['ss']); |
|
436 | 3 | if (isset($columnData_ss['Index'])) { |
|
437 | 3 | $columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']); |
|
438 | } |
||
439 | 3 | if (isset($columnData_ss['Width'])) { |
|
440 | 3 | $columnWidth = $columnData_ss['Width']; |
|
441 | 3 | $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4); |
|
442 | } |
||
443 | 3 | ++$columnID; |
|
444 | } |
||
445 | } |
||
446 | |||
447 | 3 | $rowID = 1; |
|
448 | 3 | if (isset($worksheet->Table->Row)) { |
|
449 | 3 | $additionalMergedCells = 0; |
|
450 | 3 | foreach ($worksheet->Table->Row as $rowData) { |
|
451 | 3 | $rowHasData = false; |
|
452 | 3 | $row_ss = $rowData->attributes($namespaces['ss']); |
|
453 | 3 | if (isset($row_ss['Index'])) { |
|
454 | 2 | $rowID = (int) $row_ss['Index']; |
|
455 | } |
||
456 | |||
457 | 3 | $columnID = 'A'; |
|
458 | 3 | foreach ($rowData->Cell as $cell) { |
|
459 | 3 | $cell_ss = $cell->attributes($namespaces['ss']); |
|
460 | 3 | if (isset($cell_ss['Index'])) { |
|
461 | 2 | $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']); |
|
462 | } |
||
463 | 3 | $cellRange = $columnID . $rowID; |
|
464 | |||
465 | 3 | if ($this->getReadFilter() !== null) { |
|
466 | 3 | if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { |
|
467 | ++$columnID; |
||
468 | |||
469 | continue; |
||
470 | } |
||
471 | } |
||
472 | |||
473 | 3 | if (isset($cell_ss['HRef'])) { |
|
474 | 2 | $spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl($cell_ss['HRef']); |
|
475 | } |
||
476 | |||
477 | 3 | if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) { |
|
478 | 2 | $columnTo = $columnID; |
|
479 | 2 | if (isset($cell_ss['MergeAcross'])) { |
|
480 | 2 | $additionalMergedCells += (int) $cell_ss['MergeAcross']; |
|
481 | 2 | $columnTo = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross']); |
|
482 | } |
||
483 | 2 | $rowTo = $rowID; |
|
484 | 2 | if (isset($cell_ss['MergeDown'])) { |
|
485 | 2 | $rowTo = $rowTo + $cell_ss['MergeDown']; |
|
486 | } |
||
487 | 2 | $cellRange .= ':' . $columnTo . $rowTo; |
|
488 | 2 | $spreadsheet->getActiveSheet()->mergeCells($cellRange); |
|
489 | } |
||
490 | |||
491 | 3 | $cellIsSet = $hasCalculatedValue = false; |
|
492 | 3 | $cellDataFormula = ''; |
|
493 | 3 | if (isset($cell_ss['Formula'])) { |
|
494 | 2 | $cellDataFormula = $cell_ss['Formula']; |
|
495 | 2 | $hasCalculatedValue = true; |
|
496 | } |
||
497 | 3 | if (isset($cell->Data)) { |
|
498 | 3 | $cellValue = $cellData = $cell->Data; |
|
499 | 3 | $type = DataType::TYPE_NULL; |
|
500 | 3 | $cellData_ss = $cellData->attributes($namespaces['ss']); |
|
501 | 3 | if (isset($cellData_ss['Type'])) { |
|
502 | 3 | $cellDataType = $cellData_ss['Type']; |
|
503 | 3 | switch ($cellDataType) { |
|
504 | /* |
||
505 | const TYPE_STRING = 's'; |
||
506 | const TYPE_FORMULA = 'f'; |
||
507 | const TYPE_NUMERIC = 'n'; |
||
508 | const TYPE_BOOL = 'b'; |
||
509 | const TYPE_NULL = 'null'; |
||
510 | const TYPE_INLINE = 'inlineStr'; |
||
511 | const TYPE_ERROR = 'e'; |
||
512 | */ |
||
513 | 3 | case 'String': |
|
514 | 3 | $cellValue = self::convertStringEncoding($cellValue, $this->charSet); |
|
515 | 3 | $type = DataType::TYPE_STRING; |
|
516 | |||
517 | 3 | break; |
|
518 | 3 | case 'Number': |
|
519 | 3 | $type = DataType::TYPE_NUMERIC; |
|
520 | 3 | $cellValue = (float) $cellValue; |
|
521 | 3 | if (floor($cellValue) == $cellValue) { |
|
522 | 3 | $cellValue = (int) $cellValue; |
|
523 | } |
||
524 | |||
525 | 3 | break; |
|
526 | 2 | case 'Boolean': |
|
527 | 2 | $type = DataType::TYPE_BOOL; |
|
528 | 2 | $cellValue = ($cellValue != 0); |
|
529 | |||
530 | 2 | break; |
|
531 | 2 | case 'DateTime': |
|
532 | 2 | $type = DataType::TYPE_NUMERIC; |
|
533 | 2 | $cellValue = Date::PHPToExcel(strtotime($cellValue)); |
|
534 | |||
535 | 2 | break; |
|
536 | case 'Error': |
||
537 | $type = DataType::TYPE_ERROR; |
||
538 | |||
539 | break; |
||
540 | } |
||
541 | } |
||
542 | |||
543 | 3 | if ($hasCalculatedValue) { |
|
544 | 2 | $type = DataType::TYPE_FORMULA; |
|
545 | 2 | $columnNumber = Coordinate::columnIndexFromString($columnID); |
|
546 | 2 | if (substr($cellDataFormula, 0, 3) == 'of:') { |
|
547 | 2 | $cellDataFormula = substr($cellDataFormula, 3); |
|
548 | 2 | $temp = explode('"', $cellDataFormula); |
|
549 | 2 | $key = false; |
|
550 | 2 | foreach ($temp as &$value) { |
|
551 | // Only replace in alternate array entries (i.e. non-quoted blocks) |
||
552 | 2 | if ($key = !$key) { |
|
553 | 2 | $value = str_replace(['[.', '.', ']'], '', $value); |
|
554 | } |
||
555 | } |
||
556 | } else { |
||
557 | // Convert R1C1 style references to A1 style references (but only when not quoted) |
||
558 | $temp = explode('"', $cellDataFormula); |
||
559 | $key = false; |
||
560 | foreach ($temp as &$value) { |
||
561 | // Only replace in alternate array entries (i.e. non-quoted blocks) |
||
562 | if ($key = !$key) { |
||
563 | preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); |
||
564 | // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way |
||
565 | // through the formula from left to right. Reversing means that we work right to left.through |
||
566 | // the formula |
||
567 | $cellReferences = array_reverse($cellReferences); |
||
568 | // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, |
||
569 | // then modify the formula to use that new reference |
||
570 | foreach ($cellReferences as $cellReference) { |
||
571 | $rowReference = $cellReference[2][0]; |
||
572 | // Empty R reference is the current row |
||
573 | if ($rowReference == '') { |
||
574 | $rowReference = $rowID; |
||
575 | } |
||
576 | // Bracketed R references are relative to the current row |
||
577 | if ($rowReference[0] == '[') { |
||
578 | $rowReference = $rowID + trim($rowReference, '[]'); |
||
579 | } |
||
580 | $columnReference = $cellReference[4][0]; |
||
581 | // Empty C reference is the current column |
||
582 | if ($columnReference == '') { |
||
583 | $columnReference = $columnNumber; |
||
584 | } |
||
585 | // Bracketed C references are relative to the current column |
||
586 | if ($columnReference[0] == '[') { |
||
587 | $columnReference = $columnNumber + trim($columnReference, '[]'); |
||
588 | } |
||
589 | $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference; |
||
590 | $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); |
||
591 | } |
||
592 | } |
||
593 | } |
||
594 | } |
||
595 | 2 | unset($value); |
|
596 | // Then rebuild the formula string |
||
597 | 2 | $cellDataFormula = implode('"', $temp); |
|
598 | } |
||
599 | |||
600 | 3 | $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type); |
|
601 | 3 | if ($hasCalculatedValue) { |
|
602 | 2 | $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue); |
|
603 | } |
||
604 | 3 | $cellIsSet = $rowHasData = true; |
|
605 | } |
||
606 | |||
607 | 3 | if (isset($cell->Comment)) { |
|
608 | 3 | $commentAttributes = $cell->Comment->attributes($namespaces['ss']); |
|
609 | 3 | $author = 'unknown'; |
|
610 | 3 | if (isset($commentAttributes->Author)) { |
|
611 | $author = (string) $commentAttributes->Author; |
||
612 | } |
||
613 | 3 | $node = $cell->Comment->Data->asXML(); |
|
614 | 3 | $annotation = strip_tags($node); |
|
615 | 3 | $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)->setAuthor(self::convertStringEncoding($author, $this->charSet))->setText($this->parseRichText($annotation)); |
|
616 | } |
||
617 | |||
618 | 3 | if (($cellIsSet) && (isset($cell_ss['StyleID']))) { |
|
619 | 2 | $style = (string) $cell_ss['StyleID']; |
|
620 | 2 | if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) { |
|
621 | 2 | if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) { |
|
622 | $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null); |
||
623 | } |
||
624 | 2 | $spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($this->styles[$style]); |
|
625 | } |
||
626 | } |
||
627 | 3 | ++$columnID; |
|
628 | 3 | while ($additionalMergedCells > 0) { |
|
629 | 2 | ++$columnID; |
|
630 | 2 | --$additionalMergedCells; |
|
631 | } |
||
632 | } |
||
633 | |||
634 | 3 | if ($rowHasData) { |
|
635 | 3 | if (isset($row_ss['Height'])) { |
|
636 | 3 | $rowHeight = $row_ss['Height']; |
|
637 | 3 | $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight($rowHeight); |
|
638 | } |
||
639 | } |
||
640 | |||
641 | 3 | ++$rowID; |
|
642 | } |
||
643 | } |
||
644 | 3 | ++$worksheetID; |
|
645 | } |
||
646 | |||
647 | // Return |
||
648 | 3 | return $spreadsheet; |
|
649 | } |
||
650 | |||
651 | 3 | protected static function convertStringEncoding($string, $charset) |
|
652 | { |
||
653 | 3 | if ($charset != 'UTF-8') { |
|
654 | return StringHelper::convertEncoding($string, 'UTF-8', $charset); |
||
655 | } |
||
656 | |||
657 | 3 | return $string; |
|
658 | } |
||
659 | |||
660 | 3 | protected function parseRichText($is) |
|
661 | { |
||
662 | 3 | $value = new RichText(); |
|
663 | |||
664 | 3 | $value->createText(self::convertStringEncoding($is, $this->charSet)); |
|
665 | |||
666 | 3 | return $value; |
|
667 | } |
||
668 | |||
669 | /** |
||
670 | * @param SimpleXMLElement $xml |
||
671 | * @param array $namespaces |
||
672 | */ |
||
673 | 3 | private function parseStyles(SimpleXMLElement $xml, array $namespaces) |
|
674 | { |
||
675 | 3 | if (!isset($xml->Styles)) { |
|
676 | 1 | return; |
|
677 | } |
||
678 | |||
679 | 2 | foreach ($xml->Styles[0] as $style) { |
|
680 | 2 | $style_ss = $style->attributes($namespaces['ss']); |
|
681 | 2 | $styleID = (string) $style_ss['ID']; |
|
682 | 2 | $this->styles[$styleID] = (isset($this->styles['Default'])) ? $this->styles['Default'] : []; |
|
683 | 2 | foreach ($style as $styleType => $styleData) { |
|
684 | 2 | $styleAttributes = $styleData->attributes($namespaces['ss']); |
|
685 | 2 | switch ($styleType) { |
|
686 | 2 | case 'Alignment': |
|
687 | 2 | $this->parseStyleAlignment($styleID, $styleAttributes); |
|
688 | |||
689 | 2 | break; |
|
690 | 2 | case 'Borders': |
|
691 | 2 | $this->parseStyleBorders($styleID, $styleData, $namespaces); |
|
692 | |||
693 | 2 | break; |
|
694 | 2 | case 'Font': |
|
695 | 2 | $this->parseStyleFont($styleID, $styleAttributes); |
|
696 | |||
697 | 2 | break; |
|
698 | 2 | case 'Interior': |
|
699 | 2 | $this->parseStyleInterior($styleID, $styleAttributes); |
|
700 | |||
701 | 2 | break; |
|
702 | 2 | case 'NumberFormat': |
|
703 | 2 | $this->parseStyleNumberFormat($styleID, $styleAttributes); |
|
704 | |||
705 | 2 | break; |
|
706 | } |
||
707 | } |
||
708 | } |
||
709 | 2 | } |
|
710 | |||
711 | /** |
||
712 | * @param string $styleID |
||
713 | * @param SimpleXMLElement $styleAttributes |
||
714 | */ |
||
715 | 2 | private function parseStyleAlignment($styleID, SimpleXMLElement $styleAttributes) |
|
716 | { |
||
717 | $verticalAlignmentStyles = [ |
||
718 | 2 | Alignment::VERTICAL_BOTTOM, |
|
719 | 1 | Alignment::VERTICAL_TOP, |
|
720 | 1 | Alignment::VERTICAL_CENTER, |
|
721 | 1 | Alignment::VERTICAL_JUSTIFY, |
|
722 | ]; |
||
723 | $horizontalAlignmentStyles = [ |
||
724 | 2 | Alignment::HORIZONTAL_GENERAL, |
|
725 | 1 | Alignment::HORIZONTAL_LEFT, |
|
726 | 1 | Alignment::HORIZONTAL_RIGHT, |
|
727 | 1 | Alignment::HORIZONTAL_CENTER, |
|
728 | 1 | Alignment::HORIZONTAL_CENTER_CONTINUOUS, |
|
729 | 1 | Alignment::HORIZONTAL_JUSTIFY, |
|
730 | ]; |
||
731 | |||
732 | 2 | foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { |
|
733 | 2 | $styleAttributeValue = (string) $styleAttributeValue; |
|
734 | 2 | switch ($styleAttributeKey) { |
|
735 | 2 | case 'Vertical': |
|
736 | 2 | if (self::identifyFixedStyleValue($verticalAlignmentStyles, $styleAttributeValue)) { |
|
737 | 2 | $this->styles[$styleID]['alignment']['vertical'] = $styleAttributeValue; |
|
738 | } |
||
739 | |||
740 | 2 | break; |
|
741 | 2 | case 'Horizontal': |
|
742 | 2 | if (self::identifyFixedStyleValue($horizontalAlignmentStyles, $styleAttributeValue)) { |
|
743 | 2 | $this->styles[$styleID]['alignment']['horizontal'] = $styleAttributeValue; |
|
744 | } |
||
745 | |||
746 | 2 | break; |
|
747 | 2 | case 'WrapText': |
|
748 | 2 | $this->styles[$styleID]['alignment']['wrapText'] = true; |
|
749 | |||
750 | 2 | break; |
|
751 | } |
||
752 | } |
||
753 | 2 | } |
|
754 | |||
755 | /** |
||
756 | * @param $styleID |
||
757 | * @param SimpleXMLElement $styleData |
||
758 | * @param array $namespaces |
||
759 | */ |
||
760 | 2 | private function parseStyleBorders($styleID, SimpleXMLElement $styleData, array $namespaces) |
|
761 | { |
||
762 | 2 | foreach ($styleData->Border as $borderStyle) { |
|
763 | 2 | $borderAttributes = $borderStyle->attributes($namespaces['ss']); |
|
764 | 2 | $thisBorder = []; |
|
765 | 2 | foreach ($borderAttributes as $borderStyleKey => $borderStyleValue) { |
|
766 | 2 | switch ($borderStyleKey) { |
|
767 | 2 | case 'LineStyle': |
|
768 | 2 | $thisBorder['borderStyle'] = Border::BORDER_MEDIUM; |
|
769 | |||
770 | 2 | break; |
|
771 | 2 | case 'Weight': |
|
772 | 2 | break; |
|
773 | 2 | case 'Position': |
|
774 | 2 | $borderPosition = strtolower($borderStyleValue); |
|
775 | |||
776 | 2 | break; |
|
777 | 2 | case 'Color': |
|
778 | 2 | $borderColour = substr($borderStyleValue, 1); |
|
779 | 2 | $thisBorder['color']['rgb'] = $borderColour; |
|
780 | |||
781 | 2 | break; |
|
782 | } |
||
783 | } |
||
784 | 2 | if (!empty($thisBorder)) { |
|
785 | 2 | if (($borderPosition == 'left') || ($borderPosition == 'right') || ($borderPosition == 'top') || ($borderPosition == 'bottom')) { |
|
786 | 2 | $this->styles[$styleID]['borders'][$borderPosition] = $thisBorder; |
|
787 | } |
||
788 | } |
||
789 | } |
||
790 | 2 | } |
|
791 | |||
792 | /** |
||
793 | * @param $styleID |
||
794 | * @param SimpleXMLElement $styleAttributes |
||
795 | */ |
||
796 | 2 | private function parseStyleFont($styleID, SimpleXMLElement $styleAttributes) |
|
835 | } |
||
836 | } |
||
837 | 2 | } |
|
838 | |||
839 | /** |
||
840 | * @param $styleID |
||
841 | * @param SimpleXMLElement $styleAttributes |
||
842 | */ |
||
843 | 2 | private function parseStyleInterior($styleID, SimpleXMLElement $styleAttributes) |
|
855 | } |
||
856 | } |
||
857 | 2 | } |
|
858 | |||
859 | /** |
||
860 | * @param $styleID |
||
861 | * @param SimpleXMLElement $styleAttributes |
||
862 | */ |
||
863 | 2 | private function parseStyleNumberFormat($styleID, SimpleXMLElement $styleAttributes) |
|
879 | } |
||
880 | } |
||
881 | 2 | } |
|
882 | } |
||
883 |