getActiveSheet(); $sheet->setTitle('TestSheet'); // Numeric values $sheet->setCellValue('A1', 42); $sheet->setCellValue('A2', 3.14); $sheet->setCellValue('A3', 0); $sheet->setCellValue('A4', -100); // String values (will become shared strings) $sheet->setCellValue('B1', 'Hello'); $sheet->setCellValue('B2', 'World'); $sheet->setCellValue('B3', ''); $sheet->setCellValue('B4', 'Hello'); // duplicate shared string // Boolean values $sheet->getCell('C1')->setValueExplicit(true, DataType::TYPE_BOOL); $sheet->getCell('C2')->setValueExplicit(false, DataType::TYPE_BOOL); // Formula cells $sheet->setCellValue('D1', '=A1+A2'); $sheet->setCellValue('D2', '=SUM(A1:A4)'); $sheet->setCellValue('D3', '=B1&" "&B2'); // Inline string (set explicitly as inline type) $sheet->getCell('E1')->setValueExplicit('Inline text', DataType::TYPE_INLINE); $this->tempFile = tempnam(sys_get_temp_dir(), 'phpspreadsheet_streaming_test_') . '.xlsx'; $writer = new XlsxWriter($spreadsheet); $writer->save($this->tempFile); $spreadsheet->disconnectWorksheets(); } protected function tearDown(): void { if ($this->tempFile !== '' && file_exists($this->tempFile)) { unlink($this->tempFile); } foreach ($this->extraTempFiles as $file) { if (file_exists($file)) { unlink($file); } } } private function createTempFile(): string { $path = tempnam(sys_get_temp_dir(), 'phpspreadsheet_streaming_test_') . '.xlsx'; $this->extraTempFiles[] = $path; return $path; } public function testStreamingLoadsIdenticalToSimpleXml(): void { // Load with SimpleXML (default) $readerSimple = new Xlsx(); $readerSimple->setReadDataOnly(true); $spreadsheetSimple = $readerSimple->load($this->tempFile); $sheetSimple = $spreadsheetSimple->getActiveSheet(); // Load with streaming XMLReader $readerStreaming = new Xlsx2(); $readerStreaming->setReadDataOnly(true); $spreadsheetStreaming = $readerStreaming->load($this->tempFile); $sheetStreaming = $spreadsheetStreaming->getActiveSheet(); // Compare all populated cells foreach ($sheetSimple->getRowIterator() as $row) { foreach ($row->getCellIterator() as $cell) { $coord = $cell->getCoordinate(); $simpleValue = $sheetSimple->getCell($coord)->getValue(); $streamValue = $sheetStreaming->getCell($coord)->getValue(); self::assertEquals( $simpleValue, $streamValue, "Cell {$coord} value mismatch: SimpleXML=" . var_export($simpleValue, true) . ' vs Streaming=' . var_export($streamValue, true) ); } } $spreadsheetSimple->disconnectWorksheets(); $spreadsheetStreaming->disconnectWorksheets(); } public function testStreamingNumericCells(): void { $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); self::assertSame(42, $sheet->getCell('A1')->getValue()); self::assertSame(3.14, $sheet->getCell('A2')->getValue()); self::assertSame(0, $sheet->getCell('A3')->getValue()); self::assertSame(-100, $sheet->getCell('A4')->getValue()); $spreadsheet->disconnectWorksheets(); } public function testStreamingSharedStrings(): void { $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); self::assertSame('Hello', $sheet->getCell('B1')->getValue()); self::assertSame('World', $sheet->getCell('B2')->getValue()); // B3 is empty string self::assertSame('Hello', $sheet->getCell('B4')->getValue()); $spreadsheet->disconnectWorksheets(); } public function testStreamingBooleanCells(): void { $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); self::assertTrue($sheet->getCell('C1')->getValue()); self::assertFalse($sheet->getCell('C2')->getValue()); $spreadsheet->disconnectWorksheets(); } public function testStreamingFormulaCells(): void { $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); self::assertSame('=A1+A2', $sheet->getCell('D1')->getValue()); self::assertSame(DataType::TYPE_FORMULA, $sheet->getCell('D1')->getDataType()); self::assertSame('=SUM(A1:A4)', $sheet->getCell('D2')->getValue()); self::assertSame(DataType::TYPE_FORMULA, $sheet->getCell('D2')->getDataType()); self::assertSame('=B1&" "&B2', $sheet->getCell('D3')->getValue()); self::assertSame(DataType::TYPE_FORMULA, $sheet->getCell('D3')->getDataType()); $spreadsheet->disconnectWorksheets(); } public function testStreamingBooleanFormulaCalculatedValue(): void { $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); // C1 is a boolean cell - verify the type is preserved $cell = $sheet->getCell('C1'); self::assertTrue($cell->getValue()); $spreadsheet->disconnectWorksheets(); } public function testStreamingWithExistingXlsxFile(): void { // Use an existing test file to compare both reading modes $filename = 'tests/data/Reader/XLSX/rowColumnAttributeTest.xlsx'; $readerSimple = new Xlsx(); $readerSimple->setReadDataOnly(true); $spreadsheetSimple = $readerSimple->load($filename); $readerStreaming = new Xlsx2(); $readerStreaming->setReadDataOnly(true); $spreadsheetStreaming = $readerStreaming->load($filename); foreach ($spreadsheetSimple->getActiveSheet()->getRowIterator() as $row) { foreach ($row->getCellIterator() as $cell) { $coord = $cell->getCoordinate(); $simpleVal = $spreadsheetSimple->getActiveSheet()->getCell($coord)->getValue(); $streamVal = $spreadsheetStreaming->getActiveSheet()->getCell($coord)->getValue(); self::assertEquals( $simpleVal, $streamVal, "Cell {$coord} value mismatch in existing file" ); } } $spreadsheetSimple->disconnectWorksheets(); $spreadsheetStreaming->disconnectWorksheets(); } public function testStreamingWithStyles(): void { // Verify streaming mode applies styles correctly $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); // Cell A1 should have a valid xf index (no crash, basic validation) $cell = $sheet->getCell('A1'); self::assertGreaterThanOrEqual(0, $cell->getXfIndex()); $spreadsheet->disconnectWorksheets(); } public function testStreamingInlineString(): void { $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); // The inline string should be readable $value = $sheet->getCell('E1')->getValue(); self::assertNotNull($value); // The value could be a RichText object or a string depending on writer output $plainValue = ($value instanceof RichText) ? $value->getPlainText() : (is_string($value) ? $value : ''); self::assertSame('Inline text', $plainValue); $spreadsheet->disconnectWorksheets(); } public function testStreamingErrorCells(): void { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // Error values $sheet->getCell('A1')->setValueExplicit('#DIV/0!', DataType::TYPE_ERROR); $sheet->getCell('A2')->setValueExplicit('#VALUE!', DataType::TYPE_ERROR); $sheet->getCell('A3')->setValueExplicit('#REF!', DataType::TYPE_ERROR); $sheet->getCell('A4')->setValueExplicit('#N/A', DataType::TYPE_ERROR); // Error-producing formula $sheet->setCellValue('B1', '=1/0'); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); // Load with streaming and compare with SimpleXML $readerSimple = new Xlsx(); $simple = $readerSimple->load($file); $readerStream = new Xlsx2(); $readerStream->setReadDataOnly(true); $stream = $readerStream->load($file); foreach (['A1', 'A2', 'A3', 'A4'] as $coord) { self::assertSame( $simple->getActiveSheet()->getCell($coord)->getValue(), $stream->getActiveSheet()->getCell($coord)->getValue(), "Error cell {$coord} mismatch" ); } // B1 is an error-producing formula self::assertSame( $simple->getActiveSheet()->getCell('B1')->getValue(), $stream->getActiveSheet()->getCell('B1')->getValue(), 'Error formula cell B1 mismatch' ); $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testStreamingMultipleSheets(): void { $spreadsheet = new Spreadsheet(); $sheet1 = $spreadsheet->getActiveSheet(); $sheet1->setTitle('Sheet1'); $sheet1->setCellValue('A1', 'First'); $sheet1->setCellValue('B1', 100); $sheet2 = $spreadsheet->createSheet(); $sheet2->setTitle('Sheet2'); $sheet2->setCellValue('A1', 'Second'); $sheet2->setCellValue('B1', 200); $sheet3 = $spreadsheet->createSheet(); $sheet3->setTitle('Sheet3'); $sheet3->setCellValue('A1', 'Third'); $sheet3->setCellValue('B1', 300); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); $reader = new Xlsx2(); $result = $reader->load($file); self::assertSame(3, $result->getSheetCount()); self::assertSame('First', $result->getSheetByNameOrThrow('Sheet1')->getCell('A1')->getValue()); self::assertSame(100, $result->getSheetByNameOrThrow('Sheet1')->getCell('B1')->getValue()); self::assertSame('Second', $result->getSheetByNameOrThrow('Sheet2')->getCell('A1')->getValue()); self::assertSame(200, $result->getSheetByNameOrThrow('Sheet2')->getCell('B1')->getValue()); self::assertSame('Third', $result->getSheetByNameOrThrow('Sheet3')->getCell('A1')->getValue()); self::assertSame(300, $result->getSheetByNameOrThrow('Sheet3')->getCell('B1')->getValue()); $result->disconnectWorksheets(); } public function testStreamingLargeCoordinates(): void { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // High row number $sheet->setCellValue('A1000', 'row1000'); // High column (XFD is max column in Excel = 16384) $sheet->setCellValue('ZZ1', 'colZZ'); // Both high $sheet->setCellValue('AB500', 12345); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); $reader = new Xlsx2(); $reader->setReadDataOnly(true); $result = $reader->load($file); $sheet = $result->getActiveSheet(); self::assertSame('row1000', $sheet->getCell('A1000')->getValue()); self::assertSame('colZZ', $sheet->getCell('ZZ1')->getValue()); self::assertSame(12345, $sheet->getCell('AB500')->getValue()); $result->disconnectWorksheets(); } public function testStreamingDateFormattedCells(): void { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // A date value stored as a number with date format $sheet->setCellValue('A1', 45000); // Excel serial date $sheet->getStyle('A1')->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); // A time value $sheet->setCellValue('A2', 0.75); // 6:00 PM $sheet->getStyle('A2')->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_TIME4); // Regular number (no date format) for comparison $sheet->setCellValue('A3', 45000); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); // Compare streaming vs SimpleXML for date formatted cells $readerSimple = new Xlsx(); $simple = $readerSimple->load($file); $readerStream = new Xlsx2(); $stream = $readerStream->load($file); foreach (['A1', 'A2', 'A3'] as $coord) { self::assertEquals( $simple->getActiveSheet()->getCell($coord)->getValue(), $stream->getActiveSheet()->getCell($coord)->getValue(), "Date cell {$coord} value mismatch" ); } $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testStreamingStyleIndexHandling(): void { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setCellValue('A1', 'Bold text'); $sheet->getStyle('A1')->getFont()->setBold(true); $sheet->setCellValue('A2', 'Italic text'); $sheet->getStyle('A2')->getFont()->setItalic(true); $sheet->setCellValue('A3', 42); $sheet->getStyle('A3')->getNumberFormat()->setFormatCode('#,##0.00'); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); // Load with styles (readDataOnly=false) using streaming $readerStream = new Xlsx2(); // readDataOnly defaults to false $stream = $readerStream->load($file); $sheetStream = $stream->getActiveSheet(); // Load with SimpleXML for comparison $readerSimple = new Xlsx(); $simple = $readerSimple->load($file); $sheetSimple = $simple->getActiveSheet(); // Verify style indices match foreach (['A1', 'A2', 'A3'] as $coord) { self::assertSame( $sheetSimple->getCell($coord)->getXfIndex(), $sheetStream->getCell($coord)->getXfIndex(), "Style index mismatch for cell {$coord}" ); } // Verify actual style properties were applied self::assertTrue($sheetStream->getStyle('A1')->getFont()->getBold()); self::assertTrue($sheetStream->getStyle('A2')->getFont()->getItalic()); $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testStreamingSharedFormulas(): void { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // Create data for shared formulas $sheet->setCellValue('A1', 10); $sheet->setCellValue('A2', 20); $sheet->setCellValue('A3', 30); $sheet->setCellValue('A4', 40); $sheet->setCellValue('A5', 50); $sheet->setCellValue('B1', 1); $sheet->setCellValue('B2', 2); $sheet->setCellValue('B3', 3); $sheet->setCellValue('B4', 4); $sheet->setCellValue('B5', 5); // Create formulas that Excel may serialize as shared formulas // When the writer outputs these, adjacent identical-pattern formulas // may become shared formulas $sheet->setCellValue('C1', '=A1+B1'); $sheet->setCellValue('C2', '=A2+B2'); $sheet->setCellValue('C3', '=A3+B3'); $sheet->setCellValue('C4', '=A4+B4'); $sheet->setCellValue('C5', '=A5+B5'); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); // Compare streaming vs SimpleXML $readerSimple = new Xlsx(); $readerSimple->setReadDataOnly(true); $simple = $readerSimple->load($file); $readerStream = new Xlsx2(); $readerStream->setReadDataOnly(true); $stream = $readerStream->load($file); for ($row = 1; $row <= 5; ++$row) { $coord = "C{$row}"; self::assertEquals( $simple->getActiveSheet()->getCell($coord)->getValue(), $stream->getActiveSheet()->getCell($coord)->getValue(), "Shared formula cell {$coord} mismatch" ); self::assertSame( DataType::TYPE_FORMULA, $stream->getActiveSheet()->getCell($coord)->getDataType(), "Cell {$coord} should be a formula" ); } $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testStreamingReadDataOnlyFalseWithFormulas(): void { // Test the readDataOnly=false path which applies style indices // and handles quotePrefix for formula cells $reader = new Xlsx2(); // readDataOnly defaults to false $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); // Formula cells should have their formulas preserved self::assertSame('=A1+A2', $sheet->getCell('D1')->getValue()); self::assertSame(DataType::TYPE_FORMULA, $sheet->getCell('D1')->getDataType()); // Style indices should be set (not -1 / uninitialized) self::assertGreaterThanOrEqual(0, $sheet->getCell('D1')->getXfIndex()); self::assertGreaterThanOrEqual(0, $sheet->getCell('A1')->getXfIndex()); // Verify all cells that exist have valid xf indices foreach ($sheet->getRowIterator() as $row) { foreach ($row->getCellIterator() as $cell) { self::assertGreaterThanOrEqual( 0, $cell->getXfIndex(), "Cell {$cell->getCoordinate()} has invalid xf index" ); } } $spreadsheet->disconnectWorksheets(); } public function testStreamingEmptyValueCells(): void { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // Cell with a value $sheet->setCellValue('A1', 'has value'); // Cells left completely empty (no value set) // A2, A3 etc are never touched // Null-ish value $sheet->setCellValue('B1', null); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); $readerSimple = new Xlsx(); $readerSimple->setReadDataOnly(true); $simple = $readerSimple->load($file); $readerStream = new Xlsx2(); $readerStream->setReadDataOnly(true); $stream = $readerStream->load($file); self::assertSame( $simple->getActiveSheet()->getCell('A1')->getValue(), $stream->getActiveSheet()->getCell('A1')->getValue() ); $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testStreamingCalculatedValuePreserved(): void { // Test that calculated values are preserved for formula cells $reader = new Xlsx2(); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); // D1 = =A1+A2 should have a calculated value of 42 + 3.14 = 45.14 $cell = $sheet->getCell('D1'); self::assertSame('=A1+A2', $cell->getValue()); $calcValue = $cell->getOldCalculatedValue(); if ($calcValue !== null) { self::assertEqualsWithDelta(45.14, $calcValue, 0.001); } // D2 = =SUM(A1:A4) should have a calculated value of 42 + 3.14 + 0 + (-100) = -54.86 $cell2 = $sheet->getCell('D2'); self::assertSame('=SUM(A1:A4)', $cell2->getValue()); $calcValue2 = $cell2->getOldCalculatedValue(); if ($calcValue2 !== null) { self::assertEqualsWithDelta(-54.86, $calcValue2, 0.001); } $spreadsheet->disconnectWorksheets(); } public function testStreamingStringFormula(): void { // Test TYPE_STRING2 (str) formula - a formula that returns a string // D3 in setUp is =B1&" "&B2 which returns "Hello World" (str type) $readerSimple = new Xlsx(); $simple = $readerSimple->load($this->tempFile); $readerStream = new Xlsx2(); $stream = $readerStream->load($this->tempFile); // String formula should match self::assertEquals( $simple->getActiveSheet()->getCell('D3')->getValue(), $stream->getActiveSheet()->getCell('D3')->getValue(), 'String formula value mismatch' ); self::assertEquals( $simple->getActiveSheet()->getCell('D3')->getOldCalculatedValue(), $stream->getActiveSheet()->getCell('D3')->getOldCalculatedValue(), 'String formula calculated value mismatch' ); $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testStreamingRealSharedFormulae(): void { // Uses a real XLSX file with shared formulas (t="shared" attribute), // including boolean formulas like =A1>3 $filename = 'tests/data/Reader/XLSX/sharedformulae.xlsx'; $readerSimple = new Xlsx(); $simple = $readerSimple->load($filename); $sheetSimple = $simple->getActiveSheet(); $readerStream = new Xlsx2(); $stream = $readerStream->load($filename); $sheetStream = $stream->getActiveSheet(); // Verify formulas match (raw values, not calculated) $expectedFormulas = [ ['B1', '=A1+1'], ['B2', '=A2+1'], ['B3', '=A3+1'], ['B4', '=A4+1'], ['B5', '=A5+1'], ['C1', '=A1>3'], ['C2', '=A2>3'], ['C3', '=A3>3'], ['C4', '=A4>3'], ['C5', '=A5>3'], ['D1', '="x"&A1'], ['D2', '="x"&A2'], ['D3', '="x"&A3'], ['D4', '="x"&A4'], ['D5', '="x"&A5'], ]; foreach ($expectedFormulas as [$coord, $formula]) { self::assertSame( $formula, $sheetStream->getCell($coord)->getValue(), "Shared formula mismatch at {$coord}" ); self::assertSame( $sheetSimple->getCell($coord)->getValue(), $sheetStream->getCell($coord)->getValue(), "Streaming vs SimpleXML formula mismatch at {$coord}" ); } // Verify formula attributes BEFORE toArray() (which triggers calculation and resets them) // B2 (numeric shared formula) and D2 (string shared formula) are dependents with attributes $b2Attrs = $sheetStream->getCell('B2')->getFormulaAttributes(); self::assertNotEmpty($b2Attrs, 'B2 should have formula attributes'); self::assertSame('shared', $b2Attrs['t'] ?? null); self::assertArrayHasKey('ref', $b2Attrs); $d2Attrs = $sheetStream->getCell('D2')->getFormulaAttributes(); self::assertNotEmpty($d2Attrs, 'D2 should have formula attributes'); self::assertSame('shared', $d2Attrs['t'] ?? null); self::assertArrayHasKey('ref', $d2Attrs); // Master cells (row 1) should NOT have formula attributes (matches SimpleXML behavior) self::assertNull($sheetStream->getCell('B1')->getFormulaAttributes()); self::assertNull($sheetStream->getCell('D1')->getFormulaAttributes()); // Verify calculated values match $expectedCalcValues = [ [1, 2, false, 'x1'], [2, 3, false, 'x2'], [3, 4, false, 'x3'], [4, 5, true, 'x4'], [5, 6, true, 'x5'], ]; self::assertSame($expectedCalcValues, $sheetStream->toArray(null, true, false)); $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testStreamingFullComparisonWithStyles(): void { // Comprehensive comparison of streaming vs SimpleXML with styles enabled $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // Mix of data types $sheet->setCellValue('A1', 42); $sheet->setCellValue('A2', 'text'); $sheet->getCell('A3')->setValueExplicit(true, DataType::TYPE_BOOL); $sheet->setCellValue('A4', '=A1*2'); $sheet->getCell('A5')->setValueExplicit('#DIV/0!', DataType::TYPE_ERROR); // Apply styles $sheet->getStyle('A1')->getFont()->setBold(true); $sheet->getStyle('A2')->getFont()->setItalic(true); $sheet->getStyle('A1')->getNumberFormat()->setFormatCode('#,##0'); $file = $this->createTempFile(); $writer = new XlsxWriter($spreadsheet); $writer->save($file); $spreadsheet->disconnectWorksheets(); $readerSimple = new Xlsx(); $simple = $readerSimple->load($file); $sheetSimple = $simple->getActiveSheet(); $readerStream = new Xlsx2(); $stream = $readerStream->load($file); $sheetStream = $stream->getActiveSheet(); foreach ($sheetSimple->getRowIterator() as $row) { foreach ($row->getCellIterator() as $cell) { $coord = $cell->getCoordinate(); $simpleCell = $sheetSimple->getCell($coord); $streamCell = $sheetStream->getCell($coord); self::assertEquals( $simpleCell->getValue(), $streamCell->getValue(), "Value mismatch at {$coord}" ); self::assertSame( $simpleCell->getXfIndex(), $streamCell->getXfIndex(), "XfIndex mismatch at {$coord}" ); } } $simple->disconnectWorksheets(); $stream->disconnectWorksheets(); } public function testParseHugeOption(): void { $reader = new Xlsx2(); $reader->setParseHuge(true); $spreadsheet = $reader->load($this->tempFile); $sheet = $spreadsheet->getActiveSheet(); self::assertSame(42, $sheet->getCell('A1')->getValue()); $spreadsheet->disconnectWorksheets(); } }