• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 21013444139

14 Jan 2026 11:20PM UTC coverage: 96.199% (+0.2%) from 95.962%
21013444139

Pull #4657

github

web-flow
Merge dcaff346a into 48f2fe37d
Pull Request #4657: Handling Unions as Function Arguments

19 of 20 new or added lines in 1 file covered. (95.0%)

359 existing lines in 16 files now uncovered.

46287 of 48116 relevant lines covered (96.2%)

387.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.59
/src/PhpSpreadsheet/Reader/Xml.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Reader;
4

5
use DateTime;
6
use DateTimeZone;
7
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10
use PhpOffice\PhpSpreadsheet\DefinedName;
11
use PhpOffice\PhpSpreadsheet\Helper\Html as HelperHtml;
12
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
13
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
14
use PhpOffice\PhpSpreadsheet\Reader\Xml\PageSettings;
15
use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties;
16
use PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
17
use PhpOffice\PhpSpreadsheet\RichText\RichText;
18
use PhpOffice\PhpSpreadsheet\Shared\Date;
19
use PhpOffice\PhpSpreadsheet\Shared\File;
20
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
21
use PhpOffice\PhpSpreadsheet\Spreadsheet;
22
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
23
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
24
use SimpleXMLElement;
25
use Throwable;
26

27
/**
28
 * Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003.
29
 */
30
class Xml extends BaseReader
31
{
32
    public const NAMESPACES_SS = 'urn:schemas-microsoft-com:office:spreadsheet';
33

34
    /**
35
     * Formats.
36
     *
37
     * @var mixed[]
38
     */
39
    protected array $styles = [];
40

41
    /**
42
     * Create a new Excel2003XML Reader instance.
43
     */
44
    public function __construct()
91✔
45
    {
46
        parent::__construct();
91✔
47
        $this->securityScanner = XmlScanner::getInstance($this);
91✔
48
        /** @var callable */
49
        $unentity = [self::class, 'unentity'];
91✔
50
        $this->securityScanner->setAdditionalCallback($unentity);
91✔
51
    }
52

53
    public static function unentity(string $contents): string
75✔
54
    {
55
        $contents = preg_replace('/&(amp|lt|gt|quot|apos);/', "\u{fffe}\u{feff}\$1;", trim($contents)) ?? $contents;
75✔
56
        $contents = html_entity_decode($contents, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
75✔
57
        $contents = str_replace("\u{fffe}\u{feff}", '&', $contents);
75✔
58

59
        return $contents;
75✔
60
    }
61

62
    private string $fileContents = '';
63

64
    private string $xmlFailMessage = '';
65

66
    /** @return mixed[] */
67
    public static function xmlMappings(): array
50✔
68
    {
69
        return array_merge(
50✔
70
            Style\Fill::FILL_MAPPINGS,
50✔
71
            Style\Border::BORDER_MAPPINGS
50✔
72
        );
50✔
73
    }
74

75
    /**
76
     * Can the current IReader read the file?
77
     */
78
    public function canRead(string $filename): bool
60✔
79
    {
80
        //    Office                    xmlns:o="urn:schemas-microsoft-com:office:office"
81
        //    Excel                    xmlns:x="urn:schemas-microsoft-com:office:excel"
82
        //    XML Spreadsheet            xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
83
        //    Spreadsheet component    xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet"
84
        //    XML schema                 xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
85
        //    XML data type            xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
86
        //    MS-persist recordset    xmlns:rs="urn:schemas-microsoft-com:rowset"
87
        //    Rowset                    xmlns:z="#RowsetSchema"
88
        //
89

90
        $signature = [
60✔
91
            '<?xml version="1.0"',
60✔
92
            'xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet',
60✔
93
        ];
60✔
94

95
        // Open file
96
        $data = (string) file_get_contents($filename);
60✔
97
        $data = $this->getSecurityScannerOrThrow()->scan($data);
60✔
98

99
        // Why?
100
        //$data = str_replace("'", '"', $data); // fix headers with single quote
101

102
        $valid = true;
60✔
103
        foreach ($signature as $match) {
60✔
104
            // every part of the signature must be present
105
            if (!str_contains($data, $match)) {
60✔
106
                $valid = false;
22✔
107

108
                break;
22✔
109
            }
110
        }
111

112
        $this->fileContents = $data;
60✔
113

114
        return $valid;
60✔
115
    }
116

117
    /** @return false|SimpleXMLElement */
118
    private function trySimpleXMLLoadStringPrivate(string $filename, string $fileOrString = 'file'): SimpleXMLElement|bool
53✔
119
    {
120
        $this->xmlFailMessage = "Cannot load invalid XML $fileOrString: " . $filename;
53✔
121
        $xml = false;
53✔
122

123
        try {
124
            $data = $this->fileContents;
53✔
125
            $continue = true;
53✔
126
            if ($data === '' && $fileOrString === 'file') {
53✔
127
                if ($filename === '') {
×
128
                    $this->xmlFailMessage = 'Cannot load empty path';
×
129
                    $continue = false;
×
130
                } else {
131
                    $datax = @file_get_contents($filename);
×
132
                    $data = $datax ?: '';
×
133
                    $continue = $datax !== false;
×
134
                }
135
            }
136
            if ($continue) {
53✔
137
                $xml = @simplexml_load_string(
53✔
138
                    $this->getSecurityScannerOrThrow()
53✔
139
                        ->scan($data)
53✔
140
                );
53✔
141
            }
142
        } catch (Throwable $e) {
×
143
            throw new Exception($this->xmlFailMessage, 0, $e);
×
144
        }
145
        $this->fileContents = '';
53✔
146

147
        return $xml;
53✔
148
    }
149

150
    /**
151
     * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
152
     *
153
     * @return string[]
154
     */
155
    public function listWorksheetNames(string $filename): array
4✔
156
    {
157
        File::assertFile($filename);
4✔
158
        if (!$this->canRead($filename)) {
4✔
159
            throw new Exception($filename . ' is an Invalid Spreadsheet file.');
2✔
160
        }
161

162
        $worksheetNames = [];
2✔
163

164
        $xml = $this->trySimpleXMLLoadStringPrivate($filename);
2✔
165
        if ($xml === false) {
2✔
166
            throw new Exception("Problem reading {$filename}");
1✔
167
        }
168

169
        $xml_ss = $xml->children(self::NAMESPACES_SS);
1✔
170
        foreach ($xml_ss->Worksheet as $worksheet) {
1✔
171
            $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
1✔
172
            $worksheetNames[] = (string) $worksheet_ss['Name'];
1✔
173
        }
174

175
        return $worksheetNames;
1✔
176
    }
177

178
    /**
179
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
180
     *
181
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
182
     */
183
    public function listWorksheetInfo(string $filename): array
4✔
184
    {
185
        File::assertFile($filename);
4✔
186
        if (!$this->canRead($filename)) {
4✔
187
            throw new Exception($filename . ' is an Invalid Spreadsheet file.');
2✔
188
        }
189

190
        $worksheetInfo = [];
2✔
191

192
        $xml = $this->trySimpleXMLLoadStringPrivate($filename);
2✔
193
        if ($xml === false) {
2✔
194
            throw new Exception("Problem reading {$filename}");
1✔
195
        }
196

197
        $worksheetID = 1;
1✔
198
        $xml_ss = $xml->children(self::NAMESPACES_SS);
1✔
199
        foreach ($xml_ss->Worksheet as $worksheet) {
1✔
200
            $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
1✔
201

202
            $tmpInfo = [];
1✔
203
            $tmpInfo['worksheetName'] = '';
1✔
204
            $tmpInfo['lastColumnLetter'] = 'A';
1✔
205
            $tmpInfo['lastColumnIndex'] = 0;
1✔
206
            $tmpInfo['totalRows'] = 0;
1✔
207
            $tmpInfo['totalColumns'] = 0;
1✔
208

209
            $tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}";
1✔
210
            if (isset($worksheet_ss['Name'])) {
1✔
211
                $tmpInfo['worksheetName'] = (string) $worksheet_ss['Name'];
1✔
212
            }
213

214
            if (isset($worksheet->Table->Row)) {
1✔
215
                $rowIndex = 0;
1✔
216

217
                foreach ($worksheet->Table->Row as $rowData) {
1✔
218
                    $columnIndex = 0;
1✔
219
                    $rowHasData = false;
1✔
220

221
                    foreach ($rowData->Cell as $cell) {
1✔
222
                        if (isset($cell->Data)) {
1✔
223
                            $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
1✔
224
                            $rowHasData = true;
1✔
225
                        }
226

227
                        ++$columnIndex;
1✔
228
                    }
229

230
                    ++$rowIndex;
1✔
231

232
                    if ($rowHasData) {
1✔
233
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
1✔
234
                    }
235
                }
236
            }
237

238
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
1✔
239
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
1✔
240
            $tmpInfo['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1✔
241

242
            $worksheetInfo[] = $tmpInfo;
1✔
243
            ++$worksheetID;
1✔
244
        }
245

246
        return $worksheetInfo;
1✔
247
    }
248

249
    /**
250
     * Loads Spreadsheet from string.
251
     */
252
    public function loadSpreadsheetFromString(string $contents): Spreadsheet
15✔
253
    {
254
        $spreadsheet = $this->newSpreadsheet();
15✔
255
        $spreadsheet->setValueBinder($this->valueBinder);
15✔
256
        $spreadsheet->removeSheetByIndex(0);
15✔
257

258
        // Load into this instance
259
        return $this->loadIntoExisting($contents, $spreadsheet, true);
15✔
260
    }
261

262
    /**
263
     * Loads Spreadsheet from file.
264
     */
265
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
38✔
266
    {
267
        $spreadsheet = $this->newSpreadsheet();
38✔
268
        $spreadsheet->setValueBinder($this->valueBinder);
38✔
269
        $spreadsheet->removeSheetByIndex(0);
38✔
270

271
        // Load into this instance
272
        return $this->loadIntoExisting($filename, $spreadsheet);
38✔
273
    }
274

275
    /**
276
     * Loads from file or contents into Spreadsheet instance.
277
     *
278
     * @param string $filename file name if useContents is false else file contents
279
     */
280
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, bool $useContents = false): Spreadsheet
53✔
281
    {
282
        if ($useContents) {
53✔
283
            $this->fileContents = $filename;
15✔
284
            $fileOrString = 'string';
15✔
285
        } else {
286
            File::assertFile($filename);
38✔
287
            if (!$this->canRead($filename)) {
37✔
288
                throw new Exception($filename . ' is an Invalid Spreadsheet file.');
3✔
289
            }
290
            $fileOrString = 'file';
34✔
291
        }
292

293
        $xml = $this->trySimpleXMLLoadStringPrivate($filename, $fileOrString);
49✔
294
        if ($xml === false) {
49✔
295
            throw new Exception($this->xmlFailMessage);
3✔
296
        }
297

298
        $namespaces = $xml->getNamespaces(true);
46✔
299

300
        (new Properties($spreadsheet))->readProperties($xml, $namespaces);
46✔
301

302
        $this->styles = (new Style())->parseStyles($xml, $namespaces);
46✔
303
        if (isset($this->styles['Default']) && is_array($this->styles['Default'])) {
46✔
304
            $spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']);
40✔
305
        }
306

307
        $worksheetID = 0;
46✔
308
        $xml_ss = $xml->children(self::NAMESPACES_SS);
46✔
309

310
        $sheetCreated = false;
46✔
311
        /** @var null|SimpleXMLElement $worksheetx */
312
        foreach ($xml_ss->Worksheet as $worksheetx) {
46✔
313
            $worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>');
46✔
314
            $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
46✔
315

316
            if (
317
                isset($this->loadSheetsOnly, $worksheet_ss['Name'])
46✔
318
                && (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))
46✔
319
            ) {
320
                continue;
4✔
321
            }
322

323
            // Create new Worksheet
324
            $spreadsheet->createSheet();
43✔
325
            $sheetCreated = true;
43✔
326
            $spreadsheet->setActiveSheetIndex($worksheetID);
43✔
327
            $worksheetName = '';
43✔
328
            if (isset($worksheet_ss['Name'])) {
43✔
329
                $worksheetName = (string) $worksheet_ss['Name'];
43✔
330
                //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
331
                //        formula cells... during the load, all formulae should be correct, and we're simply bringing
332
                //        the worksheet name in line with the formula, not the reverse
333
                $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
43✔
334
            }
335
            if (isset($worksheet_ss['Protected'])) {
43✔
336
                $protection = (string) $worksheet_ss['Protected'] === '1';
2✔
337
                $spreadsheet->getActiveSheet()->getProtection()->setSheet($protection);
2✔
338
            }
339

340
            // locally scoped defined names
341
            if (isset($worksheet->Names[0])) {
43✔
342
                foreach ($worksheet->Names[0] as $definedName) {
2✔
343
                    $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
2✔
344
                    $name = (string) $definedName_ss['Name'];
2✔
345
                    $definedValue = (string) $definedName_ss['RefersTo'];
2✔
346
                    $convertedValue = AddressHelper::convertFormulaToA1($definedValue);
2✔
347
                    if ($convertedValue[0] === '=') {
2✔
348
                        $convertedValue = substr($convertedValue, 1);
2✔
349
                    }
350
                    $spreadsheet->addDefinedName(DefinedName::createInstance($name, $spreadsheet->getActiveSheet(), $convertedValue, true));
2✔
351
                }
352
            }
353

354
            $columnID = 'A';
43✔
355
            if (isset($worksheet->Table->Column)) {
43✔
356
                foreach ($worksheet->Table->Column as $columnData) {
18✔
357
                    $columnData_ss = self::getAttributes($columnData, self::NAMESPACES_SS);
18✔
358
                    $colspan = 0;
18✔
359
                    if (isset($columnData_ss['Span'])) {
18✔
360
                        $spanAttr = (string) $columnData_ss['Span'];
12✔
361
                        if (is_numeric($spanAttr)) {
12✔
362
                            $colspan = max(0, (int) $spanAttr);
12✔
363
                        }
364
                    }
365
                    if (isset($columnData_ss['Index'])) {
18✔
366
                        $columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']);
14✔
367
                    }
368
                    $columnWidth = null;
18✔
369
                    if (isset($columnData_ss['Width'])) {
18✔
370
                        $columnWidth = $columnData_ss['Width'];
17✔
371
                    }
372
                    $columnVisible = null;
18✔
373
                    if (isset($columnData_ss['Hidden'])) {
18✔
374
                        $columnVisible = ((string) $columnData_ss['Hidden']) !== '1';
11✔
375
                    }
376
                    while ($colspan >= 0) {
18✔
377
                        /** @var string $columnID */
378
                        if (isset($columnWidth)) {
18✔
379
                            $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
17✔
380
                        }
381
                        if (isset($columnVisible)) {
18✔
382
                            $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setVisible($columnVisible);
11✔
383
                        }
384
                        StringHelper::stringIncrement($columnID);
18✔
385
                        --$colspan;
18✔
386
                    }
387
                }
388
            }
389

390
            $rowID = 1;
43✔
391
            if (isset($worksheet->Table->Row)) {
43✔
392
                $additionalMergedCells = 0;
42✔
393
                foreach ($worksheet->Table->Row as $rowData) {
42✔
394
                    $rowHasData = false;
42✔
395
                    $row_ss = self::getAttributes($rowData, self::NAMESPACES_SS);
42✔
396
                    if (isset($row_ss['Index'])) {
42✔
397
                        $rowID = (int) $row_ss['Index'];
7✔
398
                    }
399
                    if (isset($row_ss['Hidden'])) {
42✔
400
                        $rowVisible = ((string) $row_ss['Hidden']) !== '1';
10✔
401
                        $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible);
10✔
402
                    }
403

404
                    $columnID = 'A';
42✔
405
                    foreach ($rowData->Cell as $cell) {
42✔
406
                        $arrayRef = '';
41✔
407
                        $cell_ss = self::getAttributes($cell, self::NAMESPACES_SS);
41✔
408
                        if (isset($cell_ss['Index'])) {
41✔
409
                            $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
18✔
410
                        }
411
                        $cellRange = $columnID . $rowID;
41✔
412
                        if (isset($cell_ss['ArrayRange'])) {
41✔
413
                            $arrayRange = (string) $cell_ss['ArrayRange'];
1✔
414
                            $arrayRef = AddressHelper::convertFormulaToA1($arrayRange, $rowID, Coordinate::columnIndexFromString($columnID));
1✔
415
                        }
416

417
                        if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
41✔
418
                            StringHelper::stringIncrement($columnID);
1✔
419

420
                            continue;
1✔
421
                        }
422

423
                        if (isset($cell_ss['HRef'])) {
41✔
424
                            $spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl((string) $cell_ss['HRef']);
13✔
425
                        }
426

427
                        if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) {
41✔
428
                            $columnTo = $columnID;
9✔
429
                            if (isset($cell_ss['MergeAcross'])) {
9✔
430
                                $additionalMergedCells += (int) $cell_ss['MergeAcross'];
9✔
431
                                $columnTo = Coordinate::stringFromColumnIndex((int) (Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross']));
9✔
432
                            }
433
                            $rowTo = $rowID;
9✔
434
                            if (isset($cell_ss['MergeDown'])) {
9✔
435
                                $rowTo = $rowTo + $cell_ss['MergeDown'];
9✔
436
                            }
437
                            $cellRange .= ':' . $columnTo . $rowTo;
9✔
438
                            $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
9✔
439
                        }
440

441
                        $hasCalculatedValue = false;
41✔
442
                        $cellDataFormula = '';
41✔
443
                        if (isset($cell_ss['Formula'])) {
41✔
444
                            $cellDataFormula = $cell_ss['Formula'];
20✔
445
                            $hasCalculatedValue = true;
20✔
446
                            if ($arrayRef !== '') {
20✔
447
                                $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayRef]);
1✔
448
                            }
449
                        }
450
                        if (isset($cell->Data)) {
41✔
451
                            $cellData = $cell->Data;
41✔
452
                            $cellValue = (string) $cellData;
41✔
453
                            $type = DataType::TYPE_NULL;
41✔
454
                            $cellData_ss = self::getAttributes($cellData, self::NAMESPACES_SS);
41✔
455
                            if (isset($cellData_ss['Type'])) {
41✔
456
                                $cellDataType = $cellData_ss['Type'];
41✔
457
                                switch ($cellDataType) {
458
                                    /*
459
                                    const TYPE_STRING        = 's';
460
                                    const TYPE_FORMULA        = 'f';
461
                                    const TYPE_NUMERIC        = 'n';
462
                                    const TYPE_BOOL            = 'b';
463
                                    const TYPE_NULL            = 'null';
464
                                    const TYPE_INLINE        = 'inlineStr';
465
                                    const TYPE_ERROR        = 'e';
466
                                    */
467
                                    case 'String':
41✔
468
                                        $type = DataType::TYPE_STRING;
37✔
469
                                        $rich = $cellData->children('http://www.w3.org/TR/REC-html40');
37✔
470
                                        if ($rich) {
37✔
471
                                            // in case of HTML content we extract the payload
472
                                            // and convert it into a rich text object
473
                                            $content = $cellData->asXML() ?: '';
2✔
474
                                            $html = new HelperHtml();
2✔
475
                                            $cellValue = $html->toRichTextObject($content, true);
2✔
476
                                        }
477

478
                                        break;
37✔
479
                                    case 'Number':
17✔
480
                                        $type = DataType::TYPE_NUMERIC;
17✔
481
                                        $cellValue = (float) $cellValue;
17✔
482
                                        if (floor($cellValue) == $cellValue) {
17✔
483
                                            $cellValue = (int) $cellValue;
17✔
484
                                        }
485

486
                                        break;
17✔
487
                                    case 'Boolean':
11✔
488
                                        $type = DataType::TYPE_BOOL;
9✔
489
                                        $cellValue = ($cellValue != 0);
9✔
490

491
                                        break;
9✔
492
                                    case 'DateTime':
11✔
493
                                        $type = DataType::TYPE_NUMERIC;
11✔
494
                                        $dateTime = new DateTime($cellValue, new DateTimeZone('UTC'));
11✔
495
                                        $cellValue = Date::PHPToExcel($dateTime);
11✔
496

497
                                        break;
11✔
498
                                    case 'Error':
9✔
499
                                        $type = DataType::TYPE_ERROR;
9✔
500
                                        $hasCalculatedValue = false;
9✔
501

502
                                        break;
9✔
503
                                }
504
                            }
505

506
                            $originalType = $type;
41✔
507
                            if ($hasCalculatedValue) {
41✔
508
                                $type = DataType::TYPE_FORMULA;
20✔
509
                                $columnNumber = Coordinate::columnIndexFromString($columnID);
20✔
510
                                $cellDataFormula = AddressHelper::convertFormulaToA1($cellDataFormula, $rowID, $columnNumber);
20✔
511
                            }
512

513
                            $hyperlink = null;
41✔
514
                            if ($spreadsheet->getActiveSheet()->hyperlinkExists($columnID . $rowID)) {
41✔
515
                                $hyperlink = $spreadsheet->getActiveSheet()->getHyperlink($columnID . $rowID);
13✔
516
                            }
517
                            $spreadsheet->getActiveSheet()
41✔
518
                                ->getCell($columnID . $rowID)
41✔
519
                                ->setValueExplicit(
41✔
520
                                    $hasCalculatedValue ? $cellDataFormula : $cellValue,
41✔
521
                                    $type
41✔
522
                                );
41✔
523
                            $spreadsheet->getActiveSheet()
41✔
524
                                ->setHyperlink($columnID . $rowID, $hyperlink);
41✔
525
                            if ($hasCalculatedValue) {
41✔
526
                                $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue, $originalType === DataType::TYPE_NUMERIC);
20✔
527
                            }
528
                            $rowHasData = true;
41✔
529
                        }
530

531
                        if (isset($cell->Comment)) {
41✔
532
                            $this->parseCellComment($cell->Comment, $spreadsheet, $columnID, $rowID);
10✔
533
                        }
534

535
                        if (isset($cell_ss['StyleID'])) {
41✔
536
                            $style = (string) $cell_ss['StyleID'];
25✔
537
                            if ((isset($this->styles[$style])) && is_array($this->styles[$style]) && (!empty($this->styles[$style]))) {
25✔
538
                                $spreadsheet->getActiveSheet()->getStyle($cellRange)
25✔
539
                                    ->applyFromArray($this->styles[$style]);
25✔
540
                            }
541
                        }
542
                        StringHelper::stringIncrement($columnID);
41✔
543
                        while ($additionalMergedCells > 0) {
41✔
544
                            StringHelper::stringIncrement($columnID);
9✔
545
                            --$additionalMergedCells;
9✔
546
                        }
547
                    }
548

549
                    if ($rowHasData) {
42✔
550
                        if (isset($row_ss['Height'])) {
41✔
551
                            $rowHeight = $row_ss['Height'];
17✔
552
                            $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight((float) $rowHeight);
17✔
553
                        }
554
                    }
555

556
                    ++$rowID;
42✔
557
                }
558
            }
559

560
            $dataValidations = new Xml\DataValidations();
43✔
561
            $dataValidations->loadDataValidations($worksheet, $spreadsheet);
43✔
562
            $xmlX = $worksheet->children(Namespaces::URN_EXCEL);
43✔
563
            if (isset($xmlX->WorksheetOptions)) {
43✔
564
                if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) {
37✔
565
                    $spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW);
3✔
566
                }
567
                if (isset($xmlX->WorksheetOptions->Zoom)) {
37✔
568
                    $zoomScaleNormal = (int) $xmlX->WorksheetOptions->Zoom;
3✔
569
                    if ($zoomScaleNormal > 0) {
3✔
570
                        $spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleNormal($zoomScaleNormal);
3✔
571
                        $spreadsheet->getActiveSheet()->getSheetView()->setZoomScale($zoomScaleNormal);
3✔
572
                    }
573
                }
574
                if (isset($xmlX->WorksheetOptions->PageBreakZoom)) {
37✔
575
                    $zoomScaleNormal = (int) $xmlX->WorksheetOptions->PageBreakZoom;
3✔
576
                    if ($zoomScaleNormal > 0) {
3✔
577
                        $spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleSheetLayoutView($zoomScaleNormal);
3✔
578
                    }
579
                }
580
                if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) {
37✔
581
                    $spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW);
3✔
582
                }
583
                if (isset($xmlX->WorksheetOptions->FreezePanes)) {
37✔
584
                    $freezeRow = $freezeColumn = 1;
4✔
585
                    if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
4✔
586
                        $freezeRow = (int) $xmlX->WorksheetOptions->SplitHorizontal + 1;
4✔
587
                    }
588
                    if (isset($xmlX->WorksheetOptions->SplitVertical)) {
4✔
589
                        $freezeColumn = (int) $xmlX->WorksheetOptions->SplitVertical + 1;
4✔
590
                    }
591
                    $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowBottomPane;
4✔
592
                    $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnRightPane;
4✔
593
                    if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
4✔
594
                        $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
4✔
595
                        $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, $leftTopCoordinate, !isset($xmlX->WorksheetOptions->FrozenNoSplit));
4✔
596
                    } else {
UNCOV
597
                        $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, null, !isset($xmlX->WorksheetOptions->FrozenNoSplit));
×
598
                    }
599
                } elseif (isset($xmlX->WorksheetOptions->SplitVertical) || isset($xmlX->WorksheetOptions->SplitHorizontal)) {
36✔
600
                    if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
1✔
601
                        $ySplit = (int) $xmlX->WorksheetOptions->SplitHorizontal;
1✔
602
                        $spreadsheet->getActiveSheet()->setYSplit($ySplit);
1✔
603
                    }
604
                    if (isset($xmlX->WorksheetOptions->SplitVertical)) {
1✔
605
                        $xSplit = (int) $xmlX->WorksheetOptions->SplitVertical;
1✔
606
                        $spreadsheet->getActiveSheet()->setXSplit($xSplit);
1✔
607
                    }
608
                    if (isset($xmlX->WorksheetOptions->LeftColumnVisible) || isset($xmlX->WorksheetOptions->TopRowVisible)) {
1✔
609
                        $leftTopColumn = $leftTopRow = 1;
1✔
610
                        if (isset($xmlX->WorksheetOptions->LeftColumnVisible)) {
1✔
611
                            $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnVisible;
1✔
612
                        }
613
                        if (isset($xmlX->WorksheetOptions->TopRowVisible)) {
1✔
614
                            $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowVisible;
1✔
615
                        }
616
                        $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow";
1✔
617
                        $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
1✔
618
                    }
619

620
                    $leftTopColumn = $leftTopRow = 1;
1✔
621
                    if (isset($xmlX->WorksheetOptions->LeftColumnRightPane)) {
1✔
622
                        $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnRightPane;
1✔
623
                    }
624
                    if (isset($xmlX->WorksheetOptions->TopRowBottomPane)) {
1✔
625
                        $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowBottomPane;
1✔
626
                    }
627
                    $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow";
1✔
628
                    $spreadsheet->getActiveSheet()->setPaneTopLeftCell($leftTopCoordinate);
1✔
629
                }
630
                (new PageSettings($xmlX))->loadPageSettings($spreadsheet);
37✔
631
                if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) {
37✔
632
                    $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible;
2✔
633
                    $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible;
2✔
634
                    if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
2✔
635
                        $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
2✔
636
                        $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
2✔
637
                    }
638
                }
639
                $rangeCalculated = false;
37✔
640
                if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) {
37✔
641
                    if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) {
13✔
642
                        $selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
13✔
643
                            . $selectionMatches[1]
13✔
644
                            . ':'
13✔
645
                            . Coordinate::stringFromColumnIndex((int) $selectionMatches[4])
13✔
646
                            . $selectionMatches[3];
13✔
647
                        $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
13✔
648
                        $rangeCalculated = true;
13✔
649
                    }
650
                }
651
                if (!$rangeCalculated) {
37✔
652
                    if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) {
36✔
653
                        $activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow;
27✔
654
                    } else {
655
                        $activeRow = 0;
11✔
656
                    }
657
                    if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) {
36✔
658
                        $activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol;
19✔
659
                    } else {
660
                        $activeColumn = 0;
19✔
661
                    }
662
                    if (is_numeric($activeRow) && is_numeric($activeColumn)) {
36✔
663
                        $selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1);
36✔
664
                        $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
36✔
665
                    }
666
                }
667
            }
668
            if (isset($xmlX->PageBreaks)) {
43✔
669
                if (isset($xmlX->PageBreaks->ColBreaks)) {
3✔
670
                    foreach ($xmlX->PageBreaks->ColBreaks->ColBreak as $colBreak) {
3✔
671
                        $colBreak = (string) $colBreak->Column;
3✔
672
                        $spreadsheet->getActiveSheet()->setBreak([1 + (int) $colBreak, 1], Worksheet::BREAK_COLUMN);
3✔
673
                    }
674
                }
675
                if (isset($xmlX->PageBreaks->RowBreaks)) {
3✔
676
                    foreach ($xmlX->PageBreaks->RowBreaks->RowBreak as $rowBreak) {
3✔
677
                        $rowBreak = (string) $rowBreak->Row;
3✔
678
                        $spreadsheet->getActiveSheet()->setBreak([1, (int) $rowBreak], Worksheet::BREAK_ROW);
3✔
679
                    }
680
                }
681
            }
682
            ++$worksheetID;
43✔
683
        }
684
        if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
46✔
685
            $spreadsheet->createSheet();
1✔
686
        }
687

688
        // Globally scoped defined names
689
        $activeSheetIndex = 0;
46✔
690
        if (isset($xml->ExcelWorkbook->ActiveSheet)) {
46✔
691
            $activeSheetIndex = (int) (string) $xml->ExcelWorkbook->ActiveSheet;
2✔
692
        }
693
        $activeWorksheet = $spreadsheet->setActiveSheetIndex($activeSheetIndex);
46✔
694
        if (isset($xml->Names[0])) {
44✔
695
            foreach ($xml->Names[0] as $definedName) {
11✔
696
                $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
11✔
697
                $name = (string) $definedName_ss['Name'];
11✔
698
                $definedValue = (string) $definedName_ss['RefersTo'];
11✔
699
                $convertedValue = AddressHelper::convertFormulaToA1($definedValue);
11✔
700
                if ($convertedValue[0] === '=') {
11✔
701
                    $convertedValue = substr($convertedValue, 1);
11✔
702
                }
703
                $spreadsheet->addDefinedName(DefinedName::createInstance($name, $activeWorksheet, $convertedValue));
11✔
704
            }
705
        }
706

707
        // Return
708
        return $spreadsheet;
44✔
709
    }
710

711
    protected function parseCellComment(
10✔
712
        SimpleXMLElement $comment,
713
        Spreadsheet $spreadsheet,
714
        string $columnID,
715
        int $rowID
716
    ): void {
717
        $commentAttributes = $comment->attributes(self::NAMESPACES_SS);
10✔
718
        $author = 'unknown';
10✔
719
        if (isset($commentAttributes->Author)) {
10✔
720
            $author = (string) $commentAttributes->Author;
9✔
721
        }
722

723
        $node = $comment->Data->asXML();
10✔
724
        $annotation = strip_tags((string) $node);
10✔
725
        $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)
10✔
726
            ->setAuthor($author)
10✔
727
            ->setText($this->parseRichText($annotation));
10✔
728
    }
729

730
    protected function parseRichText(string $annotation): RichText
10✔
731
    {
732
        $value = new RichText();
10✔
733

734
        $value->createText($annotation);
10✔
735

736
        return $value;
10✔
737
    }
738

739
    private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
48✔
740
    {
741
        return ($simple === null)
48✔
UNCOV
742
            ? new SimpleXMLElement('<xml></xml>')
×
743
            : ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>'));
48✔
744
    }
745
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc