From 0152f57e4487d2555163a52ff043c398e136ca47 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:36:45 -0700 Subject: [PATCH 1/3] String Value Binder Allow Setting "Ignore Number Stored As Text" When String Value Binder converts a numeric value to text, the resulting spreadsheet will be full of little green triangles to indicate to the end user that something might be wrong. It is unlikely that a spreadsheet created in this manner needs that visual clutter. This PR adds a property and setter (I can't really think of a good use case for a getter) to suppress it. Suppression should arguably be the default, but, for now, I will avoid any BC problems by leaving non-suppression as the default. --- docs/topics/accessing-cells.md | 1 + src/PhpSpreadsheet/Cell/StringValueBinder.php | 12 ++ .../Cell/StringValueBinder2Test.php | 103 ++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md index 67e177536c..d377da899d 100644 --- a/docs/topics/accessing-cells.md +++ b/docs/topics/accessing-cells.md @@ -551,6 +551,7 @@ By default, the StringValueBinder will cast any datatype passed to it into a str // Set value binder $stringValueBinder = new \PhpOffice\PhpSpreadsheet\Cell\StringValueBinder(); $stringValueBinder->setNumericConversion(false) + ->setSetIgnoredErrors(true) // suppresses "number stored as text" indicators ->setBooleanConversion(false) ->setNullConversion(false) ->setFormulaConversion(false); diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index d86cdabd33..00a15cd4c4 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -18,6 +18,15 @@ class StringValueBinder extends DefaultValueBinder implements IValueBinder protected bool $convertFormula = true; + protected bool $setIgnoredErrors = false; + + public function setSetIgnoredErrors(bool $setIgnoredErrors = false): self + { + $this->setIgnoredErrors = $setIgnoredErrors; + + return $this; + } + public function setNullConversion(bool $suppressConversion = false): self { $this->convertNull = $suppressConversion; @@ -90,6 +99,9 @@ public function bindValue(Cell $cell, mixed $value): bool } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false && parent::dataTypeForValue($value) === DataType::TYPE_FORMULA) { $cell->setValueExplicit($value, DataType::TYPE_FORMULA); } else { + if ($this->setIgnoredErrors && is_numeric($value)) { + $cell->getIgnoredErrors()->setNumberStoredAsText(true); + } $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); } diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php new file mode 100644 index 0000000000..735d388015 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php @@ -0,0 +1,103 @@ +valueBinder = Cell::getValueBinder(); + } + + protected function tearDown(): void + { + Cell::setValueBinder($this->valueBinder); + } + + public function testStringValueBinderIgnoredErrorsDefault(): void + { + $valueBinder = new StringValueBinder(); + Cell::setValueBinder($valueBinder); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([ + [1, 'x', 3.2], + ['y', -5, 'z'], + ]); + $ignoredCells = []; + foreach ($sheet->getRowIterator() as $row) { + foreach ($row->getCellIterator() as $cell) { + $coordinate = $cell->getCoordinate(); + self::assertSame(DataType::TYPE_STRING, $cell->getDataType(), "not string for cell $coordinate"); + if ($cell->getIgnoredErrors()->getNumberStoredAsText()) { + $ignoredCells[] = $coordinate; + } + } + } + self::assertSame([], $ignoredCells); + $spreadsheet->disconnectWorksheets(); + } + + public function testStringValueBinderIgnoredErrorsTrue(): void + { + $valueBinder = new StringValueBinder(); + $valueBinder->setSetIgnoredErrors(true); + Cell::setValueBinder($valueBinder); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([ + [1, 'x', 3.2], + ['y', -5, 'z'], + ]); + $ignoredCells = []; + foreach ($sheet->getRowIterator() as $row) { + foreach ($row->getCellIterator() as $cell) { + $coordinate = $cell->getCoordinate(); + self::assertSame(DataType::TYPE_STRING, $cell->getDataType(), "not string for cell $coordinate"); + if ($cell->getIgnoredErrors()->getNumberStoredAsText()) { + $ignoredCells[] = $coordinate; + } + } + } + self::assertSame(['A1', 'C1', 'B2'], $ignoredCells); + $spreadsheet->disconnectWorksheets(); + } + + public function testStringValueBinderPreserveNumeric(): void + { + $valueBinder = new StringValueBinder(); + $valueBinder->setNumericConversion(false); + $valueBinder->setSetIgnoredErrors(true); + Cell::setValueBinder($valueBinder); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([ + [1, 'x', 3.2], + ['y', -5, 'z'], + ]); + $ignoredCells = []; + foreach ($sheet->getRowIterator() as $row) { + foreach ($row->getCellIterator() as $cell) { + $coordinate = $cell->getCoordinate(); + $expected = is_numeric($cell->getValue()) ? DataType::TYPE_NUMERIC : DataType::TYPE_STRING; + self::assertSame($expected, $cell->getDataType(), "wrong type for cell $coordinate"); + if ($cell->getIgnoredErrors()->getNumberStoredAsText()) { + $ignoredCells[] = $coordinate; + } + } + } + self::assertSame([], $ignoredCells); + $spreadsheet->disconnectWorksheets(); + } +} From e27ccc8ceac8b7d418940ad558884623c9cce590 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:58:10 -0700 Subject: [PATCH 2/3] Better Handling of Stringable Objects --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 14 ++++++-- .../Cell/StringValueBinder2Test.php | 36 ++++++++++++++++--- .../Cell/StringableObject.php | 9 ++++- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index 00a15cd4c4..1509a9c7b6 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -90,6 +90,7 @@ public function bindValue(Cell $cell, mixed $value): bool $value = StringHelper::sanitizeUTF8($value); } + $ignoredErrors = false; if ($value === null && $this->convertNull === false) { $cell->setValueExplicit($value, DataType::TYPE_NULL); } elseif (is_bool($value) && $this->convertBoolean === false) { @@ -99,11 +100,12 @@ public function bindValue(Cell $cell, mixed $value): bool } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false && parent::dataTypeForValue($value) === DataType::TYPE_FORMULA) { $cell->setValueExplicit($value, DataType::TYPE_FORMULA); } else { - if ($this->setIgnoredErrors && is_numeric($value)) { - $cell->getIgnoredErrors()->setNumberStoredAsText(true); - } + $ignoredErrors = is_numeric($value); $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); } + if ($this->setIgnoredErrors) { + $cell->getIgnoredErrors()->setNumberStoredAsText($ignoredErrors); + } return true; } @@ -111,16 +113,22 @@ public function bindValue(Cell $cell, mixed $value): bool protected function bindObjectValue(Cell $cell, object $value): bool { // Handle any objects that might be injected + $ignoredErrors = false; if ($value instanceof DateTimeInterface) { $value = $value->format('Y-m-d H:i:s'); $cell->setValueExplicit($value, DataType::TYPE_STRING); } elseif ($value instanceof RichText) { $cell->setValueExplicit($value, DataType::TYPE_INLINE); + $ignoredErrors = is_numeric($value->getPlainText()); } elseif ($value instanceof Stringable) { $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); + $ignoredErrors = is_numeric((string) $value); } else { throw new SpreadsheetException('Unable to bind unstringable object of type ' . get_class($value)); } + if ($this->setIgnoredErrors) { + $cell->getIgnoredErrors()->setNumberStoredAsText($ignoredErrors); + } return true; } diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php index 735d388015..5fac7825f4 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinder2Test.php @@ -4,10 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Cell; +use DateTime; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -31,15 +33,24 @@ public function testStringValueBinderIgnoredErrorsDefault(): void Cell::setValueBinder($valueBinder); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); + $richText = new RichText(); + $richText->createTextRun('6'); + $richText2 = new RichText(); + $richText2->createTextRun('a'); $sheet->fromArray([ [1, 'x', 3.2], ['y', -5, 'z'], + [new DateTime(), $richText, $richText2], + [new StringableObject('a'), new StringableObject(2), 'z'], ]); $ignoredCells = []; foreach ($sheet->getRowIterator() as $row) { foreach ($row->getCellIterator() as $cell) { $coordinate = $cell->getCoordinate(); - self::assertSame(DataType::TYPE_STRING, $cell->getDataType(), "not string for cell $coordinate"); + $dataType = $cell->getDataType(); + if ($dataType !== DataType::TYPE_INLINE) { + self::assertSame(DataType::TYPE_STRING, $dataType, "not string for cell $coordinate"); + } if ($cell->getIgnoredErrors()->getNumberStoredAsText()) { $ignoredCells[] = $coordinate; } @@ -56,21 +67,30 @@ public function testStringValueBinderIgnoredErrorsTrue(): void Cell::setValueBinder($valueBinder); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); + $richText = new RichText(); + $richText->createTextRun('6'); + $richText2 = new RichText(); + $richText2->createTextRun('a'); $sheet->fromArray([ [1, 'x', 3.2], ['y', -5, 'z'], + [new DateTime(), $richText, $richText2], + [new StringableObject('a'), new StringableObject(2), 'z'], ]); $ignoredCells = []; foreach ($sheet->getRowIterator() as $row) { foreach ($row->getCellIterator() as $cell) { $coordinate = $cell->getCoordinate(); - self::assertSame(DataType::TYPE_STRING, $cell->getDataType(), "not string for cell $coordinate"); + $dataType = $cell->getDataType(); + if ($dataType !== DataType::TYPE_INLINE) { + self::assertSame(DataType::TYPE_STRING, $dataType, "not string for cell $coordinate"); + } if ($cell->getIgnoredErrors()->getNumberStoredAsText()) { $ignoredCells[] = $coordinate; } } } - self::assertSame(['A1', 'C1', 'B2'], $ignoredCells); + self::assertSame(['A1', 'C1', 'B2', 'B3', 'B4'], $ignoredCells); $spreadsheet->disconnectWorksheets(); } @@ -82,22 +102,28 @@ public function testStringValueBinderPreserveNumeric(): void Cell::setValueBinder($valueBinder); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); + $richText = new RichText(); + $richText->createTextRun('6'); + $richText2 = new RichText(); + $richText2->createTextRun('a'); $sheet->fromArray([ [1, 'x', 3.2], ['y', -5, 'z'], + [new DateTime(), $richText, $richText2], + [new StringableObject('a'), new StringableObject(2), 'z'], ]); $ignoredCells = []; foreach ($sheet->getRowIterator() as $row) { foreach ($row->getCellIterator() as $cell) { $coordinate = $cell->getCoordinate(); - $expected = is_numeric($cell->getValue()) ? DataType::TYPE_NUMERIC : DataType::TYPE_STRING; + $expected = (is_int($cell->getValue()) || is_float($cell->getValue())) ? DataType::TYPE_NUMERIC : (($cell->getValue() instanceof RichText) ? DataType::TYPE_INLINE : DataType::TYPE_STRING); self::assertSame($expected, $cell->getDataType(), "wrong type for cell $coordinate"); if ($cell->getIgnoredErrors()->getNumberStoredAsText()) { $ignoredCells[] = $coordinate; } } } - self::assertSame([], $ignoredCells); + self::assertSame(['B3', 'B4'], $ignoredCells); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Cell/StringableObject.php b/tests/PhpSpreadsheetTests/Cell/StringableObject.php index 31d1a774c2..1ca0f0f66e 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringableObject.php +++ b/tests/PhpSpreadsheetTests/Cell/StringableObject.php @@ -6,8 +6,15 @@ class StringableObject { + private int|string $value; + + public function __construct(int|string $value = 'abc') + { + $this->value = $value; + } + public function __toString(): string { - return 'abc'; + return (string) $this->value; } } From 176c2c8a89689b31e5cfbe84246ba934596c9eaf Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 24 Aug 2024 16:43:59 -0700 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd9b9e9260..b4acda1210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Excel Dynamic Arrays. [Issue #3901](https://github.com/PHPOffice/PhpSpreadsheet/issues/3901) [Issue #3659](https://github.com/PHPOffice/PhpSpreadsheet/issues/3659) [Issue #1834](https://github.com/PHPOffice/PhpSpreadsheet/issues/1834) [PR #3962](https://github.com/PHPOffice/PhpSpreadsheet/pull/3962) +- String Value Binder Allow Setting "Ignore Number Stored as Text". [PR #4141](https://github.com/PHPOffice/PhpSpreadsheet/pull/4141) ### Changed