PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Worksheet / Worksheet.php

Worksheet.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Worksheet/Worksheet.php

4,332 lines 123.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace TablePress\PhpOffice\PhpSpreadsheet\Worksheet;
4
5 use ArrayObject;
6 use TablePress\Composer\Pcre\Preg;
7 use Generator;
8 use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Calculation;
9 use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Functions;
10 use TablePress\PhpOffice\PhpSpreadsheet\Cell\AddressRange;
11 use TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell;
12 use TablePress\PhpOffice\PhpSpreadsheet\Cell\CellAddress;
13 use TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate;
14 use TablePress\PhpOffice\PhpSpreadsheet\Cell\DataType;
15 use TablePress\PhpOffice\PhpSpreadsheet\Cell\DataValidation;
16 use TablePress\PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
17 use TablePress\PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
18 use TablePress\PhpOffice\PhpSpreadsheet\Chart\Chart;
19 use TablePress\PhpOffice\PhpSpreadsheet\Collection\Cells;
20 use TablePress\PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
21 use TablePress\PhpOffice\PhpSpreadsheet\Comment;
22 use TablePress\PhpOffice\PhpSpreadsheet\DefinedName;
23 use TablePress\PhpOffice\PhpSpreadsheet\Exception;
24 use TablePress\PhpOffice\PhpSpreadsheet\ReferenceHelper;
25 use TablePress\PhpOffice\PhpSpreadsheet\RichText\RichText;
26 use TablePress\PhpOffice\PhpSpreadsheet\Shared;
27 use TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper;
28 use TablePress\PhpOffice\PhpSpreadsheet\Spreadsheet;
29 use TablePress\PhpOffice\PhpSpreadsheet\Style\Alignment;
30 use TablePress\PhpOffice\PhpSpreadsheet\Style\Color;
31 use TablePress\PhpOffice\PhpSpreadsheet\Style\Conditional;
32 use TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat;
33 use TablePress\PhpOffice\PhpSpreadsheet\Style\Protection as StyleProtection;
34 use TablePress\PhpOffice\PhpSpreadsheet\Style\Style;
35 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\PivotTable\PivotTable;
36 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Sparkline\Sparkline;
37 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Sparkline\SparklineGroup;
38 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Sparkline\SparklineType;
39
40 class Worksheet
41 {
42 // Break types
43 public const BREAK_NONE = 0;
44 public const BREAK_ROW = 1;
45 public const BREAK_COLUMN = 2;
46 // Maximum column for row break
47 public const BREAK_ROW_MAX_COLUMN = 16383;
48
49 // Sheet state
50 public const SHEETSTATE_VISIBLE = 'visible';
51 public const SHEETSTATE_HIDDEN = 'hidden';
52 public const SHEETSTATE_VERYHIDDEN = 'veryHidden';
53
54 public const MERGE_CELL_CONTENT_EMPTY = 'empty';
55 public const MERGE_CELL_CONTENT_HIDE = 'hide';
56 public const MERGE_CELL_CONTENT_MERGE = 'merge';
57
58 public const FUNCTION_LIKE_GROUPBY = '/\b(groupby|_xleta)\b/i'; // weird new syntax
59
60 protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui';
61
62 /**
63 * Maximum 31 characters allowed for sheet title.
64 *
65 * @var int
66 */
67 const SHEET_TITLE_MAXIMUM_LENGTH = 31;
68
69 /**
70 * Invalid characters in sheet title.
71 */
72 private const INVALID_CHARACTERS = ['*', ':', '/', '\\', '?', '[', ']'];
73
74 /**
75 * Parent spreadsheet.
76 */
77 private ?Spreadsheet $parent = null;
78
79 /**
80 * Collection of cells.
81 */
82 private Cells $cellCollection;
83
84 /**
85 * Collection of row dimensions.
86 *
87 * @var RowDimension[]
88 */
89 private array $rowDimensions = [];
90
91 /**
92 * Default row dimension.
93 */
94 private RowDimension $defaultRowDimension;
95
96 /**
97 * Collection of column dimensions.
98 *
99 * @var ColumnDimension[]
100 */
101 private array $columnDimensions = [];
102
103 /**
104 * Default column dimension.
105 */
106 private ColumnDimension $defaultColumnDimension;
107
108 /**
109 * Collection of drawings.
110 *
111 * @var ArrayObject<int, BaseDrawing>
112 */
113 private ArrayObject $drawingCollection;
114
115 /**
116 * Collection of drawings.
117 *
118 * @var ArrayObject<int, BaseDrawing>
119 */
120 private ArrayObject $inCellDrawingCollection;
121
122 /**
123 * Collection of Chart objects.
124 *
125 * @var ArrayObject<int, Chart>
126 */
127 private ArrayObject $chartCollection;
128
129 /**
130 * Collection of Table objects.
131 *
132 * @var ArrayObject<int, Table>
133 */
134 private ArrayObject $tableCollection;
135
136 /**
137 * Collection of SparklineGroup objects.
138 *
139 * @var ArrayObject<int, SparklineGroup>
140 */
141 private ArrayObject $sparklineGroupCollection;
142
143 /**
144 * Collection of PivotTable objects.
145 *
146 * @var ArrayObject<int, PivotTable>
147 */
148 private ArrayObject $pivotTableCollection;
149
150 /**
151 * Worksheet title.
152 */
153 private string $title = '';
154
155 /**
156 * Sheet state.
157 */
158 private string $sheetState;
159
160 /**
161 * Page setup.
162 */
163 private PageSetup $pageSetup;
164
165 /**
166 * Page margins.
167 */
168 private PageMargins $pageMargins;
169
170 /**
171 * Page header/footer.
172 */
173 private HeaderFooter $headerFooter;
174
175 /**
176 * Sheet view.
177 */
178 private SheetView $sheetView;
179
180 /**
181 * Protection.
182 */
183 private Protection $protection;
184
185 /**
186 * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
187 *
188 * @var Conditional[][]
189 */
190 private array $conditionalStylesCollection = [];
191
192 /**
193 * Collection of row breaks.
194 *
195 * @var PageBreak[]
196 */
197 private array $rowBreaks = [];
198
199 /**
200 * Collection of column breaks.
201 *
202 * @var PageBreak[]
203 */
204 private array $columnBreaks = [];
205
206 /**
207 * Collection of merged cell ranges.
208 *
209 * @var string[]
210 */
211 private array $mergeCells = [];
212
213 /**
214 * Collection of protected cell ranges.
215 *
216 * @var ProtectedRange[]
217 */
218 private array $protectedCells = [];
219
220 /**
221 * Autofilter Range and selection.
222 */
223 private AutoFilter $autoFilter;
224
225 /**
226 * Freeze pane.
227 */
228 private ?string $freezePane = null;
229
230 /**
231 * Default position of the right bottom pane.
232 */
233 private ?string $topLeftCell = null;
234
235 private string $paneTopLeftCell = '';
236
237 private string $activePane = '';
238
239 private int $xSplit = 0;
240
241 private int $ySplit = 0;
242
243 private string $paneState = '';
244
245 /**
246 * Properties of the 4 panes.
247 *
248 * @var (null|Pane)[]
249 */
250 private array $panes = [
251 'bottomRight' => null,
252 'bottomLeft' => null,
253 'topRight' => null,
254 'topLeft' => null,
255 ];
256
257 /**
258 * Show gridlines?
259 */
260 private bool $showGridlines = true;
261
262 /**
263 * Print gridlines?
264 */
265 private bool $printGridlines = false;
266
267 /**
268 * Show row and column headers?
269 */
270 private bool $showRowColHeaders = true;
271
272 /**
273 * Show summary below? (Row/Column outline).
274 */
275 private bool $showSummaryBelow = true;
276
277 /**
278 * Show summary right? (Row/Column outline).
279 */
280 private bool $showSummaryRight = true;
281
282 /**
283 * Collection of comments.
284 *
285 * @var Comment[]
286 */
287 private array $comments = [];
288
289 /**
290 * Active cell. (Only one!).
291 */
292 private string $activeCell = 'A1';
293
294 /**
295 * Selected cells.
296 */
297 private string $selectedCells = 'A1';
298
299 /**
300 * Cached highest column.
301 */
302 private int $cachedHighestColumn = 1;
303
304 /**
305 * Cached highest row.
306 */
307 private int $cachedHighestRow = 1;
308
309 /**
310 * Right-to-left?
311 */
312 private bool $rightToLeft = false;
313
314 /**
315 * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
316 *
317 * @var Hyperlink[]
318 */
319 private array $hyperlinkCollection = [];
320
321 /**
322 * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
323 * Index can include ranges, and multiple cells/ranges.
324 *
325 * @var DataValidation[]
326 */
327 private array $dataValidationCollection = [];
328
329 /**
330 * Tab color.
331 */
332 private ?Color $tabColor = null;
333
334 /**
335 * CodeName.
336 */
337 private ?string $codeName = null;
338
339 /**
340 * Create a new worksheet.
341 */
342 public function __construct(?Spreadsheet $parent = null, string $title = 'Worksheet')
343 {
344 // Set parent and title
345 $this->parent = $parent;
346 // Chart collection must be set before title
347 $this->chartCollection = new ArrayObject();
348 $this->setTitle($title, false, true, false);
349 // setTitle can change $pTitle
350 $this->setCodeName($this->getTitle());
351 $this->setSheetState(self::SHEETSTATE_VISIBLE);
352
353 $this->cellCollection = CellsFactory::getInstance($this);
354 // Set page setup
355 $this->pageSetup = new PageSetup();
356 // Set page margins
357 $this->pageMargins = new PageMargins();
358 // Set page header/footer
359 $this->headerFooter = new HeaderFooter();
360 // Set sheet view
361 $this->sheetView = new SheetView();
362 // Drawing collection
363 $this->drawingCollection = new ArrayObject();
364 // In Cell Drawing collection
365 $this->inCellDrawingCollection = new ArrayObject();
366 // Protection
367 $this->protection = new Protection();
368 // Default row dimension
369 $this->defaultRowDimension = new RowDimension(null);
370 // Default column dimension
371 $this->defaultColumnDimension = new ColumnDimension(null);
372 // AutoFilter
373 $this->autoFilter = new AutoFilter('', $this);
374 // Table collection
375 $this->tableCollection = new ArrayObject();
376 // Sparkline group collection
377 $this->sparklineGroupCollection = new ArrayObject();
378
379 // Pivot table collection
380 $this->pivotTableCollection = new ArrayObject();
381 }
382
383 /**
384 * Disconnect all cells from this Worksheet object,
385 * typically so that the worksheet object can be unset.
386 * The worksheet will be in an unusable state after
387 * this method has completed.
388 */
389 public function disconnectCells(): void
390 {
391 if (isset($this->cellCollection)) { //* @phpstan-ignore isset.initializedProperty (may be null at destruct time)
392 $this->cellCollection->unsetWorksheetCells();
393 unset($this->cellCollection);
394 }
395 // detach ourself from the workbook, so that it can then delete this worksheet successfully
396 $this->parent = null;
397 }
398
399 /**
400 * Code to execute when this worksheet is unset().
401 */
402 public function __destruct()
403 {
404 ($nullsafeVariable1 = Calculation::getInstanceOrNull($this->parent)) ? $nullsafeVariable1->clearCalculationCacheForWorksheet($this->title) : null;
405
406 $this->disconnectCells();
407 unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->sparklineGroupCollection, $this->drawingCollection, $this->inCellDrawingCollection, $this->chartCollection, $this->autoFilter, $this->pivotTableCollection);
408 }
409
410 /**
411 * Return the cell collection.
412 */
413 public function getCellCollection(): Cells
414 {
415 return $this->cellCollection;
416 }
417
418 /**
419 * Get array of invalid characters for sheet title.
420 *
421 * @return string[]
422 */
423 public static function getInvalidCharacters(): array
424 {
425 return self::INVALID_CHARACTERS;
426 }
427
428 /**
429 * Check sheet code name for valid Excel syntax.
430 *
431 * @param string $sheetCodeName The string to check
432 *
433 * @return string The valid string
434 */
435 private static function checkSheetCodeName(string $sheetCodeName): string
436 {
437 $charCount = StringHelper::countCharacters($sheetCodeName);
438 if ($charCount == 0) {
439 throw new Exception('Sheet code name cannot be empty.');
440 }
441 // Some of the printable ASCII characters are invalid: * : / \ ? [ ] and first and last characters cannot be a "'"
442 if (
443 (str_replace(self::INVALID_CHARACTERS, '', $sheetCodeName) !== $sheetCodeName)
444 || (StringHelper::substring($sheetCodeName, -1, 1) == '\'')
445 || (StringHelper::substring($sheetCodeName, 0, 1) == '\'')
446 ) {
447 throw new Exception('Invalid character found in sheet code name');
448 }
449
450 // Enforce maximum characters allowed for sheet title
451 if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
452 throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
453 }
454
455 return $sheetCodeName;
456 }
457
458 /**
459 * Check sheet title for valid Excel syntax.
460 *
461 * @param string $sheetTitle The string to check
462 *
463 * @return string The valid string
464 */
465 private static function checkSheetTitle(string $sheetTitle): string
466 {
467 // Some of the printable ASCII characters are invalid: * : / \ ? [ ]
468 if (str_replace(self::INVALID_CHARACTERS, '', $sheetTitle) !== $sheetTitle) {
469 throw new Exception('Invalid character found in sheet title');
470 }
471
472 // Enforce maximum characters allowed for sheet title
473 if (StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
474 throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
475 }
476
477 return $sheetTitle;
478 }
479
480 /**
481 * Get a sorted list of all cell coordinates currently held in the collection by row and column.
482 *
483 * @param bool $sorted Also sort the cell collection?
484 *
485 * @return string[]
486 */
487 public function getCoordinates(bool $sorted = true): array
488 {
489 if (!isset($this->cellCollection)) { //* @phpstan-ignore isset.initializedProperty (may be null at destruct time)
490 return [];
491 }
492
493 if ($sorted) {
494 return $this->cellCollection->getSortedCoordinates();
495 }
496
497 return $this->cellCollection->getCoordinates();
498 }
499
500 /**
501 * Get collection of row dimensions.
502 *
503 * @return RowDimension[]
504 */
505 public function getRowDimensions(): array
506 {
507 return $this->rowDimensions;
508 }
509
510 /**
511 * Get default row dimension.
512 */
513 public function getDefaultRowDimension(): RowDimension
514 {
515 return $this->defaultRowDimension;
516 }
517
518 /**
519 * Get collection of column dimensions.
520 *
521 * @return ColumnDimension[]
522 */
523 public function getColumnDimensions(): array
524 {
525 /** @var callable $callable */
526 $callable = [self::class, 'columnDimensionCompare'];
527 uasort($this->columnDimensions, $callable);
528
529 return $this->columnDimensions;
530 }
531
532 private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
533 {
534 return $a->getColumnNumeric() - $b->getColumnNumeric();
535 }
536
537 /**
538 * Get default column dimension.
539 */
540 public function getDefaultColumnDimension(): ColumnDimension
541 {
542 return $this->defaultColumnDimension;
543 }
544
545 /**
546 * Get collection of drawings.
547 *
548 * @return ArrayObject<int, BaseDrawing>
549 */
550 public function getDrawingCollection(): ArrayObject
551 {
552 return $this->drawingCollection;
553 }
554
555 /**
556 * Get collection of drawings.
557 *
558 * @return ArrayObject<int, BaseDrawing>
559 */
560 public function getInCellDrawingCollection(): ArrayObject
561 {
562 return $this->inCellDrawingCollection;
563 }
564
565 /**
566 * Get collection of charts.
567 *
568 * @return ArrayObject<int, Chart>
569 */
570 public function getChartCollection(): ArrayObject
571 {
572 return $this->chartCollection;
573 }
574
575 public function addChart(Chart $chart): Chart
576 {
577 $chart->setWorksheet($this);
578 $this->chartCollection[] = $chart;
579
580 return $chart;
581 }
582
583 /**
584 * Return the count of charts on this worksheet.
585 *
586 * @return int The number of charts
587 */
588 public function getChartCount(): int
589 {
590 return count($this->chartCollection);
591 }
592
593 /**
594 * Get a chart by its index position.
595 *
596 * @param null|int|string $index Chart index position
597 *
598 * @return Chart|false
599 */
600 public function getChartByIndex($index)
601 {
602 $chartCount = count($this->chartCollection);
603 if ($chartCount === 0 || (is_string($index) && $index !== (string) (int) $index)) {
604 return false;
605 }
606 if ($index === null) {
607 $index = --$chartCount;
608 }
609 if (!isset($this->chartCollection[$index])) {
610 return false;
611 }
612
613 return $this->chartCollection[$index];
614 }
615
616 /**
617 * Return an array of the names of charts on this worksheet.
618 *
619 * @return string[] The names of charts
620 */
621 public function getChartNames(): array
622 {
623 $chartNames = [];
624 foreach ($this->chartCollection as $chart) {
625 $chartNames[] = $chart->getName();
626 }
627
628 return $chartNames;
629 }
630
631 /**
632 * Get a chart by name.
633 *
634 * @param string $chartName Chart name
635 *
636 * @return Chart|false
637 */
638 public function getChartByName(string $chartName)
639 {
640 foreach ($this->chartCollection as $index => $chart) {
641 if ($chart->getName() == $chartName) {
642 return $chart;
643 }
644 }
645
646 return false;
647 }
648
649 public function getChartByNameOrThrow(string $chartName): Chart
650 {
651 $chart = $this->getChartByName($chartName);
652 if ($chart !== false) {
653 return $chart;
654 }
655
656 throw new Exception("Sheet does not have a chart named $chartName.");
657 }
658
659 /**
660 * Refresh column dimensions.
661 *
662 * @return $this
663 */
664 public function refreshColumnDimensions()
665 {
666 $newColumnDimensions = [];
667 foreach ($this->getColumnDimensions() as $objColumnDimension) {
668 $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
669 }
670
671 $this->columnDimensions = $newColumnDimensions;
672
673 return $this;
674 }
675
676 /**
677 * Refresh row dimensions.
678 *
679 * @return $this
680 */
681 public function refreshRowDimensions()
682 {
683 $newRowDimensions = [];
684 foreach ($this->getRowDimensions() as $objRowDimension) {
685 $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
686 }
687
688 $this->rowDimensions = $newRowDimensions;
689
690 return $this;
691 }
692
693 /**
694 * Calculate worksheet dimension.
695 *
696 * @return string String containing the dimension of this worksheet
697 */
698 public function calculateWorksheetDimension(): string
699 {
700 // Return
701 return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
702 }
703
704 /**
705 * Calculate worksheet data dimension.
706 *
707 * @return string String containing the dimension of this worksheet that actually contain data
708 */
709 public function calculateWorksheetDataDimension(): string
710 {
711 // Return
712 return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
713 }
714
715 /**
716 * Calculate widths for auto-size columns.
717 *
718 * @return $this
719 */
720 public function calculateColumnWidths()
721 {
722 $activeSheet = ($nullsafeVariable2 = $this->getParent()) ? $nullsafeVariable2->getActiveSheetIndex() : null;
723 $selectedCells = $this->selectedCells;
724 // initialize $autoSizes array
725 $autoSizes = [];
726 foreach ($this->getColumnDimensions() as $colDimension) {
727 if ($colDimension->getAutoSize()) {
728 $autoSizes[$colDimension->getColumnIndex()] = -1;
729 }
730 }
731
732 // There is only something to do if there are some auto-size columns
733 if (!empty($autoSizes)) {
734 $holdActivePane = $this->activePane;
735 // build list of cells references that participate in a merge
736 $isMergeCell = [];
737 foreach ($this->getMergeCells() as $cells) {
738 foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
739 $isMergeCell[$cellReference] = true;
740 }
741 }
742
743 $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
744
745 // loop through all cells in the worksheet
746 foreach ($this->getCoordinates(false) as $coordinate) {
747 $cell = $this->getCellOrNull($coordinate);
748
749 if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
750 //Determine if cell is in merge range
751 $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
752
753 //By default merged cells should be ignored
754 $isMergedButProceed = false;
755
756 //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
757 if ($isMerged && $cell->isMergeRangeValueCell()) {
758 $range = (string) $cell->getMergeRange();
759 $rangeBoundaries = Coordinate::rangeDimension($range);
760 if ($rangeBoundaries[0] === 1) {
761 $isMergedButProceed = true;
762 }
763 }
764
765 // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
766 if (!$isMerged || $isMergedButProceed) {
767 // Determine if we need to make an adjustment for the first row in an AutoFilter range that
768 // has a column filter dropdown
769 $filterAdjustment = false;
770 if (!empty($autoFilterIndentRanges)) {
771 foreach ($autoFilterIndentRanges as $autoFilterFirstRowRange) {
772 /** @var string $autoFilterFirstRowRange */
773 if ($cell->isInRange($autoFilterFirstRowRange)) {
774 $filterAdjustment = true;
775
776 break;
777 }
778 }
779 }
780
781 $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
782 $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
783
784 // Calculated value
785 // To formatted string
786 $cellValue = NumberFormat::toFormattedString(
787 $cell->getCalculatedValueString(),
788 (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
789 ->getNumberFormat()->getFormatCode(true)
790 );
791
792 if ($cellValue !== '') {
793 $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
794 $autoSizes[$this->cellCollection->getCurrentColumn()],
795 round(
796 Shared\Font::calculateColumnWidth(
797 $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont(),
798 $cellValue,
799 (int) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
800 ->getAlignment()->getTextRotation(),
801 $this->getParentOrThrow()->getDefaultStyle()->getFont(),
802 $filterAdjustment,
803 $indentAdjustment
804 ),
805 3
806 )
807 );
808 }
809 }
810 }
811 }
812
813 // adjust column widths
814 foreach ($autoSizes as $columnIndex => $width) {
815 if ($width == -1) {
816 $width = $this->getDefaultColumnDimension()->getWidth();
817 }
818 $this->getColumnDimension($columnIndex)->setWidth($width);
819 }
820 $this->activePane = $holdActivePane;
821 }
822 if ($activeSheet !== null && $activeSheet >= 0) {
823 // Okay, I get it now - if $activeSheet is not null,
824 // then $this->getParent() must also be non-null.
825 $this->getParent()->setActiveSheetIndex($activeSheet);
826 }
827 $this->setSelectedCells($selectedCells);
828
829 return $this;
830 }
831
832 /**
833 * Get parent or null.
834 */
835 public function getParent(): ?Spreadsheet
836 {
837 return $this->parent;
838 }
839
840 /**
841 * Get parent, throw exception if null.
842 */
843 public function getParentOrThrow(): Spreadsheet
844 {
845 if ($this->parent !== null) {
846 return $this->parent;
847 }
848
849 throw new Exception('Sheet does not have a parent.');
850 }
851
852 /**
853 * Re-bind parent.
854 *
855 * @return $this
856 */
857 public function rebindParent(Spreadsheet $parent)
858 {
859 if ($this->parent !== null) {
860 $definedNames = $this->parent->getDefinedNames();
861 foreach ($definedNames as $definedName) {
862 $parent->addDefinedName($definedName);
863 }
864
865 $this->parent->removeSheetByIndex(
866 $this->parent->getIndex($this)
867 );
868 }
869 $this->parent = $parent;
870
871 return $this;
872 }
873
874 public function setParent(Spreadsheet $parent): self
875 {
876 $this->parent = $parent;
877
878 return $this;
879 }
880
881 /**
882 * Get title.
883 */
884 public function getTitle(): string
885 {
886 return $this->title;
887 }
888
889 /**
890 * Set title.
891 *
892 * @param string $title String containing the dimension of this worksheet
893 * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
894 * be updated to reflect the new sheet name.
895 * This should be left as the default true, unless you are
896 * certain that no formula cells on any worksheet contain
897 * references to this worksheet
898 * @param bool $validate False to skip validation of new title. WARNING: This should only be set
899 * at parse time (by Readers), where titles can be assumed to be valid.
900 *
901 * @return $this
902 */
903 public function setTitle(string $title, bool $updateFormulaCellReferences = true, bool $validate = true, bool $changeChartSheetNames = true)
904 {
905 // Is this a 'rename' or not?
906 if ($this->getTitle() == $title) {
907 return $this;
908 }
909
910 // Old title
911 $oldTitle = $this->getTitle();
912
913 if ($validate) {
914 // Syntax check
915 self::checkSheetTitle($title);
916
917 if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
918 // Is there already such sheet name?
919 if ($this->parent->sheetNameExists($title)) {
920 // Use name, but append with lowest possible integer
921
922 if (StringHelper::countCharacters($title) > 29) {
923 $title = StringHelper::substring($title, 0, 29);
924 }
925 $i = 1;
926 while ($this->parent->sheetNameExists($title . ' ' . $i)) {
927 ++$i;
928 if ($i == 10) {
929 if (StringHelper::countCharacters($title) > 28) {
930 $title = StringHelper::substring($title, 0, 28);
931 }
932 } elseif ($i == 100) {
933 if (StringHelper::countCharacters($title) > 27) {
934 $title = StringHelper::substring($title, 0, 27);
935 }
936 }
937 }
938
939 $title .= " $i";
940 }
941 }
942 }
943
944 // Set title
945 $this->title = $title;
946
947 if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
948 // New title
949 $newTitle = $this->getTitle();
950 $this->parent->getCalculationEngine()
951 ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
952 if ($updateFormulaCellReferences) {
953 ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
954 }
955 }
956 if ($changeChartSheetNames) {
957 $this->changeChartSheetNames($oldTitle, $title);
958 }
959
960 return $this;
961 }
962
963 private function changeChartSheetNames(string $oldTitle, string $title): void
964 {
965 $worksheets = [$this];
966 if ($this->parent !== null) {
967 $sheets = $this->parent->getAllSheets();
968 if (in_array($this, $sheets, true)) {
969 $worksheets = $sheets;
970 }
971 }
972 $titleq = "'$title'!";
973 $oldTitleq1 = preg_quote("'$oldTitle'!");
974 $oldTitleq2 = preg_quote("$oldTitle!");
975 $preg1 = "/$oldTitleq1|\\b$oldTitleq2/";
976 foreach ($worksheets as $sheet) {
977 foreach ($sheet->getChartCollection() as $chart) {
978 foreach (((($nullsafeVariable3 = $chart->getPlotArea()) ? $nullsafeVariable3->getPlotGroup() : null) ?? []) as $plotGroup) {
979 foreach ($plotGroup->getPlotCategories() as $plotCategory) {
980 $dataSource = (string) $plotCategory->getDataSource();
981 $dataSource2 = Preg::replace($preg1, $titleq, $dataSource);
982 if ($dataSource2 !== $dataSource) {
983 $plotCategory->setDataSource(
984 $dataSource2
985 );
986 }
987 }
988 foreach ($plotGroup->getPlotLabels() as $plotLabel) {
989 $dataSource = (string) $plotLabel->getDataSource();
990 $dataSource2 = Preg::replace($preg1, $titleq, $dataSource);
991 if ($dataSource2 !== $dataSource) {
992 $plotLabel->setDataSource(
993 $dataSource2
994 );
995 }
996 }
997 foreach ($plotGroup->getPlotValues() as $plotValue) {
998 $dataSource = (string) $plotValue->getDataSource();
999 $dataSource2 = Preg::replace($preg1, $titleq, $dataSource);
1000 if ($dataSource2 !== $dataSource) {
1001 $plotValue->setDataSource(
1002 $dataSource2
1003 );
1004 }
1005 }
1006 }
1007 }
1008 }
1009 }
1010
1011 /**
1012 * Get sheet state.
1013 *
1014 * @return string Sheet state (visible, hidden, veryHidden)
1015 */
1016 public function getSheetState(): string
1017 {
1018 return $this->sheetState;
1019 }
1020
1021 /**
1022 * Set sheet state.
1023 *
1024 * @param string $value Sheet state (visible, hidden, veryHidden)
1025 *
1026 * @return $this
1027 */
1028 public function setSheetState(string $value)
1029 {
1030 $this->sheetState = $value;
1031
1032 return $this;
1033 }
1034
1035 /**
1036 * Get page setup.
1037 */
1038 public function getPageSetup(): PageSetup
1039 {
1040 return $this->pageSetup;
1041 }
1042
1043 /**
1044 * Set page setup.
1045 *
1046 * @return $this
1047 */
1048 public function setPageSetup(PageSetup $pageSetup)
1049 {
1050 $this->pageSetup = $pageSetup;
1051
1052 return $this;
1053 }
1054
1055 /**
1056 * Get page margins.
1057 */
1058 public function getPageMargins(): PageMargins
1059 {
1060 return $this->pageMargins;
1061 }
1062
1063 /**
1064 * Set page margins.
1065 *
1066 * @return $this
1067 */
1068 public function setPageMargins(PageMargins $pageMargins)
1069 {
1070 $this->pageMargins = $pageMargins;
1071
1072 return $this;
1073 }
1074
1075 /**
1076 * Get page header/footer.
1077 */
1078 public function getHeaderFooter(): HeaderFooter
1079 {
1080 return $this->headerFooter;
1081 }
1082
1083 /**
1084 * Set page header/footer.
1085 *
1086 * @return $this
1087 */
1088 public function setHeaderFooter(HeaderFooter $headerFooter)
1089 {
1090 $this->headerFooter = $headerFooter;
1091
1092 return $this;
1093 }
1094
1095 /**
1096 * Get sheet view.
1097 */
1098 public function getSheetView(): SheetView
1099 {
1100 return $this->sheetView;
1101 }
1102
1103 /**
1104 * Set sheet view.
1105 *
1106 * @return $this
1107 */
1108 public function setSheetView(SheetView $sheetView)
1109 {
1110 $this->sheetView = $sheetView;
1111
1112 return $this;
1113 }
1114
1115 /**
1116 * Get Protection.
1117 */
1118 public function getProtection(): Protection
1119 {
1120 return $this->protection;
1121 }
1122
1123 /**
1124 * Set Protection.
1125 *
1126 * @return $this
1127 */
1128 public function setProtection(Protection $protection)
1129 {
1130 $this->protection = $protection;
1131
1132 return $this;
1133 }
1134
1135 /**
1136 * Get highest worksheet column.
1137 *
1138 * @param null|int|string $row Return the data highest column for the specified row,
1139 * or the highest column of any row if no row number is passed
1140 *
1141 * @return string Highest column name
1142 */
1143 public function getHighestColumn($row = null): string
1144 {
1145 if ($row === null) {
1146 return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1147 }
1148
1149 return $this->getHighestDataColumn($row);
1150 }
1151
1152 /**
1153 * Get highest worksheet column that contains data.
1154 *
1155 * @param null|int|string $row Return the highest data column for the specified row,
1156 * or the highest data column of any row if no row number is passed
1157 *
1158 * @return string Highest column name that contains data
1159 */
1160 public function getHighestDataColumn($row = null): string
1161 {
1162 return $this->cellCollection->getHighestColumn($row);
1163 }
1164
1165 /**
1166 * Get highest worksheet row.
1167 *
1168 * @param null|string $column Return the highest data row for the specified column,
1169 * or the highest row of any column if no column letter is passed
1170 *
1171 * @return int Highest row number
1172 */
1173 public function getHighestRow(?string $column = null): int
1174 {
1175 if ($column === null) {
1176 return $this->cachedHighestRow;
1177 }
1178
1179 return $this->getHighestDataRow($column);
1180 }
1181
1182 /**
1183 * Get highest worksheet row that contains data.
1184 *
1185 * @param null|string $column Return the highest data row for the specified column,
1186 * or the highest data row of any column if no column letter is passed
1187 *
1188 * @return int Highest row number that contains data
1189 */
1190 public function getHighestDataRow(?string $column = null): int
1191 {
1192 return $this->cellCollection->getHighestRow($column);
1193 }
1194
1195 /**
1196 * Get highest worksheet column and highest row that have cell records.
1197 *
1198 * @return array{row: int, column: string} Highest column name and highest row number
1199 */
1200 public function getHighestRowAndColumn(): array
1201 {
1202 return $this->cellCollection->getHighestRowAndColumn();
1203 }
1204
1205 /**
1206 * Set a cell value.
1207 *
1208 * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1209 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1210 * @param mixed $value Value for the cell
1211 * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1212 *
1213 * @return $this
1214 */
1215 public function setCellValue($coordinate, $value, ?IValueBinder $binder = null)
1216 {
1217 $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1218 $this->getCell($cellAddress)->setValue($value, $binder);
1219
1220 return $this;
1221 }
1222
1223 /**
1224 * Set a cell value.
1225 *
1226 * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1227 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1228 * @param mixed $value Value of the cell
1229 * @param string $dataType Explicit data type, see DataType::TYPE_*
1230 * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1231 * method, then it is your responsibility as an end-user developer to validate that the value and
1232 * the datatype match.
1233 * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1234 * that you specify.
1235 *
1236 * @see DataType
1237 *
1238 * @return $this
1239 */
1240 public function setCellValueExplicit($coordinate, $value, string $dataType)
1241 {
1242 $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1243 $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
1244
1245 return $this;
1246 }
1247
1248 /**
1249 * Get cell at a specific coordinate.
1250 *
1251 * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1252 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1253 *
1254 * @return Cell Cell that was found or created
1255 * WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1256 * "active" cell at a time in memory. If you assign that cell to a variable, then select
1257 * another cell using getCell() or any of its variants, the newly selected cell becomes
1258 * the "active" cell, and any previous assignment becomes a disconnected reference because
1259 * the active cell has changed.
1260 */
1261 public function getCell($coordinate): Cell
1262 {
1263 $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1264
1265 // Shortcut for increased performance for the vast majority of simple cases
1266 if ($this->cellCollection->has($cellAddress)) {
1267 /** @var Cell $cell */
1268 $cell = $this->cellCollection->get($cellAddress);
1269
1270 return $cell;
1271 }
1272
1273 /** @var Worksheet $sheet */
1274 [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1275 $cell = $sheet->getCellCollection()->get($finalCoordinate);
1276
1277 return $cell ?? $sheet->createNewCell($finalCoordinate);
1278 }
1279
1280 /**
1281 * Get the correct Worksheet and coordinate from a coordinate that may
1282 * contains reference to another sheet or a named range.
1283 *
1284 * @return array{0: Worksheet, 1: string}
1285 */
1286 private function getWorksheetAndCoordinate(string $coordinate): array
1287 {
1288 $sheet = null;
1289 $finalCoordinate = null;
1290
1291 // Worksheet reference?
1292 if (str_contains($coordinate, '!')) {
1293 $worksheetReference = self::extractSheetTitle($coordinate, true, true);
1294
1295 $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
1296 $finalCoordinate = strtoupper($worksheetReference[1]);
1297
1298 if ($sheet === null) {
1299 throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
1300 }
1301 } elseif (
1302 !Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1303 && Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1304 ) {
1305 // Named range?
1306 $namedRange = $this->validateNamedRange($coordinate, true);
1307 if ($namedRange !== null) {
1308 $sheet = $namedRange->getWorksheet();
1309 if ($sheet === null) {
1310 throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
1311 }
1312
1313 $cellCoordinate = ltrim((string) substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
1314 $finalCoordinate = str_replace('$', '', $cellCoordinate);
1315 }
1316 }
1317
1318 if ($sheet === null || $finalCoordinate === null) {
1319 $sheet = $this;
1320 $finalCoordinate = strtoupper($coordinate);
1321 }
1322
1323 if (Coordinate::coordinateIsRange($finalCoordinate)) {
1324 throw new Exception('Cell coordinate string can not be a range of cells.');
1325 }
1326 $finalCoordinate = str_replace('$', '', $finalCoordinate);
1327
1328 return [$sheet, $finalCoordinate];
1329 }
1330
1331 /**
1332 * Get an existing cell at a specific coordinate, or null.
1333 *
1334 * @param string $coordinate Coordinate of the cell, eg: 'A1'
1335 *
1336 * @return null|Cell Cell that was found or null
1337 */
1338 private function getCellOrNull(string $coordinate): ?Cell
1339 {
1340 // Check cell collection
1341 if ($this->cellCollection->has($coordinate)) {
1342 return $this->cellCollection->get($coordinate);
1343 }
1344
1345 return null;
1346 }
1347
1348 /**
1349 * Create a new cell at the specified coordinate.
1350 *
1351 * @param string $coordinate Coordinate of the cell
1352 *
1353 * @return Cell Cell that was created
1354 * WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1355 * "active" cell at a time in memory. If you assign that cell to a variable, then select
1356 * another cell using getCell() or any of its variants, the newly selected cell becomes
1357 * the "active" cell, and any previous assignment becomes a disconnected reference because
1358 * the active cell has changed.
1359 */
1360 public function createNewCell(string $coordinate): Cell
1361 {
1362 [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
1363 $cell = new Cell(null, DataType::TYPE_NULL, $this);
1364 $this->cellCollection->add($coordinate, $cell);
1365
1366 // Coordinates
1367 if ($column > $this->cachedHighestColumn) {
1368 $this->cachedHighestColumn = $column;
1369 }
1370 if ($row > $this->cachedHighestRow) {
1371 $this->cachedHighestRow = $row;
1372 }
1373
1374 // Cell needs appropriate xfIndex from dimensions records
1375 // but don't create dimension records if they don't already exist
1376 $rowDimension = $this->rowDimensions[$row] ?? null;
1377 $columnDimension = $this->columnDimensions[$columnString] ?? null;
1378
1379 $xfSet = false;
1380 if ($rowDimension !== null) {
1381 $rowXf = (int) $rowDimension->getXfIndex();
1382 if ($rowXf > 0) {
1383 // then there is a row dimension with explicit style, assign it to the cell
1384 $cell->setXfIndex($rowXf);
1385 $xfSet = true;
1386 }
1387 }
1388 if (!$xfSet && $columnDimension !== null) {
1389 $colXf = (int) $columnDimension->getXfIndex();
1390 if ($colXf > 0) {
1391 // then there is a column dimension, assign it to the cell
1392 $cell->setXfIndex($colXf);
1393 }
1394 }
1395
1396 return $cell;
1397 }
1398
1399 /**
1400 * Does the cell at a specific coordinate exist?
1401 *
1402 * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1403 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1404 */
1405 public function cellExists($coordinate): bool
1406 {
1407 $cellAddress = Validations::validateCellAddress($coordinate);
1408 [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1409
1410 return $sheet->getCellCollection()->has($finalCoordinate);
1411 }
1412
1413 /**
1414 * Get row dimension at a specific row.
1415 *
1416 * @param int $row Numeric index of the row
1417 */
1418 public function getRowDimension(int $row): RowDimension
1419 {
1420 // Get row dimension
1421 if (!isset($this->rowDimensions[$row])) {
1422 $this->rowDimensions[$row] = new RowDimension($row);
1423
1424 $this->cachedHighestRow = max($this->cachedHighestRow, $row);
1425 }
1426
1427 return $this->rowDimensions[$row];
1428 }
1429
1430 public function getRowStyle(int $row): ?Style
1431 {
1432 return ($nullsafeVariable4 = $this->parent) ? $nullsafeVariable4->getCellXfByIndexOrNull(($nullsafeVariable6 = $this->rowDimensions[$row] ?? null) ? $nullsafeVariable6->getXfIndex() : null) : null;
1433 }
1434
1435 public function rowDimensionExists(int $row): bool
1436 {
1437 return isset($this->rowDimensions[$row]);
1438 }
1439
1440 public function columnDimensionExists(string $column): bool
1441 {
1442 return isset($this->columnDimensions[$column]);
1443 }
1444
1445 /**
1446 * Get column dimension at a specific column.
1447 *
1448 * @param string $column String index of the column eg: 'A'
1449 */
1450 public function getColumnDimension(string $column): ColumnDimension
1451 {
1452 // Uppercase coordinate
1453 $column = strtoupper($column);
1454
1455 // Fetch dimensions
1456 if (!isset($this->columnDimensions[$column])) {
1457 $this->columnDimensions[$column] = new ColumnDimension($column);
1458
1459 $columnIndex = Coordinate::columnIndexFromString($column);
1460 if ($this->cachedHighestColumn < $columnIndex) {
1461 $this->cachedHighestColumn = $columnIndex;
1462 }
1463 }
1464
1465 return $this->columnDimensions[$column];
1466 }
1467
1468 /**
1469 * Get column dimension at a specific column by using numeric cell coordinates.
1470 *
1471 * @param int $columnIndex Numeric column coordinate of the cell
1472 */
1473 public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
1474 {
1475 return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
1476 }
1477
1478 public function getColumnStyle(string $column): ?Style
1479 {
1480 return ($nullsafeVariable5 = $this->parent) ? $nullsafeVariable5->getCellXfByIndexOrNull(($nullsafeVariable7 = $this->columnDimensions[$column] ?? null) ? $nullsafeVariable7->getXfIndex() : null) : null;
1481 }
1482
1483 /**
1484 * Get style for cell.
1485 *
1486 * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellCoordinate
1487 * A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1488 * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1489 * or a CellAddress or AddressRange object.
1490 */
1491 public function getStyle($cellCoordinate): Style
1492 {
1493 if (is_string($cellCoordinate)) {
1494 $cellCoordinate = Validations::definedNameToCoordinate($cellCoordinate, $this);
1495 }
1496 $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
1497 $cellCoordinate = str_replace('$', '', $cellCoordinate);
1498
1499 // set this sheet as active
1500 $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
1501
1502 // set cell coordinate as active
1503 $this->setSelectedCells($cellCoordinate);
1504
1505 return $this->getParentOrThrow()->getCellXfSupervisor();
1506 }
1507
1508 /**
1509 * Get table styles set for the for given cell.
1510 *
1511 * @param Cell $cell
1512 * The Cell for which the tables are retrieved
1513 *
1514 * @return Table[]
1515 */
1516 public function getTablesWithStylesForCell(Cell $cell): array
1517 {
1518 $retVal = [];
1519
1520 foreach ($this->tableCollection as $table) {
1521 $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle();
1522 if ($dxfsTableStyle !== null) {
1523 if ($dxfsTableStyle->getHeaderRowStyle() !== null || $dxfsTableStyle->getFirstRowStripeStyle() !== null || $dxfsTableStyle->getSecondRowStripeStyle() !== null) {
1524 $range = $table->getRange();
1525 if ($cell->isInRange($range)) {
1526 $retVal[] = $table;
1527 }
1528 }
1529 }
1530 }
1531
1532 return $retVal;
1533 }
1534
1535 /**
1536 * Get tables without styles set for the for given cell.
1537 *
1538 * @param Cell $cell
1539 * The Cell for which the tables are retrieved
1540 *
1541 * @return Table[]
1542 */
1543 public function getTablesWithoutStylesForCell(Cell $cell): array
1544 {
1545 $retVal = [];
1546
1547 foreach ($this->tableCollection as $table) {
1548 $range = $table->getRange();
1549 if ($cell->isInRange($range)) {
1550 $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle();
1551 if ($dxfsTableStyle === null || ($dxfsTableStyle->getHeaderRowStyle() === null && $dxfsTableStyle->getFirstRowStripeStyle() === null && $dxfsTableStyle->getSecondRowStripeStyle() === null)) {
1552 $retVal[] = $table;
1553 }
1554 }
1555 }
1556
1557 return $retVal;
1558 }
1559
1560 /**
1561 * Get conditional styles for a cell.
1562 *
1563 * @param string $coordinate eg: 'A1' or 'A1:A3'.
1564 * If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1565 * included in a conditional style range.
1566 * If a range of cells is specified, then the styles will only be returned if the range matches the entire
1567 * range of the conditional.
1568 * @param bool $firstOnly default true, return all matching
1569 * conditionals ordered by priority if false, first only if true
1570 *
1571 * @return Conditional[]
1572 */
1573 public function getConditionalStyles(string $coordinate, bool $firstOnly = true): array
1574 {
1575 $coordinate = strtoupper($coordinate);
1576 if (Preg::isMatch('/[: ,]/', $coordinate)) {
1577 return $this->conditionalStylesCollection[$coordinate] ?? [];
1578 }
1579
1580 $conditionalStyles = [];
1581 foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
1582 $keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
1583 $keyParts = explode(',', $keyStyles);
1584 foreach ($keyParts as $keyPart) {
1585 if ($keyPart === $coordinate) {
1586 if ($firstOnly) {
1587 return $conditionalRange;
1588 }
1589 $conditionalStyles[$keyStylesOrig] = $conditionalRange;
1590
1591 break;
1592 } elseif (str_contains($keyPart, ':')) {
1593 if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
1594 if ($firstOnly) {
1595 return $conditionalRange;
1596 }
1597 $conditionalStyles[$keyStylesOrig] = $conditionalRange;
1598
1599 break;
1600 }
1601 }
1602 }
1603 }
1604 $outArray = [];
1605 foreach ($conditionalStyles as $conditionalArray) {
1606 foreach ($conditionalArray as $conditional) {
1607 $outArray[] = $conditional;
1608 }
1609 }
1610 usort($outArray, [self::class, 'comparePriority']);
1611
1612 return $outArray;
1613 }
1614
1615 private static function comparePriority(Conditional $condA, Conditional $condB): int
1616 {
1617 $a = $condA->getPriority();
1618 $b = $condB->getPriority();
1619 if ($a === $b) {
1620 return 0;
1621 }
1622 if ($a === 0) {
1623 return 1;
1624 }
1625 if ($b === 0) {
1626 return -1;
1627 }
1628
1629 return ($a < $b) ? -1 : 1;
1630 }
1631
1632 public function getConditionalRange(string $coordinate): ?string
1633 {
1634 $coordinate = strtoupper($coordinate);
1635 $cell = $this->getCell($coordinate);
1636 foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1637 $cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
1638 foreach ($cellBlocks as $cellBlock) {
1639 if ($cell->isInRange($cellBlock)) {
1640 return $conditionalRange;
1641 }
1642 }
1643 }
1644
1645 return null;
1646 }
1647
1648 /**
1649 * Do conditional styles exist for this cell?
1650 *
1651 * @param string $coordinate eg: 'A1' or 'A1:A3'.
1652 * If a single cell is specified, then this method will return true if that cell is included in a
1653 * conditional style range.
1654 * If a range of cells is specified, then true will only be returned if the range matches the entire
1655 * range of the conditional.
1656 */
1657 public function conditionalStylesExists(string $coordinate): bool
1658 {
1659 return !empty($this->getConditionalStyles($coordinate));
1660 }
1661
1662 /**
1663 * Removes conditional styles for a cell.
1664 *
1665 * @param string $coordinate eg: 'A1'
1666 *
1667 * @return $this
1668 */
1669 public function removeConditionalStyles(string $coordinate)
1670 {
1671 unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
1672
1673 return $this;
1674 }
1675
1676 /**
1677 * Get collection of conditional styles.
1678 *
1679 * @return Conditional[][]
1680 */
1681 public function getConditionalStylesCollection(): array
1682 {
1683 return $this->conditionalStylesCollection;
1684 }
1685
1686 /**
1687 * Set conditional styles.
1688 *
1689 * @param string $coordinate eg: 'A1'
1690 * @param Conditional[] $styles
1691 *
1692 * @return $this
1693 */
1694 public function setConditionalStyles(string $coordinate, array $styles)
1695 {
1696 $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
1697
1698 return $this;
1699 }
1700
1701 /**
1702 * Duplicate cell style to a range of cells.
1703 *
1704 * Please note that this will overwrite existing cell styles for cells in range!
1705 *
1706 * @param Style $style Cell style to duplicate
1707 * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1708 *
1709 * @return $this
1710 */
1711 public function duplicateStyle(Style $style, string $range)
1712 {
1713 // Add the style to the workbook if necessary
1714 $workbook = $this->getParentOrThrow();
1715 if ($existingStyle = $workbook->getCellXfByHashCode($style->getHashCode())) {
1716 // there is already such cell Xf in our collection
1717 $xfIndex = $existingStyle->getIndex();
1718 } else {
1719 // we don't have such a cell Xf, need to add
1720 $workbook->addCellXf($style);
1721 $xfIndex = $style->getIndex();
1722 }
1723
1724 // Calculate range outer borders
1725 [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1726
1727 // Make sure we can loop upwards on rows and columns
1728 if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1729 $tmp = $rangeStart;
1730 $rangeStart = $rangeEnd;
1731 $rangeEnd = $tmp;
1732 }
1733
1734 // Loop through cells and apply styles
1735 for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1736 for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1737 $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
1738 }
1739 }
1740
1741 return $this;
1742 }
1743
1744 /**
1745 * Duplicate conditional style to a range of cells.
1746 *
1747 * Please note that this will overwrite existing cell styles for cells in range!
1748 *
1749 * @param Conditional[] $styles Cell style to duplicate
1750 * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1751 *
1752 * @return $this
1753 */
1754 public function duplicateConditionalStyle(array $styles, string $range = '')
1755 {
1756 foreach ($styles as $cellStyle) {
1757 if (!($cellStyle instanceof Conditional)) {
1758 throw new Exception('Style is not a conditional style');
1759 }
1760 }
1761
1762 // Calculate range outer borders
1763 [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1764
1765 // Make sure we can loop upwards on rows and columns
1766 if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1767 $tmp = $rangeStart;
1768 $rangeStart = $rangeEnd;
1769 $rangeEnd = $tmp;
1770 }
1771
1772 // Loop through cells and apply styles
1773 for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1774 for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1775 $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
1776 }
1777 }
1778
1779 return $this;
1780 }
1781
1782 /**
1783 * Set break on a cell.
1784 *
1785 * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1786 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1787 * @param int $break Break type (type of Worksheet::BREAK_*)
1788 *
1789 * @return $this
1790 */
1791 public function setBreak($coordinate, int $break, int $max = -1)
1792 {
1793 $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1794
1795 if ($break === self::BREAK_NONE) {
1796 unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
1797 } elseif ($break === self::BREAK_ROW) {
1798 $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
1799 } elseif ($break === self::BREAK_COLUMN) {
1800 $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
1801 }
1802
1803 return $this;
1804 }
1805
1806 /**
1807 * Get breaks.
1808 *
1809 * @return int[]
1810 */
1811 public function getBreaks(): array
1812 {
1813 $breaks = [];
1814 /** @var callable $compareFunction */
1815 $compareFunction = [self::class, 'compareRowBreaks'];
1816 uksort($this->rowBreaks, $compareFunction);
1817 foreach ($this->rowBreaks as $break) {
1818 $breaks[$break->getCoordinate()] = self::BREAK_ROW;
1819 }
1820 /** @var callable $compareFunction */
1821 $compareFunction = [self::class, 'compareColumnBreaks'];
1822 uksort($this->columnBreaks, $compareFunction);
1823 foreach ($this->columnBreaks as $break) {
1824 $breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
1825 }
1826
1827 return $breaks;
1828 }
1829
1830 /**
1831 * Get row breaks.
1832 *
1833 * @return PageBreak[]
1834 */
1835 public function getRowBreaks(): array
1836 {
1837 /** @var callable $compareFunction */
1838 $compareFunction = [self::class, 'compareRowBreaks'];
1839 uksort($this->rowBreaks, $compareFunction);
1840
1841 return $this->rowBreaks;
1842 }
1843
1844 protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
1845 {
1846 $row1 = Coordinate::indexesFromString($coordinate1)[1];
1847 $row2 = Coordinate::indexesFromString($coordinate2)[1];
1848
1849 return $row1 - $row2;
1850 }
1851
1852 protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
1853 {
1854 $column1 = Coordinate::indexesFromString($coordinate1)[0];
1855 $column2 = Coordinate::indexesFromString($coordinate2)[0];
1856
1857 return $column1 - $column2;
1858 }
1859
1860 /**
1861 * Get column breaks.
1862 *
1863 * @return PageBreak[]
1864 */
1865 public function getColumnBreaks(): array
1866 {
1867 /** @var callable $compareFunction */
1868 $compareFunction = [self::class, 'compareColumnBreaks'];
1869 uksort($this->columnBreaks, $compareFunction);
1870
1871 return $this->columnBreaks;
1872 }
1873
1874 /**
1875 * Set merge on a cell range.
1876 *
1877 * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10'
1878 * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1879 * or an AddressRange.
1880 * @param string $behaviour How the merged cells should behave.
1881 * Possible values are:
1882 * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1883 * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1884 * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1885 *
1886 * @return $this
1887 */
1888 public function mergeCells($range, string $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
1889 {
1890 $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
1891
1892 if (!str_contains($range, ':')) {
1893 $range .= ":{$range}";
1894 }
1895
1896 if (!Preg::isMatch('/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/', $range, $matches)) {
1897 throw new Exception('Merge must be on a valid range of cells.');
1898 }
1899
1900 $this->mergeCells[$range] = $range;
1901 $firstRow = (int) $matches[2];
1902 $lastRow = (int) $matches[4];
1903 $firstColumn = $matches[1];
1904 $lastColumn = $matches[3];
1905 $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
1906 $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
1907 $numberRows = $lastRow - $firstRow;
1908 $numberColumns = $lastColumnIndex - $firstColumnIndex;
1909
1910 if ($numberRows === 1 && $numberColumns === 1) {
1911 return $this;
1912 }
1913
1914 // create upper left cell if it does not already exist
1915 $upperLeft = "{$firstColumn}{$firstRow}";
1916 if (!$this->cellExists($upperLeft)) {
1917 $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
1918 }
1919
1920 if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
1921 // Blank out the rest of the cells in the range (if they exist)
1922 if ($numberRows > $numberColumns) {
1923 $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
1924 } else {
1925 $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
1926 }
1927 }
1928
1929 return $this;
1930 }
1931
1932 private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1933 {
1934 $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1935 ? [$this->getCell($upperLeft)->getFormattedValue()]
1936 : [];
1937
1938 foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
1939 $iterator = $column->getCellIterator($firstRow);
1940 $iterator->setIterateOnlyExistingCells(true);
1941 foreach ($iterator as $cell) {
1942 $row = $cell->getRow();
1943 if ($row > $lastRow) {
1944 break;
1945 }
1946 $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1947 }
1948 }
1949
1950 if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1951 /** @var string[] $leftCellValue */
1952 $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1953 }
1954 }
1955
1956 private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1957 {
1958 $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1959 ? [$this->getCell($upperLeft)->getFormattedValue()]
1960 : [];
1961
1962 foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
1963 $iterator = $row->getCellIterator($firstColumn);
1964 $iterator->setIterateOnlyExistingCells(true);
1965 foreach ($iterator as $cell) {
1966 $column = $cell->getColumn();
1967 $columnIndex = Coordinate::columnIndexFromString($column);
1968 if ($columnIndex > $lastColumnIndex) {
1969 break;
1970 }
1971 $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1972 }
1973 }
1974
1975 if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1976 /** @var string[] $leftCellValue */
1977 $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1978 }
1979 }
1980
1981 /**
1982 * @param mixed[] $leftCellValue
1983 *
1984 * @return mixed[]
1985 */
1986 public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
1987 {
1988 if ($cell->getCoordinate() !== $upperLeft) {
1989 Calculation::getInstance($cell->getWorksheet()->getParentOrThrow())->flushInstance();
1990 if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1991 $cellValue = $cell->getFormattedValue();
1992 if ($cellValue !== '') {
1993 $leftCellValue[] = $cellValue;
1994 }
1995 }
1996 $cell->setValueExplicit(null, DataType::TYPE_NULL);
1997 }
1998
1999 return $leftCellValue;
2000 }
2001
2002 /**
2003 * Remove merge on a cell range.
2004 *
2005 * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10'
2006 * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2007 * or an AddressRange.
2008 *
2009 * @return $this
2010 */
2011 public function unmergeCells($range)
2012 {
2013 $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
2014
2015 if (str_contains($range, ':')) {
2016 if (isset($this->mergeCells[$range])) {
2017 unset($this->mergeCells[$range]);
2018 } else {
2019 throw new Exception('Cell range ' . $range . ' not known as merged.');
2020 }
2021 } else {
2022 throw new Exception('Merge can only be removed from a range of cells.');
2023 }
2024
2025 return $this;
2026 }
2027
2028 /**
2029 * Get merge cells array.
2030 *
2031 * @return string[]
2032 */
2033 public function getMergeCells(): array
2034 {
2035 return $this->mergeCells;
2036 }
2037
2038 /**
2039 * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
2040 * a single cell range.
2041 *
2042 * @param string[] $mergeCells
2043 *
2044 * @return $this
2045 */
2046 public function setMergeCells(array $mergeCells)
2047 {
2048 $this->mergeCells = $mergeCells;
2049
2050 return $this;
2051 }
2052
2053 /**
2054 * Set protection on a cell or cell range.
2055 *
2056 * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2057 * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2058 * or a CellAddress or AddressRange object.
2059 * @param string $password Password to unlock the protection
2060 * @param bool $alreadyHashed If the password has already been hashed, set this to true
2061 *
2062 * @return $this
2063 */
2064 public function protectCells($range, string $password = '', bool $alreadyHashed = false, string $name = '', string $securityDescriptor = '')
2065 {
2066 $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2067
2068 if (!$alreadyHashed && $password !== '') {
2069 $password = Shared\PasswordHasher::hashPassword($password);
2070 }
2071 $this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor);
2072
2073 return $this;
2074 }
2075
2076 /**
2077 * Remove protection on a cell or cell range.
2078 *
2079 * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2080 * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2081 * or a CellAddress or AddressRange object.
2082 *
2083 * @return $this
2084 */
2085 public function unprotectCells($range)
2086 {
2087 $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2088
2089 if (isset($this->protectedCells[$range])) {
2090 unset($this->protectedCells[$range]);
2091 } else {
2092 throw new Exception('Cell range ' . $range . ' not known as protected.');
2093 }
2094
2095 return $this;
2096 }
2097
2098 /**
2099 * Get protected cells.
2100 *
2101 * @return ProtectedRange[]
2102 */
2103 public function getProtectedCellRanges(): array
2104 {
2105 return $this->protectedCells;
2106 }
2107
2108 /**
2109 * Get Autofilter.
2110 */
2111 public function getAutoFilter(): AutoFilter
2112 {
2113 return $this->autoFilter;
2114 }
2115
2116 /**
2117 * Set AutoFilter.
2118 *
2119 * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange
2120 * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2121 * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2122 * or an AddressRange.
2123 *
2124 * @return $this
2125 */
2126 public function setAutoFilter($autoFilterOrRange)
2127 {
2128 if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
2129 $this->autoFilter = $autoFilterOrRange;
2130 } else {
2131 $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
2132
2133 $this->autoFilter->setRange($cellRange);
2134 }
2135
2136 return $this;
2137 }
2138
2139 /**
2140 * Remove autofilter.
2141 */
2142 public function removeAutoFilter(): self
2143 {
2144 $this->autoFilter->setRange('');
2145
2146 return $this;
2147 }
2148
2149 /**
2150 * Get collection of Tables.
2151 *
2152 * @return ArrayObject<int, Table>
2153 */
2154 public function getTableCollection(): ArrayObject
2155 {
2156 return $this->tableCollection;
2157 }
2158
2159 /**
2160 * Add Table.
2161 *
2162 * @return $this
2163 */
2164 public function addTable(Table $table): self
2165 {
2166 $table->setWorksheet($this);
2167 $this->tableCollection[] = $table;
2168
2169 return $this;
2170 }
2171
2172 /**
2173 * @return string[] array of Table names
2174 */
2175 public function getTableNames(): array
2176 {
2177 $tableNames = [];
2178
2179 foreach ($this->tableCollection as $table) {
2180 /** @var Table $table */
2181 $tableNames[] = $table->getName();
2182 }
2183
2184 return $tableNames;
2185 }
2186
2187 /**
2188 * @param string $name the table name to search
2189 *
2190 * @return null|Table The table from the tables collection, or null if not found
2191 */
2192 public function getTableByName(string $name): ?Table
2193 {
2194 $tableIndex = $this->getTableIndexByName($name);
2195
2196 return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
2197 }
2198
2199 /**
2200 * @param string $name the table name to search
2201 *
2202 * @return null|int The index of the located table in the tables collection, or null if not found
2203 */
2204 protected function getTableIndexByName(string $name): ?int
2205 {
2206 $name = StringHelper::strToUpper($name);
2207 foreach ($this->tableCollection as $index => $table) {
2208 /** @var Table $table */
2209 if (StringHelper::strToUpper($table->getName()) === $name) {
2210 return $index;
2211 }
2212 }
2213
2214 return null;
2215 }
2216
2217 /**
2218 * Remove Table by name.
2219 *
2220 * @param string $name Table name
2221 *
2222 * @return $this
2223 */
2224 public function removeTableByName(string $name): self
2225 {
2226 $tableIndex = $this->getTableIndexByName($name);
2227
2228 if ($tableIndex !== null) {
2229 unset($this->tableCollection[$tableIndex]);
2230 }
2231
2232 return $this;
2233 }
2234
2235 /**
2236 * Remove collection of Tables.
2237 */
2238 public function removeTableCollection(): self
2239 {
2240 $this->tableCollection = new ArrayObject();
2241
2242 return $this;
2243 }
2244
2245 /**
2246 * Get collection of SparklineGroups.
2247 *
2248 * @return ArrayObject<int, SparklineGroup>
2249 */
2250 public function getSparklineGroupCollection(): ArrayObject
2251 {
2252 return $this->sparklineGroupCollection;
2253 }
2254
2255 /**
2256 * Add a SparklineGroup.
2257 *
2258 * @return $this
2259 */
2260 public function addSparklineGroup(SparklineGroup $sparklineGroup): self
2261 {
2262 $this->sparklineGroupCollection[] = $sparklineGroup;
2263
2264 return $this;
2265 }
2266
2267 /**
2268 * Add a single Sparkline, wrapping it in its own SparklineGroup.
2269 *
2270 * This is a convenience method for the common case of adding one sparkline
2271 * with default formatting; the created group is returned so its formatting
2272 * can be adjusted.
2273 *
2274 * @param mixed $type the type of sparkline (defaults to line)
2275 * @param \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Sparkline\SparklineType::* $type
2276 */
2277 public function addSparkline(Sparkline $sparkline, $type = SparklineType::Line): SparklineGroup
2278 {
2279 $group = new SparklineGroup();
2280 $group->setType($type);
2281 $group->addSparkline($sparkline);
2282 $this->addSparklineGroup($group);
2283
2284 return $group;
2285 }
2286
2287 /**
2288 * Remove all SparklineGroups.
2289 *
2290 * @return $this
2291 */
2292 public function removeSparklineGroupCollection(): self
2293 {
2294 $this->sparklineGroupCollection = new ArrayObject();
2295
2296 return $this;
2297 }
2298
2299 /**
2300 * Get collection of PivotTables.
2301 *
2302 * @return ArrayObject<int, PivotTable>
2303 */
2304 public function getPivotTableCollection(): ArrayObject
2305 {
2306 return $this->pivotTableCollection;
2307 }
2308
2309 /**
2310 * Get collection of PivotTables (alias of getPivotTableCollection()).
2311 *
2312 * @return ArrayObject<int, PivotTable>
2313 */
2314 public function getPivotTables(): ArrayObject
2315 {
2316 return $this->pivotTableCollection;
2317 }
2318
2319 /**
2320 * Add a PivotTable to this worksheet.
2321 *
2322 * @return $this
2323 */
2324 public function addPivotTable(PivotTable $pivotTable): self
2325 {
2326 $pivotTable->setWorksheet($this);
2327 $this->pivotTableCollection[] = $pivotTable;
2328
2329 return $this;
2330 }
2331
2332 /**
2333 * @return string[] array of PivotTable names
2334 */
2335 public function getPivotTableNames(): array
2336 {
2337 $pivotTableNames = [];
2338
2339 foreach ($this->pivotTableCollection as $pivotTable) {
2340 $pivotTableNames[] = $pivotTable->getName();
2341 }
2342
2343 return $pivotTableNames;
2344 }
2345
2346 /**
2347 * @param string $name the pivot table name to search
2348 *
2349 * @return null|PivotTable The pivot table from the collection, or null if not found
2350 */
2351 public function getPivotTableByName(string $name): ?PivotTable
2352 {
2353 $name = StringHelper::strToUpper($name);
2354 foreach ($this->pivotTableCollection as $pivotTable) {
2355 if (StringHelper::strToUpper($pivotTable->getName()) === $name) {
2356 return $pivotTable;
2357 }
2358 }
2359
2360 return null;
2361 }
2362
2363 /**
2364 * Remove collection of PivotTables.
2365 */
2366 public function removePivotTableCollection(): self
2367 {
2368 $this->pivotTableCollection = new ArrayObject();
2369
2370 return $this;
2371 }
2372
2373 /**
2374 * Get Freeze Pane.
2375 */
2376 public function getFreezePane(): ?string
2377 {
2378 return $this->freezePane;
2379 }
2380
2381 /**
2382 * Freeze Pane.
2383 *
2384 * Examples:
2385 *
2386 * - A2 will freeze the rows above cell A2 (i.e row 1)
2387 * - B1 will freeze the columns to the left of cell B1 (i.e column A)
2388 * - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2389 *
2390 * @param null|array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2391 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2392 * Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2393 * @param null|array{0: int, 1: int}|CellAddress|string $topLeftCell default position of the right bottom pane
2394 * Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2395 * or a CellAddress object.
2396 *
2397 * @return $this
2398 */
2399 public function freezePane($coordinate, $topLeftCell = null, bool $frozenSplit = false)
2400 {
2401 $this->panes = [
2402 'bottomRight' => null,
2403 'bottomLeft' => null,
2404 'topRight' => null,
2405 'topLeft' => null,
2406 ];
2407 $cellAddress = ($coordinate !== null)
2408 ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
2409 : null;
2410 if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
2411 throw new Exception('Freeze pane can not be set on a range of cells.');
2412 }
2413 $topLeftCell = ($topLeftCell !== null)
2414 ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
2415 : null;
2416
2417 if ($cellAddress !== null && $topLeftCell === null) {
2418 $coordinate = Coordinate::coordinateFromString($cellAddress);
2419 $topLeftCell = $coordinate[0] . $coordinate[1];
2420 }
2421
2422 $topLeftCell = "$topLeftCell";
2423 $this->paneTopLeftCell = $topLeftCell;
2424
2425 $this->freezePane = $cellAddress;
2426 $this->topLeftCell = $topLeftCell;
2427 if ($cellAddress === null) {
2428 $this->paneState = '';
2429 $this->xSplit = $this->ySplit = 0;
2430 $this->activePane = '';
2431 } else {
2432 $coordinates = Coordinate::indexesFromString($cellAddress);
2433 $this->xSplit = $coordinates[0] - 1;
2434 $this->ySplit = $coordinates[1] - 1;
2435 if ($this->xSplit > 0 || $this->ySplit > 0) {
2436 $this->paneState = $frozenSplit ? self::PANE_FROZENSPLIT : self::PANE_FROZEN;
2437 $this->setSelectedCellsActivePane();
2438 } else {
2439 $this->paneState = '';
2440 $this->freezePane = null;
2441 $this->activePane = '';
2442 }
2443 }
2444
2445 return $this;
2446 }
2447
2448 public function setTopLeftCell(string $topLeftCell): self
2449 {
2450 $this->topLeftCell = $topLeftCell;
2451
2452 return $this;
2453 }
2454
2455 /**
2456 * Unfreeze Pane.
2457 *
2458 * @return $this
2459 */
2460 public function unfreezePane()
2461 {
2462 return $this->freezePane(null);
2463 }
2464
2465 /**
2466 * Get the default position of the right bottom pane.
2467 */
2468 public function getTopLeftCell(): ?string
2469 {
2470 return $this->topLeftCell;
2471 }
2472
2473 public function getPaneTopLeftCell(): string
2474 {
2475 return $this->paneTopLeftCell;
2476 }
2477
2478 public function setPaneTopLeftCell(string $paneTopLeftCell): self
2479 {
2480 $this->paneTopLeftCell = $paneTopLeftCell;
2481
2482 return $this;
2483 }
2484
2485 public function usesPanes(): bool
2486 {
2487 return $this->xSplit > 0 || $this->ySplit > 0;
2488 }
2489
2490 public function getPane(string $position): ?Pane
2491 {
2492 return $this->panes[$position] ?? null;
2493 }
2494
2495 public function setPane(string $position, ?Pane $pane): self
2496 {
2497 if (array_key_exists($position, $this->panes)) {
2498 $this->panes[$position] = $pane;
2499 }
2500
2501 return $this;
2502 }
2503
2504 /** @return (null|Pane)[] */
2505 public function getPanes(): array
2506 {
2507 return $this->panes;
2508 }
2509
2510 public function getActivePane(): string
2511 {
2512 return $this->activePane;
2513 }
2514
2515 public function setActivePane(string $activePane): self
2516 {
2517 $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
2518
2519 return $this;
2520 }
2521
2522 public function getXSplit(): int
2523 {
2524 return $this->xSplit;
2525 }
2526
2527 public function setXSplit(int $xSplit): self
2528 {
2529 $this->xSplit = $xSplit;
2530 if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
2531 $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
2532 }
2533
2534 return $this;
2535 }
2536
2537 public function getYSplit(): int
2538 {
2539 return $this->ySplit;
2540 }
2541
2542 public function setYSplit(int $ySplit): self
2543 {
2544 $this->ySplit = $ySplit;
2545 if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
2546 $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
2547 }
2548
2549 return $this;
2550 }
2551
2552 public function getPaneState(): string
2553 {
2554 return $this->paneState;
2555 }
2556
2557 public const PANE_FROZEN = 'frozen';
2558 public const PANE_FROZENSPLIT = 'frozenSplit';
2559 public const PANE_SPLIT = 'split';
2560 private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2561 private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2562
2563 public function setPaneState(string $paneState): self
2564 {
2565 $this->paneState = in_array($paneState, self::VALIDPANESTATE, true) ? $paneState : '';
2566 if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
2567 $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
2568 } else {
2569 $this->freezePane = null;
2570 }
2571
2572 return $this;
2573 }
2574
2575 /**
2576 * Insert a new row, updating all possible related data.
2577 *
2578 * @param int $before Insert before this row number
2579 * @param int $numberOfRows Number of new rows to insert
2580 *
2581 * @return $this
2582 */
2583 public function insertNewRowBefore(int $before, int $numberOfRows = 1)
2584 {
2585 if ($before >= 1) {
2586 $objReferenceHelper = ReferenceHelper::getInstance();
2587 $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
2588 } else {
2589 throw new Exception('Rows can only be inserted before at least row 1.');
2590 }
2591
2592 return $this;
2593 }
2594
2595 /**
2596 * Insert a new column, updating all possible related data.
2597 *
2598 * @param string $before Insert before this column Name, eg: 'A'
2599 * @param int $numberOfColumns Number of new columns to insert
2600 *
2601 * @return $this
2602 */
2603 public function insertNewColumnBefore(string $before, int $numberOfColumns = 1)
2604 {
2605 if (!is_numeric($before)) {
2606 $objReferenceHelper = ReferenceHelper::getInstance();
2607 $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
2608 } else {
2609 throw new Exception('Column references should not be numeric.');
2610 }
2611
2612 return $this;
2613 }
2614
2615 /**
2616 * Insert a new column, updating all possible related data.
2617 *
2618 * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
2619 * @param int $numberOfColumns Number of new columns to insert
2620 *
2621 * @return $this
2622 */
2623 public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1)
2624 {
2625 if ($beforeColumnIndex >= 1) {
2626 return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
2627 }
2628
2629 throw new Exception('Columns can only be inserted before at least column A (1).');
2630 }
2631
2632 /**
2633 * Delete a row, updating all possible related data.
2634 *
2635 * @param int $row Remove rows, starting with this row number
2636 * @param int $numberOfRows Number of rows to remove
2637 *
2638 * @return $this
2639 */
2640 public function removeRow(int $row, int $numberOfRows = 1)
2641 {
2642 if ($row < 1) {
2643 throw new Exception('Rows to be deleted should at least start from row 1.');
2644 }
2645 if ($numberOfRows === 0) {
2646 return $this;
2647 }
2648 if ($numberOfRows < 0) {
2649 $newRow = max(1, $row + $numberOfRows + 1);
2650 $numberOfRows = $row - $newRow + 1;
2651 $row = $newRow;
2652 }
2653 $newHighestRow = $this->cachedHighestRow;
2654 if ($newHighestRow >= $row) {
2655 $newHighestRow = max($row - 1, $this->cachedHighestRow - $numberOfRows);
2656 }
2657 $startRow = $row;
2658 $endRow = $startRow + $numberOfRows - 1;
2659 $removeKeys = [];
2660 $addKeys = [];
2661 foreach ($this->mergeCells as $key => $value) {
2662 if (
2663 Preg::isMatch(
2664 '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2665 $key,
2666 $matches
2667 )
2668 ) {
2669 $startMergeInt = (int) $matches[2];
2670 $endMergeInt = (int) $matches[4];
2671 if ($startMergeInt >= $startRow) {
2672 if ($startMergeInt <= $endRow) {
2673 $removeKeys[] = $key;
2674 }
2675 } elseif ($endMergeInt >= $startRow) {
2676 if ($endMergeInt <= $endRow) {
2677 $temp = $endMergeInt - 1;
2678 $removeKeys[] = $key;
2679 if ($temp !== $startMergeInt) {
2680 $temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
2681 $addKeys[] = $temp3;
2682 }
2683 }
2684 }
2685 }
2686 }
2687 foreach ($removeKeys as $key) {
2688 unset($this->mergeCells[$key]);
2689 }
2690 foreach ($addKeys as $key) {
2691 $this->mergeCells[$key] = $key;
2692 }
2693
2694 $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
2695 $highestRow = $this->getHighestDataRow();
2696 $removedRowsCounter = 0;
2697
2698 for ($r = 0; $r < $numberOfRows; ++$r) {
2699 if ($row + $r <= $highestRow) {
2700 $this->cellCollection->removeRow($row + $r);
2701 ++$removedRowsCounter;
2702 }
2703 }
2704
2705 $objReferenceHelper = ReferenceHelper::getInstance();
2706 $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
2707 for ($r = 0; $r < $removedRowsCounter; ++$r) {
2708 $this->cellCollection->removeRow($highestRow);
2709 --$highestRow;
2710 }
2711
2712 $this->rowDimensions = $holdRowDimensions;
2713 $this->cachedHighestRow = $newHighestRow;
2714
2715 return $this;
2716 }
2717
2718 /** @return RowDimension[] */
2719 private function removeRowDimensions(int $row, int $numberOfRows): array
2720 {
2721 $highRow = $row + $numberOfRows - 1;
2722 $holdRowDimensions = [];
2723 foreach ($this->rowDimensions as $rowDimension) {
2724 $num = $rowDimension->getRowIndex();
2725 if ($num < $row) {
2726 $holdRowDimensions[$num] = $rowDimension;
2727 } elseif ($num > $highRow) {
2728 $num -= $numberOfRows;
2729 $cloneDimension = clone $rowDimension;
2730 $cloneDimension->setRowIndex($num);
2731 $holdRowDimensions[$num] = $cloneDimension;
2732 }
2733 }
2734
2735 return $holdRowDimensions;
2736 }
2737
2738 /**
2739 * Remove a column, updating all possible related data.
2740 *
2741 * @param string $column Remove columns starting with this column name, eg: 'A'
2742 * @param int $numberOfColumns Number of columns to remove
2743 *
2744 * @return $this
2745 */
2746 public function removeColumn(string $column, int $numberOfColumns = 1)
2747 {
2748 if (is_numeric($column)) {
2749 throw new Exception('Column references should not be numeric.');
2750 }
2751 $startColumnInt = Coordinate::columnIndexFromString($column);
2752 if ($numberOfColumns === 0) {
2753 return $this;
2754 }
2755 if ($numberOfColumns < 0) {
2756 $newStartColumnInt = max(1, $startColumnInt + $numberOfColumns + 1);
2757 $numberOfColumns = $startColumnInt - $newStartColumnInt + 1;
2758 $startColumnInt = $newStartColumnInt;
2759 $column = Coordinate::stringFromColumnIndex($startColumnInt);
2760 }
2761 $newHighestColumn = $this->cachedHighestColumn;
2762 if ($newHighestColumn >= $startColumnInt) {
2763 $newHighestColumn = max($startColumnInt - 1, $this->cachedHighestColumn - $numberOfColumns);
2764 }
2765 $endColumnInt = $startColumnInt + $numberOfColumns - 1;
2766 $removeKeys = [];
2767 $addKeys = [];
2768 foreach ($this->mergeCells as $key => $value) {
2769 if (
2770 Preg::isMatch(
2771 '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2772 $key,
2773 $matches
2774 )
2775 ) {
2776 $startMergeInt = Coordinate::columnIndexFromString($matches[1]);
2777 $endMergeInt = Coordinate::columnIndexFromString($matches[3]);
2778 if ($startMergeInt >= $startColumnInt) {
2779 if ($startMergeInt <= $endColumnInt) {
2780 $removeKeys[] = $key;
2781 }
2782 } elseif ($endMergeInt >= $startColumnInt) {
2783 if ($endMergeInt <= $endColumnInt) {
2784 $temp = Coordinate::columnIndexFromString($matches[3]) - 1;
2785 $temp2 = Coordinate::stringFromColumnIndex($temp);
2786 $removeKeys[] = $key;
2787 if ($temp2 !== $matches[1]) {
2788 $temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
2789 $addKeys[] = $temp3;
2790 }
2791 }
2792 }
2793 }
2794 }
2795 foreach ($removeKeys as $key) {
2796 unset($this->mergeCells[$key]);
2797 }
2798 foreach ($addKeys as $key) {
2799 $this->mergeCells[$key] = $key;
2800 }
2801
2802 $highestColumn = $this->getHighestDataColumn();
2803 $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
2804 $pColumnIndex = Coordinate::columnIndexFromString($column);
2805
2806 $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
2807
2808 $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
2809 $objReferenceHelper = ReferenceHelper::getInstance();
2810 $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
2811
2812 $this->columnDimensions = $holdColumnDimensions;
2813
2814 if ($pColumnIndex > $highestColumnIndex) {
2815 $this->cachedHighestColumn = $newHighestColumn;
2816
2817 return $this;
2818 }
2819
2820 $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
2821
2822 for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
2823 $this->cellCollection->removeColumn($highestColumn);
2824 $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
2825 }
2826 $this->cachedHighestColumn = $newHighestColumn;
2827
2828 $this->garbageCollect();
2829
2830 return $this;
2831 }
2832
2833 /** @return ColumnDimension[] */
2834 private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
2835 {
2836 $highCol = $pColumnIndex + $numberOfColumns - 1;
2837 $holdColumnDimensions = [];
2838 foreach ($this->columnDimensions as $columnDimension) {
2839 $num = $columnDimension->getColumnNumeric();
2840 if ($num < $pColumnIndex) {
2841 $str = $columnDimension->getColumnIndex();
2842 $holdColumnDimensions[$str] = $columnDimension;
2843 } elseif ($num > $highCol) {
2844 $cloneDimension = clone $columnDimension;
2845 $cloneDimension->setColumnNumeric($num - $numberOfColumns);
2846 $str = $cloneDimension->getColumnIndex();
2847 $holdColumnDimensions[$str] = $cloneDimension;
2848 }
2849 }
2850
2851 return $holdColumnDimensions;
2852 }
2853
2854 /**
2855 * Remove a column, updating all possible related data.
2856 *
2857 * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2858 * @param int $numColumns Number of columns to remove
2859 *
2860 * @return $this
2861 */
2862 public function removeColumnByIndex(int $columnIndex, int $numColumns = 1)
2863 {
2864 if ($columnIndex >= 1) {
2865 return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2866 }
2867
2868 throw new Exception('Columns to be deleted should at least start from column A (1)');
2869 }
2870
2871 /**
2872 * Show gridlines?
2873 */
2874 public function getShowGridlines(): bool
2875 {
2876 return $this->showGridlines;
2877 }
2878
2879 /**
2880 * Set show gridlines.
2881 *
2882 * @param bool $showGridLines Show gridlines (true/false)
2883 *
2884 * @return $this
2885 */
2886 public function setShowGridlines(bool $showGridLines): self
2887 {
2888 $this->showGridlines = $showGridLines;
2889
2890 return $this;
2891 }
2892
2893 /**
2894 * Print gridlines?
2895 */
2896 public function getPrintGridlines(): bool
2897 {
2898 return $this->printGridlines;
2899 }
2900
2901 /**
2902 * Set print gridlines.
2903 *
2904 * @param bool $printGridLines Print gridlines (true/false)
2905 *
2906 * @return $this
2907 */
2908 public function setPrintGridlines(bool $printGridLines): self
2909 {
2910 $this->printGridlines = $printGridLines;
2911
2912 return $this;
2913 }
2914
2915 /**
2916 * Show row and column headers?
2917 */
2918 public function getShowRowColHeaders(): bool
2919 {
2920 return $this->showRowColHeaders;
2921 }
2922
2923 /**
2924 * Set show row and column headers.
2925 *
2926 * @param bool $showRowColHeaders Show row and column headers (true/false)
2927 *
2928 * @return $this
2929 */
2930 public function setShowRowColHeaders(bool $showRowColHeaders): self
2931 {
2932 $this->showRowColHeaders = $showRowColHeaders;
2933
2934 return $this;
2935 }
2936
2937 /**
2938 * Show summary below? (Row/Column outlining).
2939 */
2940 public function getShowSummaryBelow(): bool
2941 {
2942 return $this->showSummaryBelow;
2943 }
2944
2945 /**
2946 * Set show summary below.
2947 *
2948 * @param bool $showSummaryBelow Show summary below (true/false)
2949 *
2950 * @return $this
2951 */
2952 public function setShowSummaryBelow(bool $showSummaryBelow): self
2953 {
2954 $this->showSummaryBelow = $showSummaryBelow;
2955
2956 return $this;
2957 }
2958
2959 /**
2960 * Show summary right? (Row/Column outlining).
2961 */
2962 public function getShowSummaryRight(): bool
2963 {
2964 return $this->showSummaryRight;
2965 }
2966
2967 /**
2968 * Set show summary right.
2969 *
2970 * @param bool $showSummaryRight Show summary right (true/false)
2971 *
2972 * @return $this
2973 */
2974 public function setShowSummaryRight(bool $showSummaryRight): self
2975 {
2976 $this->showSummaryRight = $showSummaryRight;
2977
2978 return $this;
2979 }
2980
2981 /**
2982 * Get comments.
2983 *
2984 * @return Comment[]
2985 */
2986 public function getComments(): array
2987 {
2988 return $this->comments;
2989 }
2990
2991 /**
2992 * Set comments array for the entire sheet.
2993 *
2994 * @param Comment[] $comments
2995 *
2996 * @return $this
2997 */
2998 public function setComments(array $comments): self
2999 {
3000 $this->comments = $comments;
3001
3002 return $this;
3003 }
3004
3005 /**
3006 * Remove comment from cell.
3007 *
3008 * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
3009 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
3010 *
3011 * @return $this
3012 */
3013 public function removeComment($cellCoordinate): self
3014 {
3015 $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
3016
3017 if (Coordinate::coordinateIsRange($cellAddress)) {
3018 throw new Exception('Cell coordinate string can not be a range of cells.');
3019 } elseif (str_contains($cellAddress, '$')) {
3020 throw new Exception('Cell coordinate string must not be absolute.');
3021 } elseif ($cellAddress == '') {
3022 throw new Exception('Cell coordinate can not be zero-length string.');
3023 }
3024 // Check if we have a comment for this cell and delete it
3025 if (isset($this->comments[$cellAddress])) {
3026 unset($this->comments[$cellAddress]);
3027 }
3028
3029 return $this;
3030 }
3031
3032 /**
3033 * Get comment for cell.
3034 *
3035 * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
3036 * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
3037 */
3038 public function getComment($cellCoordinate, bool $attachNew = true): Comment
3039 {
3040 $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
3041
3042 if (Coordinate::coordinateIsRange($cellAddress)) {
3043 throw new Exception('Cell coordinate string can not be a range of cells.');
3044 } elseif (str_contains($cellAddress, '$')) {
3045 throw new Exception('Cell coordinate string must not be absolute.');
3046 } elseif ($cellAddress == '') {
3047 throw new Exception('Cell coordinate can not be zero-length string.');
3048 }
3049
3050 // Check if we already have a comment for this cell.
3051 if (isset($this->comments[$cellAddress])) {
3052 return $this->comments[$cellAddress];
3053 }
3054
3055 // If not, create a new comment.
3056 $newComment = new Comment();
3057 if ($attachNew) {
3058 $this->comments[$cellAddress] = $newComment;
3059 }
3060
3061 return $newComment;
3062 }
3063
3064 /**
3065 * Get active cell.
3066 *
3067 * @return string Example: 'A1'
3068 */
3069 public function getActiveCell(): string
3070 {
3071 return $this->activeCell;
3072 }
3073
3074 /**
3075 * Get selected cells.
3076 */
3077 public function getSelectedCells(): string
3078 {
3079 return $this->selectedCells;
3080 }
3081
3082 /**
3083 * Selected cell.
3084 *
3085 * @param string $coordinate Cell (i.e. A1)
3086 *
3087 * @return $this
3088 */
3089 public function setSelectedCell(string $coordinate)
3090 {
3091 return $this->setSelectedCells($coordinate);
3092 }
3093
3094 /**
3095 * Select a range of cells.
3096 *
3097 * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10'
3098 * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
3099 * or a CellAddress or AddressRange object.
3100 *
3101 * @return $this
3102 */
3103 public function setSelectedCells($coordinate)
3104 {
3105 if (is_string($coordinate)) {
3106 $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
3107 }
3108 $coordinate = Validations::validateCellOrCellRange($coordinate);
3109
3110 if (Coordinate::coordinateIsRange($coordinate)) {
3111 [$first] = Coordinate::splitRange($coordinate);
3112 $this->activeCell = $first[0];
3113 } else {
3114 $this->activeCell = $coordinate;
3115 }
3116 $this->selectedCells = $coordinate;
3117 $this->setSelectedCellsActivePane();
3118
3119 return $this;
3120 }
3121
3122 private function setSelectedCellsActivePane(): void
3123 {
3124 if (!empty($this->freezePane)) {
3125 $coordinateC = Coordinate::indexesFromString($this->freezePane);
3126 $coordinateT = Coordinate::indexesFromString($this->activeCell);
3127 if ($coordinateC[0] === 1) {
3128 $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft';
3129 } elseif ($coordinateC[1] === 1) {
3130 $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3131 } elseif ($coordinateT[1] <= $coordinateC[1]) {
3132 $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3133 } else {
3134 $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight';
3135 }
3136 $this->setActivePane($activePane);
3137 $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell);
3138 }
3139 }
3140
3141 /**
3142 * Get right-to-left.
3143 */
3144 public function getRightToLeft(): bool
3145 {
3146 return $this->rightToLeft;
3147 }
3148
3149 /**
3150 * Set right-to-left.
3151 *
3152 * @param bool $value Right-to-left true/false
3153 *
3154 * @return $this
3155 */
3156 public function setRightToLeft(bool $value)
3157 {
3158 $this->rightToLeft = $value;
3159
3160 return $this;
3161 }
3162
3163 /**
3164 * Fill worksheet from values in array.
3165 *
3166 * @param mixed[]|mixed[][] $source Source array
3167 * @param mixed $nullValue Value in source array that stands for blank cell
3168 * @param string $startCell Insert array starting from this cell address as the top left coordinate
3169 * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
3170 *
3171 * @return $this
3172 */
3173 public function fromArray(array $source, $nullValue = null, string $startCell = 'A1', bool $strictNullComparison = false)
3174 {
3175 // Convert a 1-D array to 2-D (for ease of looping)
3176 if (!is_array(end($source))) {
3177 $source = [$source];
3178 }
3179 /** @var mixed[][] $source */
3180
3181 // start coordinate
3182 [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
3183 $startRow = (int) $startRow;
3184
3185 // Loop through $source
3186 if ($strictNullComparison) {
3187 foreach ($source as $rowData) {
3188 /** @var string */
3189 $currentColumn = $startColumn;
3190 foreach ($rowData as $cellValue) {
3191 if ($cellValue !== $nullValue) {
3192 $this->getCell($currentColumn . $startRow)->setValue($cellValue);
3193 }
3194 StringHelper::stringIncrement($currentColumn);
3195 }
3196 ++$startRow;
3197 }
3198 } else {
3199 foreach ($source as $rowData) {
3200 $currentColumn = $startColumn;
3201 foreach ($rowData as $cellValue) {
3202 if ($cellValue != $nullValue) {
3203 $this->getCell($currentColumn . $startRow)->setValue($cellValue);
3204 }
3205 StringHelper::stringIncrement($currentColumn);
3206 }
3207 ++$startRow;
3208 }
3209 }
3210
3211 return $this;
3212 }
3213
3214 /**
3215 * @param bool $calculateFormulas Whether to calculate cell's value if it is a formula.
3216 * @param mixed $nullValue value to use when null
3217 * @param bool $formatData Whether to format data according to cell's style.
3218 * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3219 * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3220 *
3221 * @throws Exception
3222 * @throws \TablePress\PhpOffice\PhpSpreadsheet\Calculation\Exception
3223 * @return mixed
3224 */
3225 protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, $nullValue, bool $lessFloatPrecision = false, $oldCalculatedValue = false)
3226 {
3227 $returnValue = $nullValue;
3228
3229 if ($cell->getValue() !== null) {
3230 if ($cell->getValue() instanceof RichText) {
3231 $returnValue = $cell->getValue()->getPlainText();
3232 } elseif ($calculateFormulas) {
3233 $returnValue = $cell->getCalculatedValue();
3234 } elseif ($oldCalculatedValue && ($cell->getDataType() === DataType::TYPE_FORMULA)) {
3235 $returnValue = $cell->getOldCalculatedValue() ?? $cell->getValue();
3236 } else {
3237 $returnValue = $cell->getValue();
3238 }
3239
3240 if ($formatData) {
3241 $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
3242 /** @var null|bool|float|int|RichText|string */
3243 $returnValuex = $returnValue;
3244 $returnValue = NumberFormat::toFormattedString($returnValuex, $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL, null, $lessFloatPrecision);
3245 }
3246 }
3247
3248 return $returnValue;
3249 }
3250
3251 /**
3252 * Create array from a range of cells.
3253 *
3254 * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3255 * @param bool $calculateFormulas Should formulas be calculated?
3256 * @param bool $formatData Should formatting be applied to cell values?
3257 * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3258 * True - Return rows and columns indexed by their actual row and column IDs
3259 * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3260 * True - Don't return values for rows/columns that are defined as hidden.
3261 * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3262 * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3263 * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3264 *
3265 * @return mixed[][]
3266 */
3267 public function rangeToArray(
3268 string $range,
3269 $nullValue = null,
3270 bool $calculateFormulas = true,
3271 bool $formatData = true,
3272 bool $returnCellRef = false,
3273 bool $ignoreHidden = false,
3274 bool $reduceArrays = false,
3275 bool $lessFloatPrecision = false,
3276 bool $oldCalculatedValue = false
3277 ): array {
3278 $returnValue = [];
3279
3280 // Loop through rows
3281 foreach ($this->rangeToArrayYieldRows($range, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
3282 /** @var int $rowRef */
3283 $returnValue[$rowRef] = $rowArray;
3284 }
3285
3286 // Return
3287 return $returnValue;
3288 }
3289
3290 /**
3291 * Create array from a multiple ranges of cells. (such as A1:A3,A15,B17:C17).
3292 *
3293 * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3294 * @param bool $calculateFormulas Should formulas be calculated?
3295 * @param bool $formatData Should formatting be applied to cell values?
3296 * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3297 * True - Return rows and columns indexed by their actual row and column IDs
3298 * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3299 * True - Don't return values for rows/columns that are defined as hidden.
3300 * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3301 * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3302 * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3303 *
3304 * @return mixed[][]
3305 */
3306 public function rangesToArray(
3307 string $ranges,
3308 $nullValue = null,
3309 bool $calculateFormulas = true,
3310 bool $formatData = true,
3311 bool $returnCellRef = false,
3312 bool $ignoreHidden = false,
3313 bool $reduceArrays = false,
3314 bool $lessFloatPrecision = false,
3315 bool $oldCalculatedValue = false
3316 ): array {
3317 $returnValue = [];
3318
3319 $parts = explode(',', $ranges);
3320 foreach ($parts as $part) {
3321 // Loop through rows
3322 foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
3323 /** @var int $rowRef */
3324 $returnValue[$rowRef] = $rowArray;
3325 }
3326 }
3327
3328 // Return
3329 return $returnValue;
3330 }
3331
3332 /**
3333 * Create array from a range of cells, yielding each row in turn.
3334 *
3335 * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3336 * @param bool $calculateFormulas Should formulas be calculated?
3337 * @param bool $formatData Should formatting be applied to cell values?
3338 * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3339 * True - Return rows and columns indexed by their actual row and column IDs
3340 * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3341 * True - Don't return values for rows/columns that are defined as hidden.
3342 * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3343 * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3344 * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3345 *
3346 * @return Generator<array<mixed>>
3347 */
3348 public function rangeToArrayYieldRows(
3349 string $range,
3350 $nullValue = null,
3351 bool $calculateFormulas = true,
3352 bool $formatData = true,
3353 bool $returnCellRef = false,
3354 bool $ignoreHidden = false,
3355 bool $reduceArrays = false,
3356 bool $lessFloatPrecision = false,
3357 bool $oldCalculatedValue = false
3358 ) {
3359 $range = Validations::validateCellOrCellRange($range);
3360
3361 // Identify the range that we need to extract from the worksheet
3362 [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
3363 $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
3364 $minRow = $rangeStart[1];
3365 $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
3366 $maxRow = $rangeEnd[1];
3367 $minColInt = $rangeStart[0];
3368 $maxColInt = $rangeEnd[0];
3369
3370 StringHelper::stringIncrement($maxCol);
3371 /** @var array<string, bool> */
3372 $hiddenColumns = [];
3373 $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden, $hiddenColumns);
3374 $hideColumns = !empty($hiddenColumns);
3375
3376 $keys = $this->cellCollection->getSortedCoordinatesInt();
3377 $keyIndex = 0;
3378 $keysCount = count($keys);
3379 // Loop through rows
3380 for ($row = $minRow; $row <= $maxRow; ++$row) {
3381 if (($ignoreHidden === true) && ($this->isRowVisible($row) === false)) {
3382 continue;
3383 }
3384 $rowRef = $returnCellRef ? $row : ($row - $minRow);
3385 $returnValue = $nullRow;
3386
3387 $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1;
3388 $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1;
3389
3390 // Binary search to quickly approach the correct index
3391 $keyIndex = intdiv($keysCount, 2);
3392 $boundLow = 0;
3393 $boundHigh = $keysCount - 1;
3394 while ($boundLow <= $boundHigh) {
3395 $keyIndex = intdiv($boundLow + $boundHigh, 2);
3396 if ($keys[$keyIndex] < $index) {
3397 $boundLow = $keyIndex + 1;
3398 } elseif ($keys[$keyIndex] > $index) {
3399 $boundHigh = $keyIndex - 1;
3400 } else {
3401 break;
3402 }
3403 }
3404
3405 // Realign to the proper index value
3406 while ($keyIndex > 0 && $keys[$keyIndex] > $index) {
3407 --$keyIndex;
3408 }
3409 while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) {
3410 ++$keyIndex;
3411 }
3412
3413 while ($keyIndex < $keysCount && $keys[$keyIndex] <= $indexPlus) {
3414 $key = $keys[$keyIndex];
3415 $thisRow = intdiv($key - 1, AddressRange::MAX_COLUMN_INT) + 1;
3416 $thisCol = ($key % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
3417 if ($thisCol >= $minColInt && $thisCol <= $maxColInt) {
3418 $col = Coordinate::stringFromColumnIndex($thisCol);
3419 if ($hideColumns === false || !isset($hiddenColumns[$col])) {
3420 $columnRef = $returnCellRef ? $col : ($thisCol - $minColInt);
3421 $cell = $this->cellCollection->get("{$col}{$thisRow}");
3422 if ($cell !== null) {
3423 $value = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue, $lessFloatPrecision, $oldCalculatedValue);
3424 if ($reduceArrays) {
3425 while (is_array($value)) {
3426 $value = array_shift($value);
3427 }
3428 }
3429 if ($value !== $nullValue) {
3430 $returnValue[$columnRef] = $value;
3431 }
3432 }
3433 }
3434 }
3435 ++$keyIndex;
3436 }
3437
3438 yield $rowRef => $returnValue;
3439 }
3440 }
3441
3442 /**
3443 * Prepare a row data filled with null values to deduplicate the memory areas for empty rows.
3444 *
3445 * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3446 * @param string $minCol Start column of the range
3447 * @param string $maxCol End column of the range
3448 * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3449 * True - Return rows and columns indexed by their actual row and column IDs
3450 * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3451 * True - Don't return values for rows/columns that are defined as hidden.
3452 * @param array<string, bool> $hiddenColumns
3453 *
3454 * @return mixed[]
3455 */
3456 private function buildNullRow(
3457 $nullValue,
3458 string $minCol,
3459 string $maxCol,
3460 bool $returnCellRef,
3461 bool $ignoreHidden,
3462 array &$hiddenColumns
3463 ): array {
3464 $nullRow = [];
3465 $c = -1;
3466 for ($col = $minCol; $col !== $maxCol; StringHelper::stringIncrement($col)) {
3467 if ($ignoreHidden === true && $this->columnDimensionExists($col) && $this->getColumnDimension($col)->getVisible() === false) {
3468 $hiddenColumns[$col] = true;
3469 } else {
3470 $columnRef = $returnCellRef ? $col : ++$c;
3471 $nullRow[$columnRef] = $nullValue;
3472 }
3473 }
3474
3475 return $nullRow;
3476 }
3477
3478 private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
3479 {
3480 $namedRange = DefinedName::resolveName($definedName, $this);
3481 if ($namedRange === null) {
3482 if ($returnNullIfInvalid) {
3483 return null;
3484 }
3485
3486 throw new Exception('Named Range ' . $definedName . ' does not exist.');
3487 }
3488
3489 if ($namedRange->isFormula()) {
3490 if ($returnNullIfInvalid) {
3491 return null;
3492 }
3493
3494 throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
3495 }
3496
3497 if ($namedRange->getLocalOnly()) {
3498 $worksheet = $namedRange->getWorksheet();
3499 if ($worksheet === null || $this !== $worksheet) {
3500 if ($returnNullIfInvalid) {
3501 return null;
3502 }
3503
3504 throw new Exception(
3505 'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
3506 );
3507 }
3508 }
3509
3510 return $namedRange;
3511 }
3512
3513 /**
3514 * Create array from a range of cells.
3515 *
3516 * @param string $definedName The Named Range that should be returned
3517 * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3518 * @param bool $calculateFormulas Should formulas be calculated?
3519 * @param bool $formatData Should formatting be applied to cell values?
3520 * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3521 * True - Return rows and columns indexed by their actual row and column IDs
3522 * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3523 * True - Don't return values for rows/columns that are defined as hidden.
3524 * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3525 * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3526 * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3527 *
3528 * @return mixed[][]
3529 */
3530 public function namedRangeToArray(
3531 string $definedName,
3532 $nullValue = null,
3533 bool $calculateFormulas = true,
3534 bool $formatData = true,
3535 bool $returnCellRef = false,
3536 bool $ignoreHidden = false,
3537 bool $reduceArrays = false,
3538 bool $lessFloatPrecision = false,
3539 bool $oldCalculatedValue = false
3540 ): array {
3541 $retVal = [];
3542 $namedRange = $this->validateNamedRange($definedName);
3543 if ($namedRange !== null) {
3544 $cellRange = ltrim((string) substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
3545 $cellRange = str_replace('$', '', $cellRange);
3546 $workSheet = $namedRange->getWorksheet();
3547 if ($workSheet !== null) {
3548 $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
3549 }
3550 }
3551
3552 return $retVal;
3553 }
3554
3555 /**
3556 * Create array from worksheet.
3557 *
3558 * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3559 * @param bool $calculateFormulas Should formulas be calculated?
3560 * @param bool $formatData Should formatting be applied to cell values?
3561 * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3562 * True - Return rows and columns indexed by their actual row and column IDs
3563 * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3564 * True - Don't return values for rows/columns that are defined as hidden.
3565 * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3566 * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3567 * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3568 *
3569 * @return mixed[][]
3570 */
3571 public function toArray(
3572 $nullValue = null,
3573 bool $calculateFormulas = true,
3574 bool $formatData = true,
3575 bool $returnCellRef = false,
3576 bool $ignoreHidden = false,
3577 bool $reduceArrays = false,
3578 bool $lessFloatPrecision = false,
3579 bool $oldCalculatedValue = false
3580 ): array {
3581 // Garbage collect...
3582 $this->garbageCollect();
3583 $this->calculateArrays($calculateFormulas);
3584
3585 // Identify the range that we need to extract from the worksheet
3586 $maxCol = $this->getHighestColumn();
3587 $maxRow = $this->getHighestRow();
3588
3589 // Return
3590 return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
3591 }
3592
3593 /**
3594 * Get row iterator.
3595 *
3596 * @param int $startRow The row number at which to start iterating
3597 * @param ?int $endRow The row number at which to stop iterating
3598 */
3599 public function getRowIterator(int $startRow = 1, ?int $endRow = null): RowIterator
3600 {
3601 return new RowIterator($this, $startRow, $endRow);
3602 }
3603
3604 /**
3605 * Get column iterator.
3606 *
3607 * @param string $startColumn The column address at which to start iterating
3608 * @param ?string $endColumn The column address at which to stop iterating
3609 */
3610 public function getColumnIterator(string $startColumn = 'A', ?string $endColumn = null): ColumnIterator
3611 {
3612 return new ColumnIterator($this, $startColumn, $endColumn);
3613 }
3614
3615 /**
3616 * Run PhpSpreadsheet garbage collector.
3617 *
3618 * @return $this
3619 */
3620 public function garbageCollect()
3621 {
3622 // Flush cache
3623 $this->cellCollection->get('A1');
3624
3625 // Lookup highest column and highest row if cells are cleaned
3626 $colRow = $this->cellCollection->getHighestRowAndColumn();
3627 $highestRow = $colRow['row'];
3628 $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
3629
3630 // Loop through column dimensions
3631 foreach ($this->columnDimensions as $dimension) {
3632 $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
3633 }
3634
3635 // Loop through row dimensions
3636 foreach ($this->rowDimensions as $dimension) {
3637 $highestRow = max($highestRow, $dimension->getRowIndex());
3638 }
3639
3640 // Cache values
3641 $this->cachedHighestColumn = max(1, $highestColumn);
3642 /** @var int $highestRow */
3643 $this->cachedHighestRow = $highestRow;
3644
3645 // Return
3646 return $this;
3647 }
3648
3649 /**
3650 * @deprecated 5.2.0 Serves no useful purpose. No replacement.
3651 *
3652 * @codeCoverageIgnore
3653 */
3654 public function getHashInt(): int
3655 {
3656 return spl_object_id($this);
3657 }
3658
3659 /**
3660 * Extract worksheet title from range.
3661 *
3662 * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3663 * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3664 * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3665 * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3666 * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3667 * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3668 *
3669 * @param ?string $range Range to extract title from
3670 * @param bool $returnRange Return range? (see example)
3671 *
3672 * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null))
3673 */
3674 public static function extractSheetTitle(?string $range, bool $returnRange = false, bool $unapostrophize = false)
3675 {
3676 if (empty($range)) {
3677 return $returnRange ? [null, null] : null;
3678 }
3679
3680 // Sheet title included?
3681 if (($sep = strrpos($range, '!')) === false) {
3682 return $returnRange ? ['', $range] : '';
3683 }
3684
3685 if ($returnRange) {
3686 $title = (string) substr($range, 0, $sep);
3687 if ($unapostrophize) {
3688 $title = self::unApostrophizeTitle($title);
3689 }
3690
3691 return [$title, (string) substr($range, $sep + 1)];
3692 }
3693
3694 return (string) substr($range, $sep + 1);
3695 }
3696
3697 public static function unApostrophizeTitle(?string $title): string
3698 {
3699 $title ??= '';
3700 if (str_starts_with($title, "'") && str_ends_with($title, "'")) {
3701 $title = str_replace("''", "'", (string) substr($title, 1, -1));
3702 }
3703
3704 return $title;
3705 }
3706
3707 /**
3708 * Get hyperlink.
3709 *
3710 * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3711 */
3712 public function getHyperlink(string $cellCoordinate): Hyperlink
3713 {
3714 $this->getCell($cellCoordinate)->setHadHyperlink(true);
3715 // return hyperlink if we already have one
3716 if (isset($this->hyperlinkCollection[$cellCoordinate])) {
3717 return $this->hyperlinkCollection[$cellCoordinate];
3718 }
3719
3720 // else create hyperlink
3721 $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
3722
3723 return $this->hyperlinkCollection[$cellCoordinate];
3724 }
3725
3726 /**
3727 * Set hyperlink.
3728 *
3729 * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3730 *
3731 * @return $this
3732 */
3733 public function setHyperlink(string $cellCoordinate, ?Hyperlink $hyperlink = null, bool $reset = true)
3734 {
3735 if ($hyperlink === null) {
3736 unset($this->hyperlinkCollection[$cellCoordinate]);
3737 if ($reset) {
3738 $this->getCell($cellCoordinate)
3739 ->setHadHyperlink(false);
3740 }
3741 } else {
3742 $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
3743 $this->getCell($cellCoordinate)->setHadHyperlink(true);
3744 }
3745
3746 return $this;
3747 }
3748
3749 /**
3750 * Hyperlink at a specific coordinate exists?
3751 *
3752 * @param string $coordinate eg: 'A1'
3753 */
3754 public function hyperlinkExists(string $coordinate): bool
3755 {
3756 return isset($this->hyperlinkCollection[$coordinate]);
3757 }
3758
3759 /**
3760 * Get collection of hyperlinks.
3761 *
3762 * @return Hyperlink[]
3763 */
3764 public function getHyperlinkCollection(): array
3765 {
3766 return $this->hyperlinkCollection;
3767 }
3768
3769 /**
3770 * Get data validation.
3771 *
3772 * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3773 */
3774 public function getDataValidation(string $cellCoordinate): DataValidation
3775 {
3776 // return data validation if we already have one
3777 if (isset($this->dataValidationCollection[$cellCoordinate])) {
3778 return $this->dataValidationCollection[$cellCoordinate];
3779 }
3780
3781 // or if cell is part of a data validation range
3782 foreach ($this->dataValidationCollection as $key => $dataValidation) {
3783 $keyParts = explode(' ', $key);
3784 foreach ($keyParts as $keyPart) {
3785 if ($keyPart === $cellCoordinate) {
3786 return $dataValidation;
3787 }
3788 if (str_contains($keyPart, ':')) {
3789 if (Coordinate::coordinateIsInsideRange($keyPart, $cellCoordinate)) {
3790 return $dataValidation;
3791 }
3792 }
3793 }
3794 }
3795
3796 // else create data validation
3797 $dataValidation = new DataValidation();
3798 $dataValidation->setSqref($cellCoordinate);
3799 $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
3800
3801 return $dataValidation;
3802 }
3803
3804 /**
3805 * Set data validation.
3806 *
3807 * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3808 *
3809 * @return $this
3810 */
3811 public function setDataValidation(string $cellCoordinate, ?DataValidation $dataValidation = null)
3812 {
3813 if ($dataValidation === null) {
3814 unset($this->dataValidationCollection[$cellCoordinate]);
3815 } else {
3816 $dataValidation->setSqref($cellCoordinate);
3817 $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
3818 }
3819
3820 return $this;
3821 }
3822
3823 /**
3824 * Data validation at a specific coordinate exists?
3825 *
3826 * @param string $coordinate eg: 'A1'
3827 */
3828 public function dataValidationExists(string $coordinate): bool
3829 {
3830 if (isset($this->dataValidationCollection[$coordinate])) {
3831 return true;
3832 }
3833 foreach ($this->dataValidationCollection as $key => $dataValidation) {
3834 $keyParts = explode(' ', $key);
3835 foreach ($keyParts as $keyPart) {
3836 if ($keyPart === $coordinate) {
3837 return true;
3838 }
3839 if (str_contains($keyPart, ':')) {
3840 if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
3841 return true;
3842 }
3843 }
3844 }
3845 }
3846
3847 return false;
3848 }
3849
3850 /**
3851 * Get collection of data validations.
3852 *
3853 * @return DataValidation[]
3854 */
3855 public function getDataValidationCollection(): array
3856 {
3857 $collectionCells = [];
3858 $collectionRanges = [];
3859 foreach ($this->dataValidationCollection as $key => $dataValidation) {
3860 if (Preg::isMatch('/[: ]/', $key)) {
3861 $collectionRanges[$key] = $dataValidation;
3862 } else {
3863 $collectionCells[$key] = $dataValidation;
3864 }
3865 }
3866
3867 return array_merge($collectionCells, $collectionRanges);
3868 }
3869
3870 /**
3871 * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3872 *
3873 * @return string Adjusted range value
3874 */
3875 public function shrinkRangeToFit(string $range): string
3876 {
3877 $maxCol = $this->getHighestColumn();
3878 $maxRow = $this->getHighestRow();
3879 $maxCol = Coordinate::columnIndexFromString($maxCol);
3880
3881 $rangeBlocks = explode(' ', $range);
3882 foreach ($rangeBlocks as &$rangeSet) {
3883 $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
3884
3885 if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
3886 $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
3887 }
3888 if ($rangeBoundaries[0][1] > $maxRow) {
3889 $rangeBoundaries[0][1] = $maxRow;
3890 }
3891 if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
3892 $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
3893 }
3894 if ($rangeBoundaries[1][1] > $maxRow) {
3895 $rangeBoundaries[1][1] = $maxRow;
3896 }
3897 $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
3898 }
3899 unset($rangeSet);
3900
3901 return implode(' ', $rangeBlocks);
3902 }
3903
3904 /**
3905 * Get tab color.
3906 */
3907 public function getTabColor(): Color
3908 {
3909 if ($this->tabColor === null) {
3910 $this->tabColor = new Color();
3911 }
3912
3913 return $this->tabColor;
3914 }
3915
3916 /**
3917 * Reset tab color.
3918 *
3919 * @return $this
3920 */
3921 public function resetTabColor()
3922 {
3923 $this->tabColor = null;
3924
3925 return $this;
3926 }
3927
3928 /**
3929 * Tab color set?
3930 */
3931 public function isTabColorSet(): bool
3932 {
3933 return $this->tabColor !== null;
3934 }
3935
3936 /**
3937 * Copy worksheet (!= clone!).
3938 * @return static
3939 */
3940 public function copy()
3941 {
3942 return clone $this;
3943 }
3944
3945 /**
3946 * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3947 * exist in the collection for this row. false will be returned otherwise.
3948 * This rule can be modified by passing a $definitionOfEmptyFlags value:
3949 * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3950 * cells, then the row will be considered empty.
3951 * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3952 * string value cells, then the row will be considered empty.
3953 * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3954 * If the only cells in the collection are null value or empty string value cells, then the row
3955 * will be considered empty.
3956 *
3957 * @param int $definitionOfEmptyFlags
3958 * Possible Flag Values are:
3959 * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3960 * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3961 */
3962 public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
3963 {
3964 try {
3965 $iterator = new RowIterator($this, $rowId, $rowId);
3966 $iterator->seek($rowId);
3967 $row = $iterator->current();
3968 } catch (Exception $exception) {
3969 return true;
3970 }
3971
3972 return $row->isEmpty($definitionOfEmptyFlags);
3973 }
3974
3975 /**
3976 * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3977 * exist in the collection for this column. false will be returned otherwise.
3978 * This rule can be modified by passing a $definitionOfEmptyFlags value:
3979 * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3980 * cells, then the column will be considered empty.
3981 * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3982 * string value cells, then the column will be considered empty.
3983 * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3984 * If the only cells in the collection are null value or empty string value cells, then the column
3985 * will be considered empty.
3986 *
3987 * @param int $definitionOfEmptyFlags
3988 * Possible Flag Values are:
3989 * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3990 * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3991 */
3992 public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
3993 {
3994 try {
3995 $iterator = new ColumnIterator($this, $columnId, $columnId);
3996 $iterator->seek($columnId);
3997 $column = $iterator->current();
3998 } catch (Exception $exception) {
3999 return true;
4000 }
4001
4002 return $column->isEmpty($definitionOfEmptyFlags);
4003 }
4004
4005 /**
4006 * Implement PHP __clone to create a deep clone, not just a shallow copy.
4007 */
4008 public function __clone()
4009 {
4010 foreach (get_object_vars($this) as $key => $val) {
4011 if ($key == 'parent') {
4012 continue;
4013 }
4014
4015 if (is_object($val) || (is_array($val))) {
4016 if ($key === 'cellCollection') {
4017 $newCollection = $this->cellCollection->cloneCellCollection($this);
4018 $this->cellCollection = $newCollection;
4019 } elseif ($key === 'drawingCollection') {
4020 $currentCollection = $this->drawingCollection;
4021 $this->drawingCollection = new ArrayObject();
4022 foreach ($currentCollection as $item) {
4023 $newDrawing = clone $item;
4024 $newDrawing->setWorksheet($this);
4025 }
4026 } elseif ($key === 'inCellDrawingCollection') {
4027 $currentCollection = $this->inCellDrawingCollection;
4028 $this->inCellDrawingCollection = new ArrayObject();
4029 foreach ($currentCollection as $item) {
4030 $newDrawing = clone $item;
4031 $newDrawing->setWorksheet($this);
4032 }
4033 } elseif ($key === 'tableCollection') {
4034 $currentCollection = $this->tableCollection;
4035 $this->tableCollection = new ArrayObject();
4036 foreach ($currentCollection as $item) {
4037 $newTable = clone $item;
4038 $newTable->setName($item->getName() . 'clone');
4039 $this->addTable($newTable);
4040 }
4041 } elseif ($key === 'chartCollection') {
4042 $currentCollection = $this->chartCollection;
4043 $this->chartCollection = new ArrayObject();
4044 foreach ($currentCollection as $item) {
4045 $newChart = clone $item;
4046 $this->addChart($newChart);
4047 }
4048 } elseif ($key === 'autoFilter') {
4049 $newAutoFilter = clone $this->autoFilter;
4050 $this->autoFilter = $newAutoFilter;
4051 $this->autoFilter->setParent($this);
4052 } else {
4053 $this->{$key} = unserialize(serialize($val));
4054 }
4055 }
4056 }
4057 }
4058
4059 /**
4060 * Define the code name of the sheet.
4061 *
4062 * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
4063 * silently space to underscore)
4064 * @param bool $validate False to skip validation of new title. WARNING: This should only be set
4065 * at parse time (by Readers), where titles can be assumed to be valid.
4066 *
4067 * @return $this
4068 */
4069 public function setCodeName(string $codeName, bool $validate = true)
4070 {
4071 // Is this a 'rename' or not?
4072 if ($this->getCodeName() == $codeName) {
4073 return $this;
4074 }
4075
4076 if ($validate) {
4077 $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
4078
4079 // Syntax check
4080 // throw an exception if not valid
4081 self::checkSheetCodeName($codeName);
4082
4083 // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
4084
4085 if ($this->parent !== null) {
4086 // Is there already such sheet name?
4087 if ($this->parent->sheetCodeNameExists($codeName)) {
4088 // Use name, but append with lowest possible integer
4089
4090 if (StringHelper::countCharacters($codeName) > 29) {
4091 $codeName = StringHelper::substring($codeName, 0, 29);
4092 }
4093 $i = 1;
4094 while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
4095 ++$i;
4096 if ($i == 10) {
4097 if (StringHelper::countCharacters($codeName) > 28) {
4098 $codeName = StringHelper::substring($codeName, 0, 28);
4099 }
4100 } elseif ($i == 100) {
4101 if (StringHelper::countCharacters($codeName) > 27) {
4102 $codeName = StringHelper::substring($codeName, 0, 27);
4103 }
4104 }
4105 }
4106
4107 $codeName .= '_' . $i; // ok, we have a valid name
4108 }
4109 }
4110 }
4111
4112 $this->codeName = $codeName;
4113
4114 return $this;
4115 }
4116
4117 /**
4118 * Return the code name of the sheet.
4119 */
4120 public function getCodeName(): ?string
4121 {
4122 return $this->codeName;
4123 }
4124
4125 /**
4126 * Sheet has a code name ?
4127 */
4128 public function hasCodeName(): bool
4129 {
4130 return $this->codeName !== null;
4131 }
4132
4133 public static function nameRequiresQuotes(string $sheetName): bool
4134 {
4135 return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
4136 }
4137
4138 public function isRowVisible(int $row): bool
4139 {
4140 return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
4141 }
4142
4143 /**
4144 * Same as Cell->isLocked, but without creating cell if it doesn't exist.
4145 */
4146 public function isCellLocked(string $coordinate): bool
4147 {
4148 if ($this->getProtection()->getsheet() !== true) {
4149 return false;
4150 }
4151 if ($this->cellExists($coordinate)) {
4152 return $this->getCell($coordinate)->isLocked();
4153 }
4154 $spreadsheet = $this->parent;
4155 $xfIndex = $this->getXfIndex($coordinate);
4156 if ($spreadsheet === null || $xfIndex === null) {
4157 return true;
4158 }
4159
4160 return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
4161 }
4162
4163 /**
4164 * Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
4165 */
4166 public function isCellHiddenOnFormulaBar(string $coordinate): bool
4167 {
4168 if ($this->cellExists($coordinate)) {
4169 return $this->getCell($coordinate)->isHiddenOnFormulaBar();
4170 }
4171
4172 // cell doesn't exist, therefore isn't a formula,
4173 // therefore isn't hidden on formula bar.
4174 return false;
4175 }
4176
4177 private function getXfIndex(string $coordinate): ?int
4178 {
4179 [$column, $row] = Coordinate::coordinateFromString($coordinate);
4180 $row = (int) $row;
4181 $xfIndex = null;
4182 if ($this->rowDimensionExists($row)) {
4183 $xfIndex = $this->getRowDimension($row)->getXfIndex();
4184 }
4185 if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
4186 $xfIndex = $this->getColumnDimension($column)->getXfIndex();
4187 }
4188
4189 return $xfIndex;
4190 }
4191
4192 private string $backgroundImage = '';
4193
4194 private string $backgroundMime = '';
4195
4196 private string $backgroundExtension = '';
4197
4198 public function getBackgroundImage(): string
4199 {
4200 return $this->backgroundImage;
4201 }
4202
4203 public function getBackgroundMime(): string
4204 {
4205 return $this->backgroundMime;
4206 }
4207
4208 public function getBackgroundExtension(): string
4209 {
4210 return $this->backgroundExtension;
4211 }
4212
4213 /**
4214 * Set background image.
4215 * Used on read/write for Xlsx.
4216 * Used on write for Html.
4217 *
4218 * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
4219 */
4220 public function setBackgroundImage(string $backgroundImage): self
4221 {
4222 $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
4223 $mime = $imageArray['mime'];
4224 if ($mime !== '') {
4225 $extension = explode('/', $mime);
4226 $extension = $extension[1];
4227 $this->backgroundImage = $backgroundImage;
4228 $this->backgroundMime = $mime;
4229 $this->backgroundExtension = $extension;
4230 }
4231
4232 return $this;
4233 }
4234
4235 /**
4236 * Copy cells, adjusting relative cell references in formulas.
4237 * Acts similarly to Excel "fill handle" feature.
4238 *
4239 * @param string $fromCell Single source cell, e.g. C3
4240 * @param string $toCells Single cell or cell range, e.g. C4 or C4:C10
4241 * @param bool $copyStyle Copy styles as well as values, defaults to true
4242 */
4243 public function copyCells(string $fromCell, string $toCells, bool $copyStyle = true): void
4244 {
4245 $toArray = Coordinate::extractAllCellReferencesInRange($toCells);
4246 $valueString = $this->getCell($fromCell)->getValueString();
4247 /** @var mixed[][] */
4248 $style = $this->getStyle($fromCell)->exportArray();
4249 $fromIndexes = Coordinate::indexesFromString($fromCell);
4250 $referenceHelper = ReferenceHelper::getInstance();
4251 foreach ($toArray as $destination) {
4252 if ($destination !== $fromCell) {
4253 $toIndexes = Coordinate::indexesFromString($destination);
4254 $this->getCell($destination)->setValue($referenceHelper->updateFormulaReferences($valueString, 'A1', $toIndexes[0] - $fromIndexes[0], $toIndexes[1] - $fromIndexes[1]));
4255 if ($copyStyle) {
4256 $this->getCell($destination)->getStyle()->applyFromArray($style);
4257 }
4258 }
4259 }
4260 }
4261
4262 public function calculateArrays(bool $preCalculateFormulas = true): void
4263 {
4264 if ($preCalculateFormulas && Calculation::getInstance($this->parent)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
4265 $keys = $this->cellCollection->getCoordinates();
4266 foreach ($keys as $key) {
4267 if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
4268 if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString())) {
4269 $this->getCell($key)->getCalculatedValue();
4270 }
4271 }
4272 }
4273 }
4274 }
4275
4276 public function isCellInSpillRange(string $coordinate): bool
4277 {
4278 if (Calculation::getInstance($this->parent)->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
4279 return false;
4280 }
4281 $this->calculateArrays();
4282 $keys = $this->cellCollection->getCoordinates();
4283 foreach ($keys as $key) {
4284 $attributes = $this->getCell($key)->getFormulaAttributes();
4285 if (isset($attributes['ref'])) {
4286 if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) {
4287 // false for first cell in range, true otherwise
4288 return $coordinate !== $key;
4289 }
4290 }
4291 }
4292
4293 return false;
4294 }
4295
4296 /** @param mixed[][] $styleArray */
4297 public function applyStylesFromArray(string $coordinate, array $styleArray): bool
4298 {
4299 $spreadsheet = $this->parent;
4300 if ($spreadsheet === null) {
4301 return false;
4302 }
4303 $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
4304 $originalSelected = $this->selectedCells;
4305 $this->getStyle($coordinate)->applyFromArray($styleArray);
4306 $this->setSelectedCells($originalSelected);
4307 if ($activeSheetIndex >= 0) {
4308 $spreadsheet->setActiveSheetIndex($activeSheetIndex);
4309 }
4310
4311 return true;
4312 }
4313
4314 public function copyFormula(string $fromCell, string $toCell): void
4315 {
4316 $formula = $this->getCell($fromCell)->getValue();
4317 $newFormula = $formula;
4318 if (is_string($formula) && $this->getCell($fromCell)->getDataType() === DataType::TYPE_FORMULA) {
4319 [$fromColInt, $fromRow] = Coordinate::indexesFromString($fromCell);
4320 [$toColInt, $toRow] = Coordinate::indexesFromString($toCell);
4321 $helper = ReferenceHelper::getInstance();
4322 $newFormula = $helper->updateFormulaReferences(
4323 $formula,
4324 'A1',
4325 $toColInt - $fromColInt,
4326 $toRow - $fromRow
4327 );
4328 }
4329 $this->setCellValue($toCell, $newFormula);
4330 }
4331 }
4332