• 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

98.87
/src/PhpSpreadsheet/Spreadsheet.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet;
4

5
use JsonSerializable;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
8
use PhpOffice\PhpSpreadsheet\Document\Properties;
9
use PhpOffice\PhpSpreadsheet\Document\Security;
10
use PhpOffice\PhpSpreadsheet\Shared\Date;
11
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
12
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
13
use PhpOffice\PhpSpreadsheet\Style\Style;
14
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
15
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
16
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
17

18
class Spreadsheet implements JsonSerializable
19
{
20
    // Allowable values for workbook window visibility
21
    const VISIBILITY_VISIBLE = 'visible';
22
    const VISIBILITY_HIDDEN = 'hidden';
23
    const VISIBILITY_VERY_HIDDEN = 'veryHidden';
24

25
    private const DEFINED_NAME_IS_RANGE = false;
26
    private const DEFINED_NAME_IS_FORMULA = true;
27

28
    private const WORKBOOK_VIEW_VISIBILITY_VALUES = [
29
        self::VISIBILITY_VISIBLE,
30
        self::VISIBILITY_HIDDEN,
31
        self::VISIBILITY_VERY_HIDDEN,
32
    ];
33

34
    protected int $excelCalendar = Date::CALENDAR_WINDOWS_1900;
35

36
    /**
37
     * Unique ID.
38
     */
39
    private string $uniqueID;
40

41
    /**
42
     * Document properties.
43
     */
44
    private Properties $properties;
45

46
    /**
47
     * Document security.
48
     */
49
    private Security $security;
50

51
    /**
52
     * Collection of Worksheet objects.
53
     *
54
     * @var Worksheet[]
55
     */
56
    private array $workSheetCollection;
57

58
    /**
59
     * Calculation Engine.
60
     */
61
    private Calculation $calculationEngine;
62

63
    /**
64
     * Active sheet index.
65
     */
66
    private int $activeSheetIndex;
67

68
    /**
69
     * Named ranges.
70
     *
71
     * @var DefinedName[]
72
     */
73
    private array $definedNames;
74

75
    /**
76
     * CellXf supervisor.
77
     */
78
    private Style $cellXfSupervisor;
79

80
    /**
81
     * CellXf collection.
82
     *
83
     * @var Style[]
84
     */
85
    private array $cellXfCollection = [];
86

87
    /**
88
     * CellStyleXf collection.
89
     *
90
     * @var Style[]
91
     */
92
    private array $cellStyleXfCollection = [];
93

94
    /**
95
     * hasMacros : this workbook have macros ?
96
     */
97
    private bool $hasMacros = false;
98

99
    /**
100
     * macrosCode : all macros code as binary data (the vbaProject.bin file, this include form, code,  etc.), null if no macro.
101
     */
102
    private ?string $macrosCode = null;
103

104
    /**
105
     * macrosCertificate : if macros are signed, contains binary data vbaProjectSignature.bin file, null if not signed.
106
     */
107
    private ?string $macrosCertificate = null;
108

109
    /**
110
     * ribbonXMLData : null if workbook isn't Excel 2007 or not contain a customized UI.
111
     *
112
     * @var null|array{target: string, data: string}
113
     */
114
    private ?array $ribbonXMLData = null;
115

116
    /**
117
     * ribbonBinObjects : null if workbook isn't Excel 2007 or not contain embedded objects (picture(s)) for Ribbon Elements
118
     * ignored if $ribbonXMLData is null.
119
     *
120
     * @var null|mixed[]
121
     */
122
    private ?array $ribbonBinObjects = null;
123

124
    /**
125
     * List of unparsed loaded data for export to same format with better compatibility.
126
     * It has to be minimized when the library start to support currently unparsed data.
127
     *
128
     * @var array<array<array<array<string>|string>>>
129
     */
130
    private array $unparsedLoadedData = [];
131

132
    /**
133
     * Controls visibility of the horizonal scroll bar in the application.
134
     */
135
    private bool $showHorizontalScroll = true;
136

137
    /**
138
     * Controls visibility of the horizonal scroll bar in the application.
139
     */
140
    private bool $showVerticalScroll = true;
141

142
    /**
143
     * Controls visibility of the sheet tabs in the application.
144
     */
145
    private bool $showSheetTabs = true;
146

147
    /**
148
     * Specifies a boolean value that indicates whether the workbook window
149
     * is minimized.
150
     */
151
    private bool $minimized = false;
152

153
    /**
154
     * Specifies a boolean value that indicates whether to group dates
155
     * when presenting the user with filtering options in the user
156
     * interface.
157
     */
158
    private bool $autoFilterDateGrouping = true;
159

160
    /**
161
     * Specifies the index to the first sheet in the book view.
162
     */
163
    private int $firstSheetIndex = 0;
164

165
    /**
166
     * Specifies the visible status of the workbook.
167
     */
168
    private string $visibility = self::VISIBILITY_VISIBLE;
169

170
    /**
171
     * Specifies the ratio between the workbook tabs bar and the horizontal
172
     * scroll bar.  TabRatio is assumed to be out of 1000 of the horizontal
173
     * window width.
174
     */
175
    private int $tabRatio = 600;
176

177
    private Theme $theme;
178

179
    private ?IValueBinder $valueBinder = null;
180

181
    /** @var array<string, int> */
182
    private array $fontCharsets = [
183
        'B Nazanin' => SharedFont::CHARSET_ANSI_ARABIC,
184
    ];
185

186
    /**
187
     * @param int $charset uses any value from Shared\Font,
188
     *    but defaults to ARABIC because that is the only known
189
     *    charset for which this declaration might be needed
190
     */
191
    public function addFontCharset(string $fontName, int $charset = SharedFont::CHARSET_ANSI_ARABIC): void
33✔
192
    {
193
        $this->fontCharsets[$fontName] = $charset;
33✔
194
    }
195

196
    public function getFontCharset(string $fontName): int
437✔
197
    {
198
        return $this->fontCharsets[$fontName] ?? -1;
437✔
199
    }
200

201
    /**
202
     * Return all fontCharsets.
203
     *
204
     * @return array<string, int>
205
     */
206
    public function getFontCharsets(): array
1✔
207
    {
208
        return $this->fontCharsets;
1✔
209
    }
210

211
    public function getTheme(): Theme
867✔
212
    {
213
        return $this->theme;
867✔
214
    }
215

216
    /**
217
     * The workbook has macros ?
218
     */
219
    public function hasMacros(): bool
506✔
220
    {
221
        return $this->hasMacros;
506✔
222
    }
223

224
    /**
225
     * Define if a workbook has macros.
226
     *
227
     * @param bool $hasMacros true|false
228
     */
229
    public function setHasMacros(bool $hasMacros): void
3✔
230
    {
231
        $this->hasMacros = (bool) $hasMacros;
3✔
232
    }
233

234
    /**
235
     * Set the macros code.
236
     */
237
    public function setMacrosCode(?string $macroCode): void
3✔
238
    {
239
        $this->macrosCode = $macroCode;
3✔
240
        $this->setHasMacros($macroCode !== null);
3✔
241
    }
242

243
    /**
244
     * Return the macros code.
245
     */
246
    public function getMacrosCode(): ?string
3✔
247
    {
248
        return $this->macrosCode;
3✔
249
    }
250

251
    /**
252
     * Set the macros certificate.
253
     */
254
    public function setMacrosCertificate(?string $certificate): void
3✔
255
    {
256
        $this->macrosCertificate = $certificate;
3✔
257
    }
258

259
    /**
260
     * Is the project signed ?
261
     *
262
     * @return bool true|false
263
     */
264
    public function hasMacrosCertificate(): bool
2✔
265
    {
266
        return $this->macrosCertificate !== null;
2✔
267
    }
268

269
    /**
270
     * Return the macros certificate.
271
     */
272
    public function getMacrosCertificate(): ?string
2✔
273
    {
274
        return $this->macrosCertificate;
2✔
275
    }
276

277
    /**
278
     * Remove all macros, certificate from spreadsheet.
279
     */
280
    public function discardMacros(): void
1✔
281
    {
282
        $this->hasMacros = false;
1✔
283
        $this->macrosCode = null;
1✔
284
        $this->macrosCertificate = null;
1✔
285
    }
286

287
    /**
288
     * set ribbon XML data.
289
     */
290
    public function setRibbonXMLData(mixed $target, mixed $xmlData): void
2✔
291
    {
292
        if (is_string($target) && is_string($xmlData)) {
2✔
293
            $this->ribbonXMLData = ['target' => $target, 'data' => $xmlData];
2✔
294
        } else {
295
            $this->ribbonXMLData = null;
×
296
        }
297
    }
298

299
    /**
300
     * retrieve ribbon XML Data.
301
     *
302
     * @return mixed[]
303
     */
304
    public function getRibbonXMLData(string $what = 'all'): null|array|string //we need some constants here...
435✔
305
    {
306
        $returnData = null;
435✔
307
        $what = strtolower($what);
435✔
308
        switch ($what) {
309
            case 'all':
435✔
310
                $returnData = $this->ribbonXMLData;
2✔
311

312
                break;
2✔
313
            case 'target':
435✔
314
            case 'data':
2✔
315
                if (is_array($this->ribbonXMLData)) {
435✔
316
                    $returnData = $this->ribbonXMLData[$what];
2✔
317
                }
318

319
                break;
435✔
320
        }
321

322
        return $returnData;
435✔
323
    }
324

325
    /**
326
     * store binaries ribbon objects (pictures).
327
     */
328
    public function setRibbonBinObjects(mixed $binObjectsNames, mixed $binObjectsData): void
2✔
329
    {
330
        if ($binObjectsNames !== null && $binObjectsData !== null) {
2✔
331
            $this->ribbonBinObjects = ['names' => $binObjectsNames, 'data' => $binObjectsData];
×
332
        } else {
333
            $this->ribbonBinObjects = null;
2✔
334
        }
335
    }
336

337
    /**
338
     * List of unparsed loaded data for export to same format with better compatibility.
339
     * It has to be minimized when the library start to support currently unparsed data.
340
     *
341
     * @internal
342
     *
343
     * @return mixed[]
344
     */
345
    public function getUnparsedLoadedData(): array
508✔
346
    {
347
        return $this->unparsedLoadedData;
508✔
348
    }
349

350
    /**
351
     * List of unparsed loaded data for export to same format with better compatibility.
352
     * It has to be minimized when the library start to support currently unparsed data.
353
     *
354
     * @internal
355
     *
356
     * @param array<array<array<array<string>|string>>> $unparsedLoadedData
357
     */
358
    public function setUnparsedLoadedData(array $unparsedLoadedData): void
743✔
359
    {
360
        $this->unparsedLoadedData = $unparsedLoadedData;
743✔
361
    }
362

363
    /**
364
     * retrieve Binaries Ribbon Objects.
365
     *
366
     * @return mixed[]
367
     */
368
    public function getRibbonBinObjects(string $what = 'all'): ?array
2✔
369
    {
370
        $ReturnData = null;
2✔
371
        $what = strtolower($what);
2✔
372
        switch ($what) {
373
            case 'all':
2✔
374
                return $this->ribbonBinObjects;
2✔
375
            case 'names':
1✔
376
            case 'data':
1✔
377
                if (is_array($this->ribbonBinObjects) && is_array($this->ribbonBinObjects[$what] ?? null)) {
1✔
378
                    $ReturnData = $this->ribbonBinObjects[$what];
×
379
                }
380

381
                break;
1✔
382
            case 'types':
1✔
383
                if (
384
                    is_array($this->ribbonBinObjects)
1✔
385
                    && isset($this->ribbonBinObjects['data']) && is_array($this->ribbonBinObjects['data'])
1✔
386
                ) {
387
                    $tmpTypes = array_keys($this->ribbonBinObjects['data']);
×
388
                    $ReturnData = array_unique(array_map(fn (string $path): string => pathinfo($path, PATHINFO_EXTENSION), $tmpTypes));
×
389
                } else {
390
                    $ReturnData = []; // the caller want an array... not null if empty
1✔
391
                }
392

393
                break;
1✔
394
        }
395

396
        return $ReturnData;
1✔
397
    }
398

399
    /**
400
     * This workbook have a custom UI ?
401
     */
402
    public function hasRibbon(): bool
435✔
403
    {
404
        return $this->ribbonXMLData !== null;
435✔
405
    }
406

407
    /**
408
     * This workbook have additional object for the ribbon ?
409
     */
410
    public function hasRibbonBinObjects(): bool
435✔
411
    {
412
        return $this->ribbonBinObjects !== null;
435✔
413
    }
414

415
    /**
416
     * This workbook has in cell images.
417
     */
418
    public function hasInCellDrawings(): bool
435✔
419
    {
420
        $sheetCount = $this->getSheetCount();
435✔
421
        for ($i = 0; $i < $sheetCount; ++$i) {
435✔
422
            if ($this->getSheet($i)->getInCellDrawingCollection()->count() > 0) {
435✔
423
                return true;
8✔
424
            }
425
        }
426

427
        return false;
427✔
428
    }
429

430
    /**
431
     * Check if a sheet with a specified code name already exists.
432
     *
433
     * @param string $codeName Name of the worksheet to check
434
     */
435
    public function sheetCodeNameExists(string $codeName): bool
11,153✔
436
    {
437
        return $this->getSheetByCodeName($codeName) !== null;
11,153✔
438
    }
439

440
    /**
441
     * Get sheet by code name. Warning : sheet don't have always a code name !
442
     *
443
     * @param string $codeName Sheet name
444
     */
445
    public function getSheetByCodeName(string $codeName): ?Worksheet
11,153✔
446
    {
447
        $worksheetCount = count($this->workSheetCollection);
11,153✔
448
        for ($i = 0; $i < $worksheetCount; ++$i) {
11,153✔
449
            if ($this->workSheetCollection[$i]->getCodeName() == $codeName) {
740✔
450
                return $this->workSheetCollection[$i];
706✔
451
            }
452
        }
453

454
        return null;
11,153✔
455
    }
456

457
    /**
458
     * Create a new PhpSpreadsheet with one Worksheet.
459
     */
460
    public function __construct()
11,153✔
461
    {
462
        $this->uniqueID = uniqid('', true);
11,153✔
463
        $this->calculationEngine = new Calculation($this);
11,153✔
464
        $this->theme = new Theme();
11,153✔
465

466
        // Initialise worksheet collection and add one worksheet
467
        $this->workSheetCollection = [];
11,153✔
468
        $this->workSheetCollection[] = new Worksheet($this);
11,153✔
469
        $this->activeSheetIndex = 0;
11,153✔
470

471
        // Create document properties
472
        $this->properties = new Properties();
11,153✔
473

474
        // Create document security
475
        $this->security = new Security();
11,153✔
476

477
        // Set defined names
478
        $this->definedNames = [];
11,153✔
479

480
        // Create the cellXf supervisor
481
        $this->cellXfSupervisor = new Style(true);
11,153✔
482
        $this->cellXfSupervisor->bindParent($this);
11,153✔
483

484
        // Create the default style
485
        $this->addCellXf(new Style());
11,153✔
486
        $this->addCellStyleXf(new Style());
11,153✔
487
    }
488

489
    /**
490
     * Code to execute when this worksheet is unset().
491
     */
492
    public function __destruct()
130✔
493
    {
494
        $this->disconnectWorksheets();
130✔
495
        unset($this->calculationEngine);
130✔
496
        $this->cellXfCollection = [];
130✔
497
        $this->cellStyleXfCollection = [];
130✔
498
        $this->definedNames = [];
130✔
499
    }
500

501
    /**
502
     * Disconnect all worksheets from this PhpSpreadsheet workbook object,
503
     * typically so that the PhpSpreadsheet object can be unset.
504
     */
505
    public function disconnectWorksheets(): void
10,217✔
506
    {
507
        foreach ($this->workSheetCollection as $worksheet) {
10,217✔
508
            $worksheet->disconnectCells();
10,216✔
509
            unset($worksheet);
10,216✔
510
        }
511
        $this->workSheetCollection = [];
10,217✔
512
    }
513

514
    /**
515
     * Return the calculation engine for this worksheet.
516
     */
517
    public function getCalculationEngine(): Calculation
10,129✔
518
    {
519
        return $this->calculationEngine;
10,129✔
520
    }
521

522
    /**
523
     * Intended for use only via a destructor.
524
     *
525
     * @internal
526
     */
527
    public function getCalculationEngineOrNull(): ?Calculation
89✔
528
    {
529
        if (!isset($this->calculationEngine)) { //* @phpstan-ignore-line
89✔
530
            return null;
63✔
531
        }
532

533
        return $this->calculationEngine;
88✔
534
    }
535

536
    /**
537
     * Get properties.
538
     */
539
    public function getProperties(): Properties
1,746✔
540
    {
541
        return $this->properties;
1,746✔
542
    }
543

544
    /**
545
     * Set properties.
546
     */
547
    public function setProperties(Properties $documentProperties): void
1✔
548
    {
549
        $this->properties = $documentProperties;
1✔
550
    }
551

552
    /**
553
     * Get security.
554
     */
555
    public function getSecurity(): Security
452✔
556
    {
557
        return $this->security;
452✔
558
    }
559

560
    /**
561
     * Set security.
562
     */
563
    public function setSecurity(Security $documentSecurity): void
1✔
564
    {
565
        $this->security = $documentSecurity;
1✔
566
    }
567

568
    /**
569
     * Get active sheet.
570
     */
571
    public function getActiveSheet(): Worksheet
11,067✔
572
    {
573
        return $this->getSheet($this->activeSheetIndex);
11,067✔
574
    }
575

576
    /**
577
     * Create sheet and add it to this workbook.
578
     *
579
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
580
     */
581
    public function createSheet(?int $sheetIndex = null): Worksheet
1,340✔
582
    {
583
        $newSheet = new Worksheet($this);
1,340✔
584
        $this->addSheet($newSheet, $sheetIndex, true);
1,340✔
585

586
        return $newSheet;
1,340✔
587
    }
588

589
    /**
590
     * Check if a sheet with a specified name already exists.
591
     *
592
     * @param string $worksheetName Name of the worksheet to check
593
     */
594
    public function sheetNameExists(string $worksheetName): bool
1,927✔
595
    {
596
        return $this->getSheetByName($worksheetName) !== null;
1,927✔
597
    }
598

599
    public function duplicateWorksheetByTitle(string $title): Worksheet
1✔
600
    {
601
        $original = $this->getSheetByNameOrThrow($title);
1✔
602
        $index = $this->getIndex($original) + 1;
1✔
603
        $clone = clone $original;
1✔
604

605
        return $this->addSheet($clone, $index, true);
1✔
606
    }
607

608
    /**
609
     * Add sheet.
610
     *
611
     * @param Worksheet $worksheet The worksheet to add
612
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
613
     */
614
    public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null, bool $retitleIfNeeded = false): Worksheet
1,431✔
615
    {
616
        if ($retitleIfNeeded) {
1,431✔
617
            $title = $worksheet->getTitle();
1,340✔
618
            if ($this->sheetNameExists($title)) {
1,340✔
619
                $i = 1;
173✔
620
                $newTitle = "$title $i";
173✔
621
                while ($this->sheetNameExists($newTitle)) {
173✔
622
                    ++$i;
35✔
623
                    $newTitle = "$title $i";
35✔
624
                }
625
                $worksheet->setTitle($newTitle);
173✔
626
            }
627
        }
628
        if ($this->sheetNameExists($worksheet->getTitle())) {
1,431✔
629
            throw new Exception(
2✔
630
                "Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename this worksheet first."
2✔
631
            );
2✔
632
        }
633

634
        if ($sheetIndex === null) {
1,431✔
635
            if ($this->activeSheetIndex < 0) {
1,398✔
636
                $this->activeSheetIndex = 0;
1,003✔
637
            }
638
            $this->workSheetCollection[] = $worksheet;
1,398✔
639
        } else {
640
            // Insert the sheet at the requested index
641
            array_splice(
38✔
642
                $this->workSheetCollection,
38✔
643
                $sheetIndex,
38✔
644
                0,
38✔
645
                [$worksheet]
38✔
646
            );
38✔
647

648
            // Adjust active sheet index if necessary
649
            if ($this->activeSheetIndex >= $sheetIndex) {
38✔
650
                ++$this->activeSheetIndex;
30✔
651
            }
652
            if ($this->activeSheetIndex < 0) {
38✔
653
                $this->activeSheetIndex = 0;
3✔
654
            }
655
        }
656

657
        if ($worksheet->getParent() === null) {
1,431✔
658
            $worksheet->rebindParent($this);
51✔
659
        }
660

661
        return $worksheet;
1,431✔
662
    }
663

664
    /**
665
     * Remove sheet by index.
666
     *
667
     * @param int $sheetIndex Index position of the worksheet to remove
668
     */
669
    public function removeSheetByIndex(int $sheetIndex): void
1,050✔
670
    {
671
        $numSheets = count($this->workSheetCollection);
1,050✔
672
        if ($sheetIndex > $numSheets - 1) {
1,050✔
673
            throw new Exception(
1✔
674
                "You tried to remove a sheet by the out of bounds index: {$sheetIndex}. The actual number of sheets is {$numSheets}."
1✔
675
            );
1✔
676
        }
677
        array_splice($this->workSheetCollection, $sheetIndex, 1);
1,049✔
678

679
        // Adjust active sheet index if necessary
680
        if (
681
            ($this->activeSheetIndex >= $sheetIndex)
1,049✔
682
            && ($this->activeSheetIndex > 0 || $numSheets <= 1)
1,049✔
683
        ) {
684
            --$this->activeSheetIndex;
1,047✔
685
        }
686
    }
687

688
    /**
689
     * Get sheet by index.
690
     *
691
     * @param int $sheetIndex Sheet index
692
     */
693
    public function getSheet(int $sheetIndex): Worksheet
11,073✔
694
    {
695
        if (!isset($this->workSheetCollection[$sheetIndex])) {
11,073✔
696
            $numSheets = $this->getSheetCount();
1✔
697

698
            throw new Exception(
1✔
699
                "Your requested sheet index: {$sheetIndex} is out of bounds. The actual number of sheets is {$numSheets}."
1✔
700
            );
1✔
701
        }
702

703
        return $this->workSheetCollection[$sheetIndex];
11,073✔
704
    }
705

706
    /**
707
     * Get all sheets.
708
     *
709
     * @return Worksheet[]
710
     */
711
    public function getAllSheets(): array
187✔
712
    {
713
        return $this->workSheetCollection;
187✔
714
    }
715

716
    /**
717
     * Get sheet by name.
718
     *
719
     * @param string $worksheetName Sheet name
720
     */
721
    public function getSheetByName(string $worksheetName): ?Worksheet
9,919✔
722
    {
723
        $trimWorksheetName = StringHelper::strToUpper(trim($worksheetName, "'"));
9,919✔
724
        foreach ($this->workSheetCollection as $worksheet) {
9,919✔
725
            if (StringHelper::strToUpper($worksheet->getTitle()) === $trimWorksheetName) {
9,523✔
726
                return $worksheet;
8,927✔
727
            }
728
        }
729

730
        return null;
1,944✔
731
    }
732

733
    /**
734
     * Get sheet by name, throwing exception if not found.
735
     */
736
    public function getSheetByNameOrThrow(string $worksheetName): Worksheet
241✔
737
    {
738
        $worksheet = $this->getSheetByName($worksheetName);
241✔
739
        if ($worksheet === null) {
241✔
740
            throw new Exception("Sheet $worksheetName does not exist.");
1✔
741
        }
742

743
        return $worksheet;
240✔
744
    }
745

746
    /**
747
     * Get index for sheet.
748
     *
749
     * @return int index
750
     */
751
    public function getIndex(Worksheet $worksheet, bool $noThrow = false): int
11,153✔
752
    {
753
        foreach ($this->workSheetCollection as $key => $value) {
11,153✔
754
            if ($value === $worksheet) {
10,881✔
755
                return $key;
10,869✔
756
            }
757
        }
758
        if ($noThrow) {
11,153✔
759
            return -1;
11,153✔
760
        }
761

762
        throw new Exception('Sheet does not exist.');
3✔
763
    }
764

765
    /**
766
     * Set index for sheet by sheet name.
767
     *
768
     * @param string $worksheetName Sheet name to modify index for
769
     * @param int $newIndexPosition New index for the sheet
770
     *
771
     * @return int New sheet index
772
     */
773
    public function setIndexByName(string $worksheetName, int $newIndexPosition): int
1✔
774
    {
775
        $oldIndex = $this->getIndex($this->getSheetByNameOrThrow($worksheetName));
1✔
776
        $worksheet = array_splice(
1✔
777
            $this->workSheetCollection,
1✔
778
            $oldIndex,
1✔
779
            1
1✔
780
        );
1✔
781
        array_splice(
1✔
782
            $this->workSheetCollection,
1✔
783
            $newIndexPosition,
1✔
784
            0,
1✔
785
            $worksheet
1✔
786
        );
1✔
787

788
        return $newIndexPosition;
1✔
789
    }
790

791
    /**
792
     * Get sheet count.
793
     */
794
    public function getSheetCount(): int
1,710✔
795
    {
796
        return count($this->workSheetCollection);
1,710✔
797
    }
798

799
    /**
800
     * Get active sheet index.
801
     *
802
     * @return int Active sheet index
803
     */
804
    public function getActiveSheetIndex(): int
10,747✔
805
    {
806
        return $this->activeSheetIndex;
10,747✔
807
    }
808

809
    /**
810
     * Set active sheet index.
811
     *
812
     * @param int $worksheetIndex Active sheet index
813
     */
814
    public function setActiveSheetIndex(int $worksheetIndex): Worksheet
10,823✔
815
    {
816
        $numSheets = count($this->workSheetCollection);
10,823✔
817

818
        if ($worksheetIndex > $numSheets - 1) {
10,823✔
819
            throw new Exception(
11✔
820
                "You tried to set a sheet active by the out of bounds index: {$worksheetIndex}. The actual number of sheets is {$numSheets}."
11✔
821
            );
11✔
822
        }
823
        $this->activeSheetIndex = $worksheetIndex;
10,812✔
824

825
        return $this->getActiveSheet();
10,812✔
826
    }
827

828
    /**
829
     * Set active sheet index by name.
830
     *
831
     * @param string $worksheetName Sheet title
832
     */
833
    public function setActiveSheetIndexByName(string $worksheetName): Worksheet
108✔
834
    {
835
        if (($worksheet = $this->getSheetByName($worksheetName)) instanceof Worksheet) {
108✔
836
            $this->setActiveSheetIndex($this->getIndex($worksheet));
105✔
837

838
            return $worksheet;
105✔
839
        }
840

841
        throw new Exception('Workbook does not contain sheet:' . $worksheetName);
3✔
842
    }
843

844
    /**
845
     * Get sheet names.
846
     *
847
     * @return string[]
848
     */
849
    public function getSheetNames(): array
10✔
850
    {
851
        $returnValue = [];
10✔
852
        $worksheetCount = $this->getSheetCount();
10✔
853
        for ($i = 0; $i < $worksheetCount; ++$i) {
10✔
854
            $returnValue[] = $this->getSheet($i)->getTitle();
10✔
855
        }
856

857
        return $returnValue;
10✔
858
    }
859

860
    /**
861
     * Add external sheet.
862
     *
863
     * @param Worksheet $worksheet External sheet to add
864
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
865
     */
866
    public function addExternalSheet(Worksheet $worksheet, ?int $sheetIndex = null): Worksheet
5✔
867
    {
868
        if ($this->sheetNameExists($worksheet->getTitle())) {
5✔
869
            throw new Exception("Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename the external sheet first.");
1✔
870
        }
871

872
        // count how many cellXfs there are in this workbook currently, we will need this below
873
        $countCellXfs = count($this->cellXfCollection);
4✔
874

875
        // copy all the shared cellXfs from the external workbook and append them to the current
876
        foreach ($worksheet->getParentOrThrow()->getCellXfCollection() as $cellXf) {
4✔
877
            $this->addCellXf(clone $cellXf);
4✔
878
        }
879

880
        // move sheet to this workbook
881
        $worksheet->rebindParent($this);
4✔
882

883
        // update the cellXfs
884
        foreach ($worksheet->getCoordinates(false) as $coordinate) {
4✔
885
            $cell = $worksheet->getCell($coordinate);
4✔
886
            $cell->setXfIndex($cell->getXfIndex() + $countCellXfs);
4✔
887
        }
888

889
        // update the column dimensions Xfs
890
        foreach ($worksheet->getColumnDimensions() as $columnDimension) {
4✔
891
            $columnDimension->setXfIndex($columnDimension->getXfIndex() + $countCellXfs);
1✔
892
        }
893

894
        // update the row dimensions Xfs
895
        foreach ($worksheet->getRowDimensions() as $rowDimension) {
4✔
896
            $xfIndex = $rowDimension->getXfIndex();
1✔
897
            if ($xfIndex !== null) {
1✔
898
                $rowDimension->setXfIndex($xfIndex + $countCellXfs);
1✔
899
            }
900
        }
901

902
        return $this->addSheet($worksheet, $sheetIndex);
4✔
903
    }
904

905
    /**
906
     * Get an array of all Named Ranges.
907
     *
908
     * @return DefinedName[]
909
     */
910
    public function getNamedRanges(): array
9✔
911
    {
912
        return array_filter(
9✔
913
            $this->definedNames,
9✔
914
            fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_RANGE
9✔
915
        );
9✔
916
    }
917

918
    /**
919
     * Get an array of all Named Formulae.
920
     *
921
     * @return DefinedName[]
922
     */
923
    public function getNamedFormulae(): array
15✔
924
    {
925
        return array_filter(
15✔
926
            $this->definedNames,
15✔
927
            fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_FORMULA
15✔
928
        );
15✔
929
    }
930

931
    /**
932
     * Get an array of all Defined Names (both named ranges and named formulae).
933
     *
934
     * @return DefinedName[]
935
     */
936
    public function getDefinedNames(): array
680✔
937
    {
938
        return $this->definedNames;
680✔
939
    }
940

941
    /**
942
     * Add a named range.
943
     * If a named range with this name already exists, then this will replace the existing value.
944
     */
945
    public function addNamedRange(NamedRange $namedRange): void
323✔
946
    {
947
        $this->addDefinedName($namedRange);
323✔
948
    }
949

950
    /**
951
     * Add a named formula.
952
     * If a named formula with this name already exists, then this will replace the existing value.
953
     */
954
    public function addNamedFormula(NamedFormula $namedFormula): void
12✔
955
    {
956
        $this->addDefinedName($namedFormula);
12✔
957
    }
958

959
    /**
960
     * Add a defined name (either a named range or a named formula).
961
     * If a defined named with this name already exists, then this will replace the existing value.
962
     */
963
    public function addDefinedName(DefinedName $definedName): void
465✔
964
    {
965
        $upperCaseName = StringHelper::strToUpper($definedName->getName());
465✔
966
        if ($definedName->getScope() == null) {
465✔
967
            // global scope
968
            $this->definedNames[$upperCaseName] = $definedName;
451✔
969
        } else {
970
            // local scope
971
            $this->definedNames[$definedName->getScope()->getTitle() . '!' . $upperCaseName] = $definedName;
140✔
972
        }
973
    }
974

975
    /**
976
     * Get named range.
977
     *
978
     * @param null|Worksheet $worksheet Scope. Use null for global scope
979
     */
980
    public function getNamedRange(string $namedRange, ?Worksheet $worksheet = null): ?NamedRange
34✔
981
    {
982
        $returnValue = null;
34✔
983

984
        if ($namedRange !== '') {
34✔
985
            $namedRange = StringHelper::strToUpper($namedRange);
33✔
986
            // first look for global named range
987
            $returnValue = $this->getGlobalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE);
33✔
988
            // then look for local named range (has priority over global named range if both names exist)
989
            $returnValue = $this->getLocalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE, $worksheet) ?: $returnValue;
33✔
990
        }
991

992
        return $returnValue instanceof NamedRange ? $returnValue : null;
34✔
993
    }
994

995
    /**
996
     * Get named formula.
997
     *
998
     * @param null|Worksheet $worksheet Scope. Use null for global scope
999
     */
1000
    public function getNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): ?NamedFormula
11✔
1001
    {
1002
        $returnValue = null;
11✔
1003

1004
        if ($namedFormula !== '') {
11✔
1005
            $namedFormula = StringHelper::strToUpper($namedFormula);
11✔
1006
            // first look for global named formula
1007
            $returnValue = $this->getGlobalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA);
11✔
1008
            // then look for local named formula (has priority over global named formula if both names exist)
1009
            $returnValue = $this->getLocalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA, $worksheet) ?: $returnValue;
11✔
1010
        }
1011

1012
        return $returnValue instanceof NamedFormula ? $returnValue : null;
11✔
1013
    }
1014

1015
    private function getGlobalDefinedNameByType(string $name, bool $type): ?DefinedName
44✔
1016
    {
1017
        if (isset($this->definedNames[$name]) && $this->definedNames[$name]->isFormula() === $type) {
44✔
1018
            return $this->definedNames[$name];
30✔
1019
        }
1020

1021
        return null;
16✔
1022
    }
1023

1024
    private function getLocalDefinedNameByType(string $name, bool $type, ?Worksheet $worksheet = null): ?DefinedName
44✔
1025
    {
1026
        if (
1027
            ($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $name])
44✔
1028
            && $this->definedNames[$worksheet->getTitle() . '!' . $name]->isFormula() === $type
44✔
1029
        ) {
1030
            return $this->definedNames[$worksheet->getTitle() . '!' . $name];
8✔
1031
        }
1032

1033
        return null;
42✔
1034
    }
1035

1036
    /**
1037
     * Get named range.
1038
     *
1039
     * @param null|Worksheet $worksheet Scope. Use null for global scope
1040
     */
1041
    public function getDefinedName(string $definedName, ?Worksheet $worksheet = null): ?DefinedName
10,764✔
1042
    {
1043
        $returnValue = null;
10,764✔
1044

1045
        if ($definedName !== '') {
10,764✔
1046
            $definedName = StringHelper::strToUpper($definedName);
10,764✔
1047
            // first look for global defined name
1048
            foreach ($this->definedNames as $dn) {
10,764✔
1049
                $upper = StringHelper::strToUpper($dn->getName());
387✔
1050
                if (
1051
                    !$dn->getLocalOnly()
387✔
1052
                    && $definedName === $upper
387✔
1053
                ) {
1054
                    $returnValue = $dn;
144✔
1055

1056
                    break;
144✔
1057
                }
1058
            }
1059

1060
            // then look for local defined name (has priority over global defined name if both names exist)
1061
            if ($worksheet !== null) {
10,764✔
1062
                $wsTitle = StringHelper::strToUpper($worksheet->getTitle());
10,762✔
1063
                $definedName = (string) preg_replace('/^.*!/', '', $definedName);
10,762✔
1064
                foreach ($this->definedNames as $dn) {
10,762✔
1065
                    $sheet = $dn->getScope() ?? $dn->getWorksheet();
385✔
1066
                    $upper = StringHelper::strToUpper($dn->getName());
385✔
1067
                    $upperTitle = StringHelper::strToUpper((string) $sheet?->getTitle());
385✔
1068
                    if (
1069
                        $dn->getLocalOnly()
385✔
1070
                        && $upper === $definedName
385✔
1071
                        && $upperTitle === $wsTitle
385✔
1072
                    ) {
1073
                        return $dn;
28✔
1074
                    }
1075
                }
1076
            }
1077
        }
1078

1079
        return $returnValue;
10,763✔
1080
    }
1081

1082
    /**
1083
     * Remove named range.
1084
     *
1085
     * @param null|Worksheet $worksheet scope: use null for global scope
1086
     *
1087
     * @return $this
1088
     */
1089
    public function removeNamedRange(string $namedRange, ?Worksheet $worksheet = null): self
5✔
1090
    {
1091
        if ($this->getNamedRange($namedRange, $worksheet) === null) {
5✔
1092
            return $this;
1✔
1093
        }
1094

1095
        return $this->removeDefinedName($namedRange, $worksheet);
4✔
1096
    }
1097

1098
    /**
1099
     * Remove named formula.
1100
     *
1101
     * @param null|Worksheet $worksheet scope: use null for global scope
1102
     *
1103
     * @return $this
1104
     */
1105
    public function removeNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): self
4✔
1106
    {
1107
        if ($this->getNamedFormula($namedFormula, $worksheet) === null) {
4✔
1108
            return $this;
1✔
1109
        }
1110

1111
        return $this->removeDefinedName($namedFormula, $worksheet);
3✔
1112
    }
1113

1114
    /**
1115
     * Remove defined name.
1116
     *
1117
     * @param null|Worksheet $worksheet scope: use null for global scope
1118
     *
1119
     * @return $this
1120
     */
1121
    public function removeDefinedName(string $definedName, ?Worksheet $worksheet = null): self
11✔
1122
    {
1123
        $definedName = StringHelper::strToUpper($definedName);
11✔
1124

1125
        if ($worksheet === null) {
11✔
1126
            if (isset($this->definedNames[$definedName])) {
1✔
1127
                unset($this->definedNames[$definedName]);
1✔
1128
            }
1129
        } else {
1130
            if (isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) {
10✔
1131
                unset($this->definedNames[$worksheet->getTitle() . '!' . $definedName]);
3✔
1132
            } elseif (isset($this->definedNames[$definedName])) {
7✔
1133
                unset($this->definedNames[$definedName]);
7✔
1134
            }
1135
        }
1136

1137
        return $this;
11✔
1138
    }
1139

1140
    /**
1141
     * Get worksheet iterator.
1142
     */
1143
    public function getWorksheetIterator(): Iterator
1,498✔
1144
    {
1145
        return new Iterator($this);
1,498✔
1146
    }
1147

1148
    /**
1149
     * Copy workbook (!= clone!).
1150
     */
1151
    public function copy(): self
7✔
1152
    {
1153
        return unserialize(serialize($this)); //* @phpstan-ignore-line
7✔
1154
    }
1155

1156
    /**
1157
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
1158
     */
1159
    public function __clone()
7✔
1160
    {
1161
        $this->uniqueID = uniqid('', true);
7✔
1162

1163
        $usedKeys = [];
7✔
1164
        // I don't know why new Style rather than clone.
1165
        $this->cellXfSupervisor = new Style(true);
7✔
1166
        //$this->cellXfSupervisor = clone $this->cellXfSupervisor;
1167
        $this->cellXfSupervisor->bindParent($this);
7✔
1168
        $usedKeys['cellXfSupervisor'] = true;
7✔
1169

1170
        $oldCalc = $this->calculationEngine;
7✔
1171
        $this->calculationEngine = new Calculation($this);
7✔
1172
        $this->calculationEngine
7✔
1173
            ->setSuppressFormulaErrors(
7✔
1174
                $oldCalc->getSuppressFormulaErrors()
7✔
1175
            )
7✔
1176
            ->setCalculationCacheEnabled(
7✔
1177
                $oldCalc->getCalculationCacheEnabled()
7✔
1178
            )
7✔
1179
            ->setBranchPruningEnabled(
7✔
1180
                $oldCalc->getBranchPruningEnabled()
7✔
1181
            )
7✔
1182
            ->setInstanceArrayReturnType(
7✔
1183
                $oldCalc->getInstanceArrayReturnType()
7✔
1184
            );
7✔
1185
        $usedKeys['calculationEngine'] = true;
7✔
1186

1187
        $currentCollection = $this->cellStyleXfCollection;
7✔
1188
        $this->cellStyleXfCollection = [];
7✔
1189
        foreach ($currentCollection as $item) {
7✔
1190
            $clone = $item->exportArray();
7✔
1191
            $style = (new Style())->applyFromArray($clone);
7✔
1192
            $this->addCellStyleXf($style);
7✔
1193
        }
1194
        $usedKeys['cellStyleXfCollection'] = true;
7✔
1195

1196
        $currentCollection = $this->cellXfCollection;
7✔
1197
        $this->cellXfCollection = [];
7✔
1198
        foreach ($currentCollection as $item) {
7✔
1199
            $clone = $item->exportArray();
7✔
1200
            $style = (new Style())->applyFromArray($clone);
7✔
1201
            $this->addCellXf($style);
7✔
1202
        }
1203
        $usedKeys['cellXfCollection'] = true;
7✔
1204

1205
        $currentCollection = $this->workSheetCollection;
7✔
1206
        $this->workSheetCollection = [];
7✔
1207
        foreach ($currentCollection as $item) {
7✔
1208
            $clone = clone $item;
7✔
1209
            $clone->setParent($this);
7✔
1210
            $this->workSheetCollection[] = $clone;
7✔
1211
        }
1212
        $usedKeys['workSheetCollection'] = true;
7✔
1213

1214
        foreach (get_object_vars($this) as $key => $val) {
7✔
1215
            if (isset($usedKeys[$key])) {
7✔
1216
                continue;
7✔
1217
            }
1218
            switch ($key) {
1219
                // arrays of objects not covered above
1220
                case 'definedNames':
7✔
1221
                    /** @var DefinedName[] */
1222
                    $currentCollection = $val;
7✔
1223
                    $this->$key = [];
7✔
1224
                    foreach ($currentCollection as $item) {
7✔
1225
                        $clone = clone $item;
1✔
1226
                        $title = $clone->getWorksheet()?->getTitle();
1✔
1227
                        if ($title !== null) {
1✔
1228
                            $ws = $this->getSheetByName($title);
1✔
1229
                            $clone->setWorksheet($ws);
1✔
1230
                        }
1231
                        $title = $clone->getScope()?->getTitle();
1✔
1232
                        if ($title !== null) {
1✔
1233
                            $ws = $this->getSheetByName($title);
1✔
1234
                            $clone->setScope($ws);
1✔
1235
                        }
1236
                        $this->{$key}[] = $clone;
1✔
1237
                    }
1238

1239
                    break;
7✔
1240
                default:
1241
                    if (is_object($val)) {
7✔
1242
                        $this->$key = clone $val;
7✔
1243
                    }
1244
            }
1245
        }
1246
    }
1247

1248
    /**
1249
     * Get the workbook collection of cellXfs.
1250
     *
1251
     * @return Style[]
1252
     */
1253
    public function getCellXfCollection(): array
1,219✔
1254
    {
1255
        return $this->cellXfCollection;
1,219✔
1256
    }
1257

1258
    /**
1259
     * Get cellXf by index.
1260
     */
1261
    public function getCellXfByIndex(int $cellStyleIndex): Style
10,668✔
1262
    {
1263
        return $this->cellXfCollection[$cellStyleIndex];
10,668✔
1264
    }
1265

1266
    public function getCellXfByIndexOrNull(?int $cellStyleIndex): ?Style
2✔
1267
    {
1268
        return ($cellStyleIndex === null) ? null : ($this->cellXfCollection[$cellStyleIndex] ?? null);
2✔
1269
    }
1270

1271
    /**
1272
     * Get cellXf by hash code.
1273
     *
1274
     * @return false|Style
1275
     */
1276
    public function getCellXfByHashCode(string $hashcode): bool|Style
973✔
1277
    {
1278
        foreach ($this->cellXfCollection as $cellXf) {
973✔
1279
            if ($cellXf->getHashCode() === $hashcode) {
973✔
1280
                return $cellXf;
277✔
1281
            }
1282
        }
1283

1284
        return false;
908✔
1285
    }
1286

1287
    /**
1288
     * Check if style exists in style collection.
1289
     */
1290
    public function cellXfExists(Style $cellStyleIndex): bool
1✔
1291
    {
1292
        return in_array($cellStyleIndex, $this->cellXfCollection, true);
1✔
1293
    }
1294

1295
    /**
1296
     * Get default style.
1297
     */
1298
    public function getDefaultStyle(): Style
1,104✔
1299
    {
1300
        if (isset($this->cellXfCollection[0])) {
1,104✔
1301
            return $this->cellXfCollection[0];
1,103✔
1302
        }
1303

1304
        throw new Exception('No default style found for this workbook');
1✔
1305
    }
1306

1307
    /**
1308
     * Add a cellXf to the workbook.
1309
     */
1310
    public function addCellXf(Style $style): void
11,153✔
1311
    {
1312
        $this->cellXfCollection[] = $style;
11,153✔
1313
        $style->setIndex(count($this->cellXfCollection) - 1);
11,153✔
1314
    }
1315

1316
    /**
1317
     * Remove cellXf by index. It is ensured that all cells get their xf index updated.
1318
     *
1319
     * @param int $cellStyleIndex Index to cellXf
1320
     */
1321
    public function removeCellXfByIndex(int $cellStyleIndex): void
879✔
1322
    {
1323
        if ($cellStyleIndex > count($this->cellXfCollection) - 1) {
879✔
1324
            throw new Exception('CellXf index is out of bounds.');
1✔
1325
        }
1326

1327
        // first remove the cellXf
1328
        array_splice($this->cellXfCollection, $cellStyleIndex, 1);
878✔
1329

1330
        // then update cellXf indexes for cells
1331
        foreach ($this->workSheetCollection as $worksheet) {
878✔
1332
            foreach ($worksheet->getCoordinates(false) as $coordinate) {
2✔
1333
                $cell = $worksheet->getCell($coordinate);
1✔
1334
                $xfIndex = $cell->getXfIndex();
1✔
1335
                if ($xfIndex > $cellStyleIndex) {
1✔
1336
                    // decrease xf index by 1
1337
                    $cell->setXfIndex($xfIndex - 1);
1✔
1338
                } elseif ($xfIndex == $cellStyleIndex) {
1✔
1339
                    // set to default xf index 0
1340
                    $cell->setXfIndex(0);
1✔
1341
                }
1342
            }
1343
        }
1344
    }
1345

1346
    /**
1347
     * Get the cellXf supervisor.
1348
     */
1349
    public function getCellXfSupervisor(): Style
10,692✔
1350
    {
1351
        return $this->cellXfSupervisor;
10,692✔
1352
    }
1353

1354
    /**
1355
     * Get the workbook collection of cellStyleXfs.
1356
     *
1357
     * @return Style[]
1358
     */
1359
    public function getCellStyleXfCollection(): array
1✔
1360
    {
1361
        return $this->cellStyleXfCollection;
1✔
1362
    }
1363

1364
    /**
1365
     * Get cellStyleXf by index.
1366
     *
1367
     * @param int $cellStyleIndex Index to cellXf
1368
     */
1369
    public function getCellStyleXfByIndex(int $cellStyleIndex): Style
1✔
1370
    {
1371
        return $this->cellStyleXfCollection[$cellStyleIndex];
1✔
1372
    }
1373

1374
    /**
1375
     * Get cellStyleXf by hash code.
1376
     *
1377
     * @return false|Style
1378
     */
1379
    public function getCellStyleXfByHashCode(string $hashcode): bool|Style
1✔
1380
    {
1381
        foreach ($this->cellStyleXfCollection as $cellStyleXf) {
1✔
1382
            if ($cellStyleXf->getHashCode() === $hashcode) {
1✔
1383
                return $cellStyleXf;
1✔
1384
            }
1385
        }
1386

1387
        return false;
1✔
1388
    }
1389

1390
    /**
1391
     * Add a cellStyleXf to the workbook.
1392
     */
1393
    public function addCellStyleXf(Style $style): void
11,153✔
1394
    {
1395
        $this->cellStyleXfCollection[] = $style;
11,153✔
1396
        $style->setIndex(count($this->cellStyleXfCollection) - 1);
11,153✔
1397
    }
1398

1399
    /**
1400
     * Remove cellStyleXf by index.
1401
     *
1402
     * @param int $cellStyleIndex Index to cellXf
1403
     */
1404
    public function removeCellStyleXfByIndex(int $cellStyleIndex): void
876✔
1405
    {
1406
        if ($cellStyleIndex > count($this->cellStyleXfCollection) - 1) {
876✔
1407
            throw new Exception('CellStyleXf index is out of bounds.');
1✔
1408
        }
1409
        array_splice($this->cellStyleXfCollection, $cellStyleIndex, 1);
875✔
1410
    }
1411

1412
    /**
1413
     * Eliminate all unneeded cellXf and afterwards update the xfIndex for all cells
1414
     * and columns in the workbook.
1415
     */
1416
    public function garbageCollect(): void
1,104✔
1417
    {
1418
        // how many references are there to each cellXf ?
1419
        $countReferencesCellXf = [];
1,104✔
1420
        foreach ($this->cellXfCollection as $index => $cellXf) {
1,104✔
1421
            $countReferencesCellXf[$index] = 0;
1,104✔
1422
        }
1423

1424
        foreach ($this->getWorksheetIterator() as $sheet) {
1,104✔
1425
            // from cells
1426
            foreach ($sheet->getCoordinates(false) as $coordinate) {
1,104✔
1427
                $cell = $sheet->getCell($coordinate);
1,073✔
1428
                ++$countReferencesCellXf[$cell->getXfIndex()];
1,073✔
1429
            }
1430

1431
            // from row dimensions
1432
            foreach ($sheet->getRowDimensions() as $rowDimension) {
1,104✔
1433
                if ($rowDimension->getXfIndex() !== null) {
109✔
1434
                    ++$countReferencesCellXf[$rowDimension->getXfIndex()];
11✔
1435
                }
1436
            }
1437

1438
            // from column dimensions
1439
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
1,104✔
1440
                ++$countReferencesCellXf[$columnDimension->getXfIndex()];
163✔
1441
            }
1442
        }
1443

1444
        // remove cellXfs without references and create mapping so we can update xfIndex
1445
        // for all cells and columns
1446
        $countNeededCellXfs = 0;
1,104✔
1447
        $map = [];
1,104✔
1448
        foreach ($this->cellXfCollection as $index => $cellXf) {
1,104✔
1449
            if ($countReferencesCellXf[$index] > 0 || $index == 0) { // we must never remove the first cellXf
1,104✔
1450
                ++$countNeededCellXfs;
1,104✔
1451
            } else {
1452
                unset($this->cellXfCollection[$index]);
43✔
1453
            }
1454
            $map[$index] = $countNeededCellXfs - 1;
1,104✔
1455
        }
1456
        $this->cellXfCollection = array_values($this->cellXfCollection);
1,104✔
1457

1458
        // update the index for all cellXfs
1459
        foreach ($this->cellXfCollection as $i => $cellXf) {
1,104✔
1460
            $cellXf->setIndex($i);
1,104✔
1461
        }
1462

1463
        // make sure there is always at least one cellXf (there should be)
1464
        if (empty($this->cellXfCollection)) {
1,104✔
UNCOV
1465
            $this->cellXfCollection[] = new Style();
×
1466
        }
1467

1468
        // update the xfIndex for all cells, row dimensions, column dimensions
1469
        foreach ($this->getWorksheetIterator() as $sheet) {
1,104✔
1470
            // for all cells
1471
            foreach ($sheet->getCoordinates(false) as $coordinate) {
1,104✔
1472
                $cell = $sheet->getCell($coordinate);
1,073✔
1473
                $cell->setXfIndex($map[$cell->getXfIndex()]);
1,073✔
1474
            }
1475

1476
            // for all row dimensions
1477
            foreach ($sheet->getRowDimensions() as $rowDimension) {
1,104✔
1478
                if ($rowDimension->getXfIndex() !== null) {
109✔
1479
                    $rowDimension->setXfIndex($map[$rowDimension->getXfIndex()]);
11✔
1480
                }
1481
            }
1482

1483
            // for all column dimensions
1484
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
1,104✔
1485
                $columnDimension->setXfIndex($map[$columnDimension->getXfIndex()]);
163✔
1486
            }
1487

1488
            // also do garbage collection for all the sheets
1489
            $sheet->garbageCollect();
1,104✔
1490
        }
1491
    }
1492

1493
    /**
1494
     * Return the unique ID value assigned to this spreadsheet workbook.
1495
     *
1496
     * @deprecated 5.2.0 Serves no useful purpose. No replacement.
1497
     *
1498
     * @codeCoverageIgnore
1499
     */
1500
    public function getID(): string
1501
    {
1502
        return $this->uniqueID;
1503
    }
1504

1505
    /**
1506
     * Get the visibility of the horizonal scroll bar in the application.
1507
     *
1508
     * @return bool True if horizonal scroll bar is visible
1509
     */
1510
    public function getShowHorizontalScroll(): bool
436✔
1511
    {
1512
        return $this->showHorizontalScroll;
436✔
1513
    }
1514

1515
    /**
1516
     * Set the visibility of the horizonal scroll bar in the application.
1517
     *
1518
     * @param bool $showHorizontalScroll True if horizonal scroll bar is visible
1519
     */
1520
    public function setShowHorizontalScroll(bool $showHorizontalScroll): void
311✔
1521
    {
1522
        $this->showHorizontalScroll = (bool) $showHorizontalScroll;
311✔
1523
    }
1524

1525
    /**
1526
     * Get the visibility of the vertical scroll bar in the application.
1527
     *
1528
     * @return bool True if vertical scroll bar is visible
1529
     */
1530
    public function getShowVerticalScroll(): bool
436✔
1531
    {
1532
        return $this->showVerticalScroll;
436✔
1533
    }
1534

1535
    /**
1536
     * Set the visibility of the vertical scroll bar in the application.
1537
     *
1538
     * @param bool $showVerticalScroll True if vertical scroll bar is visible
1539
     */
1540
    public function setShowVerticalScroll(bool $showVerticalScroll): void
311✔
1541
    {
1542
        $this->showVerticalScroll = (bool) $showVerticalScroll;
311✔
1543
    }
1544

1545
    /**
1546
     * Get the visibility of the sheet tabs in the application.
1547
     *
1548
     * @return bool True if the sheet tabs are visible
1549
     */
1550
    public function getShowSheetTabs(): bool
436✔
1551
    {
1552
        return $this->showSheetTabs;
436✔
1553
    }
1554

1555
    /**
1556
     * Set the visibility of the sheet tabs  in the application.
1557
     *
1558
     * @param bool $showSheetTabs True if sheet tabs are visible
1559
     */
1560
    public function setShowSheetTabs(bool $showSheetTabs): void
311✔
1561
    {
1562
        $this->showSheetTabs = (bool) $showSheetTabs;
311✔
1563
    }
1564

1565
    /**
1566
     * Return whether the workbook window is minimized.
1567
     *
1568
     * @return bool true if workbook window is minimized
1569
     */
1570
    public function getMinimized(): bool
436✔
1571
    {
1572
        return $this->minimized;
436✔
1573
    }
1574

1575
    /**
1576
     * Set whether the workbook window is minimized.
1577
     *
1578
     * @param bool $minimized true if workbook window is minimized
1579
     */
1580
    public function setMinimized(bool $minimized): void
294✔
1581
    {
1582
        $this->minimized = (bool) $minimized;
294✔
1583
    }
1584

1585
    /**
1586
     * Return whether to group dates when presenting the user with
1587
     * filtering options in the user interface.
1588
     *
1589
     * @return bool true if workbook window is minimized
1590
     */
1591
    public function getAutoFilterDateGrouping(): bool
436✔
1592
    {
1593
        return $this->autoFilterDateGrouping;
436✔
1594
    }
1595

1596
    /**
1597
     * Set whether to group dates when presenting the user with
1598
     * filtering options in the user interface.
1599
     *
1600
     * @param bool $autoFilterDateGrouping true if workbook window is minimized
1601
     */
1602
    public function setAutoFilterDateGrouping(bool $autoFilterDateGrouping): void
294✔
1603
    {
1604
        $this->autoFilterDateGrouping = (bool) $autoFilterDateGrouping;
294✔
1605
    }
1606

1607
    /**
1608
     * Return the first sheet in the book view.
1609
     *
1610
     * @return int First sheet in book view
1611
     */
1612
    public function getFirstSheetIndex(): int
436✔
1613
    {
1614
        return $this->firstSheetIndex;
436✔
1615
    }
1616

1617
    /**
1618
     * Set the first sheet in the book view.
1619
     *
1620
     * @param int $firstSheetIndex First sheet in book view
1621
     */
1622
    public function setFirstSheetIndex(int $firstSheetIndex): void
312✔
1623
    {
1624
        if ($firstSheetIndex >= 0) {
312✔
1625
            $this->firstSheetIndex = (int) $firstSheetIndex;
311✔
1626
        } else {
1627
            throw new Exception('First sheet index must be a positive integer.');
1✔
1628
        }
1629
    }
1630

1631
    /**
1632
     * Return the visibility status of the workbook.
1633
     *
1634
     * This may be one of the following three values:
1635
     * - visibile
1636
     *
1637
     * @return string Visible status
1638
     */
1639
    public function getVisibility(): string
437✔
1640
    {
1641
        return $this->visibility;
437✔
1642
    }
1643

1644
    /**
1645
     * Set the visibility status of the workbook.
1646
     *
1647
     * Valid values are:
1648
     *  - 'visible' (self::VISIBILITY_VISIBLE):
1649
     *       Workbook window is visible
1650
     *  - 'hidden' (self::VISIBILITY_HIDDEN):
1651
     *       Workbook window is hidden, but can be shown by the user
1652
     *       via the user interface
1653
     *  - 'veryHidden' (self::VISIBILITY_VERY_HIDDEN):
1654
     *       Workbook window is hidden and cannot be shown in the
1655
     *       user interface.
1656
     *
1657
     * @param null|string $visibility visibility status of the workbook
1658
     */
1659
    public function setVisibility(?string $visibility): void
295✔
1660
    {
1661
        if ($visibility === null) {
295✔
1662
            $visibility = self::VISIBILITY_VISIBLE;
1✔
1663
        }
1664

1665
        if (in_array($visibility, self::WORKBOOK_VIEW_VISIBILITY_VALUES)) {
295✔
1666
            $this->visibility = $visibility;
295✔
1667
        } else {
1668
            throw new Exception('Invalid visibility value.');
1✔
1669
        }
1670
    }
1671

1672
    /**
1673
     * Get the ratio between the workbook tabs bar and the horizontal scroll bar.
1674
     * TabRatio is assumed to be out of 1000 of the horizontal window width.
1675
     *
1676
     * @return int Ratio between the workbook tabs bar and the horizontal scroll bar
1677
     */
1678
    public function getTabRatio(): int
436✔
1679
    {
1680
        return $this->tabRatio;
436✔
1681
    }
1682

1683
    /**
1684
     * Set the ratio between the workbook tabs bar and the horizontal scroll bar
1685
     * TabRatio is assumed to be out of 1000 of the horizontal window width.
1686
     *
1687
     * @param int $tabRatio Ratio between the tabs bar and the horizontal scroll bar
1688
     */
1689
    public function setTabRatio(int $tabRatio): void
316✔
1690
    {
1691
        if ($tabRatio >= 0 && $tabRatio <= 1000) {
316✔
1692
            $this->tabRatio = (int) $tabRatio;
315✔
1693
        } else {
1694
            throw new Exception('Tab ratio must be between 0 and 1000.');
1✔
1695
        }
1696
    }
1697

1698
    public function reevaluateAutoFilters(bool $resetToMax): void
2✔
1699
    {
1700
        foreach ($this->workSheetCollection as $sheet) {
2✔
1701
            $filter = $sheet->getAutoFilter();
2✔
1702
            if (!empty($filter->getRange())) {
2✔
1703
                if ($resetToMax) {
2✔
1704
                    $filter->setRangeToMaxRow();
1✔
1705
                }
1706
                $filter->showHideRows();
2✔
1707
            }
1708
        }
1709
    }
1710

1711
    /**
1712
     * @throws Exception
1713
     */
1714
    public function jsonSerialize(): mixed
1✔
1715
    {
1716
        throw new Exception('Spreadsheet objects cannot be json encoded');
1✔
1717
    }
1718

1719
    public function resetThemeFonts(): void
1✔
1720
    {
1721
        $majorFontLatin = $this->theme->getMajorFontLatin();
1✔
1722
        $minorFontLatin = $this->theme->getMinorFontLatin();
1✔
1723
        foreach ($this->cellXfCollection as $cellStyleXf) {
1✔
1724
            $scheme = $cellStyleXf->getFont()->getScheme();
1✔
1725
            if ($scheme === 'major') {
1✔
1726
                $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
1✔
1727
            } elseif ($scheme === 'minor') {
1✔
1728
                $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
1✔
1729
            }
1730
        }
1731
        foreach ($this->cellStyleXfCollection as $cellStyleXf) {
1✔
1732
            $scheme = $cellStyleXf->getFont()->getScheme();
1✔
1733
            if ($scheme === 'major') {
1✔
UNCOV
1734
                $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
×
1735
            } elseif ($scheme === 'minor') {
1✔
1736
                $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
1✔
1737
            }
1738
        }
1739
    }
1740

1741
    public function getTableByName(string $tableName): ?Table
41✔
1742
    {
1743
        $table = null;
41✔
1744
        foreach ($this->workSheetCollection as $sheet) {
41✔
1745
            $table = $sheet->getTableByName($tableName);
41✔
1746
            if ($table !== null) {
41✔
1747
                break;
5✔
1748
            }
1749
        }
1750

1751
        return $table;
41✔
1752
    }
1753

1754
    /**
1755
     * @return bool Success or failure
1756
     */
1757
    public function setExcelCalendar(int $baseYear): bool
872✔
1758
    {
1759
        if (($baseYear === Date::CALENDAR_WINDOWS_1900) || ($baseYear === Date::CALENDAR_MAC_1904)) {
872✔
1760
            $this->excelCalendar = $baseYear;
872✔
1761

1762
            return true;
872✔
1763
        }
1764

1765
        return false;
1✔
1766
    }
1767

1768
    /**
1769
     * @return int Excel base date (1900 or 1904)
1770
     */
1771
    public function getExcelCalendar(): int
8,875✔
1772
    {
1773
        return $this->excelCalendar;
8,875✔
1774
    }
1775

1776
    public function deleteLegacyDrawing(Worksheet $worksheet): void
2✔
1777
    {
1778
        unset($this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing']);
2✔
1779
    }
1780

1781
    public function getLegacyDrawing(Worksheet $worksheet): ?string
3✔
1782
    {
1783
        /** @var ?string */
1784
        $temp = $this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'] ?? null;
3✔
1785

1786
        return $temp;
3✔
1787
    }
1788

1789
    public function getValueBinder(): ?IValueBinder
10,515✔
1790
    {
1791
        return $this->valueBinder;
10,515✔
1792
    }
1793

1794
    public function setValueBinder(?IValueBinder $valueBinder): self
1,673✔
1795
    {
1796
        $this->valueBinder = $valueBinder;
1,673✔
1797

1798
        return $this;
1,673✔
1799
    }
1800

1801
    /**
1802
     * All the PDF writers treat charts as if they occupy a single cell.
1803
     * This will be better most of the time.
1804
     * It is not needed for any other output type.
1805
     * It changes the contents of the spreadsheet, so you might
1806
     * be better off cloning the spreadsheet and then using
1807
     * this method on, and then writing, the clone.
1808
     */
1809
    public function mergeChartCellsForPdf(): void
1✔
1810
    {
1811
        foreach ($this->workSheetCollection as $worksheet) {
1✔
1812
            foreach ($worksheet->getChartCollection() as $chart) {
1✔
1813
                $br = $chart->getBottomRightCell();
1✔
1814
                $tl = $chart->getTopLeftCell();
1✔
1815
                if ($br !== '' && $br !== $tl) {
1✔
1816
                    if (!$worksheet->cellExists($br)) {
1✔
1817
                        $worksheet->getCell($br)->setValue(' ');
1✔
1818
                    }
1819
                    $worksheet->mergeCells("$tl:$br");
1✔
1820
                }
1821
            }
1822
        }
1823
    }
1824

1825
    /**
1826
     * All the PDF writers do better with drawings than charts.
1827
     * This will be better some of the time.
1828
     * It is not needed for any other output type.
1829
     * It changes the contents of the spreadsheet, so you might
1830
     * be better off cloning the spreadsheet and then using
1831
     * this method on, and then writing, the clone.
1832
     */
1833
    public function mergeDrawingCellsForPdf(): void
1✔
1834
    {
1835
        foreach ($this->workSheetCollection as $worksheet) {
1✔
1836
            foreach ($worksheet->getDrawingCollection() as $drawing) {
1✔
1837
                $br = $drawing->getCoordinates2();
1✔
1838
                $tl = $drawing->getCoordinates();
1✔
1839
                if ($br !== '' && $br !== $tl) {
1✔
1840
                    if (!$worksheet->cellExists($br)) {
1✔
1841
                        $worksheet->getCell($br)->setValue(' ');
1✔
1842
                    }
1843
                    $worksheet->mergeCells("$tl:$br");
1✔
1844
                }
1845
            }
1846
        }
1847
    }
1848

1849
    /**
1850
     * Excel will sometimes replace user's formatting choice
1851
     * with a built-in choice that it thinks is equivalent.
1852
     * Its choice is often not equivalent after all.
1853
     * Such treatment is astonishingly user-hostile.
1854
     * This function will undo such changes.
1855
     */
1856
    public function replaceBuiltinNumberFormat(int $builtinFormatIndex, string $formatCode): void
1✔
1857
    {
1858
        foreach ($this->cellXfCollection as $style) {
1✔
1859
            $numberFormat = $style->getNumberFormat();
1✔
1860
            if ($numberFormat->getBuiltInFormatCode() === $builtinFormatIndex) {
1✔
1861
                $numberFormat->setFormatCode($formatCode);
1✔
1862
            }
1863
        }
1864
    }
1865

1866
    public function returnArrayAsArray(): void
82✔
1867
    {
1868
        $this->calculationEngine->setInstanceArrayReturnType(
82✔
1869
            Calculation::RETURN_ARRAY_AS_ARRAY
82✔
1870
        );
82✔
1871
    }
1872

1873
    public function returnArrayAsValue(): void
2✔
1874
    {
1875
        $this->calculationEngine->setInstanceArrayReturnType(
2✔
1876
            Calculation::RETURN_ARRAY_AS_VALUE
2✔
1877
        );
2✔
1878
    }
1879

1880
    /** @var string[] */
1881
    private $domainWhiteList = [];
1882

1883
    /**
1884
     * Currently used only by WEBSERVICE function.
1885
     *
1886
     * @param string[] $domainWhiteList
1887
     */
1888
    public function setDomainWhiteList(array $domainWhiteList): self
9✔
1889
    {
1890
        $this->domainWhiteList = $domainWhiteList;
9✔
1891

1892
        return $this;
9✔
1893
    }
1894

1895
    /** @return string[] */
1896
    public function getDomainWhiteList(): array
6✔
1897
    {
1898
        return $this->domainWhiteList;
6✔
1899
    }
1900
}
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