From 816596183fbcf7dd7dd158630b15b7e0620d7f4b Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Tue, 29 Dec 2020 09:18:59 +1300 Subject: [PATCH 01/26] Add full support for PHP 8.0 Unfortunately due to PHPUnit 8.5 dependency this also drops support for PHP 7.1 --- .gitignore | 1 + .travis.yml | 8 ++++---- README.md | 2 +- UPGRADE-3.0.md | 2 +- composer.json | 6 +++--- docs/_pages/getting-started.md | 2 +- phpunit.xml | 2 +- tests/Spout/Common/Entity/RowTest.php | 2 +- tests/Spout/Common/Helper/FileSystemHelperTest.php | 2 +- tests/Spout/Common/Manager/OptionsManagerTest.php | 2 +- tests/Spout/Reader/ODS/ReaderTest.php | 8 ++++++++ .../Reader/XLSX/Manager/SharedStringsManagerTest.php | 4 ++-- tests/Spout/Reader/XLSX/ReaderTest.php | 4 ++++ tests/Spout/Writer/CSV/WriterTest.php | 4 ++-- tests/Spout/Writer/Common/Entity/SheetTest.php | 2 +- .../Spout/Writer/Common/Manager/Style/StyleMergerTest.php | 2 +- .../Writer/Common/Manager/Style/StyleRegistryTest.php | 2 +- tests/Spout/Writer/ODS/SheetTest.php | 4 ++-- tests/Spout/Writer/ODS/WriterTest.php | 6 +++--- tests/Spout/Writer/ODS/WriterWithStyleTest.php | 2 +- tests/Spout/Writer/XLSX/SheetTest.php | 4 ++-- tests/Spout/Writer/XLSX/WriterTest.php | 6 +++--- tests/Spout/Writer/XLSX/WriterWithStyleTest.php | 2 +- 23 files changed, 46 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 048320b..6253664 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /vendor /composer.lock /.php_cs.cache +/.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index ac931ed..9babd77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,16 +5,16 @@ language: php matrix: include: - - php: 7.1 - env: WITH_CS=true - - php: 7.1 - env: WITH_PHPUNIT=true WITH_COVERAGE=true - php: 7.2 env: WITH_PHPUNIT=true - php: 7.3 env: WITH_PHPUNIT=true + - php: 7.3 + env: WITH_PHPUNIT=true WITH_COVERAGE=true - php: 7.4 env: WITH_PHPUNIT=true + - php: 8.0 + env: WITH_PHPUNIT=true cache: diff --git a/README.md b/README.md index 517f5a4..a9a0a9c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Full documentation can be found at [https://opensource.box.com/spout/](https://o ## Requirements -* PHP version 7.1 or higher +* PHP version 7.2 or higher * PHP extension `php_zip` enabled * PHP extension `php_xmlreader` enabled diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 3952443..9b2e370 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -11,7 +11,7 @@ With the 3.0 version, this is now possible: each cell can have its own style. Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value. -Finally, **_Spout 3.0 only supports PHP 7.1 and above_**, as other PHP versions are no longer supported by the community. +Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community. Reader changes -------------- diff --git a/composer.json b/composer.json index 8943469..4d9642b 100644 --- a/composer.json +++ b/composer.json @@ -12,12 +12,12 @@ } ], "require": { - "php": ">=7.1.0", + "php": ">=7.2.0", "ext-zip": "*", "ext-xmlreader" : "*" }, "require-dev": { - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^8", "friendsofphp/php-cs-fixer": "^2" }, "suggest": { @@ -36,7 +36,7 @@ }, "config": { "platform": { - "php": "7.1" + "php": "7.2" } } } diff --git a/docs/_pages/getting-started.md b/docs/_pages/getting-started.md index 0004d4d..0e0b336 100755 --- a/docs/_pages/getting-started.md +++ b/docs/_pages/getting-started.md @@ -10,7 +10,7 @@ This guide will help you install {{ site.spout_html }} and teach you how to use ## Requirements -* PHP version 7.1 or higher +* PHP version 7.2 or higher * PHP extension `ext-zip` enabled * PHP extension `ext-xmlreader` enabled diff --git a/phpunit.xml b/phpunit.xml index 4c56189..4e410e2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ setStyle($this->getStyleMock()) ->setCells([]); - $this->assertInternalType('object', $row); + $this->assertInstanceOf(Row::class, $row); } } diff --git a/tests/Spout/Common/Helper/FileSystemHelperTest.php b/tests/Spout/Common/Helper/FileSystemHelperTest.php index f99ab30..7eb0183 100644 --- a/tests/Spout/Common/Helper/FileSystemHelperTest.php +++ b/tests/Spout/Common/Helper/FileSystemHelperTest.php @@ -16,7 +16,7 @@ class FileSystemHelperTest extends TestCase /** * @return void */ - public function setUp() + public function setUp() : void { $baseFolder = \sys_get_temp_dir(); $this->fileSystemHelper = new FileSystemHelper($baseFolder); diff --git a/tests/Spout/Common/Manager/OptionsManagerTest.php b/tests/Spout/Common/Manager/OptionsManagerTest.php index e2f0c5c..b1cac58 100644 --- a/tests/Spout/Common/Manager/OptionsManagerTest.php +++ b/tests/Spout/Common/Manager/OptionsManagerTest.php @@ -14,7 +14,7 @@ class OptionsManagerTest extends TestCase */ protected $optionsManager; - protected function setUp() + protected function setUp(): void { $this->optionsManager = new class() extends OptionsManagerAbstract { protected function getSupportedOptions() diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php index ce56e4b..cd30ae5 100644 --- a/tests/Spout/Reader/ODS/ReaderTest.php +++ b/tests/Spout/Reader/ODS/ReaderTest.php @@ -295,6 +295,10 @@ class ReaderTest extends TestCase */ public function testReadShouldBeProtectedAgainstBillionLaughsAttack() { + if (function_exists('xdebug_code_coverage_started') && xdebug_code_coverage_started()) { + $this->markTestSkipped('test not compatible with code coverage'); + } + $startTime = microtime(true); $fileName = 'attack_billion_laughs.ods'; @@ -318,6 +322,10 @@ class ReaderTest extends TestCase */ public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack() { + if (function_exists('xdebug_code_coverage_started') && xdebug_code_coverage_started()) { + $this->markTestSkipped('test not compatible with code coverage'); + } + $startTime = microtime(true); $fileName = 'attack_quadratic_blowup.ods'; diff --git a/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php b/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php index 2f85f34..22df444 100644 --- a/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php +++ b/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php @@ -25,7 +25,7 @@ class SharedStringsManagerTest extends TestCase /** * @return void */ - public function setUp() + public function setUp(): void { $this->sharedStringsManager = null; } @@ -33,7 +33,7 @@ class SharedStringsManagerTest extends TestCase /** * @return void */ - public function tearDown() + public function tearDown(): void { if ($this->sharedStringsManager !== null) { $this->sharedStringsManager->cleanup(); diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index 51a7b0e..7d30f97 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -539,6 +539,10 @@ class ReaderTest extends TestCase */ public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack() { + if (function_exists('xdebug_code_coverage_started') && xdebug_code_coverage_started()) { + $this->markTestSkipped('test not compatible with code coverage'); + } + $startTime = microtime(true); $this->getAllRowsForFile('attack_quadratic_blowup.xlsx'); diff --git a/tests/Spout/Writer/CSV/WriterTest.php b/tests/Spout/Writer/CSV/WriterTest.php index 0b3b238..deb3f22 100644 --- a/tests/Spout/Writer/CSV/WriterTest.php +++ b/tests/Spout/Writer/CSV/WriterTest.php @@ -101,7 +101,7 @@ class WriterTest extends TestCase ]); $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_utf8_bom.csv'); - $this->assertContains(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should contain a UTF-8 BOM'); + $this->assertStringStartsWith(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should contain a UTF-8 BOM'); } /** @@ -114,7 +114,7 @@ class WriterTest extends TestCase ]); $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_no_bom.csv', ',', '"', false); - $this->assertNotContains(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should not contain a UTF-8 BOM'); + $this->assertStringNotContainsString(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should not contain a UTF-8 BOM'); } /** diff --git a/tests/Spout/Writer/Common/Entity/SheetTest.php b/tests/Spout/Writer/Common/Entity/SheetTest.php index cb19d6f..3c50734 100644 --- a/tests/Spout/Writer/Common/Entity/SheetTest.php +++ b/tests/Spout/Writer/Common/Entity/SheetTest.php @@ -18,7 +18,7 @@ class SheetTest extends TestCase /** * @return void */ - public function setUp() + public function setUp(): void { $this->sheetManager = new SheetManager(new StringHelper()); } diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php index 46820c0..a91d13f 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php @@ -18,7 +18,7 @@ class StyleMergerTest extends TestCase /** * @return void */ - public function setUp() + public function setUp(): void { $this->styleMerger = new StyleMerger(); } diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php index 41cc571..20a764c 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php @@ -20,7 +20,7 @@ class StyleRegistryTest extends TestCase /** * @return void */ - public function setUp() + public function setUp(): void { $this->defaultStyle = (new StyleBuilder())->build(); $this->styleRegistry = new StyleRegistry($this->defaultStyle); diff --git a/tests/Spout/Writer/ODS/SheetTest.php b/tests/Spout/Writer/ODS/SheetTest.php index a964a59..3bd2193 100644 --- a/tests/Spout/Writer/ODS/SheetTest.php +++ b/tests/Spout/Writer/ODS/SheetTest.php @@ -89,7 +89,7 @@ class SheetTest extends TestCase $pathToContentFile = $resourcePath . '#content.xml'; $xmlContents = file_get_contents('zip://' . $pathToContentFile); - $this->assertContains(' table:display="false"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); + $this->assertStringContainsString(' table:display="false"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); } /** @@ -164,6 +164,6 @@ class SheetTest extends TestCase $pathToWorkbookFile = $resourcePath . '#content.xml'; $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); - $this->assertContains("table:name=\"$expectedName\"", $xmlContents, $message); + $this->assertStringContainsString("table:name=\"$expectedName\"", $xmlContents, $message); } } diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php index b30a8f6..31dcccf 100644 --- a/tests/Spout/Writer/ODS/WriterTest.php +++ b/tests/Spout/Writer/ODS/WriterTest.php @@ -547,7 +547,7 @@ class WriterTest extends TestCase $pathToContentFile = $resourcePath . '#content.xml'; $xmlContents = file_get_contents('zip://' . $pathToContentFile); - $this->assertContains($value, $xmlContents, $message); + $this->assertStringContainsString($value, $xmlContents, $message); } /** @@ -562,7 +562,7 @@ class WriterTest extends TestCase $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); $valueAsXmlString = "$value"; - $this->assertContains($valueAsXmlString, $sheetXmlAsString, $message); + $this->assertStringContainsString($valueAsXmlString, $sheetXmlAsString, $message); } /** @@ -577,7 +577,7 @@ class WriterTest extends TestCase $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); $valueAsXmlString = "$value"; - $this->assertNotContains($valueAsXmlString, $sheetXmlAsString, $message); + $this->assertStringNotContainsString($valueAsXmlString, $sheetXmlAsString, $message); } /** diff --git a/tests/Spout/Writer/ODS/WriterWithStyleTest.php b/tests/Spout/Writer/ODS/WriterWithStyleTest.php index dcdd364..669d438 100644 --- a/tests/Spout/Writer/ODS/WriterWithStyleTest.php +++ b/tests/Spout/Writer/ODS/WriterWithStyleTest.php @@ -30,7 +30,7 @@ class WriterWithStyleTest extends TestCase /** * @return void */ - public function setUp() + public function setUp(): void { $this->defaultStyle = (new StyleBuilder())->build(); } diff --git a/tests/Spout/Writer/XLSX/SheetTest.php b/tests/Spout/Writer/XLSX/SheetTest.php index cebd72e..a243161 100644 --- a/tests/Spout/Writer/XLSX/SheetTest.php +++ b/tests/Spout/Writer/XLSX/SheetTest.php @@ -89,7 +89,7 @@ class SheetTest extends TestCase $pathToWorkbookFile = $resourcePath . '#xl/workbook.xml'; $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); - $this->assertContains(' state="hidden"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); + $this->assertStringContainsString(' state="hidden"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); } /** @@ -166,6 +166,6 @@ class SheetTest extends TestCase $pathToWorkbookFile = $resourcePath . '#xl/workbook.xml'; $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); - $this->assertContains("assertStringContainsString("assertContains((string) $inlineData, $xmlContents, $message); + $this->assertStringContainsString((string) $inlineData, $xmlContents, $message); } /** @@ -624,7 +624,7 @@ class WriterTest extends TestCase $pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml'; $xmlContents = file_get_contents('zip://' . $pathToSheetFile); - $this->assertNotContains((string) $inlineData, $xmlContents, $message); + $this->assertStringNotContainsString((string) $inlineData, $xmlContents, $message); } /** @@ -639,6 +639,6 @@ class WriterTest extends TestCase $pathToSharedStringsFile = $resourcePath . '#xl/sharedStrings.xml'; $xmlContents = file_get_contents('zip://' . $pathToSharedStringsFile); - $this->assertContains($sharedString, $xmlContents, $message); + $this->assertStringContainsString($sharedString, $xmlContents, $message); } } diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index da0aec2..da063e8 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -32,7 +32,7 @@ class WriterWithStyleTest extends TestCase /** * @return void */ - public function setUp() + public function setUp(): void { $this->defaultStyle = (new StyleBuilder())->build(); } From c29d1877b8a5aef44c3a39930aa5b800db7cb35f Mon Sep 17 00:00:00 2001 From: jmsche Date: Fri, 29 May 2020 11:42:25 +0200 Subject: [PATCH 02/26] Fixed code style (probably due to recent php-cs-fixer version) --- src/Spout/Reader/ODS/SheetIterator.php | 4 ++-- src/Spout/Reader/XLSX/Manager/SharedStringsManager.php | 2 +- src/Spout/Reader/XLSX/RowIterator.php | 2 +- src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php | 2 +- src/Spout/Writer/ODS/Creator/ManagerFactory.php | 2 +- src/Spout/Writer/WriterAbstract.php | 2 +- src/Spout/Writer/XLSX/Creator/ManagerFactory.php | 2 +- tests/Spout/Reader/CSV/SpoutTestStream.php | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php index f35b852..c7b8cd9 100644 --- a/src/Spout/Reader/ODS/SheetIterator.php +++ b/src/Spout/Reader/ODS/SheetIterator.php @@ -28,13 +28,13 @@ class SheetIterator implements IteratorInterface const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name'; const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display'; - /** @var string $filePath Path of the file to be read */ + /** @var string Path of the file to be read */ protected $filePath; /** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */ protected $optionsManager; - /** @var InternalEntityFactory $entityFactory Factory to create entities */ + /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; /** @var XMLReader The XMLReader object that will help read sheet's XML data */ diff --git a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php index caaeed7..8850a69 100644 --- a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php +++ b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php @@ -43,7 +43,7 @@ class SharedStringsManager /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; - /** @var HelperFactory $helperFactory Factory to create helpers */ + /** @var HelperFactory Factory to create helpers */ protected $helperFactory; /** @var CachingStrategyFactory Factory to create shared strings caching strategies */ diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php index 4af4530..a54b8b1 100644 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ b/src/Spout/Reader/XLSX/RowIterator.php @@ -35,7 +35,7 @@ class RowIterator implements IteratorInterface /** @var string Path of the XLSX file being read */ protected $filePath; - /** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */ + /** @var string Path of the sheet data XML file as in [Content_Types].xml */ protected $sheetDataXMLFilePath; /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ diff --git a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php index b513555..653778c 100644 --- a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php +++ b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php @@ -44,7 +44,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; - /** @var ManagerFactoryInterface $managerFactory Factory to create managers */ + /** @var ManagerFactoryInterface Factory to create managers */ protected $managerFactory; /** @var Worksheet The worksheet where data will be written to */ diff --git a/src/Spout/Writer/ODS/Creator/ManagerFactory.php b/src/Spout/Writer/ODS/Creator/ManagerFactory.php index f38c500..a5b77ee 100644 --- a/src/Spout/Writer/ODS/Creator/ManagerFactory.php +++ b/src/Spout/Writer/ODS/Creator/ManagerFactory.php @@ -22,7 +22,7 @@ class ManagerFactory implements ManagerFactoryInterface /** @var InternalEntityFactory */ protected $entityFactory; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** diff --git a/src/Spout/Writer/WriterAbstract.php b/src/Spout/Writer/WriterAbstract.php index d96a628..bbaa735 100644 --- a/src/Spout/Writer/WriterAbstract.php +++ b/src/Spout/Writer/WriterAbstract.php @@ -33,7 +33,7 @@ abstract class WriterAbstract implements WriterInterface /** @var GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** @var OptionsManagerInterface Writer options manager */ diff --git a/src/Spout/Writer/XLSX/Creator/ManagerFactory.php b/src/Spout/Writer/XLSX/Creator/ManagerFactory.php index f27a2f2..aa3bcd5 100644 --- a/src/Spout/Writer/XLSX/Creator/ManagerFactory.php +++ b/src/Spout/Writer/XLSX/Creator/ManagerFactory.php @@ -24,7 +24,7 @@ class ManagerFactory implements ManagerFactoryInterface /** @var InternalEntityFactory */ protected $entityFactory; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** diff --git a/tests/Spout/Reader/CSV/SpoutTestStream.php b/tests/Spout/Reader/CSV/SpoutTestStream.php index 3bfd06e..d66e811 100644 --- a/tests/Spout/Reader/CSV/SpoutTestStream.php +++ b/tests/Spout/Reader/CSV/SpoutTestStream.php @@ -14,10 +14,10 @@ class SpoutTestStream const PATH_TO_CSV_RESOURCES = 'tests/resources/csv/'; const CSV_EXTENSION = '.csv'; - /** @var int $position */ + /** @var int */ private $position; - /** @var resource $fileHandle */ + /** @var resource */ private $fileHandle; /** From ad913f0100aa1d994ca10fc6ed13c491e0f2e582 Mon Sep 17 00:00:00 2001 From: Oded Arbel Date: Sun, 7 Feb 2021 11:29:01 +0200 Subject: [PATCH 03/26] write boolean value according to http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean instead of just "1" for true or "" for false; --- src/Spout/Writer/ODS/Manager/WorksheetManager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 4dfe9c8..0d6942e 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -197,7 +197,8 @@ class WorksheetManager implements WorksheetManagerInterface $data .= ''; } elseif ($cell->isBoolean()) { - $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cell->getValue() . '">'; + $value = $cell->getValue() ? 'true' : 'false'; + $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $value . '">'; $data .= '' . $cell->getValue() . ''; $data .= ''; } elseif ($cell->isNumeric()) { From 73347517f064b3ef03126db643b7d53bd40c19cf Mon Sep 17 00:00:00 2001 From: Oded Arbel Date: Mon, 8 Feb 2021 15:52:13 +0200 Subject: [PATCH 04/26] added comment with spec link, as requested --- src/Spout/Writer/ODS/Manager/WorksheetManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 0d6942e..169e4e6 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -197,7 +197,7 @@ class WorksheetManager implements WorksheetManagerInterface $data .= ''; } elseif ($cell->isBoolean()) { - $value = $cell->getValue() ? 'true' : 'false'; + $value = $cell->getValue() ? 'true' : 'false'; // boolean-value spec: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $value . '">'; $data .= '' . $cell->getValue() . ''; $data .= ''; From ed9322e30921070f469a66faa7f401aa9bb13de9 Mon Sep 17 00:00:00 2001 From: jmsche Date: Fri, 29 May 2020 11:43:51 +0200 Subject: [PATCH 05/26] Shorter (relevant) diff by php-cs-fixer for Travis CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9babd77..365ee59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,7 @@ before_script: script: - | if [[ "$WITH_CS" == "true" ]]; then - vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run + vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff fi - | if [[ "$WITH_PHPUNIT" == "true" ]]; then From 9ab0b10a0f9343bb149571a82f128abb62e4b19a Mon Sep 17 00:00:00 2001 From: jmsche Date: Fri, 29 May 2020 11:55:08 +0200 Subject: [PATCH 06/26] Contributing: added info about code style --- CONTRIBUTING.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fa35c5..5db0f6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,21 @@ This will add your changes on top of what's already in upstream, minimizing merg Make sure that all tests are passing before submitting a pull request. -### Step 8: Send the pull request +### Step 8: Fix code style + +Run the following command to check the code style of your changes: + +``` +vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff +``` + +This will print a diff of proposed code style changes. To apply these suggestions, run the following command: + +``` +vendor/bin/php-cs-fixer fix --config=.php_cs.dist +``` + +### Step 9: Send the pull request Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. From 0f20c99a7f8b8792840ac061c34f3450641a03cc Mon Sep 17 00:00:00 2001 From: Andrii Dembitskyi Date: Fri, 2 Oct 2020 21:28:30 +0300 Subject: [PATCH 07/26] Fix constant usage in example --- .../_pages/guides/4-symfony-stream-content-large-spreadsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_pages/guides/4-symfony-stream-content-large-spreadsheet.md b/docs/_pages/guides/4-symfony-stream-content-large-spreadsheet.md index fb203ae..165a4b3 100644 --- a/docs/_pages/guides/4-symfony-stream-content-large-spreadsheet.md +++ b/docs/_pages/guides/4-symfony-stream-content-large-spreadsheet.md @@ -94,7 +94,7 @@ class MyStreamController extends Controller $i++; // Flushing the buffer every N rows to stream echo'ed content. - if ($i % FLUSH_THRESHOLD === 0) { + if ($i % self::FLUSH_THRESHOLD === 0) { flush(); } } From df9d96366f775c2b059d96ec9ef66767d6599942 Mon Sep 17 00:00:00 2001 From: yiranzai Date: Wed, 27 May 2020 20:24:14 +0800 Subject: [PATCH 08/26] Fixed WriterAbstract::openToBrowser meet RFC6266 --- src/Spout/Writer/WriterAbstract.php | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Spout/Writer/WriterAbstract.php b/src/Spout/Writer/WriterAbstract.php index bbaa735..93f9891 100644 --- a/src/Spout/Writer/WriterAbstract.php +++ b/src/Spout/Writer/WriterAbstract.php @@ -112,7 +112,7 @@ abstract class WriterAbstract implements WriterInterface * @codeCoverageIgnore * {@inheritdoc} */ - public function openToBrowser($outputFileName) + public function openToBrowser($outputFileName, array $headers = []) { $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName); @@ -123,9 +123,26 @@ abstract class WriterAbstract implements WriterInterface // @see https://github.com/box/spout/issues/241 $this->globalFunctionsHelper->ob_end_clean(); - // Set headers + /* + * Set headers + * + * For newer browsers such as Firefox, Chrome, Opera, Safari, etc., they all support and use `filename*` + * specified by the new standard, even if they do not automatically decode filename; it does not matter; + * and for older versions of Internet Explorer, they are not recognized `filename*`, will automatically + * ignore it and use the old `filename` (the only minor flaw is that there must be an English suffix name). + * In this way, the multi-browser multi-language compatibility problem is perfectly solved, which does not + * require UA judgment and is more in line with the standard. + * + * @see https://github.com/box/spout/issues/745 + * @see https://tools.ietf.org/html/rfc6266 + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + */ $this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType); - $this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"'); + $this->globalFunctionsHelper->header( + 'Content-Disposition: attachment; ' . + 'filename="' . rawurldecode($this->outputFilePath) . '"; ' . + 'filename*=UTF-8\'\'' . rawurldecode($this->outputFilePath) + ); /* * When forcing the download of a file over SSL,IE8 and lower browsers fail @@ -137,6 +154,16 @@ abstract class WriterAbstract implements WriterInterface $this->globalFunctionsHelper->header('Cache-Control: max-age=0'); $this->globalFunctionsHelper->header('Pragma: public'); + /* + * Set custom Headers + * Sometimes need to output or cover more headers. + * + * @see https://github.com/box/spout/issues/745 + */ + foreach ($headers as $header){ + $this->globalFunctionsHelper->header($header); + } + $this->openWriter(); $this->isWriterOpened = true; From 03e1ce438a7c4099b192240c8dec7f28ae6098cc Mon Sep 17 00:00:00 2001 From: yiranzai Date: Wed, 27 May 2020 20:41:01 +0800 Subject: [PATCH 09/26] Fixed Code Style --- src/Spout/Writer/WriterAbstract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spout/Writer/WriterAbstract.php b/src/Spout/Writer/WriterAbstract.php index 93f9891..6c4191a 100644 --- a/src/Spout/Writer/WriterAbstract.php +++ b/src/Spout/Writer/WriterAbstract.php @@ -160,7 +160,7 @@ abstract class WriterAbstract implements WriterInterface * * @see https://github.com/box/spout/issues/745 */ - foreach ($headers as $header){ + foreach ($headers as $header) { $this->globalFunctionsHelper->header($header); } From 91f756be0b0bf78f79f93a36ffdc240a5bf4099d Mon Sep 17 00:00:00 2001 From: yiranzai Date: Thu, 18 Mar 2021 16:13:48 +0800 Subject: [PATCH 10/26] remove custom headers --- src/Spout/Writer/WriterAbstract.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Spout/Writer/WriterAbstract.php b/src/Spout/Writer/WriterAbstract.php index 6c4191a..36a583f 100644 --- a/src/Spout/Writer/WriterAbstract.php +++ b/src/Spout/Writer/WriterAbstract.php @@ -112,7 +112,7 @@ abstract class WriterAbstract implements WriterInterface * @codeCoverageIgnore * {@inheritdoc} */ - public function openToBrowser($outputFileName, array $headers = []) + public function openToBrowser($outputFileName) { $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName); @@ -154,16 +154,6 @@ abstract class WriterAbstract implements WriterInterface $this->globalFunctionsHelper->header('Cache-Control: max-age=0'); $this->globalFunctionsHelper->header('Pragma: public'); - /* - * Set custom Headers - * Sometimes need to output or cover more headers. - * - * @see https://github.com/box/spout/issues/745 - */ - foreach ($headers as $header) { - $this->globalFunctionsHelper->header($header); - } - $this->openWriter(); $this->isWriterOpened = true; From 57b6e87a65cf4a8b73565139f53abb0c74e30672 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Tue, 25 Aug 2020 14:14:37 +0200 Subject: [PATCH 11/26] Begin optimize xlsx write --- src/Spout/Common/Entity/Style/EmptyStyle.php | 7 +++++++ .../Writer/Common/Manager/Style/StyleRegistry.php | 13 ++++++++++++- src/Spout/Writer/ODS/Manager/WorksheetManager.php | 11 ++++++++--- src/Spout/Writer/XLSX/Manager/WorksheetManager.php | 12 ++++++++---- 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 src/Spout/Common/Entity/Style/EmptyStyle.php diff --git a/src/Spout/Common/Entity/Style/EmptyStyle.php b/src/Spout/Common/Entity/Style/EmptyStyle.php new file mode 100644 index 0000000..56fa034 --- /dev/null +++ b/src/Spout/Common/Entity/Style/EmptyStyle.php @@ -0,0 +1,7 @@ +serialize($style); - if (!$this->hasStyleAlreadyBeenRegistered($style)) { + if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) { $nextStyleId = \count($this->serializedStyleToStyleIdMappingTable); $style->setId($nextStyleId); @@ -57,6 +57,17 @@ class StyleRegistry { $serializedStyle = $this->serialize($style); + return $this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle); + } + + /** + * Returns whether the serialized style has already been registered. + * + * @param string $serializedStyle The serialized style + * @return bool + */ + protected function hasSerializedStyleAlreadyBeenRegistered(string $serializedStyle) + { // Using isset here because it is way faster than array_key_exists... return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); } diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 169e4e6..970cace 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -4,6 +4,7 @@ namespace Box\Spout\Writer\ODS\Manager; use Box\Spout\Common\Entity\Cell; use Box\Spout\Common\Entity\Row; +use Box\Spout\Common\Entity\Style\EmptyStyle; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; @@ -157,9 +158,13 @@ class WorksheetManager implements WorksheetManagerInterface */ private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $currentCellIndex, $nextCellIndex) { - // Apply row and extra styles - $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); - $cell->setStyle($mergedCellAndRowStyle); + if ($cell->getStyle() instanceof EmptyStyle) { + $cell->setStyle($rowStyle); + } else { + $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); + $cell->setStyle($mergedCellAndRowStyle); + } + $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); $registeredStyle = $this->styleManager->registerStyle($newCellStyle); diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 741d0aa..e49c1d8 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -4,6 +4,7 @@ namespace Box\Spout\Writer\XLSX\Manager; use Box\Spout\Common\Entity\Cell; use Box\Spout\Common\Entity\Row; +use Box\Spout\Common\Entity\Style\EmptyStyle; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; @@ -185,11 +186,14 @@ EOD; */ private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $rowIndexOneBased, $columnIndexZeroBased) { - // Apply row and extra styles - $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); - $cell->setStyle($mergedCellAndRowStyle); - $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + if ($cell->getStyle() instanceof EmptyStyle) { + $cell->setStyle($rowStyle); + } else { + $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); + $cell->setStyle($mergedCellAndRowStyle); + } + $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); $registeredStyle = $this->styleManager->registerStyle($newCellStyle); return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId()); From a58b340835be5f3469cca9b7060162159f8aea7c Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Tue, 25 Aug 2020 16:38:44 +0200 Subject: [PATCH 12/26] Empty style on cell --- src/Spout/Common/Entity/Cell.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Spout/Common/Entity/Cell.php b/src/Spout/Common/Entity/Cell.php index c1de389..6ffda46 100644 --- a/src/Spout/Common/Entity/Cell.php +++ b/src/Spout/Common/Entity/Cell.php @@ -2,6 +2,7 @@ namespace Box\Spout\Common\Entity; +use Box\Spout\Common\Entity\Style\EmptyStyle; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Helper\CellTypeHelper; @@ -104,7 +105,7 @@ class Cell */ public function setStyle($style) { - $this->style = $style ?: new Style(); + $this->style = $style ?: new EmptyStyle(); } /** From 197fb9987af2b9c7dee84d63103c3505996a3c0a Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Fri, 28 Aug 2020 12:39:55 +0200 Subject: [PATCH 13/26] Register style can be skipped when already registered --- src/Spout/Common/Entity/Style/Style.php | 22 ++++++++++++++++ .../Common/Manager/Style/StyleManager.php | 18 ++++++------- .../Manager/Style/StyleManagerInterface.php | 4 +-- .../Common/Manager/Style/StyleRegistry.php | 5 ++-- .../ODS/Manager/Style/StyleRegistry.php | 4 +++ .../Writer/ODS/Manager/WorksheetManager.php | 16 +++++++++--- .../XLSX/Manager/Style/StyleRegistry.php | 4 +++ .../Writer/XLSX/Manager/WorksheetManager.php | 16 +++++++++--- .../Common/Manager/Style/StyleManagerTest.php | 26 ++++++++++++------- 9 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index 7e989a4..bda0eb7 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -84,6 +84,9 @@ class Style /** @var bool */ private $hasSetFormat = false; + /** @var bool */ + private $isRegistered = false; + /** * @return int|null */ @@ -463,4 +466,23 @@ class Style { return $this->hasSetFormat; } + + public function setRegistered(bool $isRegistered = true) : void + { + $this->isRegistered = $isRegistered; + } + + /** + * @return bool + */ + public function isRegistered() : bool + { + return $this->isRegistered; + } + + public function register(int $id) : void + { + $this->setId($id); + $this->isRegistered = true; + } } diff --git a/src/Spout/Writer/Common/Manager/Style/StyleManager.php b/src/Spout/Writer/Common/Manager/Style/StyleManager.php index 77eec73..bf87958 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleManager.php @@ -50,13 +50,11 @@ class StyleManager implements StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return Style + * @return Style|null The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell) + public function applyExtraStylesIfNeeded(Cell $cell) : ?Style { - $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($cell); - - return $updatedStyle; + return $this->applyWrapTextIfCellContainsNewLine($cell); } /** @@ -69,21 +67,23 @@ class StyleManager implements StyleManagerInterface * on the Windows version of Excel... * * @param Cell $cell The cell the style should be applied to - * @return \Box\Spout\Common\Entity\Style\Style The eventually updated style + * @return Style|null The eventually updated style */ - protected function applyWrapTextIfCellContainsNewLine(Cell $cell) + protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : ?Style { $cellStyle = $cell->getStyle(); // if the "wrap text" option is already set, no-op if ($cellStyle->hasSetWrapText()) { - return $cellStyle; + return null; } if ($cell->isString() && \strpos($cell->getValue(), "\n") !== false) { $cellStyle->setShouldWrapText(); + + return $cellStyle; } - return $cellStyle; + return null; } } diff --git a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php index 312588e..9929eeb 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php @@ -24,7 +24,7 @@ interface StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return Style The updated style + * @return Style|null The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell); + public function applyExtraStylesIfNeeded(Cell $cell) : ?Style; } diff --git a/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php b/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php index 4d8062c..ea7b7a4 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php @@ -38,7 +38,7 @@ class StyleRegistry if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) { $nextStyleId = \count($this->serializedStyleToStyleIdMappingTable); - $style->setId($nextStyleId); + $style->register($nextStyleId); $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; $this->styleIdToStyleMappingTable[$nextStyleId] = $style; @@ -112,9 +112,10 @@ class StyleRegistry */ public function serialize(Style $style) { - // In order to be able to properly compare style, set static ID value + // In order to be able to properly compare style, set static ID value and reset registration $currentId = $style->getId(); $style->setId(0); + $style->setRegistered(false); $serializedStyle = \serialize($style); diff --git a/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php b/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php index 6c580d4..42484f2 100644 --- a/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php +++ b/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php @@ -22,6 +22,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry */ public function registerStyle(Style $style) { + if ($style->isRegistered()) { + return $style; + } + $registeredStyle = parent::registerStyle($style); $this->usedFontsSet[$style->getFontName()] = true; diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 970cace..b7c4e6e 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -156,18 +156,26 @@ class WorksheetManager implements WorksheetManagerInterface * @throws InvalidArgumentException If a cell value's type is not supported * @return string */ - private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $currentCellIndex, $nextCellIndex) + private function applyStyleAndGetCellXML(Cell $cell, Style &$rowStyle, $currentCellIndex, $nextCellIndex) { if ($cell->getStyle() instanceof EmptyStyle) { $cell->setStyle($rowStyle); + + $extraStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + + if ($extraStyle) { + $registeredStyle = $this->styleManager->registerStyle($extraStyle); + } else { + $registeredStyle = $rowStyle = $this->styleManager->registerStyle($rowStyle); + } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); $cell->setStyle($mergedCellAndRowStyle); + + $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell) ?: $mergedCellAndRowStyle; + $registeredStyle = $this->styleManager->registerStyle($newCellStyle); } - $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - - $registeredStyle = $this->styleManager->registerStyle($newCellStyle); $styleIndex = $registeredStyle->getId() + 1; // 1-based $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex); diff --git a/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php b/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php index ace607c..14eb986 100644 --- a/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php +++ b/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php @@ -119,6 +119,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry */ public function registerStyle(Style $style) { + if ($style->isRegistered()) { + return $style; + } + $registeredStyle = parent::registerStyle($style); $this->registerFill($registeredStyle); $this->registerFormat($registeredStyle); diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index e49c1d8..ae1ecbb 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -184,17 +184,25 @@ EOD; * @throws InvalidArgumentException If the given value cannot be processed * @return string */ - private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $rowIndexOneBased, $columnIndexZeroBased) + private function applyStyleAndGetCellXML(Cell $cell, Style &$rowStyle, $rowIndexOneBased, $columnIndexZeroBased) { if ($cell->getStyle() instanceof EmptyStyle) { $cell->setStyle($rowStyle); + + $extraStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + + if ($extraStyle) { + $registeredStyle = $this->styleManager->registerStyle($extraStyle); + } else { + $registeredStyle = $rowStyle = $this->styleManager->registerStyle($rowStyle); + } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); $cell->setStyle($mergedCellAndRowStyle); - } - $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - $registeredStyle = $this->styleManager->registerStyle($newCellStyle); + $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell) ?: $mergedCellAndRowStyle; + $registeredStyle = $this->styleManager->registerStyle($newCellStyle); + } return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId()); } diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php index 82292bb..d326f9d 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php @@ -14,7 +14,7 @@ class StyleManagerTest extends TestCase /** * @return StyleManager */ - private function getStyleManager() + private function getStyleManager() : StyleManager { $style = (new StyleBuilder())->build(); $styleRegistry = new StyleRegistry($style); @@ -22,10 +22,7 @@ class StyleManagerTest extends TestCase return new StyleManager($styleRegistry); } - /** - * @return void - */ - public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine() + public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine() : void { $style = (new StyleBuilder())->build(); $this->assertFalse($style->shouldWrapText()); @@ -33,13 +30,22 @@ class StyleManagerTest extends TestCase $styleManager = $this->getStyleManager(); $updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); + $this->assertNotNull($updatedStyle); $this->assertTrue($updatedStyle->shouldWrapText()); } - /** - * @return void - */ - public function testApplyExtraStylesIfNeededShouldDoNothingIfWrapTextAlreadyApplied() + public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextNotNeeded() : void + { + $style = (new StyleBuilder())->build(); + $this->assertFalse($style->shouldWrapText()); + + $styleManager = $this->getStyleManager(); + $updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell('oneline', $style)); + + $this->assertNull($updatedStyle); + } + + public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextAlreadyApplied() : void { $style = (new StyleBuilder())->setShouldWrapText()->build(); $this->assertTrue($style->shouldWrapText()); @@ -47,6 +53,6 @@ class StyleManagerTest extends TestCase $styleManager = $this->getStyleManager(); $updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); - $this->assertTrue($updatedStyle->shouldWrapText()); + $this->assertNull($updatedStyle); } } From 11d91e1740f9361ba45dc2964bceec39b43d9c28 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Mon, 22 Feb 2021 20:04:35 +0100 Subject: [PATCH 14/26] Code review changes --- src/Spout/Common/Entity/Cell.php | 3 +- src/Spout/Common/Entity/Style/EmptyStyle.php | 7 ----- src/Spout/Common/Entity/Style/Style.php | 31 ++++++++++++++++--- .../Common/Manager/Style/ManagedStyle.php | 27 ++++++++++++++++ .../Common/Manager/Style/StyleManager.php | 18 +++++------ .../Manager/Style/StyleManagerInterface.php | 4 +-- .../Common/Manager/Style/StyleRegistry.php | 3 +- .../Writer/ODS/Manager/WorksheetManager.php | 17 ++++++---- .../Writer/XLSX/Manager/WorksheetManager.php | 18 +++++++---- .../Common/Manager/OptionsManagerTest.php | 2 +- .../XLSX/Manager/SharedStringsManagerTest.php | 4 +-- tests/Spout/Writer/CSV/WriterPerfTest.php | 2 +- .../Spout/Writer/Common/Entity/SheetTest.php | 2 +- .../Common/Manager/Style/StyleManagerTest.php | 14 ++++----- .../Common/Manager/Style/StyleMergerTest.php | 2 +- .../Manager/Style/StyleRegistryTest.php | 2 +- tests/Spout/Writer/ODS/WriterPerfTest.php | 2 +- .../Spout/Writer/ODS/WriterWithStyleTest.php | 2 +- tests/Spout/Writer/XLSX/WriterPerfTest.php | 2 +- .../Spout/Writer/XLSX/WriterWithStyleTest.php | 2 +- 20 files changed, 105 insertions(+), 59 deletions(-) delete mode 100644 src/Spout/Common/Entity/Style/EmptyStyle.php create mode 100644 src/Spout/Writer/Common/Manager/Style/ManagedStyle.php diff --git a/src/Spout/Common/Entity/Cell.php b/src/Spout/Common/Entity/Cell.php index 6ffda46..c1de389 100644 --- a/src/Spout/Common/Entity/Cell.php +++ b/src/Spout/Common/Entity/Cell.php @@ -2,7 +2,6 @@ namespace Box\Spout\Common\Entity; -use Box\Spout\Common\Entity\Style\EmptyStyle; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Helper\CellTypeHelper; @@ -105,7 +104,7 @@ class Cell */ public function setStyle($style) { - $this->style = $style ?: new EmptyStyle(); + $this->style = $style ?: new Style(); } /** diff --git a/src/Spout/Common/Entity/Style/EmptyStyle.php b/src/Spout/Common/Entity/Style/EmptyStyle.php deleted file mode 100644 index 56fa034..0000000 --- a/src/Spout/Common/Entity/Style/EmptyStyle.php +++ /dev/null @@ -1,7 +0,0 @@ -shouldApplyBorder = true; $this->border = $border; + $this->isEmpty = false; return $this; } @@ -150,6 +154,7 @@ class Style $this->fontBold = true; $this->hasSetFontBold = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -178,6 +183,7 @@ class Style $this->fontItalic = true; $this->hasSetFontItalic = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -206,6 +212,7 @@ class Style $this->fontUnderline = true; $this->hasSetFontUnderline = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -234,6 +241,7 @@ class Style $this->fontStrikethrough = true; $this->hasSetFontStrikethrough = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -263,6 +271,7 @@ class Style $this->fontSize = $fontSize; $this->hasSetFontSize = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -294,6 +303,7 @@ class Style $this->fontColor = $fontColor; $this->hasSetFontColor = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -323,6 +333,7 @@ class Style $this->fontName = $fontName; $this->hasSetFontName = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -353,6 +364,7 @@ class Style $this->cellAlignment = $cellAlignment; $this->hasSetCellAlignment = true; $this->shouldApplyCellAlignment = true; + $this->isEmpty = false; return $this; } @@ -389,6 +401,7 @@ class Style { $this->shouldWrapText = $shouldWrap; $this->hasSetWrapText = true; + $this->isEmpty = false; return $this; } @@ -418,6 +431,7 @@ class Style { $this->hasSetBackgroundColor = true; $this->backgroundColor = $color; + $this->isEmpty = false; return $this; } @@ -447,6 +461,7 @@ class Style { $this->hasSetFormat = true; $this->format = $format; + $this->isEmpty = false; return $this; } @@ -467,11 +482,6 @@ class Style return $this->hasSetFormat; } - public function setRegistered(bool $isRegistered = true) : void - { - $this->isRegistered = $isRegistered; - } - /** * @return bool */ @@ -485,4 +495,15 @@ class Style $this->setId($id); $this->isRegistered = true; } + + public function unregister() : void + { + $this->setId(0); + $this->isRegistered = false; + } + + public function isEmpty() : bool + { + return $this->isEmpty; + } } diff --git a/src/Spout/Writer/Common/Manager/Style/ManagedStyle.php b/src/Spout/Writer/Common/Manager/Style/ManagedStyle.php new file mode 100644 index 0000000..0701ac3 --- /dev/null +++ b/src/Spout/Writer/Common/Manager/Style/ManagedStyle.php @@ -0,0 +1,27 @@ +style = $style; + $this->isUpdated = $isUpdated; + } + + public function getStyle() : Style + { + return $this->style; + } + + public function isUpdated() : bool + { + return $this->isUpdated; + } +} diff --git a/src/Spout/Writer/Common/Manager/Style/StyleManager.php b/src/Spout/Writer/Common/Manager/Style/StyleManager.php index bf87958..2f21763 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleManager.php @@ -50,9 +50,9 @@ class StyleManager implements StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return Style|null The eventually updated style + * @return ManagedStyle The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell) : ?Style + public function applyExtraStylesIfNeeded(Cell $cell) : ManagedStyle { return $this->applyWrapTextIfCellContainsNewLine($cell); } @@ -67,23 +67,19 @@ class StyleManager implements StyleManagerInterface * on the Windows version of Excel... * * @param Cell $cell The cell the style should be applied to - * @return Style|null The eventually updated style + * @return ManagedStyle The eventually updated style */ - protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : ?Style + protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : ManagedStyle { $cellStyle = $cell->getStyle(); // if the "wrap text" option is already set, no-op - if ($cellStyle->hasSetWrapText()) { - return null; - } - - if ($cell->isString() && \strpos($cell->getValue(), "\n") !== false) { + if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) { $cellStyle->setShouldWrapText(); - return $cellStyle; + return new ManagedStyle($cellStyle, true); } - return null; + return new ManagedStyle($cellStyle, false); } } diff --git a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php index 9929eeb..be77110 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php @@ -24,7 +24,7 @@ interface StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return Style|null The eventually updated style + * @return ManagedStyle|null The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell) : ?Style; + public function applyExtraStylesIfNeeded(Cell $cell) : ManagedStyle; } diff --git a/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php b/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php index ea7b7a4..dcfa267 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php @@ -114,8 +114,7 @@ class StyleRegistry { // In order to be able to properly compare style, set static ID value and reset registration $currentId = $style->getId(); - $style->setId(0); - $style->setRegistered(false); + $style->unregister(); $serializedStyle = \serialize($style); diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index b7c4e6e..894c82f 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -4,7 +4,6 @@ namespace Box\Spout\Writer\ODS\Manager; use Box\Spout\Common\Entity\Cell; use Box\Spout\Common\Entity\Row; -use Box\Spout\Common\Entity\Style\EmptyStyle; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; @@ -158,13 +157,13 @@ class WorksheetManager implements WorksheetManagerInterface */ private function applyStyleAndGetCellXML(Cell $cell, Style &$rowStyle, $currentCellIndex, $nextCellIndex) { - if ($cell->getStyle() instanceof EmptyStyle) { + if ($cell->getStyle()->isEmpty()) { $cell->setStyle($rowStyle); - $extraStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - if ($extraStyle) { - $registeredStyle = $this->styleManager->registerStyle($extraStyle); + if ($managedStyle->isUpdated()) { + $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); } else { $registeredStyle = $rowStyle = $this->styleManager->registerStyle($rowStyle); } @@ -172,7 +171,13 @@ class WorksheetManager implements WorksheetManagerInterface $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); $cell->setStyle($mergedCellAndRowStyle); - $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell) ?: $mergedCellAndRowStyle; + $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + if ($managedStyle->isUpdated()) { + $newCellStyle = $managedStyle->getStyle(); + } else { + $newCellStyle = $mergedCellAndRowStyle; + } + $registeredStyle = $this->styleManager->registerStyle($newCellStyle); } diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index ae1ecbb..d68d8f3 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -4,7 +4,6 @@ namespace Box\Spout\Writer\XLSX\Manager; use Box\Spout\Common\Entity\Cell; use Box\Spout\Common\Entity\Row; -use Box\Spout\Common\Entity\Style\EmptyStyle; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; @@ -186,13 +185,13 @@ EOD; */ private function applyStyleAndGetCellXML(Cell $cell, Style &$rowStyle, $rowIndexOneBased, $columnIndexZeroBased) { - if ($cell->getStyle() instanceof EmptyStyle) { + if ($cell->getStyle()->isEmpty()) { $cell->setStyle($rowStyle); - $extraStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - if ($extraStyle) { - $registeredStyle = $this->styleManager->registerStyle($extraStyle); + if ($managedStyle->isUpdated()) { + $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); } else { $registeredStyle = $rowStyle = $this->styleManager->registerStyle($rowStyle); } @@ -200,7 +199,14 @@ EOD; $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); $cell->setStyle($mergedCellAndRowStyle); - $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell) ?: $mergedCellAndRowStyle; + $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + + if ($managedStyle->isUpdated()) { + $newCellStyle = $managedStyle->getStyle(); + } else { + $newCellStyle = $mergedCellAndRowStyle; + } + $registeredStyle = $this->styleManager->registerStyle($newCellStyle); } diff --git a/tests/Spout/Common/Manager/OptionsManagerTest.php b/tests/Spout/Common/Manager/OptionsManagerTest.php index b1cac58..d5fe5d8 100644 --- a/tests/Spout/Common/Manager/OptionsManagerTest.php +++ b/tests/Spout/Common/Manager/OptionsManagerTest.php @@ -14,7 +14,7 @@ class OptionsManagerTest extends TestCase */ protected $optionsManager; - protected function setUp(): void + protected function setUp() : void { $this->optionsManager = new class() extends OptionsManagerAbstract { protected function getSupportedOptions() diff --git a/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php b/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php index 22df444..f344b35 100644 --- a/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php +++ b/tests/Spout/Reader/XLSX/Manager/SharedStringsManagerTest.php @@ -25,7 +25,7 @@ class SharedStringsManagerTest extends TestCase /** * @return void */ - public function setUp(): void + public function setUp() : void { $this->sharedStringsManager = null; } @@ -33,7 +33,7 @@ class SharedStringsManagerTest extends TestCase /** * @return void */ - public function tearDown(): void + public function tearDown() : void { if ($this->sharedStringsManager !== null) { $this->sharedStringsManager->cleanup(); diff --git a/tests/Spout/Writer/CSV/WriterPerfTest.php b/tests/Spout/Writer/CSV/WriterPerfTest.php index 4172bf5..cbfcae0 100644 --- a/tests/Spout/Writer/CSV/WriterPerfTest.php +++ b/tests/Spout/Writer/CSV/WriterPerfTest.php @@ -61,7 +61,7 @@ class WriterPerfTest extends TestCase */ private function getNumWrittenRows($resourcePath) { - $lineCountResult = `wc -l $resourcePath`; + $lineCountResult = shell_exec("wc -l $resourcePath"); return (int) $lineCountResult; } diff --git a/tests/Spout/Writer/Common/Entity/SheetTest.php b/tests/Spout/Writer/Common/Entity/SheetTest.php index 3c50734..e0cf8ee 100644 --- a/tests/Spout/Writer/Common/Entity/SheetTest.php +++ b/tests/Spout/Writer/Common/Entity/SheetTest.php @@ -18,7 +18,7 @@ class SheetTest extends TestCase /** * @return void */ - public function setUp(): void + public function setUp() : void { $this->sheetManager = new SheetManager(new StringHelper()); } diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php index d326f9d..daf4abb 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php @@ -28,10 +28,10 @@ class StyleManagerTest extends TestCase $this->assertFalse($style->shouldWrapText()); $styleManager = $this->getStyleManager(); - $updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); + $managedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); - $this->assertNotNull($updatedStyle); - $this->assertTrue($updatedStyle->shouldWrapText()); + $this->assertTrue($managedStyle->isUpdated()); + $this->assertTrue($managedStyle->getStyle()->shouldWrapText()); } public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextNotNeeded() : void @@ -40,9 +40,9 @@ class StyleManagerTest extends TestCase $this->assertFalse($style->shouldWrapText()); $styleManager = $this->getStyleManager(); - $updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell('oneline', $style)); + $managedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell('oneline', $style)); - $this->assertNull($updatedStyle); + $this->assertFalse($managedStyle->isUpdated()); } public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextAlreadyApplied() : void @@ -51,8 +51,8 @@ class StyleManagerTest extends TestCase $this->assertTrue($style->shouldWrapText()); $styleManager = $this->getStyleManager(); - $updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); + $managedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); - $this->assertNull($updatedStyle); + $this->assertFalse($managedStyle->isUpdated()); } } diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php index a91d13f..b7c1607 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php @@ -18,7 +18,7 @@ class StyleMergerTest extends TestCase /** * @return void */ - public function setUp(): void + public function setUp() : void { $this->styleMerger = new StyleMerger(); } diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php index 20a764c..ac78c9e 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleRegistryTest.php @@ -20,7 +20,7 @@ class StyleRegistryTest extends TestCase /** * @return void */ - public function setUp(): void + public function setUp() : void { $this->defaultStyle = (new StyleBuilder())->build(); $this->styleRegistry = new StyleRegistry($this->defaultStyle); diff --git a/tests/Spout/Writer/ODS/WriterPerfTest.php b/tests/Spout/Writer/ODS/WriterPerfTest.php index 40c4ee0..5917b49 100644 --- a/tests/Spout/Writer/ODS/WriterPerfTest.php +++ b/tests/Spout/Writer/ODS/WriterPerfTest.php @@ -88,7 +88,7 @@ class WriterPerfTest extends TestCase copy($pathToContentXmlFile, $tmpFile); // Get the last 200 characters - $lastCharacters = `tail -c 200 $tmpFile`; + $lastCharacters = shell_exec("tail -c 200 $tmpFile"); // remove the temporary file unlink($tmpFile); diff --git a/tests/Spout/Writer/ODS/WriterWithStyleTest.php b/tests/Spout/Writer/ODS/WriterWithStyleTest.php index 669d438..75177da 100644 --- a/tests/Spout/Writer/ODS/WriterWithStyleTest.php +++ b/tests/Spout/Writer/ODS/WriterWithStyleTest.php @@ -30,7 +30,7 @@ class WriterWithStyleTest extends TestCase /** * @return void */ - public function setUp(): void + public function setUp() : void { $this->defaultStyle = (new StyleBuilder())->build(); } diff --git a/tests/Spout/Writer/XLSX/WriterPerfTest.php b/tests/Spout/Writer/XLSX/WriterPerfTest.php index f43381f..66ff337 100644 --- a/tests/Spout/Writer/XLSX/WriterPerfTest.php +++ b/tests/Spout/Writer/XLSX/WriterPerfTest.php @@ -135,7 +135,7 @@ class WriterPerfTest extends TestCase copy($filePath, $tmpFile); // Get the last 200 characters - $lastCharacters = `tail -c $numCharacters $tmpFile`; + $lastCharacters = shell_exec("tail -c $numCharacters $tmpFile"); // remove the temporary file unlink($tmpFile); diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index da063e8..2571df4 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -32,7 +32,7 @@ class WriterWithStyleTest extends TestCase /** * @return void */ - public function setUp(): void + public function setUp() : void { $this->defaultStyle = (new StyleBuilder())->build(); } From c6f596c77697adc3bd83b8f85b41c92950ab02eb Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Fri, 5 Mar 2021 14:12:20 +0100 Subject: [PATCH 15/26] New code review fixs --- src/Spout/Common/Entity/Style/Style.php | 4 ++-- .../Manager/Style/StyleManagerInterface.php | 2 +- .../Common/Manager/Style/StyleRegistry.php | 19 +++---------------- .../Writer/ODS/Manager/WorksheetManager.php | 3 ++- .../Writer/XLSX/Manager/WorksheetManager.php | 3 ++- 5 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index f4beb97..2bacc7a 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -490,13 +490,13 @@ class Style return $this->isRegistered; } - public function register(int $id) : void + public function markAsRegistered(?int $id) : void { $this->setId($id); $this->isRegistered = true; } - public function unregister() : void + public function unmarkAsRegistered() : void { $this->setId(0); $this->isRegistered = false; diff --git a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php index be77110..6d01bd5 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php @@ -24,7 +24,7 @@ interface StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return ManagedStyle|null The eventually updated style + * @return ManagedStyle The eventually updated style */ public function applyExtraStylesIfNeeded(Cell $cell) : ManagedStyle; } diff --git a/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php b/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php index dcfa267..6b439a7 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php @@ -38,7 +38,7 @@ class StyleRegistry if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) { $nextStyleId = \count($this->serializedStyleToStyleIdMappingTable); - $style->register($nextStyleId); + $style->markAsRegistered($nextStyleId); $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; $this->styleIdToStyleMappingTable[$nextStyleId] = $style; @@ -47,19 +47,6 @@ class StyleRegistry return $this->getStyleFromSerializedStyle($serializedStyle); } - /** - * Returns whether the given style has already been registered. - * - * @param Style $style - * @return bool - */ - protected function hasStyleAlreadyBeenRegistered(Style $style) - { - $serializedStyle = $this->serialize($style); - - return $this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle); - } - /** * Returns whether the serialized style has already been registered. * @@ -114,11 +101,11 @@ class StyleRegistry { // In order to be able to properly compare style, set static ID value and reset registration $currentId = $style->getId(); - $style->unregister(); + $style->unmarkAsRegistered(); $serializedStyle = \serialize($style); - $style->setId($currentId); + $style->markAsRegistered($currentId); return $serializedStyle; } diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 894c82f..1997e9e 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -165,7 +165,8 @@ class WorksheetManager implements WorksheetManagerInterface if ($managedStyle->isUpdated()) { $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); } else { - $registeredStyle = $rowStyle = $this->styleManager->registerStyle($rowStyle); + $registeredStyle = $this->styleManager->registerStyle($rowStyle); + $rowStyle = $registeredStyle; } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index d68d8f3..3a210d6 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -193,7 +193,8 @@ EOD; if ($managedStyle->isUpdated()) { $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); } else { - $registeredStyle = $rowStyle = $this->styleManager->registerStyle($rowStyle); + $registeredStyle = $this->styleManager->registerStyle($rowStyle); + $rowStyle = $registeredStyle; } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); From 8a17d6c71f04b4d045b564ddb89af82920e57af4 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Thu, 25 Mar 2021 08:44:08 +0100 Subject: [PATCH 16/26] Remove rowStyle reference and replace it by new RegisteredStyle class --- .../Writer/Common/Manager/RegisteredStyle.php | 34 +++++++++++++++++++ .../Writer/ODS/Manager/WorksheetManager.php | 30 ++++++++++------ .../Writer/XLSX/Manager/WorksheetManager.php | 20 ++++++----- 3 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 src/Spout/Writer/Common/Manager/RegisteredStyle.php diff --git a/src/Spout/Writer/Common/Manager/RegisteredStyle.php b/src/Spout/Writer/Common/Manager/RegisteredStyle.php new file mode 100644 index 0000000..5cf0e90 --- /dev/null +++ b/src/Spout/Writer/Common/Manager/RegisteredStyle.php @@ -0,0 +1,34 @@ +style = $style; + $this->isRowStyle = $isRowStyle; + } + + public function getStyle() : Style + { + return $this->style; + } + + public function isRowStyle() : bool + { + return $this->isRowStyle; + } +} diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 1997e9e..52f81a0 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -10,6 +10,7 @@ use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\Escaper\ODS as ODSEscaper; use Box\Spout\Common\Helper\StringHelper; use Box\Spout\Writer\Common\Entity\Worksheet; +use Box\Spout\Writer\Common\Manager\RegisteredStyle; use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface; use Box\Spout\Writer\ODS\Manager\Style\StyleManager; @@ -93,7 +94,7 @@ class WorksheetManager implements WorksheetManagerInterface $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName()); $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1); - $tableElement = ''; + $tableElement = ''; $tableElement .= ''; return $tableElement; @@ -104,8 +105,8 @@ class WorksheetManager implements WorksheetManagerInterface * * @param Worksheet $worksheet The worksheet to add the row to * @param Row $row The row to be added - * @throws IOException If the data cannot be written * @throws InvalidArgumentException If a cell value's type is not supported + * @throws IOException If the data cannot be written * @return void */ public function addRow(Worksheet $worksheet, Row $row) @@ -125,7 +126,13 @@ class WorksheetManager implements WorksheetManagerInterface $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null; if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) { - $data .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $currentCellIndex, $nextCellIndex); + $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); + $cellStyle = $registeredStyle->getStyle(); + if ($registeredStyle->isRowStyle()) { + $rowStyle = $cellStyle; + } + + $data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex); $currentCellIndex = $nextCellIndex; } @@ -146,17 +153,15 @@ class WorksheetManager implements WorksheetManagerInterface /** * Applies styles to the given style, merging the cell's style with its row's style - * Then builds and returns xml for the cell. * * @param Cell $cell * @param Style $rowStyle - * @param int $currentCellIndex - * @param int $nextCellIndex * @throws InvalidArgumentException If a cell value's type is not supported - * @return string + * @return RegisteredStyle */ - private function applyStyleAndGetCellXML(Cell $cell, Style &$rowStyle, $currentCellIndex, $nextCellIndex) + private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle { + $isRowStyle = false; if ($cell->getStyle()->isEmpty()) { $cell->setStyle($rowStyle); @@ -166,7 +171,7 @@ class WorksheetManager implements WorksheetManagerInterface $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); } else { $registeredStyle = $this->styleManager->registerStyle($rowStyle); - $rowStyle = $registeredStyle; + $isRowStyle = true; } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); @@ -182,7 +187,12 @@ class WorksheetManager implements WorksheetManagerInterface $registeredStyle = $this->styleManager->registerStyle($newCellStyle); } - $styleIndex = $registeredStyle->getId() + 1; // 1-based + return new RegisteredStyle($registeredStyle, $isRowStyle); + } + + private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string + { + $styleIndex = $style->getId() + 1; // 1-based $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex); diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 3a210d6..2108dc1 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -14,6 +14,7 @@ use Box\Spout\Writer\Common\Creator\InternalEntityFactory; use Box\Spout\Writer\Common\Entity\Options; use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\Common\Helper\CellHelper; +use Box\Spout\Writer\Common\Manager\RegisteredStyle; use Box\Spout\Writer\Common\Manager\RowManager; use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface; @@ -160,7 +161,12 @@ EOD; $rowXML = ''; foreach ($row->getCells() as $columnIndexZeroBased => $cell) { - $rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndexOneBased, $columnIndexZeroBased); + $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); + $cellStyle = $registeredStyle->getStyle(); + if ($registeredStyle->isRowStyle()) { + $rowStyle = $cellStyle; + } + $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId()); } $rowXML .= ''; @@ -173,18 +179,16 @@ EOD; /** * Applies styles to the given style, merging the cell's style with its row's style - * Then builds and returns xml for the cell. * * @param Cell $cell * @param Style $rowStyle - * @param int $rowIndexOneBased - * @param int $columnIndexZeroBased * * @throws InvalidArgumentException If the given value cannot be processed - * @return string + * @return RegisteredStyle */ - private function applyStyleAndGetCellXML(Cell $cell, Style &$rowStyle, $rowIndexOneBased, $columnIndexZeroBased) + private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle { + $isRowStyle = false; if ($cell->getStyle()->isEmpty()) { $cell->setStyle($rowStyle); @@ -194,7 +198,7 @@ EOD; $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); } else { $registeredStyle = $this->styleManager->registerStyle($rowStyle); - $rowStyle = $registeredStyle; + $isRowStyle = true; } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); @@ -211,7 +215,7 @@ EOD; $registeredStyle = $this->styleManager->registerStyle($newCellStyle); } - return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId()); + return new RegisteredStyle($registeredStyle, $isRowStyle); } /** From eb84ec9364fee1442c7d26833d406420e3657d14 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Tue, 30 Mar 2021 14:04:40 +0200 Subject: [PATCH 17/26] Rename ManagedStyle to PossiblyUpdatedStyle and add documentation --- .../Writer/Common/Manager/RegisteredStyle.php | 14 +++++++----- ...agedStyle.php => PossiblyUpdatedStyle.php} | 7 +++++- .../Common/Manager/Style/StyleManager.php | 12 +++++----- .../Manager/Style/StyleManagerInterface.php | 4 ++-- .../Writer/ODS/Manager/WorksheetManager.php | 22 +++++++++---------- .../Writer/XLSX/Manager/WorksheetManager.php | 22 +++++++++---------- .../Common/Manager/Style/StyleManagerTest.php | 14 ++++++------ 7 files changed, 52 insertions(+), 43 deletions(-) rename src/Spout/Writer/Common/Manager/Style/{ManagedStyle.php => PossiblyUpdatedStyle.php} (74%) diff --git a/src/Spout/Writer/Common/Manager/RegisteredStyle.php b/src/Spout/Writer/Common/Manager/RegisteredStyle.php index 5cf0e90..734c2b6 100644 --- a/src/Spout/Writer/Common/Manager/RegisteredStyle.php +++ b/src/Spout/Writer/Common/Manager/RegisteredStyle.php @@ -4,6 +4,10 @@ namespace Box\Spout\Writer\Common\Manager; use Box\Spout\Common\Entity\Style\Style; +/** + * Class RegisteredStyle + * Allow to know if this style must replace actual row style. + */ class RegisteredStyle { /** @@ -14,12 +18,12 @@ class RegisteredStyle /** * @var bool */ - private $isRowStyle; + private $isMatchingRowStyle; - public function __construct(Style $style, bool $isRowStyle) + public function __construct(Style $style, bool $isMatchingRowStyle) { $this->style = $style; - $this->isRowStyle = $isRowStyle; + $this->isMatchingRowStyle = $isMatchingRowStyle; } public function getStyle() : Style @@ -27,8 +31,8 @@ class RegisteredStyle return $this->style; } - public function isRowStyle() : bool + public function isMatchingRowStyle() : bool { - return $this->isRowStyle; + return $this->isMatchingRowStyle; } } diff --git a/src/Spout/Writer/Common/Manager/Style/ManagedStyle.php b/src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php similarity index 74% rename from src/Spout/Writer/Common/Manager/Style/ManagedStyle.php rename to src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php index 0701ac3..6ccaa29 100644 --- a/src/Spout/Writer/Common/Manager/Style/ManagedStyle.php +++ b/src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php @@ -4,7 +4,12 @@ namespace Box\Spout\Writer\Common\Manager\Style; use Box\Spout\Common\Entity\Style\Style; -class ManagedStyle +/** + * Class PossiblyUpdatedStyle + * Indicates if style is updated. + * It allow to know if style registration must be done. + */ +class PossiblyUpdatedStyle { private $style; private $isUpdated; diff --git a/src/Spout/Writer/Common/Manager/Style/StyleManager.php b/src/Spout/Writer/Common/Manager/Style/StyleManager.php index 2f21763..e2b5ebd 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleManager.php @@ -50,9 +50,9 @@ class StyleManager implements StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return ManagedStyle The eventually updated style + * @return PossiblyUpdatedStyle The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell) : ManagedStyle + public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle { return $this->applyWrapTextIfCellContainsNewLine($cell); } @@ -67,9 +67,9 @@ class StyleManager implements StyleManagerInterface * on the Windows version of Excel... * * @param Cell $cell The cell the style should be applied to - * @return ManagedStyle The eventually updated style + * @return PossiblyUpdatedStyle The eventually updated style */ - protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : ManagedStyle + protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : PossiblyUpdatedStyle { $cellStyle = $cell->getStyle(); @@ -77,9 +77,9 @@ class StyleManager implements StyleManagerInterface if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) { $cellStyle->setShouldWrapText(); - return new ManagedStyle($cellStyle, true); + return new PossiblyUpdatedStyle($cellStyle, true); } - return new ManagedStyle($cellStyle, false); + return new PossiblyUpdatedStyle($cellStyle, false); } } diff --git a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php index 6d01bd5..6b320b1 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php @@ -24,7 +24,7 @@ interface StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return ManagedStyle The eventually updated style + * @return PossiblyUpdatedStyle The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell) : ManagedStyle; + public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle; } diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 52f81a0..e0366d1 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -128,8 +128,8 @@ class WorksheetManager implements WorksheetManagerInterface if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) { $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); $cellStyle = $registeredStyle->getStyle(); - if ($registeredStyle->isRowStyle()) { - $rowStyle = $cellStyle; + if ($registeredStyle->isMatchingRowStyle()) { + $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id) } $data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex); @@ -161,25 +161,25 @@ class WorksheetManager implements WorksheetManagerInterface */ private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle { - $isRowStyle = false; + $isMatchingRowStyle = false; if ($cell->getStyle()->isEmpty()) { $cell->setStyle($rowStyle); - $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - if ($managedStyle->isUpdated()) { - $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); + if ($possiblyUpdatedStyle->isUpdated()) { + $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle()); } else { $registeredStyle = $this->styleManager->registerStyle($rowStyle); - $isRowStyle = true; + $isMatchingRowStyle = true; } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); $cell->setStyle($mergedCellAndRowStyle); - $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - if ($managedStyle->isUpdated()) { - $newCellStyle = $managedStyle->getStyle(); + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + if ($possiblyUpdatedStyle->isUpdated()) { + $newCellStyle = $possiblyUpdatedStyle->getStyle(); } else { $newCellStyle = $mergedCellAndRowStyle; } @@ -187,7 +187,7 @@ class WorksheetManager implements WorksheetManagerInterface $registeredStyle = $this->styleManager->registerStyle($newCellStyle); } - return new RegisteredStyle($registeredStyle, $isRowStyle); + return new RegisteredStyle($registeredStyle, $isMatchingRowStyle); } private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 2108dc1..028bdef 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -163,8 +163,8 @@ EOD; foreach ($row->getCells() as $columnIndexZeroBased => $cell) { $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); $cellStyle = $registeredStyle->getStyle(); - if ($registeredStyle->isRowStyle()) { - $rowStyle = $cellStyle; + if ($registeredStyle->isMatchingRowStyle()) { + $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id) } $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId()); } @@ -188,26 +188,26 @@ EOD; */ private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle { - $isRowStyle = false; + $isMatchingRowStyle = false; if ($cell->getStyle()->isEmpty()) { $cell->setStyle($rowStyle); - $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - if ($managedStyle->isUpdated()) { - $registeredStyle = $this->styleManager->registerStyle($managedStyle->getStyle()); + if ($possiblyUpdatedStyle->isUpdated()) { + $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle()); } else { $registeredStyle = $this->styleManager->registerStyle($rowStyle); - $isRowStyle = true; + $isMatchingRowStyle = true; } } else { $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); $cell->setStyle($mergedCellAndRowStyle); - $managedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - if ($managedStyle->isUpdated()) { - $newCellStyle = $managedStyle->getStyle(); + if ($possiblyUpdatedStyle->isUpdated()) { + $newCellStyle = $possiblyUpdatedStyle->getStyle(); } else { $newCellStyle = $mergedCellAndRowStyle; } @@ -215,7 +215,7 @@ EOD; $registeredStyle = $this->styleManager->registerStyle($newCellStyle); } - return new RegisteredStyle($registeredStyle, $isRowStyle); + return new RegisteredStyle($registeredStyle, $isMatchingRowStyle); } /** diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php index daf4abb..5bbbe5c 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleManagerTest.php @@ -28,10 +28,10 @@ class StyleManagerTest extends TestCase $this->assertFalse($style->shouldWrapText()); $styleManager = $this->getStyleManager(); - $managedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); + $possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); - $this->assertTrue($managedStyle->isUpdated()); - $this->assertTrue($managedStyle->getStyle()->shouldWrapText()); + $this->assertTrue($possiblyUpdatedStyle->isUpdated()); + $this->assertTrue($possiblyUpdatedStyle->getStyle()->shouldWrapText()); } public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextNotNeeded() : void @@ -40,9 +40,9 @@ class StyleManagerTest extends TestCase $this->assertFalse($style->shouldWrapText()); $styleManager = $this->getStyleManager(); - $managedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell('oneline', $style)); + $possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell('oneline', $style)); - $this->assertFalse($managedStyle->isUpdated()); + $this->assertFalse($possiblyUpdatedStyle->isUpdated()); } public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextAlreadyApplied() : void @@ -51,8 +51,8 @@ class StyleManagerTest extends TestCase $this->assertTrue($style->shouldWrapText()); $styleManager = $this->getStyleManager(); - $managedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); + $possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); - $this->assertFalse($managedStyle->isUpdated()); + $this->assertFalse($possiblyUpdatedStyle->isUpdated()); } } From 8c1f0cc447fd8db57308552c2413fc5d5ac9e5f6 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Tue, 4 May 2021 19:33:24 +0200 Subject: [PATCH 18/26] Floats must not be stored as locale dependent Floats are currently stored formatted per the locale setting. This leads to different values being written whether the locale uses "." or "," for the decimal point for instance. This poses a problem as floats must be stored using "." as the decimal point to be valid. This commit ensures that the floats are stored correctly by forcing the formatting of the value. --- src/Spout/Common/Helper/StringHelper.php | 34 ++++++++++++++ .../Writer/ODS/Manager/WorksheetManager.php | 5 +- .../Writer/XLSX/Manager/WorksheetManager.php | 2 +- tests/Spout/Writer/ODS/WriterTest.php | 46 +++++++++++++++++++ tests/Spout/Writer/XLSX/WriterTest.php | 46 +++++++++++++++++++ 5 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/Spout/Common/Helper/StringHelper.php b/src/Spout/Common/Helper/StringHelper.php index 0906132..6256b1e 100644 --- a/src/Spout/Common/Helper/StringHelper.php +++ b/src/Spout/Common/Helper/StringHelper.php @@ -13,12 +13,20 @@ class StringHelper /** @var bool Whether the mbstring extension is loaded */ protected $hasMbstringSupport; + /** @var bool Whether the code is running with PHP7 or older versions */ + private $isRunningPhp7OrOlder; + + /** @var array Locale info, used for number formatting */ + private $localeInfo; + /** * */ public function __construct() { $this->hasMbstringSupport = \extension_loaded('mbstring'); + $this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0; + $this->localeInfo = \localeconv(); } /** @@ -68,4 +76,30 @@ class StringHelper return ($position !== false) ? $position : -1; } + + /** + * Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format. + * + * Formatting of float values is locale dependent in PHP < 8. + * Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34). + * However, float values must be formatted with no thousands separator and a "." as decimal point + * to work properly. This method can be used to convert the value to the correct format before storing it. + * + * @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8. + * + * @param int|float $numericValue + * @return string + */ + public function formatNumericValue($numericValue) + { + if ($this->isRunningPhp7OrOlder && is_float($numericValue)) { + return str_replace( + [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']], + ['', '.'], + $numericValue + ); + } + + return $numericValue; + } } diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index e0366d1..7d7cb0e 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -231,8 +231,9 @@ class WorksheetManager implements WorksheetManagerInterface $data .= '' . $cell->getValue() . ''; $data .= ''; } elseif ($cell->isNumeric()) { - $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">'; - $data .= '' . $cell->getValue() . ''; + $cellValue = $this->stringHelper->formatNumericValue($cell->getValue()); + $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">'; + $data .= '' . $cellValue . ''; $data .= ''; } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) { // only writes the error value if it's a string diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 028bdef..61b93a1 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -240,7 +240,7 @@ EOD; } elseif ($cell->isBoolean()) { $cellXML .= ' t="b">' . (int) ($cell->getValue()) . ''; } elseif ($cell->isNumeric()) { - $cellXML .= '>' . $cell->getValue() . ''; + $cellXML .= '>' . $this->stringHelper->formatNumericValue($cell->getValue()) . ''; } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) { // only writes the error value if it's a string $cellXML .= ' t="e">' . $cell->getValueEvenIfError() . ''; diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php index 31dcccf..61c1b9e 100644 --- a/tests/Spout/Writer/ODS/WriterTest.php +++ b/tests/Spout/Writer/ODS/WriterTest.php @@ -279,6 +279,52 @@ class WriterTest extends TestCase $this->assertValueWasWrittenToSheet($fileName, 1, 10.2); } + /** + * @return void + */ + public function testAddRowShouldSupportFloatValuesInDifferentLocale() + { + $previousLocale = \setlocale(LC_ALL, 0); + + try { + // Pick a supported locale whose decimal point is a comma. + // Installed locales differ from one system to another, so we can't pick + // a given locale. + $supportedLocales = explode("\n", shell_exec('locale -a')); + foreach ($supportedLocales as $supportedLocale) { + \setlocale(LC_ALL, $supportedLocale); + if (\localeconv()['decimal_point'] === ',') { + break; + } + } + $this->assertEquals(',', \localeconv()['decimal_point']); + + $cellValue = 1234.5; + var_dump("Cell value before: " . $cellValue); + $cellValue = str_replace( + [\localeconv()['thousands_sep'], \localeconv()['decimal_point']], + ['', '.'], + $cellValue + ); + var_dump("Cell value after: " . $cellValue); + var_dump("Thousands sep: " . \localeconv()['thousands_sep']); + var_dump("Decimal point: " . \localeconv()['decimal_point']); + + $fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx'; + $dataRows = $this->createRowsFromValues([ + [1234.5], + ]); + + $this->writeToODSFile($dataRows, $fileName); + + $this->assertValueWasNotWrittenToSheet($fileName, 1, "1234,5"); + $this->assertValueWasWrittenToSheet($fileName, 1, "1234.5"); + } finally { + // reset locale + \setlocale(LC_ALL, $previousLocale); + } + } + /** * @return array */ diff --git a/tests/Spout/Writer/XLSX/WriterTest.php b/tests/Spout/Writer/XLSX/WriterTest.php index 9cc157a..e2a8e4e 100644 --- a/tests/Spout/Writer/XLSX/WriterTest.php +++ b/tests/Spout/Writer/XLSX/WriterTest.php @@ -393,6 +393,52 @@ class WriterTest extends TestCase $this->assertInlineDataWasWrittenToSheet($fileName, 1, 't="e">#DIV/0'); } + /** + * @return void + */ + public function testAddRowShouldSupportFloatValuesInDifferentLocale() + { + $previousLocale = \setlocale(LC_ALL, 0); + $valueToWrite = 1234.5; // needs to be defined before changing the locale as PHP8 would expect 1234,5 + + try { + // Pick a supported locale whose decimal point is a comma. + // Installed locales differ from one system to another, so we can't pick + // a given locale. + $supportedLocales = explode("\n", shell_exec('locale -a')); + foreach ($supportedLocales as $supportedLocale) { + \setlocale(LC_ALL, $supportedLocale); + if (\localeconv()['decimal_point'] === ',') { + break; + } + } + $this->assertEquals(',', \localeconv()['decimal_point']); + + var_dump("Cell value before: " . $valueToWrite); + $cellValue = str_replace( + [\localeconv()['thousands_sep'], \localeconv()['decimal_point']], + ['', '.'], + $valueToWrite + ); + var_dump("Cell value after: " . $cellValue); + var_dump("Thousands sep: " . \localeconv()['thousands_sep']); + var_dump("Decimal point: " . \localeconv()['decimal_point']); + + $fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx'; + $dataRows = $this->createRowsFromValues([ + [$valueToWrite], + ]); + + $this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = false); + + $this->assertInlineDataWasNotWrittenToSheet($fileName, 1, "1234,5"); + $this->assertInlineDataWasWrittenToSheet($fileName, 1, "1234.5"); + } finally { + // reset locale + \setlocale(LC_ALL, $previousLocale); + } + } + /** * @return void */ From 110876e32c1a02b1a4d4c8b511318ac23bdf499c Mon Sep 17 00:00:00 2001 From: Toby Allen Date: Sun, 9 May 2021 11:10:12 +0100 Subject: [PATCH 19/26] Some small grammar changes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a9a0a9c..18c2dab 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ [![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. -Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB). +Unlike other file readers or writers, it is capable of processing very large files, while keeping, the memory usage really low (less than 3MB). -Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) ## Documentation @@ -43,7 +43,7 @@ For information, the performance tests take about 10 minutes to run (processing ## Support -You can ask questions, submit new features ideas or discuss about Spout in the chat room:
+You can ask questions, submit new features ideas or discuss Spout in the chat room:
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) ## Copyright and License From 0837d49c2b1e65a614b7758af03c9ac8e8a5c1f3 Mon Sep 17 00:00:00 2001 From: Toby Allen Date: Sun, 9 May 2021 20:33:54 +0100 Subject: [PATCH 20/26] Remove unneeded comma --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18c2dab..7c77242 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. -Unlike other file readers or writers, it is capable of processing very large files, while keeping, the memory usage really low (less than 3MB). +Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB). Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) From 2ff515c306b9c66d0ce3d84ea047a07562621d9f Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Wed, 12 May 2021 22:45:44 +0200 Subject: [PATCH 21/26] Support for strict OOXML There are 2 types of OOXML format: transitional and strict. Transitional is what's mostly used but some softwares still allow XLSX to be generated using the strict OOXML format. In this format, namespaces of the XML files are different: `http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings` is replaced by `http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings` for instance. To support both formats, Spout needs to be able to look for both. --- src/Spout/Common/Entity/Cell.php | 2 +- .../Reader/XLSX/Helper/CellValueFormatter.php | 8 -------- .../XLSX/Manager/SharedStringsManager.php | 3 --- .../Manager/WorkbookRelationshipsManager.php | 17 +++++++++++------ tests/Spout/Reader/XLSX/ReaderTest.php | 12 ++++++++++++ .../xlsx/sheet_with_strict_ooxml.xlsx | Bin 0 -> 98075 bytes 6 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 tests/resources/xlsx/sheet_with_strict_ooxml.xlsx diff --git a/src/Spout/Common/Entity/Cell.php b/src/Spout/Common/Entity/Cell.php index c1de389..174831c 100644 --- a/src/Spout/Common/Entity/Cell.php +++ b/src/Spout/Common/Entity/Cell.php @@ -65,7 +65,7 @@ class Cell protected $style; /** - * @param $value mixed + * @param mixed|null $value * @param Style|null $style */ public function __construct($value, Style $style = null) diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index 169c395..e95f186 100644 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -31,14 +31,6 @@ class CellValueFormatter /** Constants used for date formatting */ const NUM_SECONDS_IN_ONE_DAY = 86400; - const NUM_SECONDS_IN_ONE_HOUR = 3600; - const NUM_SECONDS_IN_ONE_MINUTE = 60; - - /** - * February 29th, 1900 is NOT a leap year but Excel thinks it is... - * @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel - */ - const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60; /** @var SharedStringsManager Manages shared strings */ protected $sharedStringsManager; diff --git a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php index 8850a69..81b4ba2 100644 --- a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php +++ b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php @@ -16,9 +16,6 @@ use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface; */ class SharedStringsManager { - /** Main namespace for the sharedStrings.xml file */ - const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'; - /** Definition of XML nodes names used to parse data */ const XML_NODE_SST = 'sst'; const XML_NODE_SI = 'si'; diff --git a/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php b/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php index a153d78..0a9f6f5 100644 --- a/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php +++ b/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php @@ -17,10 +17,11 @@ class WorkbookRelationshipsManager /** Path of workbook relationships XML file inside the XLSX file */ const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels'; - /** Relationships types */ + /** Relationships types - For Transitional and Strict OOXML */ const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings'; const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'; - const RELATIONSHIP_TYPE_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; + const RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings'; + const RELATIONSHIP_TYPE_STYLES_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/styles'; /** Nodes and attributes used to find relevant information in the workbook relationships XML file */ const XML_NODE_RELATIONSHIP = 'Relationship'; @@ -52,7 +53,8 @@ class WorkbookRelationshipsManager public function getSharedStringsXMLFilePath() { $workbookRelationships = $this->getWorkbookRelationships(); - $sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]; + $sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS] + ?? $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]; // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml") $doesContainBasePath = (\strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false); @@ -71,7 +73,8 @@ class WorkbookRelationshipsManager { $workbookRelationships = $this->getWorkbookRelationships(); - return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]); + return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]) + || isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]); } /** @@ -81,7 +84,8 @@ class WorkbookRelationshipsManager { $workbookRelationships = $this->getWorkbookRelationships(); - return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]); + return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]) + || isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]); } /** @@ -90,7 +94,8 @@ class WorkbookRelationshipsManager public function getStylesXMLFilePath() { $workbookRelationships = $this->getWorkbookRelationships(); - $stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]; + $stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES] + ?? $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]; // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml") $doesContainBasePath = (\strpos($stylesXMLFilePath, self::BASE_PATH) !== false); diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index 7d30f97..e9888d8 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -703,6 +703,18 @@ class ReaderTest extends TestCase $this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed'); } + /** + * https://github.com/box/spout/issues/726 + * @return void + */ + public function testReaderShouldSupportStrictOOXML() + { + $allRows = $this->getAllRowsForFile('sheet_with_strict_ooxml.xlsx'); + + $this->assertEquals('UNIQUE_ACCOUNT_IDENTIFIER', $allRows[0][0]); + $this->assertEquals('A2Z34NJA7N2ESJ', $allRows[1][0]); + } + /** * @param string $fileName * @param bool $shouldFormatDates diff --git a/tests/resources/xlsx/sheet_with_strict_ooxml.xlsx b/tests/resources/xlsx/sheet_with_strict_ooxml.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0fba9f9a926ee7007aff65a88635e500d9fa890d GIT binary patch literal 98075 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Ⓢ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 JyFO4T{~y8`6VLzv literal 0 HcmV?d00001 From 69eeeff478b212146c616e9d428b9a33206fe8ef Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Wed, 12 May 2021 22:59:11 +0200 Subject: [PATCH 22/26] Remove var_dump --- tests/Spout/Writer/ODS/WriterTest.php | 11 ----------- tests/Spout/Writer/XLSX/WriterTest.php | 10 ---------- 2 files changed, 21 deletions(-) diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php index 61c1b9e..a9aaa0e 100644 --- a/tests/Spout/Writer/ODS/WriterTest.php +++ b/tests/Spout/Writer/ODS/WriterTest.php @@ -299,17 +299,6 @@ class WriterTest extends TestCase } $this->assertEquals(',', \localeconv()['decimal_point']); - $cellValue = 1234.5; - var_dump("Cell value before: " . $cellValue); - $cellValue = str_replace( - [\localeconv()['thousands_sep'], \localeconv()['decimal_point']], - ['', '.'], - $cellValue - ); - var_dump("Cell value after: " . $cellValue); - var_dump("Thousands sep: " . \localeconv()['thousands_sep']); - var_dump("Decimal point: " . \localeconv()['decimal_point']); - $fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx'; $dataRows = $this->createRowsFromValues([ [1234.5], diff --git a/tests/Spout/Writer/XLSX/WriterTest.php b/tests/Spout/Writer/XLSX/WriterTest.php index e2a8e4e..26b3792 100644 --- a/tests/Spout/Writer/XLSX/WriterTest.php +++ b/tests/Spout/Writer/XLSX/WriterTest.php @@ -414,16 +414,6 @@ class WriterTest extends TestCase } $this->assertEquals(',', \localeconv()['decimal_point']); - var_dump("Cell value before: " . $valueToWrite); - $cellValue = str_replace( - [\localeconv()['thousands_sep'], \localeconv()['decimal_point']], - ['', '.'], - $valueToWrite - ); - var_dump("Cell value after: " . $cellValue); - var_dump("Thousands sep: " . \localeconv()['thousands_sep']); - var_dump("Decimal point: " . \localeconv()['decimal_point']); - $fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx'; $dataRows = $this->createRowsFromValues([ [$valueToWrite], From fde8a495ca31d66ff2c3a93e4af2505dead8d0ad Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Fri, 14 May 2021 16:10:12 +0200 Subject: [PATCH 23/26] Inline strings can contain multiple value nodes We were working under the assumption that XLSX's inline strings only had a single value node (``). This is incorrect. To get the actual value of an inline string node, we need to concatenate the value of all its child nodes. --- composer.json | 3 ++- .../Reader/XLSX/Helper/CellValueFormatter.php | 13 +++++++++---- .../XLSX/Helper/CellValueFormatterTest.php | 8 ++++++-- tests/Spout/Reader/XLSX/ReaderTest.php | 13 +++++++++++++ ...h_multiple_value_nodes_in_inline_strings.xlsx | Bin 0 -> 3779 bytes 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 tests/resources/xlsx/sheet_with_multiple_value_nodes_in_inline_strings.xlsx 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: - // [INLINE_STRING] - $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 nodes): + // [INLINE_STRING][INLINE_STRING_2] + $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; } 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 */ diff --git a/tests/resources/xlsx/sheet_with_multiple_value_nodes_in_inline_strings.xlsx b/tests/resources/xlsx/sheet_with_multiple_value_nodes_in_inline_strings.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2ee574c6198e61d2689ef7a6f1e1916a59e6e645 GIT binary patch literal 3779 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 literal 0 HcmV?d00001 From 76017f09494133c19ae9a093d6d291f390f54e19 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Fri, 14 May 2021 22:14:34 +0200 Subject: [PATCH 24/26] Skipped cells are in wrong order This only happens when no sheet's dimension is specified. When filling empty cells with empty strings, we push these new cells with the correct cell index but they are added at the end of the cells array (normal PHP behavior). This means that we were going from `{[0] => 'A', [2] => 'C'}` to `{[0] => 'A', [2] => 'C', [1] => ''}`. We therefore need to sort the array to get the values in the correct order ( `{[0] => 'A', [1] => '', [2] => 'C'}`). --- .../Reader/Common/Manager/RowManager.php | 13 +++++++++++++ tests/Spout/Reader/XLSX/ReaderTest.php | 18 +++++++++++++++++- .../xlsx/sheet_with_empty_cells.xlsx | Bin 4724 -> 4627 bytes ...t_with_empty_cells_without_dimensions.xlsx | Bin 0 -> 4611 bytes 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/resources/xlsx/sheet_with_empty_cells_without_dimensions.xlsx diff --git a/src/Spout/Reader/Common/Manager/RowManager.php b/src/Spout/Reader/Common/Manager/RowManager.php index 623c8d8..67c8fb1 100644 --- a/src/Spout/Reader/Common/Manager/RowManager.php +++ b/src/Spout/Reader/Common/Manager/RowManager.php @@ -56,12 +56,25 @@ class RowManager $rowCells = $row->getCells(); $maxCellIndex = $numCells; + // If the row has empty cells, calling "setCellAtIndex" will add the cell + // but in the wrong place (the new cell is added at the end of the array). + // Therefore, we need to sort the array using keys to have proper order. + // @see https://github.com/box/spout/issues/740 + $needsSorting = false; + for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) { if (!isset($rowCells[$cellIndex])) { $row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex); + $needsSorting = true; } } + if ($needsSorting) { + $rowCells = $row->getCells(); + ksort($rowCells); + $row->setCells($rowCells); + } + return $row; } } diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index af7ca37..5b155b4 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -692,13 +692,29 @@ class ReaderTest extends TestCase $allRows = $this->getAllRowsForFile('sheet_with_empty_cells.xlsx'); $expectedRows = [ - ['A', 'B', 'C'], + ['A', '', 'C'], ['0', '', ''], ['1', '1', ''], ]; $this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length'); } + /** + * https://github.com/box/spout/issues/184 + * @return void + */ + public function testReadShouldCreateOutputEmptyCellPreservedWhenNoDimensionsSpecified() + { + $allRows = $this->getAllRowsForFile('sheet_with_empty_cells_without_dimensions.xlsx'); + + $expectedRows = [ + ['A', '', 'C'], + ['0'], + ['1', '1'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * https://github.com/box/spout/issues/195 * @return void diff --git a/tests/resources/xlsx/sheet_with_empty_cells.xlsx b/tests/resources/xlsx/sheet_with_empty_cells.xlsx index d815a1c2b852441c702a1f9065e63134007659ea..8de9522c90b0bb85af382ab2c8fc4c5bd91bba63 100644 GIT binary patch delta 1448 zcmeyOGFe3;z?+#xgn@yBgJETKfk)b^rtJ?H7#OUXCQ5u}1lzhVcP7K))-FB|zpNtF)dCZ%Cum~`r$Sv6_wtE^A1H*H{&3f#snd-s% z7&sW3Hm(bLY|GHNfSG~e1_uL!1cMAiMUH-Xeo=ODMrvwFu|AkG)T_wN2@T<7V4i$) zUIGZ0R&X;gvU~?x2l58P-Z0<%+ZFUH^#hC5j|uxc`+{s=2^|n%5vUTJa_q|V#1)%`v{tEaVrw*R z4G}f92>Bgwoh##=S*T6V;ukt=G=v-7cZB?Bbrxei{lG2Fm-SGE0PCE3sU_>Z!(J=+ z#5v}0DB zUV2$;^au0i#d5Wim4$cO^#F{nJ9t zpYBsQt&QgWV4w1ax$CX>k~;3zm-ULgcXJz)i%uN>eXaN5jFjIYZ<i7HMQ0?p@mO za>=Vl>^iA}9IMUUJtA+5ct5yvO@6|eb$?raq`znSq?5Y!tcLs7Wq~GA_jkTs9=Q6m zZtbMr%d6*ZZO>4Dx|lce&WW<~p^2sISK1cnb~gm{$Q{_cb6)T9I_-=1W0tQ=cUIQ@ zQ2%51>zk)!HEo#J3a$9zu#VUAam{7b8Ek;o-US%io5uJ z+rEmv`Dfy^s+;Gn3D}l0ZMIPI<&!pj_guv!TZIziqWz5-236bFl2_r>|W* z;supn&#U^=z4Ewn(xaAN+)10`eR95a-T1O}*>(Hqhb!9`FAmgJJF}~7pSza{kI9>Z z3E!G`&T+f&ch~DUucPyp8cfl@=;PSiXR?o@Ep@%RQ~tvFD{mS5oSDn&I{9<==gQaK zhXwN2RR=!%Soy!Xe%HGtj*9gr!B+RB8Mr#jta=XL+c#V4>_WD!spl(J|8scg<`!bG zw0G<5-OFw@?zcPlYX7$I^|If@?G(hy*2d(O6n+e7idf8aBmQUr!z(_2x?FSycrz4G7lM2i=hN}uoScX$~cb@l!9 zci(U4_nN$y{}6cP37`C)>d3&4w{72iObo8Ow08EfcjrTe_zc7@PX1Zn5hna-pE=*Q zqh5y!=UTk^ukLyO4>zpT;fEAE+{qk{vw*qx(&U%C+fmAkc(Xkb2Efc6&a(L!pEn~r zBa;XNFrQD>5U^lm02WP(vYbHKl>Fp?qWpql{ltO-Pze#>4O2H?K#@^$@<9O^pz3=- z_OwZYsz9ciprXtI76w=mla!yIjjAtMP|@ZX8(2YcNo7uIF-%&H8C1+*E3hPh;tUKx z*Dx$;Y@d8UP+BoGgcVXyp%qdH=RFZrWUAzy{Ek;_vaApX$YDZ?jINVCgp|0#fu-a! Hevpv>(~wN$ delta 1590 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 X8N#%IxOWBsh;bJjcY;sOGZf%|xIE8# literal 0 HcmV?d00001 From 9bdb027d312b732515b884a341c0ad70372c6295 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Fri, 14 May 2021 15:16:53 +0200 Subject: [PATCH 25/26] Update documentation with number format --- docs/_pages/documentation.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/_pages/documentation.md b/docs/_pages/documentation.md index d63ca03..ce142cf 100755 --- a/docs/_pages/documentation.md +++ b/docs/_pages/documentation.md @@ -116,18 +116,19 @@ $reader->setShouldPreserveEmptyRows(true); For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones: -| Category | Property | API -|:----------|:---------------|:-------------------------------------- -| Font | Bold | `StyleBuilder::setFontBold()` -| | Italic | `StyleBuilder::setFontItalic()` -| | Underline | `StyleBuilder::setFontUnderline()` -| | Strikethrough | `StyleBuilder::setFontStrikethrough()` -| | Font name | `StyleBuilder::setFontName('Arial')` -| | Font size | `StyleBuilder::setFontSize(14)` -| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` -| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)` -| | Wrap text | `StyleBuilder::setShouldWrapText(true)` - +| Category | Property | API +|:---------------------|:---------------|:-------------------------------------- +| Font | Bold | `StyleBuilder::setFontBold()` +| | Italic | `StyleBuilder::setFontItalic()` +| | Underline | `StyleBuilder::setFontUnderline()` +| | Strikethrough | `StyleBuilder::setFontStrikethrough()` +| | Font name | `StyleBuilder::setFontName('Arial')` +| | Font size | `StyleBuilder::setFontSize(14)` +| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` +| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)` +| | Wrap text | `StyleBuilder::setShouldWrapText(true)` +| Format _(XLSX only)_ | Number format | `StyleBuilder::setFormat('0.000')` +| | Date format | `StyleBuilder::setFormat('m/d/yy h:mm')` ### Styling rows From 9533accd73edc020072bc7a4f0c0ddb28a5b701f Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Mon, 7 Jun 2021 12:20:48 +0200 Subject: [PATCH 26/26] Create FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..51a7b98 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: adrilo