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

PHPOffice / PhpSpreadsheet / 17999085524

25 Sep 2025 06:34AM UTC coverage: 95.867% (+0.3%) from 95.602%
17999085524

Pull #4662

github

web-flow
Merge 0dac8acdb into e3cac6f1f
Pull Request #4662: WIP Do Not Install

45116 of 47061 relevant lines covered (95.87%)

373.63 hits per line

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

94.42
/src/PhpSpreadsheet/Worksheet/Table.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4

5
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
6
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Cell\DataType;
9
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
13
use Stringable;
14

15
class Table implements Stringable
16
{
17
    /**
18
     * Table Name.
19
     */
20
    private string $name;
21

22
    /**
23
     * Show Header Row.
24
     */
25
    private bool $showHeaderRow = true;
26

27
    /**
28
     * Show Totals Row.
29
     */
30
    private bool $showTotalsRow = false;
31

32
    /**
33
     * Table Range.
34
     */
35
    private string $range = '';
36

37
    /**
38
     * Table Worksheet.
39
     */
40
    private ?Worksheet $workSheet = null;
41

42
    /**
43
     * Table allow filter.
44
     */
45
    private bool $allowFilter = true;
46

47
    /**
48
     * Table Column.
49
     *
50
     * @var Table\Column[]
51
     */
52
    private array $columns = [];
53

54
    /**
55
     * Table Style.
56
     */
57
    private TableStyle $style;
58

59
    /**
60
     * Table AutoFilter.
61
     */
62
    private AutoFilter $autoFilter;
63

64
    /**
65
     * Create a new Table.
66
     *
67
     * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range
68
     *            A simple string containing a Cell range like 'A1:E10' is permitted
69
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
70
     *              or an AddressRange object.
71
     * @param string $name (e.g. Table1)
72
     */
73
    public function __construct(AddressRange|string|array $range = '', string $name = '')
158✔
74
    {
75
        $this->style = new TableStyle();
158✔
76
        $this->autoFilter = new AutoFilter($range);
158✔
77
        $this->setRange($range);
158✔
78
        $this->setName($name);
154✔
79
    }
80

81
    /**
82
     * Code to execute when this table is unset().
83
     */
84
    public function __destruct()
52✔
85
    {
86
        $this->workSheet = null;
52✔
87
    }
88

89
    /**
90
     * Get Table name.
91
     */
92
    public function getName(): string
89✔
93
    {
94
        return $this->name;
89✔
95
    }
96

97
    /**
98
     * Set Table name.
99
     *
100
     * @throws PhpSpreadsheetException
101
     */
102
    public function setName(string $name): self
154✔
103
    {
104
        $name = trim($name);
154✔
105

106
        if (!empty($name)) {
154✔
107
            if (strlen($name) === 1 && in_array($name, ['C', 'c', 'R', 'r'])) {
117✔
108
                throw new PhpSpreadsheetException('The table name is invalid');
4✔
109
            }
110
            if (StringHelper::countCharacters($name) > 255) {
113✔
111
                throw new PhpSpreadsheetException('The table name cannot be longer than 255 characters');
1✔
112
            }
113
            // Check for A1 or R1C1 cell reference notation
114
            if (
115
                preg_match(Coordinate::A1_COORDINATE_REGEX, $name)
112✔
116
                || preg_match('/^R\[?\-?[0-9]*\]?C\[?\-?[0-9]*\]?$/i', $name)
112✔
117
            ) {
118
                throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference');
5✔
119
            }
120
            if (!preg_match('/^[\p{L}_\\\]/iu', $name)) {
107✔
121
                throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)');
3✔
122
            }
123
            if (!preg_match('/^[\p{L}_\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) {
104✔
124
                throw new PhpSpreadsheetException('The table name contains invalid characters');
1✔
125
            }
126

127
            $this->checkForDuplicateTableNames($name, $this->workSheet);
103✔
128
            $this->updateStructuredReferences($name);
103✔
129
        }
130

131
        $this->name = $name;
154✔
132

133
        return $this;
154✔
134
    }
135

136
    /**
137
     * @throws PhpSpreadsheetException
138
     */
139
    private function checkForDuplicateTableNames(string $name, ?Worksheet $worksheet): void
103✔
140
    {
141
        // Remember that table names are case-insensitive
142
        $tableName = StringHelper::strToLower($name);
103✔
143

144
        if ($worksheet !== null && StringHelper::strToLower($this->name) !== $name) {
103✔
145
            $spreadsheet = $worksheet->getParentOrThrow();
3✔
146

147
            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
3✔
148
                foreach ($sheet->getTableCollection() as $table) {
3✔
149
                    if (StringHelper::strToLower($table->getName()) === $tableName && $table != $this) {
3✔
150
                        throw new PhpSpreadsheetException("Spreadsheet already contains a table named '{$this->name}'");
1✔
151
                    }
152
                }
153
            }
154
        }
155
    }
156

157
    private function updateStructuredReferences(string $name): void
103✔
158
    {
159
        if (!$this->workSheet || !$this->name) {
103✔
160
            return;
103✔
161
        }
162

163
        // Remember that table names are case-insensitive
164
        if (StringHelper::strToLower($this->name) !== StringHelper::strToLower($name)) {
2✔
165
            // We need to check all formula cells that might contain fully-qualified Structured References
166
            //    that refer to this table, and update those formulae to reference the new table name
167
            $spreadsheet = $this->workSheet->getParentOrThrow();
2✔
168
            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
2✔
169
                $this->updateStructuredReferencesInCells($sheet, $name);
2✔
170
            }
171
            $this->updateStructuredReferencesInNamedFormulae($spreadsheet, $name);
2✔
172
        }
173
    }
174

175
    private function updateStructuredReferencesInCells(Worksheet $worksheet, string $newName): void
2✔
176
    {
177
        $pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
2✔
178

179
        foreach ($worksheet->getCoordinates(false) as $coordinate) {
2✔
180
            $cell = $worksheet->getCell($coordinate);
2✔
181
            if ($cell->getDataType() === DataType::TYPE_FORMULA) {
2✔
182
                $formula = $cell->getValueString();
2✔
183
                if (preg_match($pattern, $formula) === 1) {
2✔
184
                    $formula = preg_replace($pattern, "{$newName}[", $formula);
2✔
185
                    $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
2✔
186
                }
187
            }
188
        }
189
    }
190

191
    private function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $newName): void
2✔
192
    {
193
        $pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
2✔
194

195
        foreach ($spreadsheet->getNamedFormulae() as $namedFormula) {
2✔
196
            $formula = $namedFormula->getValue();
2✔
197
            if (preg_match($pattern, $formula) === 1) {
2✔
198
                $formula = preg_replace($pattern, "{$newName}[", $formula) ?? '';
2✔
199
                $namedFormula->setValue($formula);
2✔
200
            }
201
        }
202
    }
203

204
    /**
205
     * Get show Header Row.
206
     */
207
    public function getShowHeaderRow(): bool
73✔
208
    {
209
        return $this->showHeaderRow;
73✔
210
    }
211

212
    /**
213
     * Set show Header Row.
214
     */
215
    public function setShowHeaderRow(bool $showHeaderRow): self
36✔
216
    {
217
        $this->showHeaderRow = $showHeaderRow;
36✔
218

219
        return $this;
36✔
220
    }
221

222
    /**
223
     * Get show Totals Row.
224
     */
225
    public function getShowTotalsRow(): bool
88✔
226
    {
227
        return $this->showTotalsRow;
88✔
228
    }
229

230
    /**
231
     * Set show Totals Row.
232
     */
233
    public function setShowTotalsRow(bool $showTotalsRow): self
82✔
234
    {
235
        $this->showTotalsRow = $showTotalsRow;
82✔
236

237
        return $this;
82✔
238
    }
239

240
    /**
241
     * Get allow filter.
242
     * If false, autofiltering is disabled for the table, if true it is enabled.
243
     */
244
    public function getAllowFilter(): bool
14✔
245
    {
246
        return $this->allowFilter;
14✔
247
    }
248

249
    /**
250
     * Set show Autofiltering.
251
     * Disabling autofiltering has the same effect as hiding the filter button on all the columns in the table.
252
     */
253
    public function setAllowFilter(bool $allowFilter): self
7✔
254
    {
255
        $this->allowFilter = $allowFilter;
7✔
256

257
        return $this;
7✔
258
    }
259

260
    /**
261
     * Get Table Range.
262
     */
263
    public function getRange(): string
94✔
264
    {
265
        return $this->range;
94✔
266
    }
267

268
    /**
269
     * Set Table Cell Range.
270
     *
271
     * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range
272
     *            A simple string containing a Cell range like 'A1:E10' is permitted
273
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
274
     *              or an AddressRange object.
275
     */
276
    public function setRange(AddressRange|string|array $range = ''): self
158✔
277
    {
278
        // extract coordinate
279
        if ($range !== '') {
158✔
280
            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
154✔
281
        }
282
        if (empty($range)) {
158✔
283
            //    Discard all column rules
284
            $this->columns = [];
14✔
285
            $this->range = '';
14✔
286

287
            return $this;
14✔
288
        }
289

290
        if (!str_contains($range, ':')) {
154✔
291
            throw new PhpSpreadsheetException('Table must be set on a range of cells.');
1✔
292
        }
293

294
        [$width, $height] = Coordinate::rangeDimension($range);
153✔
295
        if ($width < 1 || $height < 1) {
153✔
296
            throw new PhpSpreadsheetException('The table range must be at least 1 column and row');
3✔
297
        }
298

299
        $this->range = $range;
150✔
300
        $this->autoFilter->setRange($range);
150✔
301

302
        //    Discard any column rules that are no longer valid within this range
303
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
150✔
304
        foreach ($this->columns as $key => $value) {
150✔
305
            $colIndex = Coordinate::columnIndexFromString($key);
2✔
306
            if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
2✔
307
                unset($this->columns[$key]);
1✔
308
            }
309
        }
310

311
        return $this;
150✔
312
    }
313

314
    /**
315
     * Set Table Cell Range to max row.
316
     */
317
    public function setRangeToMaxRow(): self
×
318
    {
319
        if ($this->workSheet !== null) {
×
320
            $thisrange = $this->range;
×
321
            $range = (string) preg_replace('/\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);
×
322
            if ($range !== $thisrange) {
×
323
                $this->setRange($range);
×
324
            }
325
        }
326

327
        return $this;
×
328
    }
329

330
    /**
331
     * Get Table's Worksheet.
332
     */
333
    public function getWorksheet(): ?Worksheet
72✔
334
    {
335
        return $this->workSheet;
72✔
336
    }
337

338
    /**
339
     * Set Table's Worksheet.
340
     */
341
    public function setWorksheet(?Worksheet $worksheet = null): self
105✔
342
    {
343
        if ($this->name !== '' && $worksheet !== null) {
105✔
344
            $spreadsheet = $worksheet->getParentOrThrow();
97✔
345
            $tableName = StringHelper::strToUpper($this->name);
97✔
346

347
            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
97✔
348
                foreach ($sheet->getTableCollection() as $table) {
97✔
349
                    if (StringHelper::strToUpper($table->getName()) === $tableName) {
7✔
350
                        throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'");
1✔
351
                    }
352
                }
353
            }
354
        }
355

356
        $this->workSheet = $worksheet;
105✔
357
        $this->autoFilter->setParent($worksheet);
105✔
358

359
        return $this;
105✔
360
    }
361

362
    /**
363
     * Get all Table Columns.
364
     *
365
     * @return Table\Column[]
366
     */
367
    public function getColumns(): array
10✔
368
    {
369
        return $this->columns;
10✔
370
    }
371

372
    /**
373
     * Validate that the specified column is in the Table range.
374
     *
375
     * @param string $column Column name (e.g. A)
376
     *
377
     * @return int The column offset within the table range
378
     */
379
    public function isColumnInRange(string $column): int
105✔
380
    {
381
        if (empty($this->range)) {
105✔
382
            throw new PhpSpreadsheetException('No table range is defined.');
2✔
383
        }
384

385
        $columnIndex = Coordinate::columnIndexFromString($column);
103✔
386
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
103✔
387
        if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
103✔
388
            throw new PhpSpreadsheetException('Column is outside of current table range.');
3✔
389
        }
390

391
        return $columnIndex - $rangeStart[0];
100✔
392
    }
393

394
    /**
395
     * Get a specified Table Column Offset within the defined Table range.
396
     *
397
     * @param string $column Column name (e.g. A)
398
     *
399
     * @return int The offset of the specified column within the table range
400
     */
401
    public function getColumnOffset(string $column): int
2✔
402
    {
403
        return $this->isColumnInRange($column);
2✔
404
    }
405

406
    /**
407
     * Get a specified Table Column.
408
     *
409
     * @param string $column Column name (e.g. A)
410
     */
411
    public function getColumn(string $column): Table\Column
94✔
412
    {
413
        $this->isColumnInRange($column);
94✔
414

415
        if (!isset($this->columns[$column])) {
92✔
416
            $this->columns[$column] = new Table\Column($column, $this);
91✔
417
        }
418

419
        return $this->columns[$column];
92✔
420
    }
421

422
    /**
423
     * Get a specified Table Column by it's offset.
424
     *
425
     * @param int $columnOffset Column offset within range (starting from 0)
426
     */
427
    public function getColumnByOffset(int $columnOffset): Table\Column
39✔
428
    {
429
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
39✔
430
        $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
39✔
431

432
        return $this->getColumn($pColumn);
39✔
433
    }
434

435
    /**
436
     * Set Table.
437
     *
438
     * @param string|Table\Column $columnObjectOrString
439
     *            A simple string containing a Column ID like 'A' is permitted
440
     */
441
    public function setColumn(string|Table\Column $columnObjectOrString): self
10✔
442
    {
443
        if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
10✔
444
            $column = $columnObjectOrString;
9✔
445
        } elseif ($columnObjectOrString instanceof Table\Column) {
1✔
446
            $column = $columnObjectOrString->getColumnIndex();
1✔
447
        } else {
448
            throw new PhpSpreadsheetException('Column is not within the table range.');
×
449
        }
450
        $this->isColumnInRange($column);
10✔
451

452
        if (is_string($columnObjectOrString)) {
8✔
453
            $this->columns[$columnObjectOrString] = new Table\Column($columnObjectOrString, $this);
7✔
454
        } else {
455
            $columnObjectOrString->setTable($this);
1✔
456
            $this->columns[$column] = $columnObjectOrString;
1✔
457
        }
458
        ksort($this->columns);
8✔
459

460
        return $this;
8✔
461
    }
462

463
    /**
464
     * Clear a specified Table Column.
465
     *
466
     * @param string $column Column name (e.g. A)
467
     */
468
    public function clearColumn(string $column): self
2✔
469
    {
470
        $this->isColumnInRange($column);
2✔
471

472
        if (isset($this->columns[$column])) {
2✔
473
            unset($this->columns[$column]);
2✔
474
        }
475

476
        return $this;
2✔
477
    }
478

479
    /**
480
     * Shift an Table Column Rule to a different column.
481
     *
482
     * Note: This method bypasses validation of the destination column to ensure it is within this Table range.
483
     *        Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
484
     *        Use with caution.
485
     *
486
     * @param string $fromColumn Column name (e.g. A)
487
     * @param string $toColumn Column name (e.g. B)
488
     */
489
    public function shiftColumn(string $fromColumn, string $toColumn): self
2✔
490
    {
491
        $fromColumn = strtoupper($fromColumn);
2✔
492
        $toColumn = strtoupper($toColumn);
2✔
493

494
        if (isset($this->columns[$fromColumn])) {
2✔
495
            $this->columns[$fromColumn]->setTable();
1✔
496
            $this->columns[$fromColumn]->setColumnIndex($toColumn);
1✔
497
            $this->columns[$toColumn] = $this->columns[$fromColumn];
1✔
498
            $this->columns[$toColumn]->setTable($this);
1✔
499
            unset($this->columns[$fromColumn]);
1✔
500

501
            ksort($this->columns);
1✔
502
        }
503

504
        return $this;
2✔
505
    }
506

507
    /**
508
     * Get table Style.
509
     */
510
    public function getStyle(): TableStyle
15✔
511
    {
512
        return $this->style;
15✔
513
    }
514

515
    /**
516
     * Set table Style.
517
     */
518
    public function setStyle(TableStyle $style): self
37✔
519
    {
520
        $this->style = $style;
37✔
521

522
        return $this;
37✔
523
    }
524

525
    /**
526
     * Get AutoFilter.
527
     */
528
    public function getAutoFilter(): AutoFilter
35✔
529
    {
530
        return $this->autoFilter;
35✔
531
    }
532

533
    /**
534
     * Set AutoFilter.
535
     */
536
    public function setAutoFilter(AutoFilter $autoFilter): self
×
537
    {
538
        $this->autoFilter = $autoFilter;
×
539

540
        return $this;
×
541
    }
542

543
    /**
544
     * Get the row number on this table for given coordinates.
545
     */
546
    public function getRowNumber(string $coordinate): int
4✔
547
    {
548
        $range = $this->getRange();
4✔
549
        $coords = Coordinate::splitRange($range);
4✔
550
        $firstCell = Coordinate::coordinateFromString($coords[0][0]);
4✔
551
        $thisCell = Coordinate::coordinateFromString($coordinate);
4✔
552

553
        return (int) $thisCell[1] - (int) $firstCell[1];
4✔
554
    }
555

556
    /**
557
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
558
     */
559
    public function __clone()
2✔
560
    {
561
        $vars = get_object_vars($this);
2✔
562
        foreach ($vars as $key => $value) {
2✔
563
            if (is_object($value)) {
2✔
564
                if ($key === 'workSheet') {
2✔
565
                    //    Detach from worksheet
566
                    $this->{$key} = null;
2✔
567
                } else {
568
                    $this->{$key} = clone $value;
2✔
569
                }
570
            } elseif ((is_array($value)) && ($key === 'columns')) {
2✔
571
                //    The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table objects
572
                $this->{$key} = [];
2✔
573
                foreach ($value as $k => $v) {
2✔
574
                    /** @var Table\Column $v */
575
                    $this->{$key}[$k] = clone $v;
1✔
576
                    // attach the new cloned Column to this new cloned Table object
577
                    $this->{$key}[$k]->setTable($this);
1✔
578
                }
579
            } else {
580
                $this->{$key} = $value;
2✔
581
            }
582
        }
583
    }
584

585
    /**
586
     * toString method replicates previous behavior by returning the range if object is
587
     * referenced as a property of its worksheet.
588
     */
589
    public function __toString(): string
1✔
590
    {
591
        return (string) $this->range;
1✔
592
    }
593
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc