From 7a2f5c4ccc3cd69234179488d0e1dd9e3f6ed7bc Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 16 Apr 2022 19:34:41 +0200 Subject: [PATCH 01/16] Ods Writer support for setting column width/row height (including Autosizing) --- CHANGELOG.md | 1 + phpstan-baseline.neon | 5 -- src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 62 +++++++++++++++++++ src/PhpSpreadsheet/Writer/Ods/Content.php | 51 ++++++++++++--- .../Writer/PreCalcTest.php | 2 +- tests/data/Writer/Ods/content-empty.xml | 1 - tests/data/Writer/Ods/content-with-data.xml | 2 - 7 files changed, 105 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3912c622..ceda96f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Ods Writer support for setting column width/row height (including the use of AutoSize) [Issue #2346](https://github.com/PHPOffice/PhpSpreadsheet/issues/2346) [PR #2753](https://github.com/PHPOffice/PhpSpreadsheet/pull/2753) - Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts. - Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions. - Implementation of the ISREF() Information function. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 12329a72..c6d3dda9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4675,11 +4675,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Ods/Content.php - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 4 - path: src/PhpSpreadsheet/Writer/Ods/Content.php - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<2, max\\> given\\.$#" count: 3 diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index f8aae20c..a3629bd6 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -2,15 +2,20 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods\Cell; +use PhpOffice\PhpSpreadsheet\Helper\Dimension; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; +use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; +use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; class Style { public const CELL_STYLE_PREFIX = 'ce'; + public const COLUMN_STYLE_PREFIX = 'co'; + public const ROW_STYLE_PREFIX = 'ro'; private $writer; @@ -159,6 +164,63 @@ class Style $this->writer->endElement(); // Close style:text-properties } + protected function writeColumnProperties(ColumnDimension $columnDimension): void + { + $this->writer->startElement('style:table-column-properties'); + $this->writer->writeAttribute( + 'style:column-width', + round($columnDimension->getWidth(Dimension::UOM_CENTIMETERS), 3) . 'cm' + ); + $this->writer->writeAttribute('fo:break-before', 'auto'); + + // End + $this->writer->endElement(); // Close style:table-column-properties + } + + public function writeColumnStyles(ColumnDimension $columnDimension, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table-column'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s_%d_%d', self::COLUMN_STYLE_PREFIX, $sheetId, $columnDimension->getColumnNumeric()) + ); + + $this->writeColumnProperties($columnDimension); + + // End + $this->writer->endElement(); // Close style:style + } + + protected function writeRowProperties(RowDimension $rowDimension): void + { + $this->writer->startElement('style:table-row-properties'); + $this->writer->writeAttribute( + 'style:row-height', + round($rowDimension->getRowHeight(Dimension::UOM_CENTIMETERS), 3) . 'cm' + ); + $this->writer->writeAttribute('style:use-optimal-row-height', 'true'); + $this->writer->writeAttribute('fo:break-before', 'auto'); + + // End + $this->writer->endElement(); // Close style:table-row-properties + } + + public function writeRowStyles(RowDimension $rowDimension, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table-row'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s_%d_%d', self::ROW_STYLE_PREFIX, $sheetId, $rowDimension->getRowIndex()) + ); + + $this->writeRowProperties($rowDimension); + + // End + $this->writer->endElement(); // Close style:style + } + public function write(CellStyle $style): void { $this->writer->startElement('style:style'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index a589e549..2cb31b36 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -119,14 +119,21 @@ class Content extends WriterPart { $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */ $sheetCount = $spreadsheet->getSheetCount(); - for ($i = 0; $i < $sheetCount; ++$i) { + for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) { $objWriter->startElement('table:table'); - $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle()); + $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); $objWriter->writeElement('office:forms'); - $objWriter->startElement('table:table-column'); - $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); - $objWriter->endElement(); - $this->writeRows($objWriter, $spreadsheet->getSheet($i)); + foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { + $objWriter->startElement('table:table-column'); + $objWriter->writeAttribute( + 'table:style-name', + sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric()) + ); + $objWriter->writeAttribute('table:default-cell-style-name', 'ce0'); +// $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); + $objWriter->endElement(); + } + $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex); $objWriter->endElement(); } } @@ -134,7 +141,7 @@ class Content extends WriterPart /** * Write rows of the specified sheet. */ - private function writeRows(XMLWriter $objWriter, Worksheet $sheet): void + private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void { $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX; $span_row = 0; @@ -148,8 +155,14 @@ class Content extends WriterPart if ($span_row > 1) { $objWriter->writeAttribute('table:number-rows-repeated', $span_row); } + if ($sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) { + $objWriter->writeAttribute( + 'table:style_name', + sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex()) + ); + } $objWriter->startElement('table:table-cell'); - $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); + $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX); $objWriter->endElement(); $objWriter->endElement(); $span_row = 0; @@ -275,6 +288,24 @@ class Content extends WriterPart private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void { $styleWriter = new Style($writer); + + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $spreadsheet->getSheet($i); + $worksheet->calculateColumnWidths(); + foreach ($worksheet->getColumnDimensions() as $columnDimension) { + $styleWriter->writeColumnStyles($columnDimension, $i); + } + } + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $spreadsheet->getSheet($i); + foreach ($worksheet->getRowDimensions() as $rowDimension) { + if ($rowDimension->getRowHeight() > 0.0) { + $styleWriter->writeRowStyles($rowDimension, $i); + } + } + } + foreach ($spreadsheet->getCellXfCollection() as $style) { $styleWriter->write($style); } @@ -296,7 +327,7 @@ class Content extends WriterPart $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1; $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1; - $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan); - $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan); + $objWriter->writeAttribute('table:number-columns-spanned', (string) $columnSpan); + $objWriter->writeAttribute('table:number-rows-spanned', (string) $rowSpan); } } diff --git a/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php index 2db372c4..73374e21 100644 --- a/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php +++ b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php @@ -64,7 +64,7 @@ class PreCalcTest extends AbstractFunctional } } - private const AUTOSIZE_TYPES = ['Xlsx', 'Xls', 'Html']; + private const AUTOSIZE_TYPES = ['Xlsx', 'Xls', 'Html', 'Ods']; private static function verifyA3B2(Calculation $calculation, string $title, ?bool $preCalc, string $type): void { diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index c9620060..867cfa3e 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -14,7 +14,6 @@ - diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index a707d197..fff47f65 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -64,7 +64,6 @@ - 1 @@ -110,7 +109,6 @@ - 2 From d5936172872d255a31855473c617f1b581bee07f Mon Sep 17 00:00:00 2001 From: redforks Date: Sun, 17 Apr 2022 23:27:28 +0800 Subject: [PATCH 02/16] Fix font index problem (#2642) * Fix font index problem * Update RichTextSizeTest.php Eliminate Phpstan failure. * Update RichTextSizeTest.php Eliminate now-unused import. --- src/PhpSpreadsheet/Reader/Xls.php | 9 +++++-- .../Reader/Xls/RichTextSizeTest.php | 22 ++++++++++++++++++ tests/data/Reader/XLS/RichTextFontSize.xls | Bin 0 -> 53760 bytes 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php create mode 100644 tests/data/Reader/XLS/RichTextFontSize.xls diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 402fea9f..d1f14ae1 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -3759,8 +3759,13 @@ class Xls extends BaseReader } else { $textRun = $richText->createTextRun($text); if (isset($fmtRuns[$i - 1])) { - $fontIndex = $fmtRuns[$i - 1]['fontIndex']; - + if ($fmtRuns[$i - 1]['fontIndex'] < 4) { + $fontIndex = $fmtRuns[$i - 1]['fontIndex']; + } else { + // this has to do with that index 4 is omitted in all BIFF versions for some stra nge reason + // check the OpenOffice documentation of the FONT record + $fontIndex = $fmtRuns[$i - 1]['fontIndex'] - 1; + } if (array_key_exists($fontIndex, $this->objFonts) === false) { $fontIndex = count($this->objFonts) - 1; } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php new file mode 100644 index 00000000..54274e8c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php @@ -0,0 +1,22 @@ +load($filename); + $sheet = $spreadsheet->getSheetByName('橱柜门板'); + self::assertNotNull($sheet); + $text = $sheet->getCell('L15')->getValue(); + $elements = $text->getRichTextElements(); + self::assertEquals(10, $elements[2]->getFont()->getSize()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLS/RichTextFontSize.xls b/tests/data/Reader/XLS/RichTextFontSize.xls new file mode 100644 index 0000000000000000000000000000000000000000..3edf47379c273edebb3c30c2484a4eb69a081fc6 GIT binary patch literal 53760 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 literal 0 HcmV?d00001 From 9545b7a0d1ff8b6a4feff1e5e13d25d2ba8bdd28 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 17 Apr 2022 08:44:29 -0700 Subject: [PATCH 03/16] Fix Fluke Failure in Document Properties Test (#2738) PR #2720 failed because a timestamp in Document Properties Test was off by 1. This was due to one of two possible reasons. The constructor for Properties set the Created and Modified times using separate calls to the time function; if those happened to occur in different seconds, the test would fail. The test might also fail if the Created and Modified times used the same timestamp, but the time used to compare against those was calculated in a different second. It is surprising that this failure hasn't shown up before. Regardless, this PR corrects both possible problems. --- src/PhpSpreadsheet/Document/Properties.php | 2 +- .../Document/PropertiesTest.php | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 3be5a67a..2d461c59 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -115,7 +115,7 @@ class Properties // Initialise values $this->lastModifiedBy = $this->creator; $this->created = self::intOrFloatTimestamp(null); - $this->modified = self::intOrFloatTimestamp(null); + $this->modified = $this->created; } /** diff --git a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php index e6c95cd4..0dfb256b 100644 --- a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Document; +use DateTime; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -14,20 +15,27 @@ class PropertiesTest extends TestCase */ private $properties; + /** @var float */ + private $startTime; + protected function setup(): void { - $this->properties = new Properties(); + do { + // loop to avoid rare situation where timestamp changes + $this->startTime = (float) (new DateTime())->format('U'); + $this->properties = new Properties(); + $endTime = (float) (new DateTime())->format('U'); + } while ($this->startTime !== $endTime); } public function testNewInstance(): void { - $createdTime = $modifiedTime = time(); self::assertSame('Unknown Creator', $this->properties->getCreator()); self::assertSame('Unknown Creator', $this->properties->getLastModifiedBy()); self::assertSame('Untitled Spreadsheet', $this->properties->getTitle()); self::assertSame('', $this->properties->getCompany()); - self::assertSame($createdTime, $this->properties->getCreated()); - self::assertSame($modifiedTime, $this->properties->getModified()); + self::assertEquals($this->startTime, $this->properties->getCreated()); + self::assertEquals($this->startTime, $this->properties->getModified()); } public function testSetCreator(): void @@ -46,10 +54,10 @@ class PropertiesTest extends TestCase */ public function testSetCreated($expectedCreationTime, $created): void { - $expectedCreationTime = $expectedCreationTime ?? time(); + $expectedCreationTime = $expectedCreationTime ?? $this->startTime; $this->properties->setCreated($created); - self::assertSame($expectedCreationTime, $this->properties->getCreated()); + self::assertEquals($expectedCreationTime, $this->properties->getCreated()); } public function providerCreationTime(): array @@ -78,10 +86,10 @@ class PropertiesTest extends TestCase */ public function testSetModified($expectedModifiedTime, $modified): void { - $expectedModifiedTime = $expectedModifiedTime ?? time(); + $expectedModifiedTime = $expectedModifiedTime ?? $this->startTime; $this->properties->setModified($modified); - self::assertSame($expectedModifiedTime, $this->properties->getModified()); + self::assertEquals($expectedModifiedTime, $this->properties->getModified()); } public function providerModifiedTime(): array From ea584301c7feb3e3617feaa58d2bef2361492690 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 17 Apr 2022 21:50:52 +0200 Subject: [PATCH 04/16] Modify Autosize calculation to make allowance for the filter dropdown icon in the first row of an AutoFilter range --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Shared/Font.php | 28 ++++++++++++++++------ src/PhpSpreadsheet/Worksheet/Worksheet.php | 28 ++++++++++++++++++---- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda96f0..e136eab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Make allowance for the AutoFilter dropdown icon in the first row of an Autofilter range when using Autosize columns. [Issue #2413](https://github.com/PHPOffice/PhpSpreadsheet/issues/2413) [PR #2754](https://github.com/PHPOffice/PhpSpreadsheet/pull/2754) - Support for "chained" ranges (e.g. `A5:C10:C20:F1`) in the Calculation Engine; and also support for using named ranges with the Range operator (e.g. `NamedRange1:NamedRange2`) [Issue #2730](https://github.com/PHPOffice/PhpSpreadsheet/issues/2730) [PR #2746](https://github.com/PHPOffice/PhpSpreadsheet/pull/2746) - Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689) - Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 9a74befe..3ee3d5a6 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -222,11 +222,15 @@ class Font * @param RichText|string $cellText Text to calculate width * @param int $rotation Rotation angle * @param null|FontStyle $defaultFont Font object - * - * @return int Column width + * @param bool $filterAdjustment Add space for Autofilter or Table dropdown */ - public static function calculateColumnWidth(FontStyle $font, $cellText = '', $rotation = 0, ?FontStyle $defaultFont = null) - { + public static function calculateColumnWidth( + FontStyle $font, + $cellText = '', + $rotation = 0, + ?FontStyle $defaultFont = null, + bool $filterAdjustment = false + ): int { // If it is rich text, use plain text if ($cellText instanceof RichText) { $cellText = $cellText->getPlainText(); @@ -237,7 +241,7 @@ class Font $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { - $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont); + $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont, $filterAdjustment); } return max($lineWidths); // width of longest line in cell @@ -247,7 +251,13 @@ class Font $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX; $columnWidth = 0; if (!$approximate) { - $columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07); + $columnWidthAdjust = ceil( + self::getTextWidthPixelsExact( + str_repeat('n', 1 * ($filterAdjustment ? 3 : 1)), + $font, + 0 + ) * 1.07 + ); try { // Width of text in pixels excl. padding @@ -259,7 +269,11 @@ class Font } if ($approximate) { - $columnWidthAdjust = self::getTextWidthPixelsApprox('n', $font, 0); + $columnWidthAdjust = self::getTextWidthPixelsApprox( + str_repeat('n', 1 * ($filterAdjustment ? 3 : 1)), + $font, + 0 + ); // Width of text in pixels excl. padding, approximation // and addition because Excel adds some padding, just use approx width of 'n' glyph $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index a1bfbfdb..e4ce2ac3 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -733,9 +733,19 @@ class Worksheet implements IComparable } } + $autoFilterRange = $autoFilterFirstRowRange = $this->autoFilter->getRange(); + if (!empty($autoFilterRange)) { + $autoFilterRangeBoundaries = Coordinate::rangeBoundaries($autoFilterRange); + $autoFilterFirstRowRange = (string) new CellRange( + CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[0][0], $autoFilterRangeBoundaries[0][1]), + CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[1][0], $autoFilterRangeBoundaries[0][1]) + ); + } + // loop through all cells in the worksheet foreach ($this->getCoordinates(false) as $coordinate) { $cell = $this->getCellOrNull($coordinate); + if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) { //Determine if cell is in merge range $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]); @@ -752,13 +762,21 @@ class Worksheet implements IComparable } } - // Determine width if cell does not participate in a merge or does and is a value cell of 1-column wide range + // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range if (!$isMerged || $isMergedButProceed) { + // Determine if we need to make an adjustment for the first row in an AutoFilter range that + // has a column filter dropdown + $filterAdjustment = false; + if (!empty($autoFilterRange) && $cell->isInRange($autoFilterFirstRowRange)) { + $filterAdjustment = true; + } + // Calculated value // To formatted string $cellValue = NumberFormat::toFormattedString( $cell->getCalculatedValue(), - $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode() + $this->getParent()->getCellXfByIndex($cell->getXfIndex()) + ->getNumberFormat()->getFormatCode() ); if ($cellValue !== null && $cellValue !== '') { @@ -767,8 +785,10 @@ class Worksheet implements IComparable (float) Shared\Font::calculateColumnWidth( $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(), $cellValue, - $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(), - $this->getParent()->getDefaultStyle()->getFont() + $this->getParent()->getCellXfByIndex($cell->getXfIndex()) + ->getAlignment()->getTextRotation(), + $this->getParent()->getDefaultStyle()->getFont(), + $filterAdjustment ) ); } From 4cd1d7039d1db8e4994a28eb7f7a6566dbb3b7d2 Mon Sep 17 00:00:00 2001 From: andres1gb Date: Mon, 18 Apr 2022 15:54:41 +0200 Subject: [PATCH 05/16] Fix reading of files in the root of a zip (#2731) * Fix reading of files in the root of a zip Xlsx.php relies in dirname($filename) for path generation. When path is a bare filename (i.e. files in the root of the zip file), dirname($filename) returns a relative path to the current directory ("."). This is ok for filesystems, but not when accesing contents in a zip file. Xlsx documents with files in the root of the zip container are not common, but legit. I've found it to happen in files generated by Google Campaign Manager 360. * Update Xlsx.php * Update Xlsx.php * Update CHANGELOG.md * Add files via upload * Create XlsxRootZipFilesTest.php * Update XlsxRootZipFilesTest.php * Add files via upload * Delete rootZipFiles.xlsx * Update XlsxRootZipFilesTest.php * Update Xlsx.php --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 3 +++ .../Reader/Xlsx/XlsxRootZipFilesTest.php | 24 ++++++++++++++++++ tests/data/Reader/XLSX/rootZipFiles.xlsx | Bin 0 -> 3363 bytes 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxRootZipFilesTest.php create mode 100644 tests/data/Reader/XLSX/rootZipFiles.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e136eab6..d5ef98a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,9 +82,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). Nor is this a perfect solution, as there may still be issues when function calls have array arguments that themselves contain function calls; but it's still better than the current logic. - Fix for escaping double quotes within a formula [Issue #1971](https://github.com/PHPOffice/PhpSpreadsheet/issues/1971) [PR #2651](https://github.com/PHPOffice/PhpSpreadsheet/pull/2651) +- Fix for reading files in the root directory of a ZipFile, which should not be prefixed by relative paths ("./") as dirname($filename) does by default. - Fix invalid style of cells in empty columns with columnDimensions and rows with rowDimensions in added external sheet. [PR #2739](https://github.com/PHPOffice/PhpSpreadsheet/pull/2739) - ## 1.22.0 - 2022-02-18 ### Added diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 18ed7987..a6e7fe03 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -368,6 +368,9 @@ class Xlsx extends BaseReader if (strpos($fileName, '//') !== false) { $fileName = substr($fileName, strpos($fileName, '//') + 1); } + // Relative paths generated by dirname($filename) when $filename + // has no path (i.e.files in root of the zip archive) + $fileName = (string) preg_replace('/^\.\//', '', $fileName); $fileName = File::realpath($fileName); // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxRootZipFilesTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxRootZipFilesTest.php new file mode 100644 index 00000000..110c70b0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxRootZipFilesTest.php @@ -0,0 +1,24 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $value = $sheet->getCell('A1')->getValue(); + self::assertSame('TEST CELL', $value->getPlainText()); + } +} diff --git a/tests/data/Reader/XLSX/rootZipFiles.xlsx b/tests/data/Reader/XLSX/rootZipFiles.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3de12790b6c7d3b026306170d2d04e83677e576a GIT binary patch literal 3363 zcmb7`c|2787soGyVQizU{gP<18`~&Jzid%yY|UgT>liA=Hj}cXWN)bOO0uPeBFZS0 zEsY7;X-uIc%Y?F2h({0o?(h_%dVY6acV74Yao(@<{hV_@=UA9RID`QJ;08Xx$e2=S zrZbEU0K(V-U@fp7IB4kYMZ$ZL95Detc%nV>1i=%F;Q=7;-S)eT!Z9w#PVfS39Ft(g zJS0D%+zg@)5C5tg!4yrGl!qG>n+hEcJtJl_T%IQKt+te>_+icQZI&%YyK|I1+~1`Z zm33rk5g&RVUo;s_l~ah8w)?2yqGW<79yy>Sz2yqdcnw1;=OTm{ z@*<_-G~4d1Sc=&UhTW$;sl3Tapul9dz+vanL>XLvzuw8uQ7HY?wn7#&IOPT1fMU&m=66;c3NEN-`S4&2D*V|n2Jxt z1}g8V>jQ#qvw#ICD-4ts0J4veG4l`q{BeP@I{M)~iK_ET?&in1K!jE($%B7fQWoYJ9t%DJc2?Wc67UWBh&C_sg)(UR39HoYNb<8-Yi`pRzBif zFrTNhbLh!ZI|+IjMNUOa;wB>7=ZuXwmacj2QM`MCqO1UuUZb0slnmY1Z-!FydLKys zE1r!&odZ&73Dc_W?Nf<;mj;_Op|cU^UM8DsXecE^n4t{zH-EphE!dQm>q9N`7yazZ zKV7l+8d)_gWajmFRKxY^?8B&O!sr*tw%Hl|4mr1c_FDI48?o`@q%Zc|j^B z`ld$&Hqb&P#xP|=#8*^7^e|WC(V_{#dNkdmq##31qbKU|k&T$TB4Gq_I1)O3!APjb z&XiQY!_cz7_d``L|MFA2G|j{pf+r4v0)PN`Y9cAX6Hh#h$CLi_m5yL*y(-n<OvwW;zcrv=q z?`WWvI$ONW`!qYXV+OI$V~wjr{mi)Z)ezenIWhogi)t&Iuq}Q{p3%^tu{Y*+2MDK& zWfRpEPBPq%7RHW4I?_$JLIm&JD%wZpJkdH=rYL*}`y|4*c7(0TST03$mfFSSF#fDu ziOvp_CDvvc>)1wVb7#0Wk5?67KH7wsc{|+crU$JnW@K3tCTko_CH2Lw45lmgVxQW+ zMy+kvF@2sy+sjP#IFjFBFG9$rH{4oJhQAW-oJrj0E{xia!?Kz57-~&k4T<6J3m&p^ z?4uDUQU}_r1U|?GXbbvvN21*O-G(>&-H2AKB1hyQ6hFkf$2_w=D02G#m+QyTjI$$k z9M<(nFAB}%*l<{itL`ZE}361N=*UfpO!v#=sMXZ3m41fTNxXvwOuOR zGRzcTU_R4H)p6RS`Px0R^l{nFc4F1mi>HoZ=Ol_B-=l2VD#+4i!D(}Ta#{$e%?Zy1 zn>G8B&9Z_I1`IxZ4GPO4z91|Zj5ONsxS!W=OulY5(+rCJQ^u@fip)3C-HBeID+z<;F1h28?L(bW>(wc@IX z`&GBzwd3Nj$0T!0{o9I9rdc>eH4;M}J9>%jq7)^q<&h4DUlUQz@5p@B3^xdto}76* zq<5*cdVnYWzfA2;dMNUGT;CdR1< zndqR<^fqC1EDhn6*>&$lOLTDN)4bvCW);rBy>Eg-n*FVis&A;OS)RC-Xe(* zlliIP>&>m<4YH`}D!p~zH#!% zx;}Hi44qGQtnHe-!^N#zoHhp}XmfWbl>~3j7M>Uhanb$q_I9g{6E?S0xBUEQ;ki*E z1W-0|-00&wjkI*aBgV8<<~o)4J;e{^P@ba_3jP-Y2jYf^LGKJJ``JXf^IEywA%tz{ zx*IL;6ok**?g}goV+7t)5UFRkl7{Rnyof4O*&=C#Fdx!W@3g`N&-gybS?X4B&s4 z7OEf4GMe+@IU AjsO4v literal 0 HcmV?d00001 From 0afec9106162ff4e2b9dbbecc87e16a51d3afc7d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 18 Apr 2022 08:28:01 -0700 Subject: [PATCH 06/16] Change Log Updates (#2756) Catch up on some undocumented changes. --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ef98a1..1854e3f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators. -- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) +- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) [#2674](https://github.com/PHPOffice/PhpSpreadsheet/pull/2674) - Limited support for Xls Reader to handle Conditional Formatting: Ranges and Rules are read, but style is currently limited to font size, weight and color; and to fill style and color. +- Add ability to suppress Mac line ending check for CSV [#2623](https://github.com/PHPOffice/PhpSpreadsheet/pull/2623) + ### Changed - Gnumeric Reader now loads number formatting for cells. @@ -75,13 +77,17 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. - Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. - Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669) -- Fixed behaviour of XLSX font style vertical align settings. +- Fixed behaviour of XLSX font style vertical align settings [PR #2619](https://github.com/PHPOffice/PhpSpreadsheet/pull/2619) - Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels. Note that this method is used when translating Excel functions between `en_us` and other locale languages, as well as when converting formulae between different spreadsheet formats (e.g. Ods to Excel). Nor is this a perfect solution, as there may still be issues when function calls have array arguments that themselves contain function calls; but it's still better than the current logic. - Fix for escaping double quotes within a formula [Issue #1971](https://github.com/PHPOffice/PhpSpreadsheet/issues/1971) [PR #2651](https://github.com/PHPOffice/PhpSpreadsheet/pull/2651) +- Change open mode for output from `wb+` to `wb` [Issue #2372](https://github.com/PHPOffice/PhpSpreadsheet/issues/2372) [PR #2657](https://github.com/PHPOffice/PhpSpreadsheet/pull/2657) +- Use color palette if supplied [Issue #2499](https://github.com/PHPOffice/PhpSpreadsheet/issues/2499) [PR #2595](https://github.com/PHPOffice/PhpSpreadsheet/pull/2595) +- Xls reader treat drawing offsets as int rather than float [PR #2648](https://github.com/PHPOffice/PhpSpreadsheet/pull/2648) +- Handle booleans in conditional styles properly [PR #2654](https://github.com/PHPOffice/PhpSpreadsheet/pull/2654) - Fix for reading files in the root directory of a ZipFile, which should not be prefixed by relative paths ("./") as dirname($filename) does by default. - Fix invalid style of cells in empty columns with columnDimensions and rows with rowDimensions in added external sheet. [PR #2739](https://github.com/PHPOffice/PhpSpreadsheet/pull/2739) From 76f486d8e323b97ea7b4c60ce2cc5f768b571fcf Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 18 Apr 2022 13:58:35 +0200 Subject: [PATCH 07/16] Initial work on supporting Freeze Pane for Ods Writer --- CHANGELOG.md | 1 + phpstan-baseline.neon | 5 - src/PhpSpreadsheet/Writer/Ods.php | 3 +- src/PhpSpreadsheet/Writer/Ods/Content.php | 4 +- src/PhpSpreadsheet/Writer/Ods/Settings.php | 108 ++++++++++++++++----- 5 files changed, 92 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e136eab6..331b2436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Ods Writer support for Freeze Pane [Issue #2013](https://github.com/PHPOffice/PhpSpreadsheet/issues/2013) [PR #2755](https://github.com/PHPOffice/PhpSpreadsheet/pull/2755) - Ods Writer support for setting column width/row height (including the use of AutoSize) [Issue #2346](https://github.com/PHPOffice/PhpSpreadsheet/issues/2346) [PR #2753](https://github.com/PHPOffice/PhpSpreadsheet/pull/2753) - Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts. - Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c6d3dda9..42b9fe70 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4690,11 +4690,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Ods/Formula.php - - - message: "#^Parameter \\#1 \\$content of method XMLWriter\\:\\:text\\(\\) expects string, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Ods/Settings.php - - message: "#^Cannot call method getHashCode\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index decd82bc..827c43b5 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -132,10 +132,11 @@ class Ods extends BaseWriter $zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write()); $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPartthumbnails()->write()); + // Settings always need to be written before Content; Styles after Content + $zip->addFile('settings.xml', $this->getWriterPartsettings()->write()); $zip->addFile('content.xml', $this->getWriterPartcontent()->write()); $zip->addFile('meta.xml', $this->getWriterPartmeta()->write()); $zip->addFile('mimetype', $this->getWriterPartmimetype()->write()); - $zip->addFile('settings.xml', $this->getWriterPartsettings()->write()); $zip->addFile('styles.xml', $this->getWriterPartstyles()->write()); // Close file diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 2cb31b36..5d227c84 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -294,7 +294,9 @@ class Content extends WriterPart $worksheet = $spreadsheet->getSheet($i); $worksheet->calculateColumnWidths(); foreach ($worksheet->getColumnDimensions() as $columnDimension) { - $styleWriter->writeColumnStyles($columnDimension, $i); + if ($columnDimension->getWidth() !== -1.0) { + $styleWriter->writeColumnStyles($columnDimension, $i); + } } } for ($i = 0; $i < $sheetCount; ++$i) { diff --git a/src/PhpSpreadsheet/Writer/Ods/Settings.php b/src/PhpSpreadsheet/Writer/Ods/Settings.php index 047bd410..06445591 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Settings.php +++ b/src/PhpSpreadsheet/Writer/Ods/Settings.php @@ -2,8 +2,11 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Cell\CellAddress; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Settings extends WriterPart { @@ -45,28 +48,9 @@ class Settings extends WriterPart $objWriter->text('view1'); $objWriter->endElement(); // ViewId $objWriter->startElement('config:config-item-map-named'); - $objWriter->writeAttribute('config:name', 'Tables'); - foreach ($spreadsheet->getWorksheetIterator() as $ws) { - $objWriter->startElement('config:config-item-map-entry'); - $objWriter->writeAttribute('config:name', $ws->getTitle()); - $selected = $ws->getSelectedCells(); - if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) { - $colSel = Coordinate::columnIndexFromString($matches[1]) - 1; - $rowSel = (int) $matches[2] - 1; - $objWriter->startElement('config:config-item'); - $objWriter->writeAttribute('config:name', 'CursorPositionX'); - $objWriter->writeAttribute('config:type', 'int'); - $objWriter->text($colSel); - $objWriter->endElement(); - $objWriter->startElement('config:config-item'); - $objWriter->writeAttribute('config:name', 'CursorPositionY'); - $objWriter->writeAttribute('config:type', 'int'); - $objWriter->text($rowSel); - $objWriter->endElement(); - } - $objWriter->endElement(); // config:config-item-map-entry - } - $objWriter->endElement(); // config:config-item-map-named + + $this->writeAllWorksheetSettings($objWriter, $spreadsheet); + $wstitle = $spreadsheet->getActiveSheet()->getTitle(); $objWriter->startElement('config:config-item'); $objWriter->writeAttribute('config:name', 'ActiveTable'); @@ -85,4 +69,84 @@ class Settings extends WriterPart return $objWriter->getData(); } + + private function writeAllWorksheetSettings(XMLWriter $objWriter, Spreadsheet $spreadsheet): void + { + $objWriter->writeAttribute('config:name', 'Tables'); + + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $this->writeWorksheetSettings($objWriter, $worksheet); + } + + $objWriter->endElement(); // config:config-item-map-entry Tables + } + + private function writeWorksheetSettings(XMLWriter $objWriter, Worksheet $worksheet): void + { + $objWriter->startElement('config:config-item-map-entry'); + $objWriter->writeAttribute('config:name', $worksheet->getTitle()); + + $this->writeSelectedCells($objWriter, $worksheet); + if ($worksheet->getFreezePane() !== null) { + $this->writeFreezePane($objWriter, $worksheet); + } + + $objWriter->endElement(); // config:config-item-map-entry Worksheet + } + + private function writeSelectedCells(XMLWriter $objWriter, Worksheet $worksheet): void + { + $selected = $worksheet->getSelectedCells(); + if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) { + $colSel = Coordinate::columnIndexFromString($matches[1]) - 1; + $rowSel = (int) $matches[2] - 1; + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'CursorPositionX'); + $objWriter->writeAttribute('config:type', 'int'); + $objWriter->text((string) $colSel); + $objWriter->endElement(); + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'CursorPositionY'); + $objWriter->writeAttribute('config:type', 'int'); + $objWriter->text((string) $rowSel); + $objWriter->endElement(); + } + } + + private function writeSplitValue(XMLWriter $objWriter, string $splitMode, string $type, string $value): void + { + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', $splitMode); + $objWriter->writeAttribute('config:type', $type); + $objWriter->text($value); + $objWriter->endElement(); + } + + private function writeFreezePane(XMLWriter $objWriter, Worksheet $worksheet): void + { + $freezePane = CellAddress::fromCellAddress($worksheet->getFreezePane()); + if ($freezePane->cellAddress() === 'A1') { + return; + } + + $columnId = $freezePane->columnId(); + $columnName = $freezePane->columnName(); + $row = $freezePane->rowId(); + + $this->writeSplitValue($objWriter, 'HorizontalSplitMode', 'short', '2'); + $this->writeSplitValue($objWriter, 'HorizontalSplitPosition', 'int', (string) ($columnId - 1)); + $this->writeSplitValue($objWriter, 'PositionLeft', 'short', '0'); + $this->writeSplitValue($objWriter, 'PositionRight', 'short', (string) ($columnId - 1)); + + for ($column = 'A'; $column !== $columnName; ++$column) { + $worksheet->getColumnDimension($column)->setAutoSize(true); + } + + $this->writeSplitValue($objWriter, 'VerticalSplitMode', 'short', '2'); + $this->writeSplitValue($objWriter, 'VerticalSplitPosition', 'int', (string) ($row - 1)); + $this->writeSplitValue($objWriter, 'PositionTop', 'short', '0'); + $this->writeSplitValue($objWriter, 'PositionBottom', 'short', (string) ($row - 1)); + + $this->writeSplitValue($objWriter, 'ActiveSplitRange', 'short', '3'); + } } From 4a65011a2f394e5bc3984bd0f5da41a732661572 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 19 Apr 2022 14:00:57 +0200 Subject: [PATCH 08/16] Allow Reader format identification to use a subset of possible Readers --- docs/topics/file-formats.md | 7 +- docs/topics/reading-files.md | 66 +++++++++++++--- src/PhpSpreadsheet/IOFactory.php | 88 +++++++++++++++------ src/PhpSpreadsheet/Reader/BaseReader.php | 4 + tests/PhpSpreadsheetTests/IOFactoryTest.php | 16 ++++ 5 files changed, 144 insertions(+), 37 deletions(-) diff --git a/docs/topics/file-formats.md b/docs/topics/file-formats.md index 6f8783e6..7318b136 100644 --- a/docs/topics/file-formats.md +++ b/docs/topics/file-formats.md @@ -80,7 +80,8 @@ semi-colon (`;`) are used as separators instead of a comma, although other symbols can be used. Because CSV is a text-only format, it doesn't support any data formatting options. -"CSV" is not a single, well-defined format (although see RFC 4180 for +"CSV" is not a single, well-defined format (although see +[RFC 4180](https://www.rfc-editor.org/rfc/rfc4180.html) for one definition that is commonly used). Rather, in practice the term "CSV" refers to any file that: @@ -117,5 +118,5 @@ Wide Web Consortium (W3C). However, in 2000, HTML also became an international standard (ISO/IEC 15445:2000). HTML 4.01 was published in late 1999, with further errata published through 2001. In 2004 development began on HTML5 in the Web Hypertext Application Technology -Working Group (WHATWG), which became a joint deliverable with the W3C in -2008. +Working Group (WHATWG), which became a joint deliverable with the W3C in 2008. + diff --git a/docs/topics/reading-files.md b/docs/topics/reading-files.md index 38428166..76705281 100644 --- a/docs/topics/reading-files.md +++ b/docs/topics/reading-files.md @@ -44,6 +44,22 @@ practise), it will reject the Xls loader that it would normally use for a .xls file; and test the file using the other loaders until it finds the appropriate loader, and then use that to read the file. +If you know that this is an `xls` file, but don't know whether it is a +genuine BIFF-format Excel or Html markup with an xls extension, you can +limit the loader to check only those two possibilities by passing in an +array of Readers to test against. + +```php +$inputFileName = './sampleData/example1.xls'; +$testAgainstFormats = [ + \PhpOffice\PhpSpreadsheet\IOFactory::READER_XLS, + \PhpOffice\PhpSpreadsheet\IOFactory::READER_HTML, +]; + +/** Load $inputFileName to a Spreadsheet Object **/ +$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($inputFileName, 0, $testAgainstFormats); +``` + While easy to implement in your code, and you don't need to worry about the file type; this isn't the most efficient method to load a file; and it lacks the flexibility to configure the loader in any way before @@ -118,6 +134,34 @@ $spreadsheet = $reader->load($inputFileName); See `samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php` for a working example of this code. +As with the IOFactory `load()` method, you can also pass an array of formats +for the `identify()` method to check against if you know that it will only +be in a subset of the possible formats that PhpSpreadsheet supports. + +```php +$inputFileName = './sampleData/example1.xls'; +$testAgainstFormats = [ + \PhpOffice\PhpSpreadsheet\IOFactory::READER_XLS, + \PhpOffice\PhpSpreadsheet\IOFactory::READER_HTML, +]; + +/** Identify the type of $inputFileName **/ +$inputFileType = \PhpOffice\PhpSpreadsheet\IOFactory::identify($inputFileName, $testAgainstFormats); +``` + +You can also use this to confirm that a file is what it claims to be: + +```php +$inputFileName = './sampleData/example1.xls'; + +try { + /** Verify that $inputFileName really is an Xls file **/ + $inputFileType = \PhpOffice\PhpSpreadsheet\IOFactory::identify($inputFileName, [\PhpOffice\PhpSpreadsheet\IOFactory::READER_XLS]); +} catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) { + // File isn't actually an Xls file, even though it has an xls extension +} +``` + ## Spreadsheet Reader Options Once you have created a reader object for the workbook that you want to @@ -146,7 +190,7 @@ $spreadsheet = $reader->load($inputFileName); See `samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php` for a working example of this code. -It is important to note that Workbooks (and PhpSpreadsheet) store dates +It is important to note that most Workbooks (and PhpSpreadsheet) store dates and times as simple numeric values: they can only be distinguished from other numeric values by the format mask that is applied to that cell. When setting read data only to true, PhpSpreadsheet doesn't read the @@ -162,8 +206,8 @@ Reading Only Data from a Spreadsheet File applies to Readers: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | YES | Xls | YES | Xml | YES | -Ods | YES | SYLK | NO | Gnumeric | YES | +Xlsx | YES | Xls | YES | Xml | YES | +Ods | YES | SYLK | NO | Gnumeric | YES | CSV | NO | HTML | NO ### Reading Only Named WorkSheets from a File @@ -233,8 +277,8 @@ Reading Only Named WorkSheets from a File applies to Readers: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | YES | Xls | YES | Xml | YES | -Ods | YES | SYLK | NO | Gnumeric | YES | +Xlsx | YES | Xls | YES | Xml | YES | +Ods | YES | SYLK | NO | Gnumeric | YES | CSV | NO | HTML | NO ### Reading Only Specific Columns and Rows from a File (Read Filters) @@ -381,7 +425,7 @@ Using Read Filters applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | YES | Xls | YES | Xml | YES | +Xlsx | YES | Xls | YES | Xml | YES | Ods | YES | SYLK | NO | Gnumeric | YES | CSV | YES | HTML | NO | | | @@ -439,7 +483,7 @@ Combining Multiple Files into a Single Spreadsheet Object applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | YES | Gnumeric | NO | CSV | YES | HTML | NO @@ -516,7 +560,7 @@ Splitting a single loaded file across multiple worksheets applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | NO | Gnumeric | NO | CSV | YES | HTML | NO @@ -556,7 +600,7 @@ Setting CSV delimiter applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | NO | Gnumeric | NO | CSV | YES | HTML | NO @@ -594,7 +638,7 @@ Applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N | ----------|:---:|--------|:---:|--------------|:---:| -Xlsx | NO | Xls | NO | Xml | NO | +Xlsx | NO | Xls | NO | Xml | NO | Ods | NO | SYLK | NO | Gnumeric | NO | CSV | YES | HTML | NO @@ -646,7 +690,7 @@ Loading using a Value Binder applies to: Reader | Y/N |Reader | Y/N |Reader | Y/N ----------|:---:|--------|:---:|--------------|:---: -Xlsx | NO | Xls | NO | Xml | NO +Xlsx | NO | Xls | NO | Xml | NO Ods | NO | SYLK | NO | Gnumeric | NO CSV | YES | HTML | YES diff --git a/src/PhpSpreadsheet/IOFactory.php b/src/PhpSpreadsheet/IOFactory.php index 91613cb4..e437a220 100644 --- a/src/PhpSpreadsheet/IOFactory.php +++ b/src/PhpSpreadsheet/IOFactory.php @@ -14,23 +14,39 @@ use PhpOffice\PhpSpreadsheet\Writer\IWriter; */ abstract class IOFactory { + public const READER_XLSX = 'Xlsx'; + public const READER_XLS = 'Xls'; + public const READER_XML = 'Xml'; + public const READER_ODS = 'Ods'; + public const READER_SYLK = 'Slk'; + public const READER_SLK = 'Slk'; + public const READER_GNUMERIC = 'Gnumeric'; + public const READER_HTML = 'Html'; + public const READER_CSV = 'Csv'; + + public const WRITER_XLSX = 'Xlsx'; + public const WRITER_XLS = 'Xls'; + public const WRITER_ODS = 'Ods'; + public const WRITER_CSV = 'Csv'; + public const WRITER_HTML = 'Html'; + private static $readers = [ - 'Xlsx' => Reader\Xlsx::class, - 'Xls' => Reader\Xls::class, - 'Xml' => Reader\Xml::class, - 'Ods' => Reader\Ods::class, - 'Slk' => Reader\Slk::class, - 'Gnumeric' => Reader\Gnumeric::class, - 'Html' => Reader\Html::class, - 'Csv' => Reader\Csv::class, + self::READER_XLSX => Reader\Xlsx::class, + self::READER_XLS => Reader\Xls::class, + self::READER_XML => Reader\Xml::class, + self::READER_ODS => Reader\Ods::class, + self::READER_SLK => Reader\Slk::class, + self::READER_GNUMERIC => Reader\Gnumeric::class, + self::READER_HTML => Reader\Html::class, + self::READER_CSV => Reader\Csv::class, ]; private static $writers = [ - 'Xls' => Writer\Xls::class, - 'Xlsx' => Writer\Xlsx::class, - 'Ods' => Writer\Ods::class, - 'Csv' => Writer\Csv::class, - 'Html' => Writer\Html::class, + self::WRITER_XLS => Writer\Xls::class, + self::WRITER_XLSX => Writer\Xlsx::class, + self::WRITER_ODS => Writer\Ods::class, + self::WRITER_CSV => Writer\Csv::class, + self::WRITER_HTML => Writer\Html::class, 'Tcpdf' => Writer\Pdf\Tcpdf::class, 'Dompdf' => Writer\Pdf\Dompdf::class, 'Mpdf' => Writer\Pdf\Mpdf::class, @@ -70,10 +86,18 @@ abstract class IOFactory * Loads Spreadsheet from file using automatic Reader\IReader resolution. * * @param string $filename The name of the spreadsheet file + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. */ - public static function load(string $filename, int $flags = 0): Spreadsheet + public static function load(string $filename, int $flags = 0, ?array $readers = null): Spreadsheet { - $reader = self::createReaderForFile($filename); + $reader = self::createReaderForFile($filename, $readers); return $reader->load($filename, $flags); } @@ -81,9 +105,9 @@ abstract class IOFactory /** * Identify file type using automatic IReader resolution. */ - public static function identify(string $filename): string + public static function identify(string $filename, ?array $readers = null): string { - $reader = self::createReaderForFile($filename); + $reader = self::createReaderForFile($filename, $readers); $className = get_class($reader); $classType = explode('\\', $className); unset($reader); @@ -93,14 +117,32 @@ abstract class IOFactory /** * Create Reader\IReader for file using automatic IReader resolution. + * + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. */ - public static function createReaderForFile(string $filename): IReader + public static function createReaderForFile(string $filename, ?array $readers = null): IReader { File::assertFile($filename); + $testReaders = self::$readers; + if ($readers !== null) { + $readers = array_map('strtoupper', $readers); + $testReaders = array_filter( + self::$readers, + function (string $readerType) use ($readers) { + return in_array(strtoupper($readerType), $readers, true); + }, + ARRAY_FILTER_USE_KEY + ); + } + // First, lucky guess by inspecting file extension $guessedReader = self::getReaderTypeFromExtension($filename); - if ($guessedReader !== null) { + if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) { $reader = self::createReader($guessedReader); // Let's see if we are lucky @@ -110,11 +152,11 @@ abstract class IOFactory } // If we reach here then "lucky guess" didn't give any result - // Try walking through all the options in self::$autoResolveClasses - foreach (self::$readers as $type => $class) { + // Try walking through all the options in self::$readers (or the selected subset) + foreach ($testReaders as $readerType => $class) { // Ignore our original guess, we know that won't work - if ($type !== $guessedReader) { - $reader = self::createReader($type); + if ($readerType !== $guessedReader) { + $reader = self::createReader($readerType); if ($reader->canRead($filename)) { return $reader; } diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php index c215e65b..a137e78c 100644 --- a/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/BaseReader.php @@ -153,6 +153,10 @@ abstract class BaseReader implements IReader /** * Loads Spreadsheet from file. + * + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file */ public function load(string $filename, int $flags = 0): Spreadsheet { diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index b516a9f4..19722dc7 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -108,6 +108,22 @@ class IOFactoryTest extends TestCase ]; } + public function testFormatAsExpected(): void + { + $fileName = 'samples/templates/30template.xls'; + + $actual = IOFactory::identify($fileName, [IOFactory::READER_XLS]); + self::assertSame('Xls', $actual); + } + + public function testFormatNotAsExpectedThrowsException(): void + { + $fileName = 'samples/templates/30template.xls'; + + $this->expectException(ReaderException::class); + IOFactory::identify($fileName, [IOFactory::READER_ODS]); + } + public function testIdentifyNonExistingFileThrowException(): void { $this->expectException(ReaderException::class); From 9275d0c59e3122a91da864f902167f1d08bc87fa Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 19 Apr 2022 16:42:34 +0200 Subject: [PATCH 09/16] Improved documentation for setting row height/column width --- docs/topics/recipes.md | 18 ++++++++++++++++-- .../Worksheet/ColumnDimension.php | 15 +++++++++------ src/PhpSpreadsheet/Worksheet/RowDimension.php | 9 +++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 404f8823..f25a9119 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1167,13 +1167,15 @@ that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), `cm` (centimeters) and `mm` (millimeters). +Setting the column width to `-1` tells MS Excel to display the column using its default width. + ```php $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(120, 'pt'); ``` If you want PhpSpreadsheet to perform an automatic width calculation, -use the following code. PhpSpreadsheet will approximate the column with -to the width of the widest column value. +use the following code. PhpSpreadsheet will approximate the column width +to the width of the widest value displayed in that column. ```php $spreadsheet->getActiveSheet()->getColumnDimension('B')->setAutoSize(true); @@ -1266,6 +1268,18 @@ Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), $spreadsheet->getActiveSheet()->getRowDimension('10')->setRowHeight(100, 'pt'); ``` +Setting the row height to `-1` tells MS Excel to display the column using its default height, which is based on the character font size. + +If you have wrapped text in a cell, then the `-1` default will only set the row height to display a single line of that wrapped text. +If you need to calculate the actual height for the row, then count the lines that should be displayed (count the `\n` and add 1); then adjust for the font. +The adjustment for Calibri 11 is approximately 14.5; for Calibri 12 15.9, etc. +```php +$spreadsheet->getActiveSheet()->getRowDimension(1)->setRowHeight( + 14.5 * (substr_count($sheet->getCell('A1')->getValue(), "\n") + 1) +); +``` + + ## Show/hide a row To set a worksheet''s row visibility, you can use the following code. diff --git a/src/PhpSpreadsheet/Worksheet/ColumnDimension.php b/src/PhpSpreadsheet/Worksheet/ColumnDimension.php index 17dd261b..b64ecec9 100644 --- a/src/PhpSpreadsheet/Worksheet/ColumnDimension.php +++ b/src/PhpSpreadsheet/Worksheet/ColumnDimension.php @@ -83,9 +83,10 @@ class ColumnDimension extends Dimension /** * Get Width. * - * Each unit of column width is equal to the width of one character in the default font size. - * By default, this will be the return value; but this method also accepts a unit of measure argument and will - * return the value converted to the specified UoM using an approximation method. + * Each unit of column width is equal to the width of one character in the default font size. A value of -1 + * tells Excel to display this column in its default width. + * By default, this will be the return value; but this method also accepts an optional unit of measure argument + * and will convert the returned value to the specified UoM.. */ public function getWidth(?string $unitOfMeasure = null): float { @@ -97,9 +98,11 @@ class ColumnDimension extends Dimension /** * Set Width. * - * Each unit of column width is equal to the width of one character in the default font size. - * By default, this will be the unit of measure for the passed value; but this method accepts a unit of measure - * argument, and will convert the value from the specified UoM using an approximation method. + * Each unit of column width is equal to the width of one character in the default font size. A value of -1 + * tells Excel to display this column in its default width. + * By default, this will be the unit of measure for the passed value; but this method also accepts an + * optional unit of measure argument, and will convert the value from the specified UoM using an + * approximation method. * * @return $this */ diff --git a/src/PhpSpreadsheet/Worksheet/RowDimension.php b/src/PhpSpreadsheet/Worksheet/RowDimension.php index 1d8aada8..acaafac5 100644 --- a/src/PhpSpreadsheet/Worksheet/RowDimension.php +++ b/src/PhpSpreadsheet/Worksheet/RowDimension.php @@ -65,8 +65,9 @@ class RowDimension extends Dimension /** * Get Row Height. - * By default, this will be in points; but this method accepts a unit of measure - * argument, and will convert the value to the specified UoM. + * By default, this will be in points; but this method also accepts an optional unit of measure + * argument, and will convert the value from points to the specified UoM. + * A value of -1 tells Excel to display this column in its default height. * * @return float */ @@ -80,8 +81,8 @@ class RowDimension extends Dimension /** * Set Row Height. * - * @param float $height in points - * By default, this will be the passed argument value; but this method accepts a unit of measure + * @param float $height in points. A value of -1 tells Excel to display this column in its default height. + * By default, this will be the passed argument value; but this method also accepts an optional unit of measure * argument, and will convert the passed argument value to points from the specified UoM * * @return $this From ad56616309700fcd09543126227307b28110e03b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 20 Apr 2022 20:01:45 +0200 Subject: [PATCH 10/16] Reduce size of Unique ID Prefix used for the Cell Collection when "in memory" (reduces the per-cell memory overhead, while still retaining a unique prefix to ensure no clash between worksheet collections). External cache (where multiple threads may be accessing the same cache with different workeets) still uses the same length and entropy in the prefix as before --- src/PhpSpreadsheet/Collection/Cells.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index e3d81cb4..3ffb8a4f 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -6,6 +6,7 @@ use Generator; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Psr\SimpleCache\CacheInterface; @@ -298,7 +299,9 @@ class Cells */ private function getUniqueID() { - return uniqid('phpspreadsheet.', true) . '.'; + return Settings::getCache() instanceof Memory + ? random_bytes(7) . ':' + : uniqid('phpspreadsheet.', true) . '.'; } /** From b5e11b130778dd0d48b17043f52ccb65930463b4 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 21 Apr 2022 11:22:40 +0200 Subject: [PATCH 11/16] Performance tweaks to cell collection --- src/PhpSpreadsheet/Collection/Cells.php | 44 ++++++++++--------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index 3ffb8a4f..a29bdb59 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -172,9 +172,9 @@ class Cells // Lookup highest column and highest row $col = ['A' => '1A']; $row = [1]; + $c = ''; + $r = 0; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; sscanf($coord, '%[A-Z]%d', $c, $r); $row[$r] = $r; $col[$c] = strlen($c) . $c; @@ -241,24 +241,21 @@ class Cells public function getHighestColumn($row = null) { if ($row === null) { - $colRow = $this->getHighestRowAndColumn(); - - return $colRow['column']; + return $this->getHighestRowAndColumn()['column']; } - $columnList = [1]; + $maxColumn = '1A'; + $c = ''; + $r = 0; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($r != $row) { continue; } - $columnList[] = Coordinate::columnIndexFromString($c); + $maxColumn = max($maxColumn, strlen($c) . $c); } - return Coordinate::stringFromColumnIndex((int) @max($columnList)); + return substr($maxColumn, 1); } /** @@ -272,24 +269,21 @@ class Cells public function getHighestRow($column = null) { if ($column === null) { - $colRow = $this->getHighestRowAndColumn(); - - return $colRow['row']; + return $this->getHighestRowAndColumn()['row']; } - $rowList = [0]; + $maxRow = 1; + $c = ''; + $r = 0; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($c != $column) { continue; } - $rowList[] = $r; + $maxRow = max($maxRow, $r); } - return max($rowList); + return $maxRow; } /** @@ -347,10 +341,9 @@ class Cells */ public function removeRow($row): void { + $c = ''; + $r = 0; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($r == $row) { $this->delete($coord); @@ -365,10 +358,9 @@ class Cells */ public function removeColumn($column): void { + $c = ''; + $r = 0; foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); if ($c == $column) { $this->delete($coord); From 8126e24faf1ec7254a91d1e4d56a5e82e7df3c2c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 22 Apr 2022 15:40:55 +0200 Subject: [PATCH 12/16] Performance tweaks to cell collection I suspect that scrutiniser may complain that the pass-by-reference values in an expression like `sscanf($coord, '%[A-Z]%d', $column, $row)` don't exist; but PHP handles that without issue, creating the variables as needed, and phpstan has no problems with that, so scrutiniser shouldn't treat it as an issue. There's no point in adding code (even if it's just pre-defining call-by-reference arguments) when it's unnecessary overhead. --- phpstan-baseline.neon | 10 ---------- src/PhpSpreadsheet/Cell/Coordinate.php | 2 +- src/PhpSpreadsheet/Collection/Cells.php | 19 ------------------- src/PhpSpreadsheet/ReferenceHelper.php | 8 ++++---- 4 files changed, 5 insertions(+), 34 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 42b9fe70..e7c917ec 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1175,11 +1175,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Cell/Coordinate.php - - - message: "#^Cannot use array destructuring on array\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Coordinate.php - - message: "#^Parameter \\#4 \\$currentRow of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:validateRange\\(\\) expects int, string given\\.$#" count: 1 @@ -3120,11 +3115,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xml/Style.php - - - message: "#^Cannot use array destructuring on array\\|null\\.$#" - count: 4 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Elseif condition is always true\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 703eac08..ea53919c 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -383,7 +383,7 @@ abstract class Coordinate // Sort the result by column and row $sortKeys = []; foreach ($cellList as $coord) { - [$column, $row] = sscanf($coord, '%[A-Z]%d'); + sscanf($coord, '%[A-Z]%d', $column, $row); $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord; } ksort($sortKeys); diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index a29bdb59..d49f5457 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Collection; use Generator; use PhpOffice\PhpSpreadsheet\Cell\Cell; -use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -152,8 +151,6 @@ class Cells { $sortKeys = []; foreach ($this->getCoordinates() as $coord) { - $column = ''; - $row = 0; sscanf($coord, '%[A-Z]%d', $column, $row); $sortKeys[sprintf('%09d%3s', $row, $column)] = $coord; } @@ -172,8 +169,6 @@ class Cells // Lookup highest column and highest row $col = ['A' => '1A']; $row = [1]; - $c = ''; - $r = 0; foreach ($this->getCoordinates() as $coord) { sscanf($coord, '%[A-Z]%d', $c, $r); $row[$r] = $r; @@ -207,9 +202,6 @@ class Cells */ public function getCurrentColumn() { - $column = ''; - $row = 0; - sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); return $column; @@ -222,9 +214,6 @@ class Cells */ public function getCurrentRow() { - $column = ''; - $row = 0; - sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); return (int) $row; @@ -245,8 +234,6 @@ class Cells } $maxColumn = '1A'; - $c = ''; - $r = 0; foreach ($this->getCoordinates() as $coord) { sscanf($coord, '%[A-Z]%d', $c, $r); if ($r != $row) { @@ -273,8 +260,6 @@ class Cells } $maxRow = 1; - $c = ''; - $r = 0; foreach ($this->getCoordinates() as $coord) { sscanf($coord, '%[A-Z]%d', $c, $r); if ($c != $column) { @@ -341,8 +326,6 @@ class Cells */ public function removeRow($row): void { - $c = ''; - $r = 0; foreach ($this->getCoordinates() as $coord) { sscanf($coord, '%[A-Z]%d', $c, $r); if ($r == $row) { @@ -358,8 +341,6 @@ class Cells */ public function removeColumn($column): void { - $c = ''; - $r = 0; foreach ($this->getCoordinates() as $coord) { sscanf($coord, '%[A-Z]%d', $c, $r); if ($c == $column) { diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 665b2e18..c5f57792 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -90,8 +90,8 @@ class ReferenceHelper */ public static function cellSort($a, $b) { - [$ac, $ar] = sscanf($a, '%[A-Z]%d'); - [$bc, $br] = sscanf($b, '%[A-Z]%d'); + sscanf($a, '%[A-Z]%d', $ac, $ar); + sscanf($b, '%[A-Z]%d', $bc, $br); if ($ar === $br) { return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); @@ -111,8 +111,8 @@ class ReferenceHelper */ public static function cellReverseSort($a, $b) { - [$ac, $ar] = sscanf($a, '%[A-Z]%d'); - [$bc, $br] = sscanf($b, '%[A-Z]%d'); + sscanf($a, '%[A-Z]%d', $ac, $ar); + sscanf($b, '%[A-Z]%d', $bc, $br); if ($ar === $br) { return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); From 69edf61ed652595acfa8b38e994e811ad55003a2 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 23 Apr 2022 13:08:06 +0200 Subject: [PATCH 13/16] Extract cell/range validations from Worksheet and move into a separate Worksheet/Validations class as public static methods Extract tryDefinedName logic from Worksheet, nd move into the Worksheet/Validations class as a public static method Apply stricter validation to autofilter range arguments --- src/PhpSpreadsheet/Calculation/Functions.php | 8 +- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 23 ++- src/PhpSpreadsheet/Worksheet/Validations.php | 100 +++++++++++++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 142 +++---------------- 4 files changed, 142 insertions(+), 131 deletions(-) create mode 100644 src/PhpSpreadsheet/Worksheet/Validations.php diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 00d8a790..dc6ee82e 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Shared\Date; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Functions { @@ -686,12 +685,13 @@ class Functions // Uppercase coordinate $pCoordinatex = strtoupper($coordinate); // Eliminate leading equal sign - $pCoordinatex = Worksheet::pregReplace('/^=/', '', $pCoordinatex); + $pCoordinatex = (string) preg_replace('/^=/', '', $pCoordinatex); $defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet); if ($defined !== null) { $worksheet2 = $defined->getWorkSheet(); if (!$defined->isFormula() && $worksheet2 !== null) { - $coordinate = "'" . $worksheet2->getTitle() . "'!" . Worksheet::pregReplace('/^=/', '', $defined->getValue()); + $coordinate = "'" . $worksheet2->getTitle() . "'!" . + (string) preg_replace('/^=/', '', $defined->getValue()); } } @@ -700,7 +700,7 @@ class Functions public static function trimTrailingRange(string $coordinate): string { - return Worksheet::pregReplace('/:[\\w\$]+$/', '', $coordinate); + return (string) preg_replace('/:[\\w\$]+$/', '', $coordinate); } public static function trimSheetFromCellReference(string $coordinate): string diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 2e6c8289..dd33d5d5 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -7,6 +7,7 @@ use DateTimeZone; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -50,9 +51,18 @@ class AutoFilter /** * Create a new AutoFilter. + * + * @param AddressRange|array|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 __construct(string $range = '', ?Worksheet $worksheet = null) + public function __construct($range = '', ?Worksheet $worksheet = null) { + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } + $this->range = $range; $this->workSheet = $worksheet; } @@ -92,12 +102,19 @@ class AutoFilter /** * Set AutoFilter Cell Range. + * + * @param AddressRange|array|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(string $range): self + public function setRange($range = ''): self { $this->evaluated = false; // extract coordinate - [$worksheet, $range] = Worksheet::extractSheetTitle($range, true); + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } if (empty($range)) { // Discard all column rules $this->columns = []; diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php new file mode 100644 index 00000000..b31b0813 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -0,0 +1,100 @@ +|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + */ + public static function validateCellAddress($cellAddress): string + { + if (is_string($cellAddress)) { + [$worksheet, $address] = Worksheet::extractSheetTitle($cellAddress, true); +// if (!empty($worksheet) && $worksheet !== $this->getTitle()) { +// throw new Exception('Reference is not for this worksheet'); +// } + + return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address); + } + + if (is_array($cellAddress)) { + $cellAddress = CellAddress::fromColumnRowArray($cellAddress); + } + + return (string) $cellAddress; + } + + /** + * Validate a cell address or cell range. + * + * @param AddressRange|array|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as a CellAddress or AddressRange object. + */ + public static function validateCellOrCellRange($cellRange): string + { + if (is_string($cellRange) || is_numeric($cellRange)) { + $cellRange = (string) $cellRange; + // Convert a single column reference like 'A' to 'A:A' + $cellRange = (string) preg_replace('/^([A-Z]+)$/', '${1}:${1}', $cellRange); + // Convert a single row reference like '1' to '1:1' + $cellRange = (string) preg_replace('/^(\d+)$/', '${1}:${1}', $cellRange); + } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { + $cellRange = new CellRange($cellRange, $cellRange); + } + + return self::validateCellRange($cellRange); + } + + /** + * Validate a cell range. + * + * @param AddressRange|array|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as an AddressRange object. + */ + public static function validateCellRange($cellRange): string + { + if (is_string($cellRange)) { + [$worksheet, $addressRange] = Worksheet::extractSheetTitle($cellRange, true); + + // Convert Column ranges like 'A:C' to 'A1:C1048576' + $addressRange = (string) preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); + // Convert Row ranges like '1:3' to 'A1:XFD3' + $addressRange = (string) preg_replace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); + + return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); + } + + if (is_array($cellRange)) { + [$from, $to] = array_chunk($cellRange, 2); + $cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to)); + } + + return (string) $cellRange; + } + + public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet): string + { + // Uppercase coordinate + $testCoordinate = strtoupper($coordinate); + // Eliminate leading equal sign + $testCoordinate = (string) preg_replace('/^=/', '', $coordinate); + $defined = $worksheet->getParent()->getDefinedName($testCoordinate, $worksheet); + if ($defined !== null) { + if ($defined->getWorksheet() === $worksheet && !$defined->isFormula()) { + $coordinate = (string) preg_replace('/^=/', '', $defined->getValue()); + } + } + + return $coordinate; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index e4ce2ac3..70994be5 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1067,7 +1067,7 @@ class Worksheet implements IComparable */ public function getHighestColumn($row = null) { - if (empty($row)) { + if ($row === null) { return Coordinate::stringFromColumnIndex($this->cachedHighestColumn); } @@ -1097,7 +1097,7 @@ class Worksheet implements IComparable */ public function getHighestRow($column = null) { - if ($column == null) { + if ($column === null) { return $this->cachedHighestRow; } @@ -1127,96 +1127,6 @@ class Worksheet implements IComparable return $this->cellCollection->getHighestRowAndColumn(); } - /** - * Validate a cell address. - * - * @param null|array|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5'; - * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. - */ - protected function validateCellAddress($cellAddress): string - { - if (is_string($cellAddress)) { - [$worksheet, $address] = self::extractSheetTitle($cellAddress, true); -// if (!empty($worksheet) && $worksheet !== $this->getTitle()) { -// throw new Exception('Reference is not for this worksheet'); -// } - - return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address); - } - - if (is_array($cellAddress)) { - $cellAddress = CellAddress::fromColumnRowArray($cellAddress); - } - - return (string) $cellAddress; - } - - private function tryDefinedName(string $coordinate): string - { - // Uppercase coordinate - $coordinate = strtoupper($coordinate); - // Eliminate leading equal sign - $coordinate = self::pregReplace('/^=/', '', $coordinate); - $defined = $this->parent->getDefinedName($coordinate, $this); - if ($defined !== null) { - if ($defined->getWorksheet() === $this && !$defined->isFormula()) { - $coordinate = self::pregReplace('/^=/', '', $defined->getValue()); - } - } - - return $coordinate; - } - - /** - * Validate a cell address or cell range. - * - * @param AddressRange|array|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; - * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), - * or as a CellAddress or AddressRange object. - */ - protected function validateCellOrCellRange($cellRange): string - { - if (is_string($cellRange) || is_numeric($cellRange)) { - $cellRange = (string) $cellRange; - // Convert a single column reference like 'A' to 'A:A' - $cellRange = self::pregReplace('/^([A-Z]+)$/', '${1}:${1}', $cellRange); - // Convert a single row reference like '1' to '1:1' - $cellRange = self::pregReplace('/^(\d+)$/', '${1}:${1}', $cellRange); - } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { - $cellRange = new CellRange($cellRange, $cellRange); - } - - return $this->validateCellRange($cellRange); - } - - /** - * Validate a cell range. - * - * @param AddressRange|array|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; - * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), - * or as an AddressRange object. - */ - protected function validateCellRange($cellRange): string - { - if (is_string($cellRange)) { - [$worksheet, $addressRange] = self::extractSheetTitle($cellRange, true); - - // Convert Column ranges like 'A:C' to 'A1:C1048576' - $addressRange = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); - // Convert Row ranges like '1:3' to 'A1:XFD3' - $addressRange = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); - - return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); - } - - if (is_array($cellRange)) { - [$from, $to] = array_chunk($cellRange, 2); - $cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to)); - } - - return (string) $cellRange; - } - /** * Set a cell value. * @@ -1228,7 +1138,7 @@ class Worksheet implements IComparable */ public function setCellValue($coordinate, $value) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); $this->getCell($cellAddress)->setValue($value); return $this; @@ -1266,7 +1176,7 @@ class Worksheet implements IComparable */ public function setCellValueExplicit($coordinate, $value, $dataType) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); $this->getCell($cellAddress)->setValueExplicit($value, $dataType); return $this; @@ -1303,7 +1213,7 @@ class Worksheet implements IComparable */ public function getCell($coordinate): Cell { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); // Shortcut for increased performance for the vast majority of simple cases if ($this->cellCollection->has($cellAddress)) { @@ -1454,7 +1364,7 @@ class Worksheet implements IComparable */ public function cellExists($coordinate): bool { - $cellAddress = $this->validateCellAddress($coordinate); + $cellAddress = Validations::validateCellAddress($coordinate); /** @var Worksheet $sheet */ [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); @@ -1546,7 +1456,7 @@ class Worksheet implements IComparable */ public function getStyle($cellCoordinate): Style { - $cellCoordinate = $this->validateCellOrCellRange($cellCoordinate); + $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate); // set this sheet as active $this->parent->setActiveSheetIndex($this->parent->getIndex($this)); @@ -1784,7 +1694,7 @@ class Worksheet implements IComparable */ public function setBreak($coordinate, $break) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); if ($break === self::BREAK_NONE) { if (isset($this->breaks[$cellAddress])) { @@ -1836,7 +1746,7 @@ class Worksheet implements IComparable */ public function mergeCells($range) { - $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) { $this->mergeCells[$range] = $range; @@ -1945,7 +1855,7 @@ class Worksheet implements IComparable */ public function unmergeCells($range) { - $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); if (strpos($range, ':') !== false) { if (isset($this->mergeCells[$range])) { @@ -2023,7 +1933,7 @@ class Worksheet implements IComparable */ public function protectCells($range, $password, $alreadyHashed = false) { - $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range)); if (!$alreadyHashed) { $password = Shared\PasswordHasher::hashPassword($password); @@ -2071,7 +1981,7 @@ class Worksheet implements IComparable */ public function unprotectCells($range) { - $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); + $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range)); if (isset($this->protectedCells[$range])) { unset($this->protectedCells[$range]); @@ -2142,7 +2052,7 @@ class Worksheet implements IComparable if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) { $this->autoFilter = $autoFilterOrRange; } else { - $cellRange = Functions::trimSheetFromCellReference($this->validateCellRange($autoFilterOrRange)); + $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange)); $this->autoFilter->setRange($cellRange); } @@ -2216,13 +2126,13 @@ class Worksheet implements IComparable public function freezePane($coordinate, $topLeftCell = null) { $cellAddress = ($coordinate !== null) - ? Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)) + ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)) : null; if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Freeze pane can not be set on a range of cells.'); } $topLeftCell = ($topLeftCell !== null) - ? Functions::trimSheetFromCellReference($this->validateCellAddress($topLeftCell)) + ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell)) : null; if ($cellAddress !== null && $topLeftCell === null) { @@ -2626,7 +2536,7 @@ class Worksheet implements IComparable */ public function getComment($cellCoordinate) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($cellCoordinate)); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); if (Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Cell coordinate string can not be a range of cells.'); @@ -2697,22 +2607,6 @@ class Worksheet implements IComparable return $this->setSelectedCells($coordinate); } - /** - * Sigh - Phpstan thinks, correctly, that preg_replace can return null. - * But Scrutinizer doesn't. Try to satisfy both. - * - * @param mixed $str - */ - private static function ensureString($str): string - { - return is_string($str) ? $str : ''; - } - - public static function pregReplace(string $pattern, string $replacement, string $subject): string - { - return self::ensureString(preg_replace($pattern, $replacement, $subject)); - } - /** * Select a range of cells. * @@ -2725,9 +2619,9 @@ class Worksheet implements IComparable public function setSelectedCells($coordinate) { if (is_string($coordinate)) { - $coordinate = $this->tryDefinedName($coordinate); + $coordinate = Validations::definedNameToCoordinate($coordinate, $this); } - $coordinate = $this->validateCellOrCellRange($coordinate); + $coordinate = Validations::validateCellOrCellRange($coordinate); if (Coordinate::coordinateIsRange($coordinate)) { [$first] = Coordinate::splitRange($coordinate); From 76310a05fdbbe1de2ab5cb7799dda19e4fcd6c37 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 23 Apr 2022 13:35:09 +0200 Subject: [PATCH 14/16] Update PR Template --- .github/PULL_REQUEST_TEMPLATE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index eba1f1e2..5d9b33ec 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,14 +3,21 @@ This is: ``` - [ ] a bugfix - [ ] a new feature +- [ ] refactoring +- [ ] additional unit tests ``` Checklist: - [ ] Changes are covered by unit tests + - [ ] Changes are covered by existing unit tests + - [ ] New unit tests have been added - [ ] Code style is respected - [ ] Commit message explains **why** the change is made (see https://github.com/erlang/otp/wiki/Writing-good-commit-messages) - [ ] CHANGELOG.md contains a short summary of the change - [ ] Documentation is updated as necessary ### Why this change is needed? + +Provide an explanation of why this change is needed, with links to any Issues (if appropriate). +If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will mae it easier to track progress with this PR. From 0facbe0c1b04a060fa2ec7601f917b6ba33bcba4 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 23 Apr 2022 13:35:56 +0200 Subject: [PATCH 15/16] Fix typo in PR template --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5d9b33ec..f42df68c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,4 +20,4 @@ Checklist: ### Why this change is needed? Provide an explanation of why this change is needed, with links to any Issues (if appropriate). -If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will mae it easier to track progress with this PR. +If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will make it easier to track progress with this PR. From ed11a41c1933c2d1a037dff132a660676b6975c1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 23 Apr 2022 13:46:42 +0200 Subject: [PATCH 16/16] Updates to the Issue template --- .github/ISSUE_TEMPLATE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 05e3b199..c3a74ede 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -26,6 +26,20 @@ $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); // add code that show the issue here... ``` +If this is an issue with reading a specific spreadsheet file, then it may be appropriate to provide a sample file that demonstrates the problem; but please keep it as small as possible, and sanitize any confidential information before uploading. + +### What features do you think are causing the issue + +- [ ] Reader +- [ ] Writer +- [ ] Styles +- [ ] Data Validations +- [ ] Formula Calulations +- [ ] Charts +- [ ] AutoFilter +- [ ] Form Elements + +### Does an issue affect all spreadsheet file formats? If not, which formats are affected? ### Which versions of PhpSpreadsheet and PHP are affected?