Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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  }