Passed
Pull Request — master (#4326)
by Owen
14:19
created

Spreadsheet::__serialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 0
crap 1
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\StringHelper;
12
use PhpOffice\PhpSpreadsheet\Style\Style;
13
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
14
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
15
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
16
17
class Spreadsheet implements JsonSerializable
18
{
19
    // Allowable values for workbook window visilbity
20
    const VISIBILITY_VISIBLE = 'visible';
21
    const VISIBILITY_HIDDEN = 'hidden';
22
    const VISIBILITY_VERY_HIDDEN = 'veryHidden';
23
24
    private const DEFINED_NAME_IS_RANGE = false;
25
    private const DEFINED_NAME_IS_FORMULA = true;
26
27
    private const WORKBOOK_VIEW_VISIBILITY_VALUES = [
28
        self::VISIBILITY_VISIBLE,
29
        self::VISIBILITY_HIDDEN,
30
        self::VISIBILITY_VERY_HIDDEN,
31
    ];
32
33
    protected int $excelCalendar = Date::CALENDAR_WINDOWS_1900;
34
35
    /**
36
     * Unique ID.
37
     */
38
    private string $uniqueID;
39
40
    /**
41
     * Document properties.
42
     */
43
    private Properties $properties;
44
45
    /**
46
     * Document security.
47
     */
48
    private Security $security;
49
50
    /**
51
     * Collection of Worksheet objects.
52
     *
53
     * @var Worksheet[]
54
     */
55
    private array $workSheetCollection;
56
57
    /**
58
     * Calculation Engine.
59
     */
60
    private ?Calculation $calculationEngine;
61
62
    /**
63
     * Active sheet index.
64
     */
65
    private int $activeSheetIndex;
66
67
    /**
68
     * Named ranges.
69
     *
70
     * @var DefinedName[]
71
     */
72
    private array $definedNames;
73
74
    /**
75
     * CellXf supervisor.
76
     */
77
    private Style $cellXfSupervisor;
78
79
    /**
80
     * CellXf collection.
81
     *
82
     * @var Style[]
83
     */
84
    private array $cellXfCollection = [];
85
86
    /**
87
     * CellStyleXf collection.
88
     *
89
     * @var Style[]
90
     */
91
    private array $cellStyleXfCollection = [];
92
93
    /**
94
     * hasMacros : this workbook have macros ?
95
     */
96
    private bool $hasMacros = false;
97
98
    /**
99
     * macrosCode : all macros code as binary data (the vbaProject.bin file, this include form, code,  etc.), null if no macro.
100
     */
101
    private ?string $macrosCode = null;
102
103
    /**
104
     * macrosCertificate : if macros are signed, contains binary data vbaProjectSignature.bin file, null if not signed.
105
     */
106
    private ?string $macrosCertificate = null;
107
108
    /**
109
     * ribbonXMLData : null if workbook is'nt Excel 2007 or not contain a customized UI.
110
     *
111
     * @var null|array{target: string, data: string}
112
     */
113
    private ?array $ribbonXMLData = null;
114
115
    /**
116
     * ribbonBinObjects : null if workbook is'nt Excel 2007 or not contain embedded objects (picture(s)) for Ribbon Elements
117
     * ignored if $ribbonXMLData is null.
118
     */
119
    private ?array $ribbonBinObjects = null;
120
121
    /**
122
     * List of unparsed loaded data for export to same format with better compatibility.
123
     * It has to be minimized when the library start to support currently unparsed data.
124
     */
125
    private array $unparsedLoadedData = [];
126
127
    /**
128
     * Controls visibility of the horizonal scroll bar in the application.
129
     */
130
    private bool $showHorizontalScroll = true;
131
132
    /**
133
     * Controls visibility of the horizonal scroll bar in the application.
134
     */
135
    private bool $showVerticalScroll = true;
136
137
    /**
138
     * Controls visibility of the sheet tabs in the application.
139
     */
140
    private bool $showSheetTabs = true;
141
142
    /**
143
     * Specifies a boolean value that indicates whether the workbook window
144
     * is minimized.
145
     */
146
    private bool $minimized = false;
147
148
    /**
149
     * Specifies a boolean value that indicates whether to group dates
150
     * when presenting the user with filtering optiomd in the user
151
     * interface.
152
     */
153
    private bool $autoFilterDateGrouping = true;
154
155
    /**
156
     * Specifies the index to the first sheet in the book view.
157
     */
158
    private int $firstSheetIndex = 0;
159
160
    /**
161
     * Specifies the visible status of the workbook.
162
     */
163
    private string $visibility = self::VISIBILITY_VISIBLE;
164
165
    /**
166
     * Specifies the ratio between the workbook tabs bar and the horizontal
167
     * scroll bar.  TabRatio is assumed to be out of 1000 of the horizontal
168
     * window width.
169
     */
170
    private int $tabRatio = 600;
171
172
    private Theme $theme;
173
174
    private ?IValueBinder $valueBinder = null;
175
176
    public function getTheme(): Theme
177
    {
178
        return $this->theme;
179 751
    }
180
181 751
    /**
182
     * The workbook has macros ?
183
     */
184
    public function hasMacros(): bool
185
    {
186
        return $this->hasMacros;
187 410
    }
188
189 410
    /**
190
     * Define if a workbook has macros.
191
     *
192
     * @param bool $hasMacros true|false
193
     */
194
    public function setHasMacros(bool $hasMacros): void
195
    {
196
        $this->hasMacros = (bool) $hasMacros;
197 3
    }
198
199 3
    /**
200
     * Set the macros code.
201
     *
202
     * @param string $macroCode string|null
203
     */
204
    public function setMacrosCode(string $macroCode): void
205
    {
206
        $this->macrosCode = $macroCode;
207 3
        $this->setHasMacros($macroCode !== null);
208
    }
209 3
210 3
    /**
211
     * Return the macros code.
212
     */
213
    public function getMacrosCode(): ?string
214
    {
215
        return $this->macrosCode;
216 3
    }
217
218 3
    /**
219
     * Set the macros certificate.
220
     */
221
    public function setMacrosCertificate(?string $certificate): void
222
    {
223
        $this->macrosCertificate = $certificate;
224 3
    }
225
226 3
    /**
227
     * Is the project signed ?
228
     *
229
     * @return bool true|false
230
     */
231
    public function hasMacrosCertificate(): bool
232
    {
233
        return $this->macrosCertificate !== null;
234 2
    }
235
236 2
    /**
237
     * Return the macros certificate.
238
     */
239
    public function getMacrosCertificate(): ?string
240
    {
241
        return $this->macrosCertificate;
242 2
    }
243
244 2
    /**
245
     * Remove all macros, certificate from spreadsheet.
246
     */
247
    public function discardMacros(): void
248
    {
249
        $this->hasMacros = false;
250 1
        $this->macrosCode = null;
251
        $this->macrosCertificate = null;
252 1
    }
253 1
254 1
    /**
255
     * set ribbon XML data.
256
     */
257
    public function setRibbonXMLData(mixed $target, mixed $xmlData): void
258
    {
259
        if (is_string($target) && is_string($xmlData)) {
260 2
            $this->ribbonXMLData = ['target' => $target, 'data' => $xmlData];
261
        } else {
262 2
            $this->ribbonXMLData = null;
263 2
        }
264
    }
265
266
    /**
267
     * retrieve ribbon XML Data.
268
     */
269
    public function getRibbonXMLData(string $what = 'all'): null|array|string //we need some constants here...
270
    {
271
        $returnData = null;
272 371
        $what = strtolower($what);
273
        switch ($what) {
274 371
            case 'all':
275 371
                $returnData = $this->ribbonXMLData;
276
277 371
                break;
278 2
            case 'target':
279
            case 'data':
280 2
                if (is_array($this->ribbonXMLData)) {
281 371
                    $returnData = $this->ribbonXMLData[$what];
282 2
                }
283 371
284 2
                break;
285
        }
286
287 371
        return $returnData;
288
    }
289
290 371
    /**
291
     * store binaries ribbon objects (pictures).
292
     */
293
    public function setRibbonBinObjects(mixed $binObjectsNames, mixed $binObjectsData): void
294
    {
295
        if ($binObjectsNames !== null && $binObjectsData !== null) {
296 2
            $this->ribbonBinObjects = ['names' => $binObjectsNames, 'data' => $binObjectsData];
297
        } else {
298 2
            $this->ribbonBinObjects = null;
299
        }
300
    }
301 2
302
    /**
303
     * List of unparsed loaded data for export to same format with better compatibility.
304
     * It has to be minimized when the library start to support currently unparsed data.
305
     *
306
     * @internal
307
     */
308
    public function getUnparsedLoadedData(): array
309
    {
310
        return $this->unparsedLoadedData;
311 411
    }
312
313 411
    /**
314
     * List of unparsed loaded data for export to same format with better compatibility.
315
     * It has to be minimized when the library start to support currently unparsed data.
316
     *
317
     * @internal
318
     */
319
    public function setUnparsedLoadedData(array $unparsedLoadedData): void
320
    {
321
        $this->unparsedLoadedData = $unparsedLoadedData;
322 644
    }
323
324 644
    /**
325
     * retrieve Binaries Ribbon Objects.
326
     */
327
    public function getRibbonBinObjects(string $what = 'all'): ?array
328
    {
329
        $ReturnData = null;
330 2
        $what = strtolower($what);
331
        switch ($what) {
332 2
            case 'all':
333 2
                return $this->ribbonBinObjects;
334
            case 'names':
335 2
            case 'data':
336 2
                if (is_array($this->ribbonBinObjects) && isset($this->ribbonBinObjects[$what])) {
337 1
                    $ReturnData = $this->ribbonBinObjects[$what];
338 1
                }
339 1
340
                break;
341
            case 'types':
342
                if (
343 1
                    is_array($this->ribbonBinObjects)
344 1
                    && isset($this->ribbonBinObjects['data']) && is_array($this->ribbonBinObjects['data'])
345
                ) {
346 1
                    $tmpTypes = array_keys($this->ribbonBinObjects['data']);
347 1
                    $ReturnData = array_unique(array_map(fn (string $path): string => pathinfo($path, PATHINFO_EXTENSION), $tmpTypes));
348
                } else {
349
                    $ReturnData = []; // the caller want an array... not null if empty
350
                }
351
352 1
                break;
353
        }
354
355 1
        return $ReturnData;
356
    }
357
358 1
    /**
359
     * This workbook have a custom UI ?
360
     */
361
    public function hasRibbon(): bool
362
    {
363
        return $this->ribbonXMLData !== null;
364 371
    }
365
366 371
    /**
367
     * This workbook have additionnal object for the ribbon ?
368
     */
369
    public function hasRibbonBinObjects(): bool
370
    {
371
        return $this->ribbonBinObjects !== null;
372 371
    }
373
374 371
    /**
375
     * Check if a sheet with a specified code name already exists.
376
     *
377
     * @param string $codeName Name of the worksheet to check
378
     */
379
    public function sheetCodeNameExists(string $codeName): bool
380
    {
381
        return $this->getSheetByCodeName($codeName) !== null;
382 10411
    }
383
384 10411
    /**
385
     * Get sheet by code name. Warning : sheet don't have always a code name !
386
     *
387
     * @param string $codeName Sheet name
388
     */
389
    public function getSheetByCodeName(string $codeName): ?Worksheet
390
    {
391
        $worksheetCount = count($this->workSheetCollection);
392 10411
        for ($i = 0; $i < $worksheetCount; ++$i) {
393
            if ($this->workSheetCollection[$i]->getCodeName() == $codeName) {
394 10411
                return $this->workSheetCollection[$i];
395 10411
            }
396 708
        }
397 674
398
        return null;
399
    }
400
401 10411
    /**
402
     * Create a new PhpSpreadsheet with one Worksheet.
403
     */
404
    public function __construct()
405
    {
406
        $this->uniqueID = uniqid('', true);
407 10411
        $this->calculationEngine = new Calculation($this);
408
        $this->theme = new Theme();
409 10411
410 10411
        // Initialise worksheet collection and add one worksheet
411 10411
        $this->workSheetCollection = [];
412
        $this->workSheetCollection[] = new Worksheet($this);
413
        $this->activeSheetIndex = 0;
414 10411
415 10411
        // Create document properties
416 10411
        $this->properties = new Properties();
417
418
        // Create document security
419 10411
        $this->security = new Security();
420
421
        // Set defined names
422 10411
        $this->definedNames = [];
423
424
        // Create the cellXf supervisor
425 10411
        $this->cellXfSupervisor = new Style(true);
426
        $this->cellXfSupervisor->bindParent($this);
427
428 10411
        // Create the default style
429 10411
        $this->addCellXf(new Style());
430
        $this->addCellStyleXf(new Style());
431
    }
432 10411
433 10411
    /**
434
     * Code to execute when this worksheet is unset().
435
     */
436
    public function __destruct()
437
    {
438
        $this->disconnectWorksheets();
439 117
        $this->calculationEngine = null;
440
        $this->cellXfCollection = [];
441 117
        $this->cellStyleXfCollection = [];
442 117
        $this->definedNames = [];
443 117
    }
444 117
445 117
    /**
446
     * Disconnect all worksheets from this PhpSpreadsheet workbook object,
447
     * typically so that the PhpSpreadsheet object can be unset.
448
     */
449
    public function disconnectWorksheets(): void
450
    {
451
        foreach ($this->workSheetCollection as $worksheet) {
452 9074
            $worksheet->disconnectCells();
453
            unset($worksheet);
454 9074
        }
455 9073
        $this->workSheetCollection = [];
456 9073
    }
457
458 9074
    /**
459
     * Return the calculation engine for this worksheet.
460
     */
461
    public function getCalculationEngine(): ?Calculation
462
    {
463
        return $this->calculationEngine;
464 9513
    }
465
466 9513
    /**
467
     * Get properties.
468
     */
469
    public function getProperties(): Properties
470
    {
471
        return $this->properties;
472 1567
    }
473
474 1567
    /**
475
     * Set properties.
476
     */
477
    public function setProperties(Properties $documentProperties): void
478
    {
479
        $this->properties = $documentProperties;
480 1
    }
481
482 1
    /**
483
     * Get security.
484
     */
485
    public function getSecurity(): Security
486
    {
487
        return $this->security;
488 384
    }
489
490 384
    /**
491
     * Set security.
492
     */
493
    public function setSecurity(Security $documentSecurity): void
494
    {
495
        $this->security = $documentSecurity;
496 1
    }
497
498 1
    /**
499
     * Get active sheet.
500
     */
501
    public function getActiveSheet(): Worksheet
502
    {
503
        return $this->getSheet($this->activeSheetIndex);
504 10344
    }
505
506 10344
    /**
507
     * Create sheet and add it to this workbook.
508
     *
509
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
510
     */
511
    public function createSheet(?int $sheetIndex = null): Worksheet
512
    {
513
        $newSheet = new Worksheet($this);
514 1191
        $this->addSheet($newSheet, $sheetIndex, true);
515
516 1191
        return $newSheet;
517 1191
    }
518
519 1191
    /**
520
     * Check if a sheet with a specified name already exists.
521
     *
522
     * @param string $worksheetName Name of the worksheet to check
523
     */
524
    public function sheetNameExists(string $worksheetName): bool
525
    {
526
        return $this->getSheetByName($worksheetName) !== null;
527 1763
    }
528
529 1763
    public function duplicateWorksheetByTitle(string $title): Worksheet
530
    {
531
        $original = $this->getSheetByNameOrThrow($title);
532 1
        $index = $this->getIndex($original) + 1;
533
        $clone = clone $original;
534 1
535 1
        return $this->addSheet($clone, $index, true);
536 1
    }
537
538 1
    /**
539
     * Add sheet.
540
     *
541
     * @param Worksheet $worksheet The worksheet to add
542
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
543
     */
544
    public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null, bool $retitleIfNeeded = false): Worksheet
545
    {
546
        if ($retitleIfNeeded) {
547 1282
            $title = $worksheet->getTitle();
548
            if ($this->sheetNameExists($title)) {
549 1282
                $i = 1;
550 1191
                $newTitle = "$title $i";
551 1191
                while ($this->sheetNameExists($newTitle)) {
552 159
                    ++$i;
553 159
                    $newTitle = "$title $i";
554 159
                }
555 27
                $worksheet->setTitle($newTitle);
556 27
            }
557
        }
558 159
        if ($this->sheetNameExists($worksheet->getTitle())) {
559
            throw new Exception(
560
                "Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename this worksheet first."
561 1282
            );
562 2
        }
563 2
564 2
        if ($sheetIndex === null) {
565
            if ($this->activeSheetIndex < 0) {
566
                $this->activeSheetIndex = 0;
567 1282
            }
568 1249
            $this->workSheetCollection[] = $worksheet;
569 871
        } else {
570
            // Insert the sheet at the requested index
571 1249
            array_splice(
572
                $this->workSheetCollection,
573
                $sheetIndex,
574 38
                0,
575 38
                [$worksheet]
576 38
            );
577 38
578 38
            // Adjust active sheet index if necessary
579 38
            if ($this->activeSheetIndex >= $sheetIndex) {
580
                ++$this->activeSheetIndex;
581
            }
582 38
            if ($this->activeSheetIndex < 0) {
583 30
                $this->activeSheetIndex = 0;
584
            }
585 38
        }
586 3
587
        if ($worksheet->getParent() === null) {
588
            $worksheet->rebindParent($this);
589
        }
590 1282
591 51
        return $worksheet;
592
    }
593
594 1282
    /**
595
     * Remove sheet by index.
596
     *
597
     * @param int $sheetIndex Index position of the worksheet to remove
598
     */
599
    public function removeSheetByIndex(int $sheetIndex): void
600
    {
601
        $numSheets = count($this->workSheetCollection);
602 911
        if ($sheetIndex > $numSheets - 1) {
603
            throw new Exception(
604 911
                "You tried to remove a sheet by the out of bounds index: {$sheetIndex}. The actual number of sheets is {$numSheets}."
605 911
            );
606 1
        }
607 1
        array_splice($this->workSheetCollection, $sheetIndex, 1);
608 1
609
        // Adjust active sheet index if necessary
610 910
        if (
611
            ($this->activeSheetIndex >= $sheetIndex)
612
            && ($this->activeSheetIndex > 0 || $numSheets <= 1)
613
        ) {
614 910
            --$this->activeSheetIndex;
615 910
        }
616
    }
617 908
618
    /**
619
     * Get sheet by index.
620
     *
621
     * @param int $sheetIndex Sheet index
622
     */
623
    public function getSheet(int $sheetIndex): Worksheet
624
    {
625
        if (!isset($this->workSheetCollection[$sheetIndex])) {
626 10349
            $numSheets = $this->getSheetCount();
627
628 10349
            throw new Exception(
629 1
                "Your requested sheet index: {$sheetIndex} is out of bounds. The actual number of sheets is {$numSheets}."
630
            );
631 1
        }
632 1
633 1
        return $this->workSheetCollection[$sheetIndex];
634
    }
635
636 10349
    /**
637
     * Get all sheets.
638
     *
639
     * @return Worksheet[]
640
     */
641
    public function getAllSheets(): array
642
    {
643
        return $this->workSheetCollection;
644 160
    }
645
646 160
    /**
647
     * Get sheet by name.
648
     *
649
     * @param string $worksheetName Sheet name
650
     */
651
    public function getSheetByName(string $worksheetName): ?Worksheet
652
    {
653
        $worksheetCount = count($this->workSheetCollection);
654 9360
        for ($i = 0; $i < $worksheetCount; ++$i) {
655
            if (strcasecmp($this->workSheetCollection[$i]->getTitle(), trim($worksheetName, "'")) === 0) {
656 9360
                return $this->workSheetCollection[$i];
657 9360
            }
658 9042
        }
659 8463
660
        return null;
661
    }
662
663 1775
    /**
664
     * Get sheet by name, throwing exception if not found.
665
     */
666
    public function getSheetByNameOrThrow(string $worksheetName): Worksheet
667
    {
668
        $worksheet = $this->getSheetByName($worksheetName);
669 233
        if ($worksheet === null) {
670
            throw new Exception("Sheet $worksheetName does not exist.");
671 233
        }
672 233
673 1
        return $worksheet;
674
    }
675
676 232
    /**
677
     * Get index for sheet.
678
     *
679
     * @return int index
680
     */
681
    public function getIndex(Worksheet $worksheet, bool $noThrow = false): int
682
    {
683
        $wsHash = $worksheet->getHashInt();
684 10411
        foreach ($this->workSheetCollection as $key => $value) {
685
            if ($value->getHashInt() === $wsHash) {
686 10411
                return $key;
687 10411
            }
688 10195
        }
689 10183
        if ($noThrow) {
690
            return -1;
691
        }
692 10411
693 10411
        throw new Exception('Sheet does not exist.');
694
    }
695
696 3
    /**
697
     * Set index for sheet by sheet name.
698
     *
699
     * @param string $worksheetName Sheet name to modify index for
700
     * @param int $newIndexPosition New index for the sheet
701
     *
702
     * @return int New sheet index
703
     */
704
    public function setIndexByName(string $worksheetName, int $newIndexPosition): int
705
    {
706
        $oldIndex = $this->getIndex($this->getSheetByNameOrThrow($worksheetName));
707 1
        $worksheet = array_splice(
708
            $this->workSheetCollection,
709 1
            $oldIndex,
710 1
            1
711 1
        );
712 1
        array_splice(
713 1
            $this->workSheetCollection,
714 1
            $newIndexPosition,
715 1
            0,
716 1
            $worksheet
717 1
        );
718 1
719 1
        return $newIndexPosition;
720 1
    }
721
722 1
    /**
723
     * Get sheet count.
724
     */
725
    public function getSheetCount(): int
726
    {
727
        return count($this->workSheetCollection);
728 1543
    }
729
730 1543
    /**
731
     * Get active sheet index.
732
     *
733
     * @return int Active sheet index
734
     */
735
    public function getActiveSheetIndex(): int
736
    {
737
        return $this->activeSheetIndex;
738 10068
    }
739
740 10068
    /**
741
     * Set active sheet index.
742
     *
743
     * @param int $worksheetIndex Active sheet index
744
     */
745
    public function setActiveSheetIndex(int $worksheetIndex): Worksheet
746
    {
747
        $numSheets = count($this->workSheetCollection);
748 10146
749
        if ($worksheetIndex > $numSheets - 1) {
750 10146
            throw new Exception(
751
                "You tried to set a sheet active by the out of bounds index: {$worksheetIndex}. The actual number of sheets is {$numSheets}."
752 10146
            );
753 6
        }
754 6
        $this->activeSheetIndex = $worksheetIndex;
755 6
756
        return $this->getActiveSheet();
757 10140
    }
758
759 10140
    /**
760
     * Set active sheet index by name.
761
     *
762
     * @param string $worksheetName Sheet title
763
     */
764
    public function setActiveSheetIndexByName(string $worksheetName): Worksheet
765
    {
766
        if (($worksheet = $this->getSheetByName($worksheetName)) instanceof Worksheet) {
767 95
            $this->setActiveSheetIndex($this->getIndex($worksheet));
768
769 95
            return $worksheet;
770 93
        }
771
772 93
        throw new Exception('Workbook does not contain sheet:' . $worksheetName);
773
    }
774
775 2
    /**
776
     * Get sheet names.
777
     *
778
     * @return string[]
779
     */
780
    public function getSheetNames(): array
781
    {
782
        $returnValue = [];
783 10
        $worksheetCount = $this->getSheetCount();
784
        for ($i = 0; $i < $worksheetCount; ++$i) {
785 10
            $returnValue[] = $this->getSheet($i)->getTitle();
786 10
        }
787 10
788 10
        return $returnValue;
789
    }
790
791 10
    /**
792
     * Add external sheet.
793
     *
794
     * @param Worksheet $worksheet External sheet to add
795
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
796
     */
797
    public function addExternalSheet(Worksheet $worksheet, ?int $sheetIndex = null): Worksheet
798
    {
799
        if ($this->sheetNameExists($worksheet->getTitle())) {
800 5
            throw new Exception("Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename the external sheet first.");
801
        }
802 5
803 1
        // count how many cellXfs there are in this workbook currently, we will need this below
804
        $countCellXfs = count($this->cellXfCollection);
805
806
        // copy all the shared cellXfs from the external workbook and append them to the current
807 4
        foreach ($worksheet->getParentOrThrow()->getCellXfCollection() as $cellXf) {
808
            $this->addCellXf(clone $cellXf);
809
        }
810 4
811 4
        // move sheet to this workbook
812
        $worksheet->rebindParent($this);
813
814
        // update the cellXfs
815 4
        foreach ($worksheet->getCoordinates(false) as $coordinate) {
816
            $cell = $worksheet->getCell($coordinate);
817
            $cell->setXfIndex($cell->getXfIndex() + $countCellXfs);
818 4
        }
819 4
820 4
        // update the column dimensions Xfs
821
        foreach ($worksheet->getColumnDimensions() as $columnDimension) {
822
            $columnDimension->setXfIndex($columnDimension->getXfIndex() + $countCellXfs);
823
        }
824 4
825 1
        // update the row dimensions Xfs
826
        foreach ($worksheet->getRowDimensions() as $rowDimension) {
827
            $xfIndex = $rowDimension->getXfIndex();
828
            if ($xfIndex !== null) {
829 4
                $rowDimension->setXfIndex($xfIndex + $countCellXfs);
830 1
            }
831 1
        }
832 1
833
        return $this->addSheet($worksheet, $sheetIndex);
834
    }
835
836 4
    /**
837
     * Get an array of all Named Ranges.
838
     *
839
     * @return DefinedName[]
840
     */
841
    public function getNamedRanges(): array
842
    {
843
        return array_filter(
844 9
            $this->definedNames,
845
            fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_RANGE
846 9
        );
847 9
    }
848 9
849 9
    /**
850
     * Get an array of all Named Formulae.
851
     *
852
     * @return DefinedName[]
853
     */
854
    public function getNamedFormulae(): array
855
    {
856
        return array_filter(
857 15
            $this->definedNames,
858
            fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_FORMULA
859 15
        );
860 15
    }
861 15
862 15
    /**
863
     * Get an array of all Defined Names (both named ranges and named formulae).
864
     *
865
     * @return DefinedName[]
866
     */
867
    public function getDefinedNames(): array
868
    {
869
        return $this->definedNames;
870 562
    }
871
872 562
    /**
873
     * Add a named range.
874
     * If a named range with this name already exists, then this will replace the existing value.
875
     */
876
    public function addNamedRange(NamedRange $namedRange): void
877
    {
878
        $this->addDefinedName($namedRange);
879 306
    }
880
881 306
    /**
882
     * Add a named formula.
883
     * If a named formula with this name already exists, then this will replace the existing value.
884
     */
885
    public function addNamedFormula(NamedFormula $namedFormula): void
886
    {
887
        $this->addDefinedName($namedFormula);
888 12
    }
889
890 12
    /**
891
     * Add a defined name (either a named range or a named formula).
892
     * If a defined named with this name already exists, then this will replace the existing value.
893
     */
894
    public function addDefinedName(DefinedName $definedName): void
895
    {
896
        $upperCaseName = StringHelper::strToUpper($definedName->getName());
897 426
        if ($definedName->getScope() == null) {
898
            // global scope
899 426
            $this->definedNames[$upperCaseName] = $definedName;
900 426
        } else {
901
            // local scope
902 413
            $this->definedNames[$definedName->getScope()->getTitle() . '!' . $upperCaseName] = $definedName;
903
        }
904
    }
905 121
906
    /**
907
     * Get named range.
908
     *
909
     * @param null|Worksheet $worksheet Scope. Use null for global scope
910
     */
911
    public function getNamedRange(string $namedRange, ?Worksheet $worksheet = null): ?NamedRange
912
    {
913
        $returnValue = null;
914 27
915
        if ($namedRange !== '') {
916 27
            $namedRange = StringHelper::strToUpper($namedRange);
917
            // first look for global named range
918 27
            $returnValue = $this->getGlobalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE);
919 27
            // then look for local named range (has priority over global named range if both names exist)
920
            $returnValue = $this->getLocalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE, $worksheet) ?: $returnValue;
921 27
        }
922
923 27
        return $returnValue instanceof NamedRange ? $returnValue : null;
924
    }
925
926 27
    /**
927
     * Get named formula.
928
     *
929
     * @param null|Worksheet $worksheet Scope. Use null for global scope
930
     */
931
    public function getNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): ?NamedFormula
932
    {
933
        $returnValue = null;
934 11
935
        if ($namedFormula !== '') {
936 11
            $namedFormula = StringHelper::strToUpper($namedFormula);
937
            // first look for global named formula
938 11
            $returnValue = $this->getGlobalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA);
939 11
            // then look for local named formula (has priority over global named formula if both names exist)
940
            $returnValue = $this->getLocalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA, $worksheet) ?: $returnValue;
941 11
        }
942
943 11
        return $returnValue instanceof NamedFormula ? $returnValue : null;
944
    }
945
946 11
    private function getGlobalDefinedNameByType(string $name, bool $type): ?DefinedName
947
    {
948
        if (isset($this->definedNames[$name]) && $this->definedNames[$name]->isFormula() === $type) {
949 38
            return $this->definedNames[$name];
950
        }
951 38
952 27
        return null;
953
    }
954
955 13
    private function getLocalDefinedNameByType(string $name, bool $type, ?Worksheet $worksheet = null): ?DefinedName
956
    {
957
        if (
958 38
            ($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $name])
959
            && $this->definedNames[$worksheet->getTitle() . '!' . $name]->isFormula() === $type
960
        ) {
961 38
            return $this->definedNames[$worksheet->getTitle() . '!' . $name];
962 38
        }
963
964 8
        return null;
965
    }
966
967 36
    /**
968
     * Get named range.
969
     *
970
     * @param null|Worksheet $worksheet Scope. Use null for global scope
971
     */
972
    public function getDefinedName(string $definedName, ?Worksheet $worksheet = null): ?DefinedName
973
    {
974
        $returnValue = null;
975 10101
976
        if ($definedName !== '') {
977 10101
            $definedName = StringHelper::strToUpper($definedName);
978
            // first look for global defined name
979 10101
            if (isset($this->definedNames[$definedName])) {
980 10101
                $returnValue = $this->definedNames[$definedName];
981
            }
982 10101
983 126
            // then look for local defined name (has priority over global defined name if both names exist)
984
            if (($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) {
985
                $returnValue = $this->definedNames[$worksheet->getTitle() . '!' . $definedName];
986
            }
987 10101
        }
988 21
989
        return $returnValue;
990
    }
991
992 10101
    /**
993
     * Remove named range.
994
     *
995
     * @param null|Worksheet $worksheet scope: use null for global scope
996
     *
997
     * @return $this
998
     */
999
    public function removeNamedRange(string $namedRange, ?Worksheet $worksheet = null): self
1000
    {
1001
        if ($this->getNamedRange($namedRange, $worksheet) === null) {
1002 5
            return $this;
1003
        }
1004 5
1005 1
        return $this->removeDefinedName($namedRange, $worksheet);
1006
    }
1007
1008 4
    /**
1009
     * Remove named formula.
1010
     *
1011
     * @param null|Worksheet $worksheet scope: use null for global scope
1012
     *
1013
     * @return $this
1014
     */
1015
    public function removeNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): self
1016
    {
1017
        if ($this->getNamedFormula($namedFormula, $worksheet) === null) {
1018 4
            return $this;
1019
        }
1020 4
1021 1
        return $this->removeDefinedName($namedFormula, $worksheet);
1022
    }
1023
1024 3
    /**
1025
     * Remove defined name.
1026
     *
1027
     * @param null|Worksheet $worksheet scope: use null for global scope
1028
     *
1029
     * @return $this
1030
     */
1031
    public function removeDefinedName(string $definedName, ?Worksheet $worksheet = null): self
1032
    {
1033
        $definedName = StringHelper::strToUpper($definedName);
1034 11
1035
        if ($worksheet === null) {
1036 11
            if (isset($this->definedNames[$definedName])) {
1037
                unset($this->definedNames[$definedName]);
1038 11
            }
1039 1
        } else {
1040 1
            if (isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) {
1041
                unset($this->definedNames[$worksheet->getTitle() . '!' . $definedName]);
1042
            } elseif (isset($this->definedNames[$definedName])) {
1043 10
                unset($this->definedNames[$definedName]);
1044 3
            }
1045 7
        }
1046 7
1047
        return $this;
1048
    }
1049
1050 11
    /**
1051
     * Get worksheet iterator.
1052
     */
1053
    public function getWorksheetIterator(): Iterator
1054
    {
1055
        return new Iterator($this);
1056 1356
    }
1057
1058 1356
    /**
1059
     * Copy workbook (!= clone!).
1060
     */
1061
    public function copy(): self
1062
    {
1063
        return unserialize(serialize($this));
1064 1
    }
1065
1066 1
    public function __clone()
1067 1
    {
1068 1
        throw new Exception(
1069 1
            'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'
1070
        );
1071 1
    }
1072 1
1073 1
    /**
1074 1
     * Get the workbook collection of cellXfs.
1075
     *
1076 1
     * @return Style[]
1077
     */
1078
    public function getCellXfCollection(): array
1079 1
    {
1080
        return $this->cellXfCollection;
1081 1
    }
1082 1
1083 1
    /**
1084
     * Get cellXf by index.
1085
     */
1086
    public function getCellXfByIndex(int $cellStyleIndex): Style
1087
    {
1088
        return $this->cellXfCollection[$cellStyleIndex];
1089
    }
1090
1091 1076
    public function getCellXfByIndexOrNull(?int $cellStyleIndex): ?Style
1092
    {
1093 1076
        return ($cellStyleIndex === null) ? null : ($this->cellXfCollection[$cellStyleIndex] ?? null);
1094
    }
1095
1096
    /**
1097
     * Get cellXf by hash code.
1098
     *
1099 10007
     * @return false|Style
1100
     */
1101 10007
    public function getCellXfByHashCode(string $hashcode): bool|Style
1102
    {
1103
        foreach ($this->cellXfCollection as $cellXf) {
1104 2
            if ($cellXf->getHashCode() === $hashcode) {
1105
                return $cellXf;
1106 2
            }
1107
        }
1108
1109
        return false;
1110
    }
1111
1112
    /**
1113
     * Check if style exists in style collection.
1114 875
     */
1115
    public function cellXfExists(Style $cellStyleIndex): bool
1116 875
    {
1117 875
        return in_array($cellStyleIndex, $this->cellXfCollection, true);
1118 241
    }
1119
1120
    /**
1121
     * Get default style.
1122 821
     */
1123
    public function getDefaultStyle(): Style
1124
    {
1125
        if (isset($this->cellXfCollection[0])) {
1126
            return $this->cellXfCollection[0];
1127
        }
1128 1
1129
        throw new Exception('No default style found for this workbook');
1130 1
    }
1131
1132
    /**
1133
     * Add a cellXf to the workbook.
1134
     */
1135
    public function addCellXf(Style $style): void
1136 982
    {
1137
        $this->cellXfCollection[] = $style;
1138 982
        $style->setIndex(count($this->cellXfCollection) - 1);
1139 981
    }
1140
1141
    /**
1142 1
     * Remove cellXf by index. It is ensured that all cells get their xf index updated.
1143
     *
1144
     * @param int $cellStyleIndex Index to cellXf
1145
     */
1146
    public function removeCellXfByIndex(int $cellStyleIndex): void
1147
    {
1148 10411
        if ($cellStyleIndex > count($this->cellXfCollection) - 1) {
1149
            throw new Exception('CellXf index is out of bounds.');
1150 10411
        }
1151 10411
1152
        // first remove the cellXf
1153
        array_splice($this->cellXfCollection, $cellStyleIndex, 1);
1154
1155
        // then update cellXf indexes for cells
1156
        foreach ($this->workSheetCollection as $worksheet) {
1157
            foreach ($worksheet->getCoordinates(false) as $coordinate) {
1158
                $cell = $worksheet->getCell($coordinate);
1159 762
                $xfIndex = $cell->getXfIndex();
1160
                if ($xfIndex > $cellStyleIndex) {
1161 762
                    // decrease xf index by 1
1162 1
                    $cell->setXfIndex($xfIndex - 1);
1163
                } elseif ($xfIndex == $cellStyleIndex) {
1164
                    // set to default xf index 0
1165
                    $cell->setXfIndex(0);
1166 761
                }
1167
            }
1168
        }
1169 761
    }
1170 2
1171 1
    /**
1172 1
     * Get the cellXf supervisor.
1173 1
     */
1174
    public function getCellXfSupervisor(): Style
1175 1
    {
1176 1
        return $this->cellXfSupervisor;
1177
    }
1178 1
1179
    /**
1180
     * Get the workbook collection of cellStyleXfs.
1181
     *
1182
     * @return Style[]
1183
     */
1184
    public function getCellStyleXfCollection(): array
1185
    {
1186
        return $this->cellStyleXfCollection;
1187 10026
    }
1188
1189 10026
    /**
1190
     * Get cellStyleXf by index.
1191
     *
1192
     * @param int $cellStyleIndex Index to cellXf
1193
     */
1194
    public function getCellStyleXfByIndex(int $cellStyleIndex): Style
1195
    {
1196
        return $this->cellStyleXfCollection[$cellStyleIndex];
1197 1
    }
1198
1199 1
    /**
1200
     * Get cellStyleXf by hash code.
1201
     *
1202
     * @return false|Style
1203
     */
1204
    public function getCellStyleXfByHashCode(string $hashcode): bool|Style
1205
    {
1206
        foreach ($this->cellStyleXfCollection as $cellStyleXf) {
1207 1
            if ($cellStyleXf->getHashCode() === $hashcode) {
1208
                return $cellStyleXf;
1209 1
            }
1210
        }
1211
1212
        return false;
1213
    }
1214
1215
    /**
1216
     * Add a cellStyleXf to the workbook.
1217 1
     */
1218
    public function addCellStyleXf(Style $style): void
1219 1
    {
1220 1
        $this->cellStyleXfCollection[] = $style;
1221 1
        $style->setIndex(count($this->cellStyleXfCollection) - 1);
1222
    }
1223
1224
    /**
1225 1
     * Remove cellStyleXf by index.
1226
     *
1227
     * @param int $cellStyleIndex Index to cellXf
1228
     */
1229
    public function removeCellStyleXfByIndex(int $cellStyleIndex): void
1230
    {
1231 10411
        if ($cellStyleIndex > count($this->cellStyleXfCollection) - 1) {
1232
            throw new Exception('CellStyleXf index is out of bounds.');
1233 10411
        }
1234 10411
        array_splice($this->cellStyleXfCollection, $cellStyleIndex, 1);
1235
    }
1236
1237
    /**
1238
     * Eliminate all unneeded cellXf and afterwards update the xfIndex for all cells
1239
     * and columns in the workbook.
1240
     */
1241
    public function garbageCollect(): void
1242 759
    {
1243
        // how many references are there to each cellXf ?
1244 759
        $countReferencesCellXf = [];
1245 1
        foreach ($this->cellXfCollection as $index => $cellXf) {
1246
            $countReferencesCellXf[$index] = 0;
1247 758
        }
1248
1249
        foreach ($this->getWorksheetIterator() as $sheet) {
1250
            // from cells
1251
            foreach ($sheet->getCoordinates(false) as $coordinate) {
1252
                $cell = $sheet->getCell($coordinate);
1253
                ++$countReferencesCellXf[$cell->getXfIndex()];
1254 986
            }
1255
1256
            // from row dimensions
1257 986
            foreach ($sheet->getRowDimensions() as $rowDimension) {
1258 986
                if ($rowDimension->getXfIndex() !== null) {
1259 986
                    ++$countReferencesCellXf[$rowDimension->getXfIndex()];
1260
                }
1261
            }
1262 986
1263
            // from column dimensions
1264 986
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
1265 957
                ++$countReferencesCellXf[$columnDimension->getXfIndex()];
1266 957
            }
1267
        }
1268
1269
        // remove cellXfs without references and create mapping so we can update xfIndex
1270 986
        // for all cells and columns
1271 84
        $countNeededCellXfs = 0;
1272 11
        $map = [];
1273
        foreach ($this->cellXfCollection as $index => $cellXf) {
1274
            if ($countReferencesCellXf[$index] > 0 || $index == 0) { // we must never remove the first cellXf
1275
                ++$countNeededCellXfs;
1276
            } else {
1277 986
                unset($this->cellXfCollection[$index]);
1278 136
            }
1279
            $map[$index] = $countNeededCellXfs - 1;
1280
        }
1281
        $this->cellXfCollection = array_values($this->cellXfCollection);
1282
1283
        // update the index for all cellXfs
1284 986
        foreach ($this->cellXfCollection as $i => $cellXf) {
1285 986
            $cellXf->setIndex($i);
1286 986
        }
1287 986
1288 986
        // make sure there is always at least one cellXf (there should be)
1289
        if (empty($this->cellXfCollection)) {
1290 35
            $this->cellXfCollection[] = new Style();
1291
        }
1292 986
1293
        // update the xfIndex for all cells, row dimensions, column dimensions
1294 986
        foreach ($this->getWorksheetIterator() as $sheet) {
1295
            // for all cells
1296
            foreach ($sheet->getCoordinates(false) as $coordinate) {
1297 986
                $cell = $sheet->getCell($coordinate);
1298 986
                $cell->setXfIndex($map[$cell->getXfIndex()]);
1299
            }
1300
1301
            // for all row dimensions
1302 986
            foreach ($sheet->getRowDimensions() as $rowDimension) {
1303
                if ($rowDimension->getXfIndex() !== null) {
1304
                    $rowDimension->setXfIndex($map[$rowDimension->getXfIndex()]);
1305
                }
1306
            }
1307 986
1308
            // for all column dimensions
1309 986
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
1310 957
                $columnDimension->setXfIndex($map[$columnDimension->getXfIndex()]);
1311 957
            }
1312
1313
            // also do garbage collection for all the sheets
1314
            $sheet->garbageCollect();
1315 986
        }
1316 84
    }
1317 11
1318
    /**
1319
     * Return the unique ID value assigned to this spreadsheet workbook.
1320
     */
1321
    public function getID(): string
1322 986
    {
1323 136
        return $this->uniqueID;
1324
    }
1325
1326
    /**
1327 986
     * Get the visibility of the horizonal scroll bar in the application.
1328
     *
1329
     * @return bool True if horizonal scroll bar is visible
1330
     */
1331
    public function getShowHorizontalScroll(): bool
1332
    {
1333
        return $this->showHorizontalScroll;
1334
    }
1335
1336
    /**
1337
     * Set the visibility of the horizonal scroll bar in the application.
1338
     *
1339
     * @param bool $showHorizontalScroll True if horizonal scroll bar is visible
1340
     */
1341
    public function setShowHorizontalScroll(bool $showHorizontalScroll): void
1342
    {
1343
        $this->showHorizontalScroll = (bool) $showHorizontalScroll;
1344 371
    }
1345
1346 371
    /**
1347
     * Get the visibility of the vertical scroll bar in the application.
1348
     *
1349
     * @return bool True if vertical scroll bar is visible
1350
     */
1351
    public function getShowVerticalScroll(): bool
1352
    {
1353
        return $this->showVerticalScroll;
1354 255
    }
1355
1356 255
    /**
1357
     * Set the visibility of the vertical scroll bar in the application.
1358
     *
1359
     * @param bool $showVerticalScroll True if vertical scroll bar is visible
1360
     */
1361
    public function setShowVerticalScroll(bool $showVerticalScroll): void
1362
    {
1363
        $this->showVerticalScroll = (bool) $showVerticalScroll;
1364 371
    }
1365
1366 371
    /**
1367
     * Get the visibility of the sheet tabs in the application.
1368
     *
1369
     * @return bool True if the sheet tabs are visible
1370
     */
1371
    public function getShowSheetTabs(): bool
1372
    {
1373
        return $this->showSheetTabs;
1374 255
    }
1375
1376 255
    /**
1377
     * Set the visibility of the sheet tabs  in the application.
1378
     *
1379
     * @param bool $showSheetTabs True if sheet tabs are visible
1380
     */
1381
    public function setShowSheetTabs(bool $showSheetTabs): void
1382
    {
1383
        $this->showSheetTabs = (bool) $showSheetTabs;
1384 371
    }
1385
1386 371
    /**
1387
     * Return whether the workbook window is minimized.
1388
     *
1389
     * @return bool true if workbook window is minimized
1390
     */
1391
    public function getMinimized(): bool
1392
    {
1393
        return $this->minimized;
1394 255
    }
1395
1396 255
    /**
1397
     * Set whether the workbook window is minimized.
1398
     *
1399
     * @param bool $minimized true if workbook window is minimized
1400
     */
1401
    public function setMinimized(bool $minimized): void
1402
    {
1403
        $this->minimized = (bool) $minimized;
1404 371
    }
1405
1406 371
    /**
1407
     * Return whether to group dates when presenting the user with
1408
     * filtering optiomd in the user interface.
1409
     *
1410
     * @return bool true if workbook window is minimized
1411
     */
1412
    public function getAutoFilterDateGrouping(): bool
1413
    {
1414 249
        return $this->autoFilterDateGrouping;
1415
    }
1416 249
1417
    /**
1418
     * Set whether to group dates when presenting the user with
1419
     * filtering optiomd in the user interface.
1420
     *
1421
     * @param bool $autoFilterDateGrouping true if workbook window is minimized
1422
     */
1423
    public function setAutoFilterDateGrouping(bool $autoFilterDateGrouping): void
1424
    {
1425 371
        $this->autoFilterDateGrouping = (bool) $autoFilterDateGrouping;
1426
    }
1427 371
1428
    /**
1429
     * Return the first sheet in the book view.
1430
     *
1431
     * @return int First sheet in book view
1432
     */
1433
    public function getFirstSheetIndex(): int
1434
    {
1435
        return $this->firstSheetIndex;
1436 249
    }
1437
1438 249
    /**
1439
     * Set the first sheet in the book view.
1440
     *
1441
     * @param int $firstSheetIndex First sheet in book view
1442
     */
1443
    public function setFirstSheetIndex(int $firstSheetIndex): void
1444
    {
1445
        if ($firstSheetIndex >= 0) {
1446 371
            $this->firstSheetIndex = (int) $firstSheetIndex;
1447
        } else {
1448 371
            throw new Exception('First sheet index must be a positive integer.');
1449
        }
1450
    }
1451
1452
    /**
1453
     * Return the visibility status of the workbook.
1454
     *
1455
     * This may be one of the following three values:
1456 257
     * - visibile
1457
     *
1458 257
     * @return string Visible status
1459 256
     */
1460
    public function getVisibility(): string
1461 1
    {
1462
        return $this->visibility;
1463
    }
1464
1465
    /**
1466
     * Set the visibility status of the workbook.
1467
     *
1468
     * Valid values are:
1469
     *  - 'visible' (self::VISIBILITY_VISIBLE):
1470
     *       Workbook window is visible
1471
     *  - 'hidden' (self::VISIBILITY_HIDDEN):
1472
     *       Workbook window is hidden, but can be shown by the user
1473 372
     *       via the user interface
1474
     *  - 'veryHidden' (self::VISIBILITY_VERY_HIDDEN):
1475 372
     *       Workbook window is hidden and cannot be shown in the
1476
     *       user interface.
1477
     *
1478
     * @param null|string $visibility visibility status of the workbook
1479
     */
1480
    public function setVisibility(?string $visibility): void
1481
    {
1482
        if ($visibility === null) {
1483
            $visibility = self::VISIBILITY_VISIBLE;
1484
        }
1485
1486
        if (in_array($visibility, self::WORKBOOK_VIEW_VISIBILITY_VALUES)) {
1487
            $this->visibility = $visibility;
1488
        } else {
1489
            throw new Exception('Invalid visibility value.');
1490
        }
1491
    }
1492
1493 250
    /**
1494
     * Get the ratio between the workbook tabs bar and the horizontal scroll bar.
1495 250
     * TabRatio is assumed to be out of 1000 of the horizontal window width.
1496 1
     *
1497
     * @return int Ratio between the workbook tabs bar and the horizontal scroll bar
1498
     */
1499 250
    public function getTabRatio(): int
1500 250
    {
1501
        return $this->tabRatio;
1502 1
    }
1503
1504
    /**
1505
     * Set the ratio between the workbook tabs bar and the horizontal scroll bar
1506
     * TabRatio is assumed to be out of 1000 of the horizontal window width.
1507
     *
1508
     * @param int $tabRatio Ratio between the tabs bar and the horizontal scroll bar
1509
     */
1510
    public function setTabRatio(int $tabRatio): void
1511
    {
1512 371
        if ($tabRatio >= 0 && $tabRatio <= 1000) {
1513
            $this->tabRatio = (int) $tabRatio;
1514 371
        } else {
1515
            throw new Exception('Tab ratio must be between 0 and 1000.');
1516
        }
1517
    }
1518
1519
    public function reevaluateAutoFilters(bool $resetToMax): void
1520
    {
1521
        foreach ($this->workSheetCollection as $sheet) {
1522
            $filter = $sheet->getAutoFilter();
1523 259
            if (!empty($filter->getRange())) {
1524
                if ($resetToMax) {
1525 259
                    $filter->setRangeToMaxRow();
1526 258
                }
1527
                $filter->showHideRows();
1528 1
            }
1529
        }
1530
    }
1531
1532 2
    /**
1533
     * @throws Exception
1534 2
     */
1535 2
    public function jsonSerialize(): mixed
1536 2
    {
1537 2
        throw new Exception('Spreadsheet objects cannot be json encoded');
1538 1
    }
1539
1540 2
    public function resetThemeFonts(): void
1541
    {
1542
        $majorFontLatin = $this->theme->getMajorFontLatin();
1543
        $minorFontLatin = $this->theme->getMinorFontLatin();
1544
        foreach ($this->cellXfCollection as $cellStyleXf) {
1545
            $scheme = $cellStyleXf->getFont()->getScheme();
1546
            if ($scheme === 'major') {
1547
                $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
1548 2
            } elseif ($scheme === 'minor') {
1549
                $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
1550 2
            }
1551
        }
1552
        foreach ($this->cellStyleXfCollection as $cellStyleXf) {
1553
            $scheme = $cellStyleXf->getFont()->getScheme();
1554
            if ($scheme === 'major') {
1555
                $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
1556 1
            } elseif ($scheme === 'minor') {
1557
                $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
1558 1
            }
1559
        }
1560
    }
1561 1
1562
    public function getTableByName(string $tableName): ?Table
1563 1
    {
1564 1
        $table = null;
1565 1
        foreach ($this->workSheetCollection as $sheet) {
1566 1
            $table = $sheet->getTableByName($tableName);
1567 1
            if ($table !== null) {
1568 1
                break;
1569 1
            }
1570 1
        }
1571
1572
        return $table;
1573 1
    }
1574 1
1575 1
    /**
1576
     * @return bool Success or failure
1577 1
     */
1578 1
    public function setExcelCalendar(int $baseYear): bool
1579
    {
1580
        if (($baseYear === Date::CALENDAR_WINDOWS_1900) || ($baseYear === Date::CALENDAR_MAC_1904)) {
1581
            $this->excelCalendar = $baseYear;
1582
1583 37
            return true;
1584
        }
1585 37
1586 37
        return false;
1587 37
    }
1588 37
1589 5
    /**
1590
     * @return int Excel base date (1900 or 1904)
1591
     */
1592
    public function getExcelCalendar(): int
1593 37
    {
1594
        return $this->excelCalendar;
1595
    }
1596
1597
    public function deleteLegacyDrawing(Worksheet $worksheet): void
1598
    {
1599 754
        unset($this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing']);
1600
    }
1601 754
1602 754
    public function getLegacyDrawing(Worksheet $worksheet): ?string
1603
    {
1604 754
        return $this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'] ?? null;
1605
    }
1606
1607
    public function getValueBinder(): ?IValueBinder
1608
    {
1609
        return $this->valueBinder;
1610
    }
1611
1612
    public function setValueBinder(?IValueBinder $valueBinder): self
1613 8362
    {
1614
        $this->valueBinder = $valueBinder;
1615 8362
1616
        return $this;
1617
    }
1618 2
1619
    /**
1620 2
     * All the PDF writers treat charts as if they occupy a single cell.
1621
     * This will be better most of the time.
1622
     * It is not needed for any other output type.
1623 3
     * It changes the contents of the spreadsheet, so you might
1624
     * be better off cloning the spreadsheet and then using
1625 3
     * this method on, and then writing, the clone.
1626
     */
1627
    public function mergeChartCellsForPdf(): void
1628 9567
    {
1629
        foreach ($this->workSheetCollection as $worksheet) {
1630 9567
            foreach ($worksheet->getChartCollection() as $chart) {
1631
                $br = $chart->getBottomRightCell();
1632
                $tl = $chart->getTopLeftCell();
1633 1512
                if ($br !== '' && $br !== $tl) {
1634
                    if (!$worksheet->cellExists($br)) {
1635 1512
                        $worksheet->getCell($br)->setValue(' ');
1636
                    }
1637 1512
                    $worksheet->mergeCells("$tl:$br");
1638
                }
1639
            }
1640
        }
1641
    }
1642
1643
    /**
1644
     * All the PDF writers do better with drawings than charts.
1645
     * This will be better some of the time.
1646
     * It is not needed for any other output type.
1647
     * It changes the contents of the spreadsheet, so you might
1648 1
     * be better off cloning the spreadsheet and then using
1649
     * this method on, and then writing, the clone.
1650 1
     */
1651 1
    public function mergeDrawingCellsForPdf(): void
1652 1
    {
1653 1
        foreach ($this->workSheetCollection as $worksheet) {
1654 1
            foreach ($worksheet->getDrawingCollection() as $drawing) {
1655 1
                $br = $drawing->getCoordinates2();
1656 1
                $tl = $drawing->getCoordinates();
1657
                if ($br !== '' && $br !== $tl) {
1658 1
                    if (!$worksheet->cellExists($br)) {
1659
                        $worksheet->getCell($br)->setValue(' ');
1660
                    }
1661
                    $worksheet->mergeCells("$tl:$br");
1662
                }
1663
            }
1664
        }
1665
    }
1666
}
1667