<?php
namespace PhpOffice\PhpSpreadsheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
class Table
{
/**
* Table Name.
*
* @var string
*/
private $name;
/**
* Show Header Row.
*
* @var bool
*/
private $showHeaderRow = true;
/**
* Show Totals Row.
*
* @var bool
*/
private $showTotalsRow = false;
/**
* Table Range.
*
* @var string
*/
private $range = '';
/**
* Table Worksheet.
*
* @var null|Worksheet
*/
private $workSheet;
/**
* Table allow filter.
*
* @var bool
*/
private $allowFilter = true;
/**
* Table Column.
*
* @var Table\Column[]
*/
private $columns = [];
/**
* Table Style.
*
* @var TableStyle
*/
private $style;
/**
* Table AutoFilter.
*
* @var AutoFilter
*/
private $autoFilter;
/**
* Create a new Table.
*
* @param AddressRange|array<int>|string $range
* A simple string containing a Cell range like 'A1:E10' is permitted
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
* @param string $name (e.g. Table1)
*/
public function __construct($range = '', string $name = '')
{
$this->style = new TableStyle();
$this->autoFilter = new AutoFilter($range);
$this->setRange($range);
$this->setName($name);
}
/**
* Get Table name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Set Table name.
*
* @throws PhpSpreadsheetException
*/
public function setName(string $name): self
{
$name = trim($name);
if (!empty($name)) {
if (strlen($name) === 1 && in_array($name, ['C', 'c', 'R', 'r'])) {
throw new PhpSpreadsheetException('The table name is invalid');
}
if (StringHelper::countCharacters($name) > 255) {
throw new PhpSpreadsheetException('The table name cannot be longer than 255 characters');
}
// Check for A1 or R1C1 cell reference notation
if (
preg_match(Coordinate::A1_COORDINATE_REGEX, $name) ||
preg_match('/^R\[?\-?[0-9]*\]?C\[?\-?[0-9]*\]?$/i', $name)
) {
throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference');
}
if (!preg_match('/^[\p{L}_\\\\]/iu', $name)) {
throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)');
}
if (!preg_match('/^[\p{L}_\\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) {
throw new PhpSpreadsheetException('The table name contains invalid characters');
}
$this->checkForDuplicateTableNames($name, $this->workSheet);
$this->updateStructuredReferences($name);
}
$this->name = $name;
return $this;
}
/**
* @throws PhpSpreadsheetException
*/
private function checkForDuplicateTableNames(string $name, ?Worksheet $worksheet): void
{
// Remember that table names are case-insensitive
$tableName = StringHelper::strToLower($name);
if ($worksheet !== null && StringHelper::strToLower($this->name) !== $name) {
$spreadsheet = $worksheet->getParentOrThrow();
foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
foreach ($sheet->getTableCollection() as $table) {
if (StringHelper::strToLower($table->getName()) === $tableName && $table != $this) {
throw new PhpSpreadsheetException("Spreadsheet already contains a table named '{$this->name}'");
}
}
}
}
}
private function updateStructuredReferences(string $name): void
{
if ($this->workSheet === null || $this->name === null || $this->name === '') {
return;
}
// Remember that table names are case-insensitive
if (StringHelper::strToLower($this->name) !== StringHelper::strToLower($name)) {
// We need to check all formula cells that might contain fully-qualified Structured References
// that refer to this table, and update those formulae to reference the new table name
$spreadsheet = $this->workSheet->getParentOrThrow();
foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
$this->updateStructuredReferencesInCells($sheet, $name);
}
$this->updateStructuredReferencesInNamedFormulae($spreadsheet, $name);
}
}
private function updateStructuredReferencesInCells(Worksheet $worksheet, string $newName): void
{
$pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
foreach ($worksheet->getCoordinates(false) as $coordinate) {
$cell = $worksheet->getCell($coordinate);
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
$formula = $cell->getValue();
if (preg_match($pattern, $formula) === 1) {
$formula = preg_replace($pattern, "{$newName}[", $formula);
$cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
}
}
}
}
private function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $newName): void
{
$pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
foreach ($spreadsheet->getNamedFormulae() as $namedFormula) {
$formula = $namedFormula->getValue();
if (preg_match($pattern, $formula) === 1) {
$formula = preg_replace($pattern, "{$newName}[", $formula);
$namedFormula->setValue($formula); // @phpstan-ignore-line
}
}
}
/**
* Get show Header Row.
*/
public function getShowHeaderRow(): bool
{
return $this->showHeaderRow;
}
/**
* Set show Header Row.
*/
public function setShowHeaderRow(bool $showHeaderRow): self
{
$this->showHeaderRow = $showHeaderRow;
return $this;
}
/**
* Get show Totals Row.
*/
public function getShowTotalsRow(): bool
{
return $this->showTotalsRow;
}
/**
* Set show Totals Row.
*/
public function setShowTotalsRow(bool $showTotalsRow): self
{
$this->showTotalsRow = $showTotalsRow;
return $this;
}
/**
* Get allow filter.
* If false, autofiltering is disabled for the table, if true it is enabled.
*/
public function getAllowFilter(): bool
{
return $this->allowFilter;
}
/**
* Set show Autofiltering.
* Disabling autofiltering has the same effect as hiding the filter button on all the columns in the table.
*/
public function setAllowFilter(bool $allowFilter): self
{
$this->allowFilter = $allowFilter;
return $this;
}
/**
* Get Table Range.
*/
public function getRange(): string
{
return $this->range;
}
/**
* Set Table Cell Range.
*
* @param AddressRange|array<int>|string $range
* A simple string containing a Cell range like 'A1:E10' is permitted
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
*/
public function setRange($range = ''): self
{
// extract coordinate
if ($range !== '') {
[, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
}
if (empty($range)) {
// Discard all column rules
$this->columns = [];
$this->range = '';
return $this;
}
if (strpos($range, ':') === false) {
throw new PhpSpreadsheetException('Table must be set on a range of cells.');
}
[$width, $height] = Coordinate::rangeDimension($range);
if ($width < 1 || $height < 1) {
throw new PhpSpreadsheetException('The table range must be at least 1 column and row');
}
$this->range = $range;
$this->autoFilter->setRange($range);
// Discard any column rules that are no longer valid within this range
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
foreach ($this->columns as $key => $value) {
$colIndex = Coordinate::columnIndexFromString($key);
if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
unset($this->columns[$key]);
}
}
return $this;
}
/**
* Set Table Cell Range to max row.
*/
public function setRangeToMaxRow(): self
{
if ($this->workSheet !== null) {
$thisrange = $this->range;
$range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);
if ($range !== $thisrange) {
$this->setRange($range);
}
}
return $this;
}
/**
* Get Table's Worksheet.
*/
public function getWorksheet(): ?Worksheet
{
return $this->workSheet;
}
/**
* Set Table's Worksheet.
*/
public function setWorksheet(?Worksheet $worksheet = null): self
{
if ($this->name !== '' && $worksheet !== null) {
$spreadsheet = $worksheet->getParentOrThrow();
$tableName = StringHelper::strToUpper($this->name);
foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
foreach ($sheet->getTableCollection() as $table) {
if (StringHelper::strToUpper($table->getName()) === $tableName) {
throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'");
}
}
}
}
$this->workSheet = $worksheet;
$this->autoFilter->setParent($worksheet);
return $this;
}
/**
* Get all Table Columns.
*
* @return Table\Column[]
*/
public function getColumns(): array
{
return $this->columns;
}
/**
* Validate that the specified column is in the Table range.
*
* @param string $column Column name (e.g. A)
*
* @return int The column offset within the table range
*/
public function isColumnInRange(string $column): int
{
if (empty($this->range)) {
throw new PhpSpreadsheetException('No table range is defined.');
}
$columnIndex = Coordinate::columnIndexFromString($column);
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
throw new PhpSpreadsheetException('Column is outside of current table range.');
}
return $columnIndex - $rangeStart[0];
}
/**
* Get a specified Table Column Offset within the defined Table range.
*
* @param string $column Column name (e.g. A)
*
* @return int The offset of the specified column within the table range
*/
public function getColumnOffset($column): int
{
return $this->isColumnInRange($column);
}
/**
* Get a specified Table Column.
*
* @param string $column Column name (e.g. A)
*/
public function getColumn($column): Table\Column
{
$this->isColumnInRange($column);
if (!isset($this->columns[$column])) {
$this->columns[$column] = new Table\Column($column, $this);
}
return $this->columns[$column];
}
/**
* Get a specified Table Column by it's offset.
*
* @param int $columnOffset Column offset within range (starting from 0)
*/
public function getColumnByOffset($columnOffset): Table\Column
{
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
$pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
return $this->getColumn($pColumn);
}
/**
* Set Table.
*
* @param string|Table\Column $columnObjectOrString
* A simple string containing a Column ID like 'A' is permitted
*/
public function setColumn($columnObjectOrString): self
{
if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
$column = $columnObjectOrString;
} elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof Table\Column)) {
$column = $columnObjectOrString->getColumnIndex();
} else {
throw new PhpSpreadsheetException('Column is not within the table range.');
}
$this->isColumnInRange($column);
if (is_string($columnObjectOrString)) {
$this->columns[$columnObjectOrString] = new Table\Column($columnObjectOrString, $this);
} else {
$columnObjectOrString->setTable($this);
$this->columns[$column] = $columnObjectOrString;
}
ksort($this->columns);
return $this;
}
/**
* Clear a specified Table Column.
*
* @param string $column Column name (e.g. A)
*/
public function clearColumn($column): self
{
$this->isColumnInRange($column);
if (isset($this->columns[$column])) {
unset($this->columns[$column]);
}
return $this;
}
/**
* Shift an Table Column Rule to a different column.
*
* Note: This method bypasses validation of the destination column to ensure it is within this Table range.
* Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
* Use with caution.
*
* @param string $fromColumn Column name (e.g. A)
* @param string $toColumn Column name (e.g. B)
*/
public function shiftColumn($fromColumn, $toColumn): self
{
$fromColumn = strtoupper($fromColumn);
$toColumn = strtoupper($toColumn);
if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
$this->columns[$fromColumn]->setTable();
$this->columns[$fromColumn]->setColumnIndex($toColumn);
$this->columns[$toColumn] = $this->columns[$fromColumn];
$this->columns[$toColumn]->setTable($this);
unset($this->columns[$fromColumn]);
ksort($this->columns);
}
return $this;
}
/**
* Get table Style.
*/
public function getStyle(): Table\TableStyle
{
return $this->style;
}
/**
* Set table Style.
*/
public function setStyle(TableStyle $style): self
{
$this->style = $style;
return $this;
}
/**
* Get AutoFilter.
*/
public function getAutoFilter(): AutoFilter
{
return $this->autoFilter;
}
/**
* Set AutoFilter.
*/
public function setAutoFilter(AutoFilter $autoFilter): self
{
$this->autoFilter = $autoFilter;
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
if (is_object($value)) {
if ($key === 'workSheet') {
// Detach from worksheet
$this->{$key} = null;
} else {
$this->{$key} = clone $value;
}
} elseif ((is_array($value)) && ($key === 'columns')) {
// The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table objects
$this->{$key} = [];
foreach ($value as $k => $v) {
$this->{$key}[$k] = clone $v;
// attach the new cloned Column to this new cloned Table object
$this->{$key}[$k]->setTable($this);
}
} else {
$this->{$key} = $value;
}
}
}
/**
* toString method replicates previous behavior by returning the range if object is
* referenced as a property of its worksheet.
*/
public function __toString()
{
return (string) $this->range;
}
}
In the digital age, privacy concerns have become increasingly paramount, prompting the European Union to enact the General Data Protection Regulation (GDPR) in 2018. Among its many provisions, GDPR sets strict guidelines for the collection and processing of personal data, including the use of cookies on websites. Privacy Policy