Completed
Push — master ( d93a30...6a259d )
by Mark
41s queued 30s
created

Table::updateStructuredReferences()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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