Completed
Push — master ( 1a094c...7b4ba9 )
by Mark
41s queued 33s
created

Table::updateStructuredReferencesInCells()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 11
ccs 3
cts 3
cp 1
rs 10
cc 4
nc 4
nop 2
crap 4
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
5
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
6
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
7
use PhpOffice\PhpSpreadsheet\Cell\DataType;
8
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
9
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
10
use PhpOffice\PhpSpreadsheet\Spreadsheet;
11
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
12
13
class Table
14
{
15
    /**
16
     * Table Name.
17
     *
18
     * @var string
19
     */
20
    private $name;
21
22
    /**
23
     * Show Header Row.
24
     *
25
     * @var bool
26
     */
27
    private $showHeaderRow = true;
28
29
    /**
30
     * Show Totals Row.
31
     *
32
     * @var bool
33
     */
34
    private $showTotalsRow = false;
35
36
    /**
37
     * Table Range.
38
     *
39
     * @var string
40
     */
41
    private $range = '';
42
43
    /**
44
     * Table Worksheet.
45
     *
46
     * @var null|Worksheet
47
     */
48
    private $workSheet;
49
50
    /**
51
     * Table Column.
52
     *
53
     * @var Table\Column[]
54
     */
55
    private $columns = [];
56
57
    /**
58
     * Table Style.
59
     *
60
     * @var TableStyle
61
     */
62
    private $style;
63
64
    /**
65
     * Table AutoFilter.
66
     *
67
     * @var AutoFilter
68
     */
69
    private $autoFilter;
70
71
    /**
72
     * Create a new Table.
73
     *
74
     * @param AddressRange|array<int>|string $range
75
     *            A simple string containing a Cell range like 'A1:E10' is permitted
76
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
77
     *              or an AddressRange object.
78 74
     * @param string $name (e.g. Table1)
79
     */
80 74
    public function __construct($range = '', string $name = '')
81 74
    {
82 74
        $this->style = new TableStyle();
83 69
        $this->autoFilter = new AutoFilter($range);
84
        $this->setRange($range);
85
        $this->setName($name);
86
    }
87
88
    /**
89 14
     * Get Table name.
90
     */
91 14
    public function getName(): string
92
    {
93
        return $this->name;
94
    }
95
96
    /**
97
     * Set Table name.
98
     *
99 69
     * @throws PhpSpreadsheetException
100
     */
101 69
    public function setName(string $name): self
102
    {
103 69
        $name = trim($name);
104 28
105 4
        if (!empty($name)) {
106
            if (strlen($name) === 1 && in_array($name, ['C', 'c', 'R', 'r'])) {
107 24
                throw new PhpSpreadsheetException('The table name is invalid');
108 1
            }
109
            if (StringHelper::countCharacters($name) > 255) {
110
                throw new PhpSpreadsheetException('The table name cannot be longer than 255 characters');
111
            }
112 23
            // Check for A1 or R1C1 cell reference notation
113 23
            if (
114
                preg_match(Coordinate::A1_COORDINATE_REGEX, $name) ||
115 5
                preg_match('/^R\[?\-?[0-9]*\]?C\[?\-?[0-9]*\]?$/i', $name)
116
            ) {
117 18
                throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference');
118 3
            }
119
            if (!preg_match('/^[\p{L}_\\\\]/iu', $name)) {
120 15
                throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)');
121
            }
122
            if (!preg_match('/^[\p{L}_\\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) {
123
                throw new PhpSpreadsheetException('The table name contains invalid characters');
124 15
            }
125 15
126
            $this->checkForDuplicateTableNames($name, $this->workSheet);
127
            $this->updateStructuredReferences($name);
128 69
        }
129
130 69
        $this->name = $name;
131
132
        return $this;
133
    }
134
135
    /**
136 15
     * @throws PhpSpreadsheetException
137
     */
138
    private function checkForDuplicateTableNames(string $name, ?Worksheet $worksheet): void
139 15
    {
140
        // Remember that table names are case-insensitive
141 15
        $tableName = StringHelper::strToLower($name);
142 1
143
        if ($worksheet !== null && StringHelper::strToLower($this->name) !== $name) {
144 1
            $spreadsheet = $worksheet->getParent();
145 1
146 1
            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
147 1
                foreach ($sheet->getTableCollection() as $table) {
148
                    if (StringHelper::strToLower($table->getName()) === $tableName && $table != $this) {
149
                        throw new PhpSpreadsheetException("Spreadsheet already contains a table named '{$this->name}'");
150
                    }
151
                }
152
            }
153
        }
154 15
    }
155
156
    private function updateStructuredReferences(string $name): void
157 15
    {
158
        if ($this->workSheet === null || $this->name === null || $this->name === '') {
159 15
            return;
160
        }
161
162
        // Remember that table names are case-insensitive
163
        if (StringHelper::strToLower($this->name) !== StringHelper::strToLower($name)) {
164
            // We need to check all formula cells that might contain fully-qualified Structured References
165
            //    that refer to this table, and update those formulae to reference the new table name
166
            $spreadsheet = $this->workSheet->getParent();
167
            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
168 5
                $this->updateStructuredReferencesInCells($sheet, $name);
169
            }
170 5
            $this->updateStructuredReferencesInNamedFormulae($spreadsheet, $name);
171
        }
172
    }
173
174
    private function updateStructuredReferencesInCells(Worksheet $worksheet, string $newName): void
175
    {
176 3
        $pattern = '/' . preg_quote($this->name) . '\[/mui';
177
178 3
        foreach ($worksheet->getCoordinates(false) as $coordinate) {
179
            $cell = $worksheet->getCell($coordinate);
180 3
            if ($cell->getDataType() === DataType::TYPE_FORMULA) {
181
                $formula = $cell->getValue();
182
                if (preg_match($pattern, $formula) === 1) {
183
                    $formula = preg_replace($pattern, "{$newName}[", $formula);
184
                    $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
185
                }
186 6
            }
187
        }
188 6
    }
189
190
    private function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $newName): void
191
    {
192
        $pattern = '/' . preg_quote($this->name) . '\[/mui';
193
194 4
        foreach ($spreadsheet->getNamedFormulae() as $namedFormula) {
195
            $formula = $namedFormula->getValue();
196 4
            if (preg_match($pattern, $formula) === 1) {
197
                $formula = preg_replace($pattern, "{$newName}[", $formula);
198 4
                $namedFormula->setValue($formula); // @phpstan-ignore-line
199
            }
200
        }
201
    }
202
203
    /**
204 18
     * Get show Header Row.
205
     */
206 18
    public function getShowHeaderRow(): bool
207
    {
208
        return $this->showHeaderRow;
209
    }
210
211
    /**
212
     * Set show Header Row.
213
     */
214
    public function setShowHeaderRow(bool $showHeaderRow): self
215
    {
216
        $this->showHeaderRow = $showHeaderRow;
217 74
218
        return $this;
219
    }
220 74
221 70
    /**
222
     * Get show Totals Row.
223 74
     */
224
    public function getShowTotalsRow(): bool
225 14
    {
226 14
        return $this->showTotalsRow;
227
    }
228 14
229
    /**
230
     * Set show Totals Row.
231 70
     */
232 1
    public function setShowTotalsRow(bool $showTotalsRow): self
233
    {
234
        $this->showTotalsRow = $showTotalsRow;
235 69
236 69
        return $this;
237 4
    }
238
239
    /**
240 65
     * Get Table Range.
241 65
     */
242
    public function getRange(): string
243
    {
244 65
        return $this->range;
245 65
    }
246 2
247 2
    /**
248 1
     * Set Table Cell Range.
249
     *
250
     * @param AddressRange|array<int>|string $range
251
     *            A simple string containing a Cell range like 'A1:E10' is permitted
252 65
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
253
     *              or an AddressRange object.
254
     */
255
    public function setRange($range = ''): self
256
    {
257
        // extract coordinate
258
        if ($range !== '') {
259
            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
260
        }
261
        if (empty($range)) {
262
            //    Discard all column rules
263
            $this->columns = [];
264
            $this->range = '';
265
266
            return $this;
267
        }
268
269
        if (strpos($range, ':') === false) {
270
            throw new PhpSpreadsheetException('Table must be set on a range of cells.');
271
        }
272
273
        [$width, $height] = Coordinate::rangeDimension($range);
274 6
        if ($width < 1 || $height < 2) {
275
            throw new PhpSpreadsheetException('The table range must be at least 1 column and 2 rows');
276 6
        }
277
278
        $this->range = $range;
279
        $this->autoFilter->setRange($range);
280
281
        //    Discard any column rules that are no longer valid within this range
282 17
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
283
        foreach ($this->columns as $key => $value) {
284 17
            $colIndex = Coordinate::columnIndexFromString($key);
285 9
            if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
286 9
                unset($this->columns[$key]);
287
            }
288 9
        }
289 9
290 2
        return $this;
291 1
    }
292
293
    /**
294
     * Set Table Cell Range to max row.
295
     */
296
    public function setRangeToMaxRow(): self
297 17
    {
298 17
        if ($this->workSheet !== null) {
299
            $thisrange = $this->range;
300 17
            $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);
301
            if ($range !== $thisrange) {
302
                $this->setRange($range);
303
            }
304
        }
305
306
        return $this;
307
    }
308 10
309
    /**
310 10
     * Get Table's Worksheet.
311
     */
312
    public function getWorksheet(): ?Worksheet
313
    {
314
        return $this->workSheet;
315
    }
316
317
    /**
318
     * Set Table's Worksheet.
319
     */
320 26
    public function setWorksheet(?Worksheet $worksheet = null): self
321
    {
322 26
        if ($this->name !== '' && $worksheet !== null) {
323 2
            $spreadsheet = $worksheet->getParent();
324
            $tableName = StringHelper::strToUpper($this->name);
325
326 24
            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
327 24
                foreach ($sheet->getTableCollection() as $table) {
328 24
                    if (StringHelper::strToUpper($table->getName()) === $tableName) {
329 3
                        throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'");
330
                    }
331
                }
332 21
            }
333
        }
334
335
        $this->workSheet = $worksheet;
336
        $this->autoFilter->setParent($worksheet);
337
338
        return $this;
339
    }
340
341
    /**
342 2
     * Get all Table Columns.
343
     *
344 2
     * @return Table\Column[]
345
     */
346
    public function getColumns(): array
347
    {
348
        return $this->columns;
349
    }
350
351
    /**
352 15
     * Validate that the specified column is in the Table range.
353
     *
354 15
     * @param string $column Column name (e.g. A)
355
     *
356 13
     * @return int The column offset within the table range
357 12
     */
358
    public function isColumnInRange(string $column): int
359
    {
360 13
        if (empty($this->range)) {
361
            throw new PhpSpreadsheetException('No table range is defined.');
362
        }
363
364
        $columnIndex = Coordinate::columnIndexFromString($column);
365
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
366
        if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
367
            throw new PhpSpreadsheetException('Column is outside of current table range.');
368 6
        }
369
370 6
        return $columnIndex - $rangeStart[0];
371 6
    }
372
373 6
    /**
374
     * Get a specified Table Column Offset within the defined Table range.
375
     *
376
     * @param string $column Column name (e.g. A)
377
     *
378
     * @return int The offset of the specified column within the table range
379
     */
380
    public function getColumnOffset($column): int
381
    {
382 11
        return $this->isColumnInRange($column);
383
    }
384 11
385 9
    /**
386 2
     * Get a specified Table Column.
387 1
     *
388
     * @param string $column Column name (e.g. A)
389 1
     */
390
    public function getColumn($column): Table\Column
391 10
    {
392
        $this->isColumnInRange($column);
393 8
394 7
        if (!isset($this->columns[$column])) {
395
            $this->columns[$column] = new Table\Column($column, $this);
396 1
        }
397 1
398
        return $this->columns[$column];
399 8
    }
400
401 8
    /**
402
     * Get a specified Table Column by it's offset.
403
     *
404
     * @param int $columnOffset Column offset within range (starting from 0)
405
     */
406
    public function getColumnByOffset($columnOffset): Table\Column
407
    {
408
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
409 2
        $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
410
411 2
        return $this->getColumn($pColumn);
412
    }
413 2
414 2
    /**
415
     * Set Table.
416
     *
417 2
     * @param string|Table\Column $columnObjectOrString
418
     *            A simple string containing a Column ID like 'A' is permitted
419
     */
420
    public function setColumn($columnObjectOrString): self
421
    {
422
        if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
423
            $column = $columnObjectOrString;
424
        } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof Table\Column)) {
425
            $column = $columnObjectOrString->getColumnIndex();
426
        } else {
427
            throw new PhpSpreadsheetException('Column is not within the table range.');
428
        }
429
        $this->isColumnInRange($column);
430 2
431
        if (is_string($columnObjectOrString)) {
432 2
            $this->columns[$columnObjectOrString] = new Table\Column($columnObjectOrString, $this);
433 2
        } else {
434
            $columnObjectOrString->setTable($this);
435 2
            $this->columns[$column] = $columnObjectOrString;
436 1
        }
437 1
        ksort($this->columns);
438 1
439 1
        return $this;
440 1
    }
441
442 1
    /**
443
     * Clear a specified Table Column.
444
     *
445 2
     * @param string $column Column name (e.g. A)
446
     */
447
    public function clearColumn($column): self
448
    {
449
        $this->isColumnInRange($column);
450
451 5
        if (isset($this->columns[$column])) {
452
            unset($this->columns[$column]);
453 5
        }
454
455
        return $this;
456
    }
457
458
    /**
459 3
     * Shift an Table Column Rule to a different column.
460
     *
461 3
     * Note: This method bypasses validation of the destination column to ensure it is within this Table range.
462
     *        Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
463 3
     *        Use with caution.
464
     *
465
     * @param string $fromColumn Column name (e.g. A)
466
     * @param string $toColumn Column name (e.g. B)
467
     */
468
    public function shiftColumn($fromColumn, $toColumn): self
469 6
    {
470
        $fromColumn = strtoupper($fromColumn);
471 6
        $toColumn = strtoupper($toColumn);
472
473
        if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
474
            $this->columns[$fromColumn]->setTable();
475
            $this->columns[$fromColumn]->setColumnIndex($toColumn);
476
            $this->columns[$toColumn] = $this->columns[$fromColumn];
477
            $this->columns[$toColumn]->setTable($this);
478
            unset($this->columns[$fromColumn]);
479
480
            ksort($this->columns);
481
        }
482
483
        return $this;
484
    }
485
486
    /**
487 1
     * Get table Style.
488
     */
489 1
    public function getStyle(): Table\TableStyle
490 1
    {
491 1
        return $this->style;
492 1
    }
493
494 1
    /**
495
     * Set table Style.
496 1
     */
497
    public function setStyle(TableStyle $style): self
498 1
    {
499
        $this->style = $style;
500 1
501 1
        return $this;
502 1
    }
503
504 1
    /**
505
     * Get AutoFilter.
506
     */
507 1
    public function getAutoFilter(): AutoFilter
508
    {
509
        return $this->autoFilter;
510
    }
511
512
    /**
513
     * Set AutoFilter.
514
     */
515
    public function setAutoFilter(AutoFilter $autoFilter): self
516 1
    {
517
        $this->autoFilter = $autoFilter;
518 1
519
        return $this;
520
    }
521
522
    /**
523
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
524
     */
525
    public function __clone()
526
    {
527
        $vars = get_object_vars($this);
528
        foreach ($vars as $key => $value) {
529
            if (is_object($value)) {
530
                if ($key === 'workSheet') {
531
                    //    Detach from worksheet
532
                    $this->{$key} = null;
533
                } else {
534
                    $this->{$key} = clone $value;
535
                }
536
            } elseif ((is_array($value)) && ($key === 'columns')) {
537
                //    The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table objects
538
                $this->{$key} = [];
539
                foreach ($value as $k => $v) {
540
                    $this->{$key}[$k] = clone $v;
541
                    // attach the new cloned Column to this new cloned Table object
542
                    $this->{$key}[$k]->setTable($this);
543
                }
544
            } else {
545
                $this->{$key} = $value;
546
            }
547
        }
548
    }
549
550
    /**
551
     * toString method replicates previous behavior by returning the range if object is
552
     * referenced as a property of its worksheet.
553
     */
554
    public function __toString()
555
    {
556
        return (string) $this->range;
557
    }
558
}
559