Skip to content

Commit e1dae99

Browse files
authored
Merge pull request #4141 from oleibman/svbignoreerrors2
String Value Binder Allow Setting "Ignore Number Stored As Text"
2 parents 9c2c976 + 176c2c8 commit e1dae99

File tree

5 files changed

+159
-1
lines changed

5 files changed

+159
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1414
### Added
1515

1616
- 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)
17+
- String Value Binder Allow Setting "Ignore Number Stored as Text". [PR #4141](https://github.com/PHPOffice/PhpSpreadsheet/pull/4141)
1718

1819
### Changed
1920

docs/topics/accessing-cells.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ By default, the StringValueBinder will cast any datatype passed to it into a str
551551
// Set value binder
552552
$stringValueBinder = new \PhpOffice\PhpSpreadsheet\Cell\StringValueBinder();
553553
$stringValueBinder->setNumericConversion(false)
554+
->setSetIgnoredErrors(true) // suppresses "number stored as text" indicators
554555
->setBooleanConversion(false)
555556
->setNullConversion(false)
556557
->setFormulaConversion(false);

src/PhpSpreadsheet/Cell/StringValueBinder.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ class StringValueBinder extends DefaultValueBinder implements IValueBinder
1818

1919
protected bool $convertFormula = true;
2020

21+
protected bool $setIgnoredErrors = false;
22+
23+
public function setSetIgnoredErrors(bool $setIgnoredErrors = false): self
24+
{
25+
$this->setIgnoredErrors = $setIgnoredErrors;
26+
27+
return $this;
28+
}
29+
2130
public function setNullConversion(bool $suppressConversion = false): self
2231
{
2332
$this->convertNull = $suppressConversion;
@@ -81,6 +90,7 @@ public function bindValue(Cell $cell, mixed $value): bool
8190
$value = StringHelper::sanitizeUTF8($value);
8291
}
8392

93+
$ignoredErrors = false;
8494
if ($value === null && $this->convertNull === false) {
8595
$cell->setValueExplicit($value, DataType::TYPE_NULL);
8696
} elseif (is_bool($value) && $this->convertBoolean === false) {
@@ -90,25 +100,35 @@ public function bindValue(Cell $cell, mixed $value): bool
90100
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false && parent::dataTypeForValue($value) === DataType::TYPE_FORMULA) {
91101
$cell->setValueExplicit($value, DataType::TYPE_FORMULA);
92102
} else {
103+
$ignoredErrors = is_numeric($value);
93104
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING);
94105
}
106+
if ($this->setIgnoredErrors) {
107+
$cell->getIgnoredErrors()->setNumberStoredAsText($ignoredErrors);
108+
}
95109

96110
return true;
97111
}
98112

99113
protected function bindObjectValue(Cell $cell, object $value): bool
100114
{
101115
// Handle any objects that might be injected
116+
$ignoredErrors = false;
102117
if ($value instanceof DateTimeInterface) {
103118
$value = $value->format('Y-m-d H:i:s');
104119
$cell->setValueExplicit($value, DataType::TYPE_STRING);
105120
} elseif ($value instanceof RichText) {
106121
$cell->setValueExplicit($value, DataType::TYPE_INLINE);
122+
$ignoredErrors = is_numeric($value->getPlainText());
107123
} elseif ($value instanceof Stringable) {
108124
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING);
125+
$ignoredErrors = is_numeric((string) $value);
109126
} else {
110127
throw new SpreadsheetException('Unable to bind unstringable object of type ' . get_class($value));
111128
}
129+
if ($this->setIgnoredErrors) {
130+
$cell->getIgnoredErrors()->setNumberStoredAsText($ignoredErrors);
131+
}
112132

113133
return true;
114134
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Cell;
6+
7+
use DateTime;
8+
use PhpOffice\PhpSpreadsheet\Cell\Cell;
9+
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10+
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
11+
use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
12+
use PhpOffice\PhpSpreadsheet\RichText\RichText;
13+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
14+
use PHPUnit\Framework\TestCase;
15+
16+
class StringValueBinder2Test extends TestCase
17+
{
18+
private IValueBinder $valueBinder;
19+
20+
protected function setUp(): void
21+
{
22+
$this->valueBinder = Cell::getValueBinder();
23+
}
24+
25+
protected function tearDown(): void
26+
{
27+
Cell::setValueBinder($this->valueBinder);
28+
}
29+
30+
public function testStringValueBinderIgnoredErrorsDefault(): void
31+
{
32+
$valueBinder = new StringValueBinder();
33+
Cell::setValueBinder($valueBinder);
34+
$spreadsheet = new Spreadsheet();
35+
$sheet = $spreadsheet->getActiveSheet();
36+
$richText = new RichText();
37+
$richText->createTextRun('6');
38+
$richText2 = new RichText();
39+
$richText2->createTextRun('a');
40+
$sheet->fromArray([
41+
[1, 'x', 3.2],
42+
['y', -5, 'z'],
43+
[new DateTime(), $richText, $richText2],
44+
[new StringableObject('a'), new StringableObject(2), 'z'],
45+
]);
46+
$ignoredCells = [];
47+
foreach ($sheet->getRowIterator() as $row) {
48+
foreach ($row->getCellIterator() as $cell) {
49+
$coordinate = $cell->getCoordinate();
50+
$dataType = $cell->getDataType();
51+
if ($dataType !== DataType::TYPE_INLINE) {
52+
self::assertSame(DataType::TYPE_STRING, $dataType, "not string for cell $coordinate");
53+
}
54+
if ($cell->getIgnoredErrors()->getNumberStoredAsText()) {
55+
$ignoredCells[] = $coordinate;
56+
}
57+
}
58+
}
59+
self::assertSame([], $ignoredCells);
60+
$spreadsheet->disconnectWorksheets();
61+
}
62+
63+
public function testStringValueBinderIgnoredErrorsTrue(): void
64+
{
65+
$valueBinder = new StringValueBinder();
66+
$valueBinder->setSetIgnoredErrors(true);
67+
Cell::setValueBinder($valueBinder);
68+
$spreadsheet = new Spreadsheet();
69+
$sheet = $spreadsheet->getActiveSheet();
70+
$richText = new RichText();
71+
$richText->createTextRun('6');
72+
$richText2 = new RichText();
73+
$richText2->createTextRun('a');
74+
$sheet->fromArray([
75+
[1, 'x', 3.2],
76+
['y', -5, 'z'],
77+
[new DateTime(), $richText, $richText2],
78+
[new StringableObject('a'), new StringableObject(2), 'z'],
79+
]);
80+
$ignoredCells = [];
81+
foreach ($sheet->getRowIterator() as $row) {
82+
foreach ($row->getCellIterator() as $cell) {
83+
$coordinate = $cell->getCoordinate();
84+
$dataType = $cell->getDataType();
85+
if ($dataType !== DataType::TYPE_INLINE) {
86+
self::assertSame(DataType::TYPE_STRING, $dataType, "not string for cell $coordinate");
87+
}
88+
if ($cell->getIgnoredErrors()->getNumberStoredAsText()) {
89+
$ignoredCells[] = $coordinate;
90+
}
91+
}
92+
}
93+
self::assertSame(['A1', 'C1', 'B2', 'B3', 'B4'], $ignoredCells);
94+
$spreadsheet->disconnectWorksheets();
95+
}
96+
97+
public function testStringValueBinderPreserveNumeric(): void
98+
{
99+
$valueBinder = new StringValueBinder();
100+
$valueBinder->setNumericConversion(false);
101+
$valueBinder->setSetIgnoredErrors(true);
102+
Cell::setValueBinder($valueBinder);
103+
$spreadsheet = new Spreadsheet();
104+
$sheet = $spreadsheet->getActiveSheet();
105+
$richText = new RichText();
106+
$richText->createTextRun('6');
107+
$richText2 = new RichText();
108+
$richText2->createTextRun('a');
109+
$sheet->fromArray([
110+
[1, 'x', 3.2],
111+
['y', -5, 'z'],
112+
[new DateTime(), $richText, $richText2],
113+
[new StringableObject('a'), new StringableObject(2), 'z'],
114+
]);
115+
$ignoredCells = [];
116+
foreach ($sheet->getRowIterator() as $row) {
117+
foreach ($row->getCellIterator() as $cell) {
118+
$coordinate = $cell->getCoordinate();
119+
$expected = (is_int($cell->getValue()) || is_float($cell->getValue())) ? DataType::TYPE_NUMERIC : (($cell->getValue() instanceof RichText) ? DataType::TYPE_INLINE : DataType::TYPE_STRING);
120+
self::assertSame($expected, $cell->getDataType(), "wrong type for cell $coordinate");
121+
if ($cell->getIgnoredErrors()->getNumberStoredAsText()) {
122+
$ignoredCells[] = $coordinate;
123+
}
124+
}
125+
}
126+
self::assertSame(['B3', 'B4'], $ignoredCells);
127+
$spreadsheet->disconnectWorksheets();
128+
}
129+
}

tests/PhpSpreadsheetTests/Cell/StringableObject.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66

77
class StringableObject
88
{
9+
private int|string $value;
10+
11+
public function __construct(int|string $value = 'abc')
12+
{
13+
$this->value = $value;
14+
}
15+
916
public function __toString(): string
1017
{
11-
return 'abc';
18+
return (string) $this->value;
1219
}
1320
}

0 commit comments

Comments
 (0)