Repository: PHPOffice/PhpSpreadsheet (php)

Issue:
The cell value binder being static can easily lead to bugs/confusing behaviour
This is:

```
- [ ] a bug report
- [x] a feature request
- [ ] **not** a usage question (ask them on https://stackoverflow.com/questions/tagged/phpspreadsheet or https://gitter.im/PHPOffice/PhpSpreadsheet)
```

### What is the current behavior?
Cell::getValue() can return quite different values for certain things (e.g. dates and times) depending on the IValueBinder used. This is fine, but a trap that is easy to fall into (in my opinion), is that this binder is saved statically, so if you are reading multiple file types it is quite easy to forget to set the cell value binder as it's not required when instantiating the reader.

For example, if you have some piece of code that instantiates PhpOffice\PhpSpreadsheet\Reader\Csv and don't specify a the value binder, it'll work fine reading dates and times from the file, they get returned as a string, but then if another developer comes along and writes a piece of code preceding the CSV code, that instantiates PhpOffice\PhpSpreadsheet\Reader\Xlsx and sets the AdvancedValueBinder then they are unknowingly affecting the way the Csv code is fetching date time values.

Maybe this sounds unlikely but it's quite easy to happen for things like unit tests, scheduled jobs etc.

It's hard to expect developers to know that they have to go and manually set Cell::setValueBinder() before reading spreadsheets so we are currently working around it by having factory methods which ensure it gets set but there's still nothing from stopping someone who doesn't know about these to instantiate the reader classes directly and forget to set it.

### What is the preferred behavior?
That the binder is not saved statically, and perhaps an argument in the constructor?

### What are the steps to reproduce?

N/A

### Which versions of PhpSpreadsheet and PHP are affected?

Tested on 1.10.0 but I presume any version where Cell::$valueBinder is static.

Anyway, hope I'm not being stupid and thanks for the great library btw :) 

Pull request description:
Add Dynamic valueBinder Property to Spreadsheet and Readers
Fix #1395, a 2020 issue which had been marked stale and is now re-opened. Static valueBinder property of Cell isn't ideal. It would be more flexible to make it a dynamic property of the spreadsheet. Static property will continue to be used, but dynamic property will be used first if it is set. Readers will also be changed to add a valueBinder property which they pass to the spreadsheet; however, it will make a difference only for Csv/Html/Slk, since the other readers use setValueExplicit which ignores valueBinder.

Documentation is updated in several places to note that dynamic property is preferred over static.

This is:

- [ ] a bugfix
- [x] a new feature
- [ ] refactoring
- [ ] additional unit tests

Checklist:

- [ ] Changes are covered by unit tests
  - [ ] Changes are covered by existing unit tests
  - [x] New unit tests have been added
- [x] Code style is respected
- [x] 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 and a link to the pull request if applicable
- [x] 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 make it easier to track progress with this PR.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21033ec6e..019311f32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
 - Image Transparency/Opacity with Html Reader Changes. [Discussion #4117](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4117) [PR #4142](https://github.com/PHPOffice/PhpSpreadsheet/pull/4142)
 - Option to Write Hyperlink Rather Than Label to Csv. [Issue #1412](https://github.com/PHPOffice/PhpSpreadsheet/issues/1412) [PR #4151](https://github.com/PHPOffice/PhpSpreadsheet/pull/4151)
 - Invalid Html Due to Cached Filesize. [Issue #1107](https://github.com/PHPOffice/PhpSpreadsheet/issues/1107) [PR #4184](https://github.com/PHPOffice/PhpSpreadsheet/pull/4184)
+- Add Dynamic valueBinder Property to Spreadsheet and Readers. [Issue #1395](https://github.com/PHPOffice/PhpSpreadsheet/issues/1395) [PR #4185](https://github.com/PHPOffice/PhpSpreadsheet/pull/4185)
 
 ## 2024-09-29 - 3.3.0 (no 3.0.\*, 3.1.\*, 3.2.\*)
 
diff --git a/docs/topics/Behind the Mask.md b/docs/topics/Behind the Mask.md
index 9648f52df..ba77fcc4e 100644
--- a/docs/topics/Behind the Mask.md
+++ b/docs/topics/Behind the Mask.md
@@ -117,7 +117,10 @@ If you wish to emulate the MS Excel behaviour, and automatically convert string
 
 You can do this by changing the Value Binder, which will then apply every time you set a Cell value.
 ```php
+// Old method using static property
 Cell::setValueBinder(new AdvancedValueBinder());
+// Preferred method using dynamic property since 3.4.0
+$spreadsheet->setValueBinder(new AdvancedValueBinder());
 
 // Set Cell C21 using a formatted string value
 $worksheet->getCell('C20')->setValue('€ -12345.6789');
diff --git a/docs/topics/The Dating Game.md b/docs/topics/The Dating Game.md
index 75726614d..5b0c2812f 100644
--- a/docs/topics/The Dating Game.md
+++ b/docs/topics/The Dating Game.md
@@ -257,7 +257,10 @@ $spreadsheet = new Spreadsheet();
 $worksheet = $spreadsheet->getActiveSheet();
 // Use the Advanced Value Binder so that our string date/time values will be automatically converted
 //     to Excel serialized date/timestamps
+// Old method using static property
 Cell::setValueBinder(new AdvancedValueBinder());
+// Preferred method using dynamic property since 3.4.0
+$spreadsheet->setValueBinder(new AdvancedValueBinder());
 
 // Write our data to the worksheet
 $worksheet->fromArray($projectHeading);
diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md
index d377da899..a9fe3695d 100644
--- a/docs/topics/accessing-cells.md
+++ b/docs/topics/accessing-cells.md
@@ -518,15 +518,15 @@ style information. The following example demonstrates how to set the
 value binder in PhpSpreadsheet:
 
 ```php
-/** PhpSpreadsheet */
-require_once 'src/Boostrap.php';
-
-// Set value binder
+// Older method using static property
 \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
-
 // Create new Spreadsheet object
 $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
 
+// Preferred method using dynamic property since 3.4.0
+$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
+$spreadsheet->setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
+
 // ...
 // Add some data, resembling some different data types
 $spreadsheet->getActiveSheet()->setCellValue('A4', 'Percentage value:');
@@ -555,13 +555,20 @@ $stringValueBinder->setNumericConversion(false)
     ->setBooleanConversion(false)
     ->setNullConversion(false)
     ->setFormulaConversion(false);
+// Older method using static property
 \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( $stringValueBinder );
+// Preferred method using dynamic property since 3.4.0
+$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
+$spreadsheet->setValueBinder( $stringValueBinder );
 ```
 
 You can override the current binder when setting individual cell values by specifying a different Binder to use in the Cell's `setValue()` or the Worksheet's `setCellValue()` methods.
 ```php
 $spreadsheet = new Spreadsheet();
+// Old method using static property
 Cell::setValueBinder(new AdvancedValueBinder());
+// Preferred method using dynamic property since 3.4.0
+$spreadsheet->setValueBinder(new AdvancedValueBinder());
 
 $value = '12.5%';
 
diff --git a/docs/topics/reading-files.md b/docs/topics/reading-files.md
index 8f74978d3..4aba019ff 100644
--- a/docs/topics/reading-files.md
+++ b/docs/topics/reading-files.md
@@ -755,14 +755,18 @@ So using a Value Binder allows a great deal more flexibility in the
 loader logic when reading unformatted text files.
 
 ```php
-/**  Tell PhpSpreadsheet that we want to use the Advanced Value Binder  **/
-\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
-
 $inputFileType = 'Csv';
 $inputFileName = './sampleData/example1.tsv';
 
 $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType);
 $reader->setDelimiter("\t");
+
+/**  Tell PhpSpreadsheet that we want to use the Advanced Value Binder  **/
+// Old method using static property
+\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
+// Preferred method using dynamic property since 3.4.0
+$reader::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
+
 $spreadsheet = $reader->load($inputFileName);
 ```
 
@@ -774,7 +778,7 @@ Loading using a Value Binder applies to:
 Reader    | Y/N |Reader  | Y/N |Reader        | Y/N
 ----------|:---:|--------|:---:|--------------|:---:
 Xlsx      | NO  | Xls    | NO  | Xml          | NO
-Ods       | NO  | SYLK   | NO  | Gnumeric     | NO
+Ods       | NO  | SYLK   | YES | Gnumeric     | NO
 CSV       | YES | HTML   | YES
 
 Note that you can also use the Binder to determine how PhpSpreadsheet identified datatypes for values when you set a cell value without explicitly setting a datatype.
diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md
index 670714c84..b5a392703 100644
--- a/docs/topics/recipes.md
+++ b/docs/topics/recipes.md
@@ -233,7 +233,10 @@ method that suits you the best. Here are some examples:
 
 ```php
 // MySQL-like timestamp '2008-12-31' or date string
+// Old method using static property
 \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
+// Preferred method using dynamic property since 3.4.0
+$spreadsheet->setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
 
 $spreadsheet->getActiveSheet()
     ->setCellValue('D1', '2008-12-31');
@@ -599,7 +602,10 @@ when it sees a newline character in a string that you are inserting in a
 cell. Just like Microsoft Office Excel. Try this:
 
 ```php
+// Old method using static property
 \PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
+// Preferred method using dynamic property since 3.4.0
+$spreadsheet->setValueBinder( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder() );
 
 $spreadsheet->getActiveSheet()->getCell('A1')->setValue("hello\nworld");
 ```
diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php
index d4e08b639..1e353db91 100644
--- a/src/PhpSpreadsheet/Cell/Cell.php
+++ b/src/PhpSpreadsheet/Cell/Cell.php
@@ -112,8 +112,11 @@ class Cell implements Stringable
                 $dataType = DataType::TYPE_STRING;
             }
             $this->dataType = $dataType;
-        } elseif (self::getValueBinder()->bindValue($this, $value) === false) {
-            throw new SpreadsheetException('Value could not be bound to cell.');
+        } else {
+            $valueBinder = $worksheet->getParent()?->getValueBinder() ?? self::getValueBinder();
+            if ($valueBinder->bindValue($this, $value) === false) {
+                throw new SpreadsheetException('Value could not be bound to cell.');
+            }
         }
         $this->ignoredErrors = new IgnoredErrors();
     }
@@ -232,7 +235,8 @@ class Cell implements Stringable
      */
     public function setValue(mixed $value, ?IValueBinder $binder = null): self
     {
-        $binder ??= self::getValueBinder();
+        // Cells?->Worksheet?->Spreadsheet
+        $binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder();
         if (!$binder->bindValue($this, $value)) {
             throw new SpreadsheetException('Value could not be bound to cell.');
         }
diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php
index 218e69fbd..de80834d1 100644
--- a/src/PhpSpreadsheet/Reader/BaseReader.php
+++ b/src/PhpSpreadsheet/Reader/BaseReader.php
@@ -2,6 +2,7 @@
 
 namespace PhpOffice\PhpSpreadsheet\Reader;
 
+use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
 use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
 use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
 use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
@@ -56,6 +57,8 @@ abstract class BaseReader implements IReader
 
     protected ?XmlScanner $securityScanner = null;
 
+    protected ?IValueBinder $valueBinder = null;
+
     public function __construct()
     {
         $this->readFilter = new DefaultReadFilter();
@@ -242,4 +245,16 @@ abstract class BaseReader implements IReader
 
         return $returnArray;
     }
+
+    public function getValueBinder(): ?IValueBinder
+    {
+        return $this->valueBinder;
+    }
+
+    public function setValueBinder(?IValueBinder $valueBinder): self
+    {
+        $this->valueBinder = $valueBinder;
+
+        return $this;
+    }
 }
diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php
index 762ec3191..fff8146d5 100644
--- a/src/PhpSpreadsheet/Reader/Csv.php
+++ b/src/PhpSpreadsheet/Reader/Csv.php
@@ -262,6 +262,7 @@ class Csv extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
 
         // Load into this instance
         return $this->loadIntoExisting($filename, $spreadsheet);
@@ -274,6 +275,7 @@ class Csv extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
 
         // Load into this instance
         return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true);
@@ -391,7 +393,7 @@ class Csv extends BaseReader
         // Loop through each line of the file in turn
         $delimiter = $this->delimiter ?? '';
         $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
-        $valueBinder = Cell::getValueBinder();
+        $valueBinder = $this->valueBinder ?? Cell::getValueBinder();
         $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
         $this->getTrue = Calculation::getTRUE();
         $this->getFalse = Calculation::getFALSE();
diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php
index 898e32db5..d80a87ecc 100644
--- a/src/PhpSpreadsheet/Reader/Gnumeric.php
+++ b/src/PhpSpreadsheet/Reader/Gnumeric.php
@@ -226,6 +226,7 @@ class Gnumeric extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
         $spreadsheet->removeSheetByIndex(0);
 
         // Load into this instance
diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php
index a95ed6066..4e00f7dd3 100644
--- a/src/PhpSpreadsheet/Reader/Html.php
+++ b/src/PhpSpreadsheet/Reader/Html.php
@@ -211,6 +211,7 @@ class Html extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
 
         // Load into this instance
         return $this->loadIntoExisting($filename, $spreadsheet);
@@ -793,6 +794,7 @@ class Html extends BaseReader
             throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
         }
         $spreadsheet = $spreadsheet ?? new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
         self::loadProperties($dom, $spreadsheet);
 
         return $this->loadDocument($dom, $spreadsheet);
diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php
index da7b85cee..85fa08ecf 100644
--- a/src/PhpSpreadsheet/Reader/Ods.php
+++ b/src/PhpSpreadsheet/Reader/Ods.php
@@ -234,6 +234,7 @@ class Ods extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
         $spreadsheet->removeSheetByIndex(0);
 
         // Load into this instance
diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php
index 2a0b2fcd9..7359f926e 100644
--- a/src/PhpSpreadsheet/Reader/Slk.php
+++ b/src/PhpSpreadsheet/Reader/Slk.php
@@ -145,6 +145,7 @@ class Slk extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
 
         // Load into this instance
         return $this->loadIntoExisting($filename, $spreadsheet);
diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php
index 833b2550f..ffbce3c5c 100644
--- a/src/PhpSpreadsheet/Reader/Xls.php
+++ b/src/PhpSpreadsheet/Reader/Xls.php
@@ -575,6 +575,7 @@ class Xls extends BaseReader
 
         // Initialisations
         $this->spreadsheet = new Spreadsheet();
+        $this->spreadsheet->setValueBinder($this->valueBinder);
         $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
         if (!$this->readDataOnly) {
             $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php
index 3ada95ee4..d86102c3c 100644
--- a/src/PhpSpreadsheet/Reader/Xlsx.php
+++ b/src/PhpSpreadsheet/Reader/Xlsx.php
@@ -395,6 +395,7 @@ class Xlsx extends BaseReader
 
         // Initialisations
         $excel = new Spreadsheet();
+        $excel->setValueBinder($this->valueBinder);
         $excel->removeSheetByIndex(0);
         $addingFirstCellStyleXf = true;
         $addingFirstCellXf = true;
@@ -914,6 +915,7 @@ class Xlsx extends BaseReader
                                                     $value = self::castToString($c);
                                                     if (is_numeric($value)) {
                                                         $value += 0;
+                                                        $cellDataType = DataType::TYPE_NUMERIC;
                                                     }
                                                 } else {
                                                     // Formula
diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php
index e0f218747..9276479f0 100644
--- a/src/PhpSpreadsheet/Reader/Xml.php
+++ b/src/PhpSpreadsheet/Reader/Xml.php
@@ -242,6 +242,7 @@ class Xml extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
         $spreadsheet->removeSheetByIndex(0);
 
         // Load into this instance
@@ -255,6 +256,7 @@ class Xml extends BaseReader
     {
         // Create new Spreadsheet
         $spreadsheet = new Spreadsheet();
+        $spreadsheet->setValueBinder($this->valueBinder);
         $spreadsheet->removeSheetByIndex(0);
 
         // Load into this instance
@@ -512,9 +514,6 @@ class Xml extends BaseReader
                         if (isset($cell_ss['StyleID'])) {
                             $style = (string) $cell_ss['StyleID'];
                             if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) {
-                                //if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) {
-                                //    $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null);
-                                //}
                                 $spreadsheet->getActiveSheet()->getStyle($cellRange)
                                     ->applyFromArray($this->styles[$style]);
                             }
diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php
index c0a3a9bc2..6ec75c33c 100644
--- a/src/PhpSpreadsheet/Spreadsheet.php
+++ b/src/PhpSpreadsheet/Spreadsheet.php
@@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet;
 
 use JsonSerializable;
 use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
 use PhpOffice\PhpSpreadsheet\Document\Properties;
 use PhpOffice\PhpSpreadsheet\Document\Security;
 use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
@@ -173,6 +174,8 @@ class Spreadsheet implements JsonSerializable
 
     private Theme $theme;
 
+    private ?IValueBinder $valueBinder = null;
+
     public function getTheme(): Theme
     {
         return $this->theme;
@@ -1591,4 +1594,16 @@ class Spreadsheet implements JsonSerializable
     {
         return $this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'] ?? null;
     }
+
+    public function getValueBinder(): ?IValueBinder
+    {
+        return $this->valueBinder;
+    }
+
+    public function setValueBinder(?IValueBinder $valueBinder): self
+    {
+        $this->valueBinder = $valueBinder;
+
+        return $this;
+    }
 }


Test patch:
diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/BinderTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/BinderTest.php
new file mode 100644
index 000000000..95d8d5cb9
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Reader/Csv/BinderTest.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv;
+
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
+use PhpOffice\PhpSpreadsheet\Reader\Csv;
+use PHPUnit\Framework\TestCase;
+
+class BinderTest extends TestCase
+{
+    public function testLoadFromString(): void
+    {
+        $data = <<<EOF
+            1,2,3
+            4,5,6
+            EOF;
+        $reader1 = new Csv();
+        $spreadsheet1 = $reader1->loadSpreadsheetFromString($data);
+        $sheet1 = $spreadsheet1->getActiveSheet();
+        $sheet1->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING);
+        $sheet1->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC);
+        $sheet1->setCellValue('C3', 9);
+        $sheet1->fromArray([10, 11, 12], null, 'A4');
+        $expected1 = [
+            [1, 2, 3],
+            [4, 5, 6],
+            ['7', 8, 9],
+            [10, 11, 12],
+        ];
+        self::AssertSame($expected1, $sheet1->toArray(null, false, false));
+
+        $reader2 = new Csv();
+        $reader2->setValueBinder(new StringValueBinder());
+        $spreadsheet2 = $reader2->loadSpreadsheetFromString($data);
+        $sheet2 = $spreadsheet2->getActiveSheet();
+        $sheet2->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING);
+        $sheet2->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC);
+        $sheet2->setCellValue('C3', 9);
+        $sheet2->fromArray([10, 11, 12], null, 'A4');
+        $expected2 = [
+            ['1', '2', '3'],
+            ['4', '5', '6'],
+            ['7', 8, '9'],
+            ['10', '11', '12'],
+        ];
+        self::AssertSame($expected2, $sheet2->toArray(null, false, false));
+
+        $spreadsheet1->disconnectWorksheets();
+        $spreadsheet2->disconnectWorksheets();
+    }
+}
diff --git a/tests/PhpSpreadsheetTests/Reader/Html/BinderTest.php b/tests/PhpSpreadsheetTests/Reader/Html/BinderTest.php
new file mode 100644
index 000000000..cbf1c4fdc
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Reader/Html/BinderTest.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Reader\Html;
+
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
+use PhpOffice\PhpSpreadsheet\Reader\Html;
+use PHPUnit\Framework\TestCase;
+
+class BinderTest extends TestCase
+{
+    public function testLoadFromString(): void
+    {
+        $data = <<<EOF
+            <table>
+            <tbody>
+            <tr><td>1</td><td>2</td><td>3</td></tr>
+            <tr><td>4</td><td>5</td><td>6</td></tr>
+            </tbody>
+            </table>
+            EOF;
+        $reader1 = new Html();
+        $spreadsheet1 = $reader1->loadFromString($data);
+        $sheet1 = $spreadsheet1->getActiveSheet();
+        $sheet1->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING);
+        $sheet1->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC);
+        $sheet1->setCellValue('C3', 9);
+        $sheet1->fromArray([10, 11, 12], null, 'A4');
+        $expected1 = [
+            [1, 2, 3],
+            [4, 5, 6],
+            ['7', 8, 9],
+            [10, 11, 12],
+        ];
+        self::AssertSame($expected1, $sheet1->toArray(null, false, false));
+
+        $reader2 = new Html();
+        $reader2->setValueBinder(new StringValueBinder());
+        $spreadsheet2 = $reader2->loadFromString($data);
+        $sheet2 = $spreadsheet2->getActiveSheet();
+        $sheet2->getCell('A3')->setValueExplicit(7, DataType::TYPE_STRING);
+        $sheet2->getCell('B3')->setValueExplicit(8, DataType::TYPE_NUMERIC);
+        $sheet2->setCellValue('C3', 9);
+        $sheet2->fromArray([10, 11, 12], null, 'A4');
+        $expected2 = [
+            ['1', '2', '3'],
+            ['4', '5', '6'],
+            ['7', 8, '9'],
+            ['10', '11', '12'],
+        ];
+        self::AssertSame($expected2, $sheet2->toArray(null, false, false));
+
+        $spreadsheet1->disconnectWorksheets();
+        $spreadsheet2->disconnectWorksheets();
+    }
+}
diff --git a/tests/PhpSpreadsheetTests/Reader/Slk/BinderTest.php b/tests/PhpSpreadsheetTests/Reader/Slk/BinderTest.php
new file mode 100644
index 000000000..4be6753f1
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Reader/Slk/BinderTest.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Reader\Slk;
+
+use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
+use PhpOffice\PhpSpreadsheet\Reader\Slk;
+use PHPUnit\Framework\TestCase;
+
+class BinderTest extends TestCase
+{
+    public function testBinder(): void
+    {
+        $infile = 'tests/data/Reader/Slk/issue.2276.slk';
+        $reader = new Slk();
+        $spreadsheet = $reader->load($infile);
+        $sheet = $spreadsheet->getActiveSheet();
+        $expected1 = [[1, 2], [3, '']];
+        self::assertSame($expected1, $sheet->toArray(null, false, false));
+        $reader2 = new Slk();
+        $reader2->setValueBinder(new StringValueBinder());
+        $spreadsheet2 = $reader2->load($infile);
+        $sheet2 = $spreadsheet2->getActiveSheet();
+        $expected2 = [['1', '2'], ['3', '']];
+        self::assertSame($expected2, $sheet2->toArray(null, false, false));
+        $spreadsheet->disconnectWorksheets();
+        $spreadsheet2->disconnectWorksheets();
+    }
+}