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