Repository: box/spout (php)

Issue:
Multiple InlineString value nodes in merged cells
The method `\Box\Spout\Reader\XLSX\Helper\CellValueFormatter::formatInlineStringCellValue` assumes that there is only going to be a single `<t>` element present, however it's possible to have any number of them when a merged cell is present.

Here's an example of a valid XML snippet:
```
<c s="4" t="inlineStr" r="A2">
	<is>
	   <r>
		  <rPr>
			 <b val="0" />
			 <i val="0" />
			 <strike val="0" />
			 <u val="none" />
			 <sz val="10" />
			 <color rgb="FF000000" />
			 <rFont val="Arial" />
		  </rPr>
		  <t xml:space="preserve">VALUE 1</t>
	   </r>
	   <r>
		  <rPr>
			 <b val="0" />
			 <i val="0" />
			 <strike val="0" />
			 <u val="none" />
			 <sz val="10" />
			 <color rgb="FF000000" />
			 <rFont val="Arial" />
		  </rPr>
		  <t xml:space="preserve"> VALUE 2</t>
	   </r>
	   <r>
		  <rPr>
			 <b val="0" />
			 <i val="0" />
			 <strike val="0" />
			 <u val="none" />
			 <sz val="10" />
			 <color rgb="FF000000" />
			 <rFont val="Arial" />
		  </rPr>
		  <t xml:space="preserve"> VALUE 3</t>
	   </r>
	   <r>
		  <rPr>
			 <b val="0" />
			 <i val="0" />
			 <strike val="0" />
			 <u val="none" />
			 <sz val="10" />
			 <color rgb="FF000000" />
			 <rFont val="Arial" />
		  </rPr>
		  <t xml:space="preserve"> VALUE 4</t>
	   </r>
	</is>
 </c>
```

Reading this column will output `VALUE 1`, when the expected column value should be `VALUE 1 VALUE 2 VALUE 3 VALUE 4`.

Possible solution involves imploding all the `<t>` node values together

Pull request description:
Inline strings can contain multiple value nodes
We were working under the assumption that XLSX's inline strings only had a single value node (`<t>`). This is incorrect.
To get the actual value of an inline string node, we need to concatenate the value of all its child nodes.

Fixes #779 

Patch:
diff --git a/composer.json b/composer.json
index 4d9642b..912349d 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,8 @@
     "require": {
         "php": ">=7.2.0",
         "ext-zip": "*",
-        "ext-xmlreader" : "*"
+        "ext-xmlreader": "*",
+        "ext-dom": "*"
     },
     "require-dev": {
         "phpunit/phpunit": "^8",
diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
index e95f186..ec83681 100644
--- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
+++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
@@ -122,10 +122,15 @@ class CellValueFormatter
      */
     protected function formatInlineStringCellValue($node)
     {
-        // inline strings are formatted this way:
-        // <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
-        $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
-        $cellValue = $this->escaper->unescape($tNode->nodeValue);
+        // inline strings are formatted this way (they can contain any number of <t> nodes):
+        // <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t><t>[INLINE_STRING_2]</t></is></c>
+        $tNodes = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE);
+
+        $cellValue = '';
+        for ($i = 0; $i < $tNodes->count(); $i++) {
+            $tNode = $tNodes->item($i);
+            $cellValue .= $this->escaper->unescape($tNode->nodeValue);
+        }
 
         return $cellValue;
     }


Test patch:
diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
index 34508b0..f981a63 100644
--- a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
+++ b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
@@ -186,18 +186,22 @@ class CellValueFormatterTest extends TestCase
     public function testFormatInlineStringCellValue($value, $expectedFormattedValue)
     {
         $nodeListMock = $this->createMock(\DOMNodeList::class);
+        $nodeListMock
+            ->expects($this->atLeastOnce())
+            ->method('count')
+            ->willReturn(1);
         $nodeListMock
             ->expects($this->atLeastOnce())
             ->method('item')
             ->with(0)
-            ->will($this->returnValue((object) ['nodeValue' => $value]));
+            ->willReturn((object) ['nodeValue' => $value]);
 
         $nodeMock = $this->createMock(\DOMElement::class);
         $nodeMock
             ->expects($this->atLeastOnce())
             ->method('getElementsByTagName')
             ->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE)
-            ->will($this->returnValue($nodeListMock));
+            ->willReturn($nodeListMock);
 
         $formatter = new CellValueFormatter(null, null, false, false, new Escaper\XLSX());
         $formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock);
diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php
index e9888d8..af7ca37 100644
--- a/tests/Spout/Reader/XLSX/ReaderTest.php
+++ b/tests/Spout/Reader/XLSX/ReaderTest.php
@@ -72,6 +72,19 @@ class ReaderTest extends TestCase
         }
     }
 
+    /**
+     * @return void
+     */
+    public function testReadShouldSupportInlineStringsWithMultipleValueNodes()
+    {
+        $allRows = $this->getAllRowsForFile('sheet_with_multiple_value_nodes_in_inline_strings.xlsx');
+
+        $expectedRows = [
+            ['VALUE 1 VALUE 2 VALUE 3 VALUE 4', 's1 - B1'],
+        ];
+        $this->assertEquals($expectedRows, $allRows);
+    }
+
     /**
      * @return void
      */