From 3ddfaebd434d94f54c06ee6eae3f64d0150373d9 Mon Sep 17 00:00:00 2001 From: raortegar Date: Wed, 22 Mar 2023 12:53:16 +0100 Subject: [PATCH] MDL-76800 lib: upgrade OpenSpout to 4.0 --- lib/openspout/README.md | 34 +- .../src/Autoloader/Psr4Autoloader.php | 147 ------- lib/openspout/src/Autoloader/autoload.php | 15 - .../src/Common/Creator/HelperFactory.php | 48 --- lib/openspout/src/Common/Entity/Cell.php | 232 ++--------- .../src/Common/Entity/Cell/BooleanCell.php | 24 ++ .../Common/Entity/Cell/DateIntervalCell.php | 25 ++ .../src/Common/Entity/Cell/DateTimeCell.php | 25 ++ .../src/Common/Entity/Cell/EmptyCell.php | 24 ++ .../src/Common/Entity/Cell/ErrorCell.php | 29 ++ .../src/Common/Entity/Cell/FormulaCell.php | 24 ++ .../src/Common/Entity/Cell/NumericCell.php | 24 ++ .../src/Common/Entity/Cell/StringCell.php | 24 ++ .../src/Common/Entity/Comment/Comment.php | 47 +++ .../src/Common/Entity/Comment/TextRun.php | 23 ++ lib/openspout/src/Common/Entity/Row.php | 132 +++--- .../src/Common/Entity/Style/Border.php | 68 +--- .../src/Common/Entity/Style/BorderPart.php | 191 +++------ .../src/Common/Entity/Style/CellAlignment.php | 12 +- .../Entity/Style/CellVerticalAlignment.php | 37 ++ .../src/Common/Entity/Style/Color.php | 20 +- .../src/Common/Entity/Style/Style.php | 364 +++++++---------- .../Exception/EncodingConversionException.php | 4 +- .../src/Common/Exception/IOException.php | 4 +- .../Exception/InvalidArgumentException.php | 4 +- .../Exception/InvalidColorException.php | 4 +- .../Common/Exception/OpenSpoutException.php | 11 + .../src/Common/Exception/SpoutException.php | 7 - .../Exception/UnsupportedTypeException.php | 4 +- .../src/Common/Helper/CellTypeHelper.php | 82 ---- .../src/Common/Helper/EncodingHelper.php | 119 +++--- .../src/Common/Helper/Escaper/CSV.php | 37 -- .../Helper/Escaper/EscaperInterface.php | 8 +- .../src/Common/Helper/Escaper/ODS.php | 42 +- .../src/Common/Helper/Escaper/XLSX.php | 40 +- .../src/Common/Helper/FileSystemHelper.php | 74 ++-- .../Helper/FileSystemHelperInterface.php | 21 +- .../Common/Helper/GlobalFunctionsHelper.php | 371 ----------------- .../src/Common/Helper/StringHelper.php | 79 ++-- .../Common/Manager/OptionsManagerAbstract.php | 82 ---- .../Manager/OptionsManagerInterface.php | 31 -- .../src/Common/TempFolderOptionTrait.php | 33 ++ lib/openspout/src/Common/Type.php | 13 - ...{ReaderAbstract.php => AbstractReader.php} | 147 ++----- .../CSV/Creator/InternalEntityFactory.php | 98 ----- .../src/Reader/CSV/Manager/OptionsManager.php | 39 -- lib/openspout/src/Reader/CSV/Options.php | 15 + lib/openspout/src/Reader/CSV/Reader.php | 132 ++---- lib/openspout/src/Reader/CSV/RowIterator.php | 116 ++---- lib/openspout/src/Reader/CSV/Sheet.php | 30 +- .../src/Reader/CSV/SheetIterator.php | 26 +- .../src/Reader/Common/ColumnWidth.php | 22 + .../InternalEntityFactoryInterface.php | 26 -- .../Common/Creator/ReaderEntityFactory.php | 72 ---- .../Reader/Common/Creator/ReaderFactory.php | 95 ++--- .../src/Reader/Common/Entity/Options.php | 22 - .../src/Reader/Common/Manager/RowManager.php | 47 +-- .../src/Reader/Common/XMLProcessor.php | 43 +- .../Exception/InvalidValueException.php | 19 +- .../IteratorNotRewindableException.php | 4 +- .../Exception/NoSheetsFoundException.php | 4 +- .../src/Reader/Exception/ReaderException.php | 6 +- .../Exception/ReaderNotOpenedException.php | 4 +- .../SharedStringNotFoundException.php | 4 +- .../Exception/XMLProcessingException.php | 4 +- .../src/Reader/IteratorInterface.php | 14 - .../src/Reader/ODS/Creator/HelperFactory.php | 43 -- .../ODS/Creator/InternalEntityFactory.php | 124 ------ .../src/Reader/ODS/Creator/ManagerFactory.php | 21 - .../Reader/ODS/Helper/CellValueFormatter.php | 166 ++++---- .../src/Reader/ODS/Helper/SettingsHelper.php | 29 +- .../src/Reader/ODS/Manager/OptionsManager.php | 32 -- lib/openspout/src/Reader/ODS/Options.php | 11 + lib/openspout/src/Reader/ODS/Reader.php | 64 ++- lib/openspout/src/Reader/ODS/RowIterator.php | 145 +++---- lib/openspout/src/Reader/ODS/Sheet.php | 38 +- .../src/Reader/ODS/SheetIterator.php | 124 +++--- lib/openspout/src/Reader/ReaderInterface.php | 14 +- .../src/Reader/RowIteratorInterface.php | 18 +- lib/openspout/src/Reader/SheetInterface.php | 19 +- .../src/Reader/SheetIteratorInterface.php | 19 +- .../Reader/SheetWithVisibilityInterface.php | 18 + .../Wrapper/XMLInternalErrorsHelper.php | 20 +- .../src/Reader/Wrapper/XMLReader.php | 54 +-- .../src/Reader/XLSX/Creator/HelperFactory.php | 38 -- .../XLSX/Creator/InternalEntityFactory.php | 163 -------- .../Reader/XLSX/Creator/ManagerFactory.php | 109 ----- .../src/Reader/XLSX/Helper/CellHelper.php | 28 +- .../Reader/XLSX/Helper/CellValueFormatter.php | 189 ++++----- .../Reader/XLSX/Helper/DateFormatHelper.php | 25 +- .../Reader/XLSX/Manager/OptionsManager.php | 36 -- .../CachingStrategyFactory.php | 76 +--- .../CachingStrategyInterface.php | 16 +- .../FileBasedStrategy.php | 97 +++-- .../SharedStringsCaching/InMemoryStrategy.php | 34 +- .../SharedStringsCaching/MemoryLimit.php | 50 +++ .../XLSX/Manager/SharedStringsManager.php | 115 +++--- .../src/Reader/XLSX/Manager/SheetManager.php | 163 +++++--- .../src/Reader/XLSX/Manager/StyleManager.php | 185 ++++----- .../XLSX/Manager/StyleManagerInterface.php | 31 ++ .../Manager/WorkbookRelationshipsManager.php | 59 +-- lib/openspout/src/Reader/XLSX/Options.php | 16 + lib/openspout/src/Reader/XLSX/Reader.php | 131 +++--- lib/openspout/src/Reader/XLSX/RowIterator.php | 178 ++++---- lib/openspout/src/Reader/XLSX/Sheet.php | 48 ++- .../src/Reader/XLSX/SheetHeaderReader.php | 119 ++++++ .../src/Reader/XLSX/SheetIterator.php | 59 +-- lib/openspout/src/Writer/AbstractWriter.php | 175 ++++++++ .../src/Writer/AbstractWriterMultiSheets.php | 129 ++++++ lib/openspout/src/Writer/AutoFilter.php | 25 ++ .../src/Writer/CSV/Manager/OptionsManager.php | 34 -- lib/openspout/src/Writer/CSV/Options.php | 15 + lib/openspout/src/Writer/CSV/Writer.php | 94 ++--- .../src/Writer/Common/AbstractOptions.php | 67 +++ .../src/Writer/Common/ColumnWidth.php | 22 + .../Common/Creator/InternalEntityFactory.php | 52 --- .../Creator/ManagerFactoryInterface.php | 23 -- .../Common/Creator/Style/BorderBuilder.php | 84 ---- .../Common/Creator/Style/StyleBuilder.php | 214 ---------- .../Common/Creator/WriterEntityFactory.php | 121 ------ .../Writer/Common/Creator/WriterFactory.php | 92 +---- .../src/Writer/Common/Entity/Options.php | 30 -- .../src/Writer/Common/Entity/Sheet.php | 133 ++++-- .../src/Writer/Common/Entity/Workbook.php | 17 +- .../src/Writer/Common/Entity/Worksheet.php | 72 +--- .../src/Writer/Common/Helper/CellHelper.php | 12 +- ...ileSystemWithRootFolderHelperInterface.php | 14 +- .../src/Writer/Common/Helper/ZipHelper.php | 126 +++--- ...stract.php => AbstractWorkbookManager.php} | 189 +++------ .../src/Writer/Common/Manager/CellManager.php | 29 -- .../Writer/Common/Manager/ManagesCellSize.php | 62 --- .../Writer/Common/Manager/RegisteredStyle.php | 16 +- .../src/Writer/Common/Manager/RowManager.php | 25 -- .../Writer/Common/Manager/SheetManager.php | 78 ++-- ...leManager.php => AbstractStyleManager.php} | 22 +- ...Registry.php => AbstractStyleRegistry.php} | 45 +- .../Manager/Style/PossiblyUpdatedStyle.php | 11 +- .../Manager/Style/StyleManagerInterface.php | 6 +- .../Common/Manager/Style/StyleMerger.php | 33 +- .../Manager/WorkbookManagerInterface.php | 47 +-- .../Manager/WorksheetManagerInterface.php | 33 +- .../Exception/Border/InvalidNameException.php | 8 +- .../Border/InvalidStyleException.php | 8 +- .../Border/InvalidWidthException.php | 8 +- .../Exception/InvalidSheetNameException.php | 4 +- .../Exception/SheetNotFoundException.php | 4 +- .../WriterAlreadyOpenedException.php | 4 +- .../src/Writer/Exception/WriterException.php | 6 +- .../Exception/WriterNotOpenedException.php | 4 +- .../src/Writer/ODS/Creator/HelperFactory.php | 54 --- .../src/Writer/ODS/Creator/ManagerFactory.php | 107 ----- .../src/Writer/ODS/Helper/BorderHelper.php | 23 +- .../Writer/ODS/Helper/FileSystemHelper.php | 138 ++++--- .../src/Writer/ODS/Manager/OptionsManager.php | 50 --- .../Writer/ODS/Manager/Style/StyleManager.php | 284 ++++++------- .../ODS/Manager/Style/StyleRegistry.php | 15 +- .../Writer/ODS/Manager/WorkbookManager.php | 54 +-- .../Writer/ODS/Manager/WorksheetManager.php | 167 +++----- lib/openspout/src/Writer/ODS/Options.php | 11 + lib/openspout/src/Writer/ODS/Writer.php | 62 ++- lib/openspout/src/Writer/WriterAbstract.php | 253 ------------ lib/openspout/src/Writer/WriterInterface.php | 39 +- .../src/Writer/WriterMultiSheetsAbstract.php | 216 ---------- .../src/Writer/XLSX/Creator/HelperFactory.php | 53 --- .../Writer/XLSX/Creator/ManagerFactory.php | 145 ------- .../src/Writer/XLSX/Entity/SheetView.php | 107 ++--- .../src/Writer/XLSX/Helper/BorderHelper.php | 26 +- .../src/Writer/XLSX/Helper/DateHelper.php | 29 +- .../Writer/XLSX/Helper/FileSystemHelper.php | 385 ++++++++++++++---- .../Writer/XLSX/Manager/CommentsManager.php | 225 ++++++++++ .../Writer/XLSX/Manager/OptionsManager.php | 64 --- .../XLSX/Manager/SharedStringsManager.php | 45 +- .../XLSX/Manager/Style/StyleManager.php | 105 ++--- .../XLSX/Manager/Style/StyleRegistry.php | 104 ++--- .../Writer/XLSX/Manager/WorkbookManager.php | 60 +-- .../Writer/XLSX/Manager/WorksheetManager.php | 218 +++------- lib/openspout/src/Writer/XLSX/MergeCell.php | 27 ++ lib/openspout/src/Writer/XLSX/Options.php | 66 +++ lib/openspout/src/Writer/XLSX/Writer.php | 112 ++--- lib/thirdpartylibs.xml | 2 +- 180 files changed, 4575 insertions(+), 7416 deletions(-) delete mode 100644 lib/openspout/src/Autoloader/Psr4Autoloader.php delete mode 100644 lib/openspout/src/Autoloader/autoload.php delete mode 100644 lib/openspout/src/Common/Creator/HelperFactory.php create mode 100644 lib/openspout/src/Common/Entity/Cell/BooleanCell.php create mode 100644 lib/openspout/src/Common/Entity/Cell/DateIntervalCell.php create mode 100644 lib/openspout/src/Common/Entity/Cell/DateTimeCell.php create mode 100644 lib/openspout/src/Common/Entity/Cell/EmptyCell.php create mode 100644 lib/openspout/src/Common/Entity/Cell/ErrorCell.php create mode 100644 lib/openspout/src/Common/Entity/Cell/FormulaCell.php create mode 100644 lib/openspout/src/Common/Entity/Cell/NumericCell.php create mode 100644 lib/openspout/src/Common/Entity/Cell/StringCell.php create mode 100644 lib/openspout/src/Common/Entity/Comment/Comment.php create mode 100644 lib/openspout/src/Common/Entity/Comment/TextRun.php create mode 100644 lib/openspout/src/Common/Entity/Style/CellVerticalAlignment.php create mode 100644 lib/openspout/src/Common/Exception/OpenSpoutException.php delete mode 100644 lib/openspout/src/Common/Exception/SpoutException.php delete mode 100644 lib/openspout/src/Common/Helper/CellTypeHelper.php delete mode 100644 lib/openspout/src/Common/Helper/Escaper/CSV.php delete mode 100644 lib/openspout/src/Common/Helper/GlobalFunctionsHelper.php delete mode 100644 lib/openspout/src/Common/Manager/OptionsManagerAbstract.php delete mode 100644 lib/openspout/src/Common/Manager/OptionsManagerInterface.php create mode 100644 lib/openspout/src/Common/TempFolderOptionTrait.php delete mode 100644 lib/openspout/src/Common/Type.php rename lib/openspout/src/Reader/{ReaderAbstract.php => AbstractReader.php} (52%) delete mode 100644 lib/openspout/src/Reader/CSV/Creator/InternalEntityFactory.php delete mode 100644 lib/openspout/src/Reader/CSV/Manager/OptionsManager.php create mode 100644 lib/openspout/src/Reader/CSV/Options.php create mode 100644 lib/openspout/src/Reader/Common/ColumnWidth.php delete mode 100644 lib/openspout/src/Reader/Common/Creator/InternalEntityFactoryInterface.php delete mode 100644 lib/openspout/src/Reader/Common/Creator/ReaderEntityFactory.php delete mode 100644 lib/openspout/src/Reader/Common/Entity/Options.php delete mode 100644 lib/openspout/src/Reader/IteratorInterface.php delete mode 100644 lib/openspout/src/Reader/ODS/Creator/HelperFactory.php delete mode 100644 lib/openspout/src/Reader/ODS/Creator/InternalEntityFactory.php delete mode 100644 lib/openspout/src/Reader/ODS/Creator/ManagerFactory.php delete mode 100644 lib/openspout/src/Reader/ODS/Manager/OptionsManager.php create mode 100644 lib/openspout/src/Reader/ODS/Options.php create mode 100644 lib/openspout/src/Reader/SheetWithVisibilityInterface.php delete mode 100644 lib/openspout/src/Reader/XLSX/Creator/HelperFactory.php delete mode 100644 lib/openspout/src/Reader/XLSX/Creator/InternalEntityFactory.php delete mode 100644 lib/openspout/src/Reader/XLSX/Creator/ManagerFactory.php delete mode 100644 lib/openspout/src/Reader/XLSX/Manager/OptionsManager.php create mode 100644 lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php create mode 100644 lib/openspout/src/Reader/XLSX/Manager/StyleManagerInterface.php create mode 100644 lib/openspout/src/Reader/XLSX/Options.php create mode 100644 lib/openspout/src/Reader/XLSX/SheetHeaderReader.php create mode 100644 lib/openspout/src/Writer/AbstractWriter.php create mode 100644 lib/openspout/src/Writer/AbstractWriterMultiSheets.php create mode 100644 lib/openspout/src/Writer/AutoFilter.php delete mode 100644 lib/openspout/src/Writer/CSV/Manager/OptionsManager.php create mode 100644 lib/openspout/src/Writer/CSV/Options.php create mode 100644 lib/openspout/src/Writer/Common/AbstractOptions.php create mode 100644 lib/openspout/src/Writer/Common/ColumnWidth.php delete mode 100644 lib/openspout/src/Writer/Common/Creator/InternalEntityFactory.php delete mode 100644 lib/openspout/src/Writer/Common/Creator/ManagerFactoryInterface.php delete mode 100644 lib/openspout/src/Writer/Common/Creator/Style/BorderBuilder.php delete mode 100644 lib/openspout/src/Writer/Common/Creator/Style/StyleBuilder.php delete mode 100644 lib/openspout/src/Writer/Common/Creator/WriterEntityFactory.php delete mode 100644 lib/openspout/src/Writer/Common/Entity/Options.php rename lib/openspout/src/Writer/Common/Manager/{WorkbookManagerAbstract.php => AbstractWorkbookManager.php} (60%) delete mode 100644 lib/openspout/src/Writer/Common/Manager/CellManager.php delete mode 100644 lib/openspout/src/Writer/Common/Manager/ManagesCellSize.php delete mode 100644 lib/openspout/src/Writer/Common/Manager/RowManager.php rename lib/openspout/src/Writer/Common/Manager/Style/{StyleManager.php => AbstractStyleManager.php} (73%) rename lib/openspout/src/Writer/Common/Manager/Style/{StyleRegistry.php => AbstractStyleRegistry.php} (66%) delete mode 100644 lib/openspout/src/Writer/ODS/Creator/HelperFactory.php delete mode 100644 lib/openspout/src/Writer/ODS/Creator/ManagerFactory.php delete mode 100644 lib/openspout/src/Writer/ODS/Manager/OptionsManager.php create mode 100644 lib/openspout/src/Writer/ODS/Options.php delete mode 100644 lib/openspout/src/Writer/WriterAbstract.php delete mode 100644 lib/openspout/src/Writer/WriterMultiSheetsAbstract.php delete mode 100644 lib/openspout/src/Writer/XLSX/Creator/HelperFactory.php delete mode 100644 lib/openspout/src/Writer/XLSX/Creator/ManagerFactory.php create mode 100644 lib/openspout/src/Writer/XLSX/Manager/CommentsManager.php delete mode 100644 lib/openspout/src/Writer/XLSX/Manager/OptionsManager.php create mode 100644 lib/openspout/src/Writer/XLSX/MergeCell.php create mode 100644 lib/openspout/src/Writer/XLSX/Options.php diff --git a/lib/openspout/README.md b/lib/openspout/README.md index 0ceb637f69e..d7499fc08da 100644 --- a/lib/openspout/README.md +++ b/lib/openspout/README.md @@ -1,43 +1,27 @@ # OpenSpout [![Latest Stable Version](https://poser.pugx.org/openspout/openspout/v/stable)](https://packagist.org/packages/openspout/openspout) -[![Build Status](https://github.com/openspout/openspout/actions/workflows/ci.yml/badge.svg)](https://github.com/openspout/openspout/actions/workflows/ci.yml) -[![Code Coverage](https://codecov.io/gh/openspout/openspout/coverage.svg?branch=main)](https://codecov.io/gh/openspout/openspout?branch=main) [![Total Downloads](https://poser.pugx.org/openspout/openspout/downloads)](https://packagist.org/packages/openspout/openspout) +[![Build Status](https://github.com/openspout/openspout/actions/workflows/ci.yml/badge.svg)](https://github.com/openspout/openspout/actions/workflows/ci.yml) +[![Infection MSI](https://badge.stryker-mutator.io/github.com/openspout/openspout/4.x)](https://dashboard.stryker-mutator.io/reports/github.com/openspout/openspout/4.x) -OpenSpout is a community driven fork of `box/spout`, 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). +OpenSpout is a community driven fork of `box/spout`, 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). ## Documentation -Documentation can be found at [https://openspout.readthedocs.io/en/latest/](https://openspout.readthedocs.io/en/latest/). +Documentation can be found at [`docs/`](docs). -## Requirements - -* PHP version 7.3 or higher -* PHP extension `php_zip` enabled -* PHP extension `php_xmlreader` enabled - -## Upgrade from `box/spout` +## Upgrade from `box/spout:v3` to `openspout/openspout:v3` 1. Replace `box/spout` with `openspout/openspout` in your `composer.json` 2. Replace `Box\Spout` with `OpenSpout` in your code ## Upgrade guide -Version 3 introduced new functionality but also some breaking changes. If you want to upgrade your Spout codebase from version 2 please consult the [Upgrade guide](UPGRADE-3.0.md). - -## Running tests - -The `main` branch includes unit, functional and performance tests. -If you just want to check that everything is working as expected, executing the unit and functional tests is enough. - -* `phpunit` - runs unit and functional tests -* `phpunit --group perf-tests` - only runs the performance tests - -For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing). - -> Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout) +Version 4 introduced new functionality but also some breaking changes. If you want to upgrade your OpenSpout codebase +please consult the [Upgrade guide](UPGRADE.md). ## Copyright and License diff --git a/lib/openspout/src/Autoloader/Psr4Autoloader.php b/lib/openspout/src/Autoloader/Psr4Autoloader.php deleted file mode 100644 index 59eac315696..00000000000 --- a/lib/openspout/src/Autoloader/Psr4Autoloader.php +++ /dev/null @@ -1,147 +0,0 @@ -prefixes[$prefix])) { - $this->prefixes[$prefix] = []; - } - - // retain the base directory for the namespace prefix - if ($prepend) { - array_unshift($this->prefixes[$prefix], $baseDir); - } else { - $this->prefixes[$prefix][] = $baseDir; - } - } - - /** - * Loads the class file for a given class name. - * - * @param string $class the fully-qualified class name - * - * @return mixed the mapped file name on success, or boolean false on - * failure - */ - public function loadClass($class) - { - // the current namespace prefix - $prefix = $class; - - // work backwards through the namespace names of the fully-qualified - // class name to find a mapped file name - while (($pos = strrpos($prefix, '\\')) !== false) { - // retain the trailing namespace separator in the prefix - $prefix = substr($class, 0, $pos + 1); - - // the rest is the relative class name - $relativeClass = substr($class, $pos + 1); - - // try to load a mapped file for the prefix and relative class - $mappedFile = $this->loadMappedFile($prefix, $relativeClass); - if (false !== $mappedFile) { - return $mappedFile; - } - - // remove the trailing namespace separator for the next iteration - // of strrpos() - $prefix = rtrim($prefix, '\\'); - } - - // never found a mapped file - return false; - } - - /** - * Load the mapped file for a namespace prefix and relative class. - * - * @param string $prefix the namespace prefix - * @param string $relativeClass the relative class name - * - * @return mixed boolean false if no mapped file can be loaded, or the - * name of the mapped file that was loaded - */ - protected function loadMappedFile($prefix, $relativeClass) - { - // are there any base directories for this namespace prefix? - if (false === isset($this->prefixes[$prefix])) { - return false; - } - - // look through base directories for this namespace prefix - foreach ($this->prefixes[$prefix] as $baseDir) { - // replace the namespace prefix with the base directory, - // replace namespace separators with directory separators - // in the relative class name, append with .php - $file = $baseDir - .str_replace('\\', '/', $relativeClass) - .'.php'; - - // if the mapped file exists, require it - if ($this->requireFile($file)) { - // yes, we're done - return $file; - } - } - - // never found it - return false; - } - - /** - * If a file exists, require it from the file system. - * - * @param string $file the file to require - * - * @return bool true if the file exists, false if not - */ - protected function requireFile($file) - { - if (file_exists($file)) { - require $file; - - return true; - } - - return false; - } -} diff --git a/lib/openspout/src/Autoloader/autoload.php b/lib/openspout/src/Autoloader/autoload.php deleted file mode 100644 index a6768d4e849..00000000000 --- a/lib/openspout/src/Autoloader/autoload.php +++ /dev/null @@ -1,15 +0,0 @@ -register(); -$loader->addNamespace('OpenSpout', $srcBaseDirectory); diff --git a/lib/openspout/src/Common/Creator/HelperFactory.php b/lib/openspout/src/Common/Creator/HelperFactory.php deleted file mode 100644 index 55f1f57bf7e..00000000000 --- a/lib/openspout/src/Common/Creator/HelperFactory.php +++ /dev/null @@ -1,48 +0,0 @@ -setValue($value); $this->setStyle($style); } - /** - * @return string - */ - public function __toString() + abstract public function getValue(): null|bool|string|int|float|DateTimeInterface|DateInterval; + + final public function setStyle(?Style $style): void { - return (string) $this->getValue(); + $this->style = $style ?? new Style(); } - /** - * @param null|mixed $value - */ - public function setValue($value) - { - $this->value = $value; - $this->type = $this->detectType($value); - } - - /** - * @return null|mixed - */ - public function getValue() - { - return !$this->isError() ? $this->value : null; - } - - /** - * @return mixed - */ - public function getValueEvenIfError() - { - return $this->value; - } - - /** - * @param null|Style $style - */ - public function setStyle($style) - { - $this->style = $style ?: new Style(); - } - - /** - * @return Style - */ - public function getStyle() + final public function getStyle(): Style { return $this->style; } - /** - * @return null|int - */ - public function getType() + final public static function fromValue(null|bool|string|int|float|DateTimeInterface|DateInterval $value, ?Style $style = null): self { - return $this->type; - } - - /** - * @param int $type - */ - public function setType($type) - { - $this->type = $type; - } - - /** - * @return bool - */ - public function isBoolean() - { - return self::TYPE_BOOLEAN === $this->type; - } - - /** - * @return bool - */ - public function isEmpty() - { - return self::TYPE_EMPTY === $this->type; - } - - /** - * @return bool - */ - public function isNumeric() - { - return self::TYPE_NUMERIC === $this->type; - } - - /** - * @return bool - */ - public function isString() - { - return self::TYPE_STRING === $this->type; - } - - /** - * @return bool - */ - public function isDate() - { - return self::TYPE_DATE === $this->type; - } - - /** - * @return bool - */ - public function isFormula() - { - return self::TYPE_FORMULA === $this->type; - } - - /** - * @return bool - */ - public function isError() - { - return self::TYPE_ERROR === $this->type; - } - - /** - * Get the current value type. - * - * @param null|mixed $value - * - * @return int - */ - protected function detectType($value) - { - if (CellTypeHelper::isBoolean($value)) { - return self::TYPE_BOOLEAN; + if (\is_bool($value)) { + return new BooleanCell($value, $style); } - if (CellTypeHelper::isEmpty($value)) { - return self::TYPE_EMPTY; + if (null === $value || '' === $value) { + return new EmptyCell($value, $style); } - if (CellTypeHelper::isNumeric($value)) { - return self::TYPE_NUMERIC; + if (\is_int($value) || \is_float($value)) { + return new NumericCell($value, $style); } - if (CellTypeHelper::isDateTimeOrDateInterval($value)) { - return self::TYPE_DATE; + if ($value instanceof DateTimeInterface) { + return new DateTimeCell($value, $style); } - if (CellTypeHelper::isFormula($value)) { - return self::TYPE_FORMULA; + if ($value instanceof DateInterval) { + return new DateIntervalCell($value, $style); } - if (CellTypeHelper::isNonEmptyString($value)) { - return self::TYPE_STRING; + if (isset($value[0]) && '=' === $value[0]) { + return new FormulaCell($value, $style); } - return self::TYPE_ERROR; + return new StringCell($value, $style); } } diff --git a/lib/openspout/src/Common/Entity/Cell/BooleanCell.php b/lib/openspout/src/Common/Entity/Cell/BooleanCell.php new file mode 100644 index 00000000000..120a72b8a00 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/BooleanCell.php @@ -0,0 +1,24 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): bool + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Cell/DateIntervalCell.php b/lib/openspout/src/Common/Entity/Cell/DateIntervalCell.php new file mode 100644 index 00000000000..75b5eaf5916 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/DateIntervalCell.php @@ -0,0 +1,25 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): DateInterval + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Cell/DateTimeCell.php b/lib/openspout/src/Common/Entity/Cell/DateTimeCell.php new file mode 100644 index 00000000000..7065abf5445 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/DateTimeCell.php @@ -0,0 +1,25 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): DateTimeInterface + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Cell/EmptyCell.php b/lib/openspout/src/Common/Entity/Cell/EmptyCell.php new file mode 100644 index 00000000000..8d3e41399c4 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/EmptyCell.php @@ -0,0 +1,24 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): ?string + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Cell/ErrorCell.php b/lib/openspout/src/Common/Entity/Cell/ErrorCell.php new file mode 100644 index 00000000000..2c1d677bd99 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/ErrorCell.php @@ -0,0 +1,29 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): ?string + { + return null; + } + + public function getRawValue(): string + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Cell/FormulaCell.php b/lib/openspout/src/Common/Entity/Cell/FormulaCell.php new file mode 100644 index 00000000000..97daa838cb6 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/FormulaCell.php @@ -0,0 +1,24 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): string + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Cell/NumericCell.php b/lib/openspout/src/Common/Entity/Cell/NumericCell.php new file mode 100644 index 00000000000..810fe1113d5 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/NumericCell.php @@ -0,0 +1,24 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): int|float + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Cell/StringCell.php b/lib/openspout/src/Common/Entity/Cell/StringCell.php new file mode 100644 index 00000000000..94ffa8fa6d2 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Cell/StringCell.php @@ -0,0 +1,24 @@ +value = $value; + parent::__construct($style); + } + + public function getValue(): string + { + return $this->value; + } +} diff --git a/lib/openspout/src/Common/Entity/Comment/Comment.php b/lib/openspout/src/Common/Entity/Comment/Comment.php new file mode 100644 index 00000000000..47753df4e96 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Comment/Comment.php @@ -0,0 +1,47 @@ +textRuns[] = $textRun; + } + + /** + * The TextRuns for this comment. + * + * @return TextRun[] + */ + public function getTextRuns(): array + { + return $this->textRuns; + } +} diff --git a/lib/openspout/src/Common/Entity/Comment/TextRun.php b/lib/openspout/src/Common/Entity/Comment/TextRun.php new file mode 100644 index 00000000000..16bc23eb5da --- /dev/null +++ b/lib/openspout/src/Common/Entity/Comment/TextRun.php @@ -0,0 +1,23 @@ +text = $text; + } +} diff --git a/lib/openspout/src/Common/Entity/Row.php b/lib/openspout/src/Common/Entity/Row.php index db6481fc4df..6443ea19bf7 100644 --- a/lib/openspout/src/Common/Entity/Row.php +++ b/lib/openspout/src/Common/Entity/Row.php @@ -1,39 +1,34 @@ setCells($cells) @@ -41,20 +36,30 @@ class Row ; } + /** + * @param list $cellValues + */ + public static function fromValues(array $cellValues = [], ?Style $rowStyle = null): self + { + $cells = array_map(static function (null|bool|string|int|float|DateTimeInterface|DateInterval $cellValue): Cell { + return Cell::fromValue($cellValue); + }, $cellValues); + + return new self($cells, $rowStyle); + } + /** * @return Cell[] $cells */ - public function getCells() + public function getCells(): array { return $this->cells; } /** * @param Cell[] $cells - * - * @return Row */ - public function setCells(array $cells) + public function setCells(array $cells): self { $this->cells = []; foreach ($cells as $cell) { @@ -64,90 +69,52 @@ class Row return $this; } - /** - * @param int $cellIndex - * - * @return Row - */ - public function setCellAtIndex(Cell $cell, $cellIndex) + public function setCellAtIndex(Cell $cell, int $cellIndex): self { $this->cells[$cellIndex] = $cell; return $this; } - /** - * @param int $cellIndex - * - * @return null|Cell - */ - public function getCellAtIndex($cellIndex) + public function getCellAtIndex(int $cellIndex): ?Cell { return $this->cells[$cellIndex] ?? null; } - /** - * @return Row - */ - public function addCell(Cell $cell) + public function addCell(Cell $cell): self { $this->cells[] = $cell; return $this; } - /** - * @return int - */ - public function getNumCells() + public function getNumCells(): int { // When using "setCellAtIndex", it's possible to // have "$this->cells" contain holes. - if (empty($this->cells)) { + if ([] === $this->cells) { return 0; } return max(array_keys($this->cells)) + 1; } - /** - * @return Style - */ - public function getStyle() + public function getStyle(): Style { return $this->style; } - /** - * @param null|Style $style - * - * @return Row - */ - public function setStyle($style) + public function setStyle(?Style $style): self { - $this->style = $style ?: new Style(); + $this->style = $style ?? new Style(); return $this; } - /** - * @return array The row values, as array - */ - public function toArray() - { - return array_map(function (Cell $cell) { - return $cell->getValue(); - }, $this->cells); - } - /** * Set row height. - * - * @param string $height - * - * @return Row */ - public function setHeight($height) + public function setHeight(float $height): self { $this->height = $height; @@ -156,11 +123,34 @@ class Row /** * Returns row height. - * - * @return string */ - public function getHeight() + public function getHeight(): float { return $this->height; } + + /** + * @return list The row values, as array + */ + public function toArray(): array + { + return array_map(static function (Cell $cell): null|bool|string|int|float|DateTimeInterface|DateInterval { + return $cell->getValue(); + }, $this->cells); + } + + /** + * Detect whether a row is considered empty. + * An empty row has all of its cells empty. + */ + public function isEmpty(): bool + { + foreach ($this->cells as $cell) { + if (!$cell instanceof Cell\EmptyCell) { + return false; + } + } + + return true; + } } diff --git a/lib/openspout/src/Common/Entity/Style/Border.php b/lib/openspout/src/Common/Entity/Style/Border.php index 53b59dab756..487edaa3234 100644 --- a/lib/openspout/src/Common/Entity/Style/Border.php +++ b/lib/openspout/src/Common/Entity/Style/Border.php @@ -1,8 +1,10 @@ */ + private array $parts; - public function __construct(array $borderParts = []) + public function __construct(BorderPart ...$borderParts) { - $this->setParts($borderParts); - } - - /** - * @param string $name The name of the border part - * - * @return null|BorderPart - */ - public function getPart($name) - { - return $this->hasPart($name) ? $this->parts[$name] : null; - } - - /** - * @param string $name The name of the border part - * - * @return bool - */ - public function hasPart($name) - { - return isset($this->parts[$name]); - } - - /** - * @return array - */ - public function getParts() - { - return $this->parts; - } - - /** - * Set BorderParts. - * - * @param array $parts - */ - public function setParts($parts) - { - $this->parts = []; - foreach ($parts as $part) { - $this->addPart($part); + foreach ($borderParts as $borderPart) { + $this->parts[$borderPart->getName()] = $borderPart; } } - /** - * @return Border - */ - public function addPart(BorderPart $borderPart) + public function getPart(string $name): ?BorderPart { - $this->parts[$borderPart->getName()] = $borderPart; + return $this->parts[$name] ?? null; + } - return $this; + /** + * @return array + */ + public function getParts(): array + { + return $this->parts; } } diff --git a/lib/openspout/src/Common/Entity/Style/BorderPart.php b/lib/openspout/src/Common/Entity/Style/BorderPart.php index afee7767fc3..e5819e36362 100644 --- a/lib/openspout/src/Common/Entity/Style/BorderPart.php +++ b/lib/openspout/src/Common/Entity/Style/BorderPart.php @@ -1,181 +1,90 @@ setName($name); - $this->setColor($color); - $this->setWidth($width); - $this->setStyle($style); + public function __construct( + string $name, + string $color = Color::BLACK, + string $width = Border::WIDTH_MEDIUM, + string $style = Border::STYLE_SOLID + ) { + if (!\in_array($name, self::allowedNames, true)) { + throw new InvalidNameException($name); + } + if (!\in_array($style, self::allowedStyles, true)) { + throw new InvalidStyleException($style); + } + if (!\in_array($width, self::allowedWidths, true)) { + throw new InvalidWidthException($width); + } + + $this->name = $name; + $this->color = $color; + $this->width = $width; + $this->style = $style; } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * @param string $name The name of the border part @see BorderPart::$allowedNames - * - * @throws InvalidNameException - */ - public function setName($name) - { - if (!\in_array($name, self::$allowedNames, true)) { - throw new InvalidNameException($name); - } - $this->name = $name; - } - - /** - * @return string - */ - public function getStyle() + public function getStyle(): string { return $this->style; } - /** - * @param string $style The style of the border part @see BorderPart::$allowedStyles - * - * @throws InvalidStyleException - */ - public function setStyle($style) - { - if (!\in_array($style, self::$allowedStyles, true)) { - throw new InvalidStyleException($style); - } - $this->style = $style; - } - - /** - * @return string - */ - public function getColor() + public function getColor(): string { return $this->color; } - /** - * @param string $color The color of the border part @see Color::rgb() - */ - public function setColor($color) - { - $this->color = $color; - } - - /** - * @return string - */ - public function getWidth() + public function getWidth(): string { return $this->width; } - - /** - * @param string $width The width of the border part @see BorderPart::$allowedWidths - * - * @throws InvalidWidthException - */ - public function setWidth($width) - { - if (!\in_array($width, self::$allowedWidths, true)) { - throw new InvalidWidthException($width); - } - $this->width = $width; - } - - /** - * @return array - */ - public static function getAllowedStyles() - { - return self::$allowedStyles; - } - - /** - * @return array - */ - public static function getAllowedNames() - { - return self::$allowedNames; - } - - /** - * @return array - */ - public static function getAllowedWidths() - { - return self::$allowedWidths; - } } diff --git a/lib/openspout/src/Common/Entity/Style/CellAlignment.php b/lib/openspout/src/Common/Entity/Style/CellAlignment.php index ecfa6d7f728..48d4da3ad4c 100644 --- a/lib/openspout/src/Common/Entity/Style/CellAlignment.php +++ b/lib/openspout/src/Common/Entity/Style/CellAlignment.php @@ -1,18 +1,20 @@ 1, self::RIGHT => 1, self::CENTER => 1, @@ -20,12 +22,10 @@ abstract class CellAlignment ]; /** - * @param string $cellAlignment - * * @return bool Whether the given cell alignment is valid */ - public static function isValid($cellAlignment) + public static function isValid(string $cellAlignment): bool { - return isset(self::$VALID_ALIGNMENTS[$cellAlignment]); + return isset(self::VALID_ALIGNMENTS[$cellAlignment]); } } diff --git a/lib/openspout/src/Common/Entity/Style/CellVerticalAlignment.php b/lib/openspout/src/Common/Entity/Style/CellVerticalAlignment.php new file mode 100644 index 00000000000..2a38fdf6933 --- /dev/null +++ b/lib/openspout/src/Common/Entity/Style/CellVerticalAlignment.php @@ -0,0 +1,37 @@ + 1, + self::BASELINE => 1, + self::BOTTOM => 1, + self::CENTER => 1, + self::DISTRIBUTED => 1, + self::JUSTIFY => 1, + self::TOP => 1, + ]; + + /** + * @return bool Whether the given cell vertical alignment is valid + */ + public static function isValid(string $cellVerticalAlignment): bool + { + return isset(self::VALID_ALIGNMENTS[$cellVerticalAlignment]); + } +} diff --git a/lib/openspout/src/Common/Entity/Style/Color.php b/lib/openspout/src/Common/Entity/Style/Color.php index cd9bdfd5cfb..4dd5e3e1b48 100644 --- a/lib/openspout/src/Common/Entity/Style/Color.php +++ b/lib/openspout/src/Common/Entity/Style/Color.php @@ -1,5 +1,7 @@ 255) { + if ($colorComponent < 0 || $colorComponent > 255) { throw new InvalidColorException("The RGB components must be between 0 and 255. Received: {$colorComponent}"); } } @@ -79,7 +81,7 @@ abstract class Color * * @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d" */ - protected static function convertColorComponentToHex($colorComponent) + private static function convertColorComponentToHex(int $colorComponent): string { return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT); } diff --git a/lib/openspout/src/Common/Entity/Style/Style.php b/lib/openspout/src/Common/Entity/Style/Style.php index 28f1597050a..22e4c7ec56b 100644 --- a/lib/openspout/src/Common/Entity/Style/Style.php +++ b/lib/openspout/src/Common/Entity/Style/Style.php @@ -1,159 +1,154 @@ id); + return $this->id; } - /** - * @param int $id - * - * @return Style - */ - public function setId($id) + public function setId(int $id): self { $this->id = $id; return $this; } - /** - * @return null|Border - */ - public function getBorder() + public function getBorder(): ?Border { return $this->border; } - /** - * @return Style - */ - public function setBorder(Border $border) + public function setBorder(Border $border): self { - $this->shouldApplyBorder = true; $this->border = $border; $this->isEmpty = false; return $this; } - /** - * @return bool - */ - public function shouldApplyBorder() - { - return $this->shouldApplyBorder; - } - - /** - * @return bool - */ - public function isFontBold() + public function isFontBold(): bool { return $this->fontBold; } - /** - * @return Style - */ - public function setFontBold() + public function setFontBold(): self { $this->fontBold = true; $this->hasSetFontBold = true; @@ -163,26 +158,17 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetFontBold() + public function hasSetFontBold(): bool { return $this->hasSetFontBold; } - /** - * @return bool - */ - public function isFontItalic() + public function isFontItalic(): bool { return $this->fontItalic; } - /** - * @return Style - */ - public function setFontItalic() + public function setFontItalic(): self { $this->fontItalic = true; $this->hasSetFontItalic = true; @@ -192,26 +178,17 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetFontItalic() + public function hasSetFontItalic(): bool { return $this->hasSetFontItalic; } - /** - * @return bool - */ - public function isFontUnderline() + public function isFontUnderline(): bool { return $this->fontUnderline; } - /** - * @return Style - */ - public function setFontUnderline() + public function setFontUnderline(): self { $this->fontUnderline = true; $this->hasSetFontUnderline = true; @@ -221,26 +198,17 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetFontUnderline() + public function hasSetFontUnderline(): bool { return $this->hasSetFontUnderline; } - /** - * @return bool - */ - public function isFontStrikethrough() + public function isFontStrikethrough(): bool { return $this->fontStrikethrough; } - /** - * @return Style - */ - public function setFontStrikethrough() + public function setFontStrikethrough(): self { $this->fontStrikethrough = true; $this->hasSetFontStrikethrough = true; @@ -250,28 +218,20 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetFontStrikethrough() + public function hasSetFontStrikethrough(): bool { return $this->hasSetFontStrikethrough; } - /** - * @return int - */ - public function getFontSize() + public function getFontSize(): int { return $this->fontSize; } /** * @param int $fontSize Font size, in pixels - * - * @return Style */ - public function setFontSize($fontSize) + public function setFontSize(int $fontSize): self { $this->fontSize = $fontSize; $this->hasSetFontSize = true; @@ -281,18 +241,12 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetFontSize() + public function hasSetFontSize(): bool { return $this->hasSetFontSize; } - /** - * @return string - */ - public function getFontColor() + public function getFontColor(): string { return $this->fontColor; } @@ -301,10 +255,8 @@ class Style * Sets the font color. * * @param string $fontColor ARGB color (@see Color) - * - * @return Style */ - public function setFontColor($fontColor) + public function setFontColor(string $fontColor): self { $this->fontColor = $fontColor; $this->hasSetFontColor = true; @@ -314,28 +266,20 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetFontColor() + public function hasSetFontColor(): bool { return $this->hasSetFontColor; } - /** - * @return string - */ - public function getFontName() + public function getFontName(): string { return $this->fontName; } /** * @param string $fontName Name of the font to use - * - * @return Style */ - public function setFontName($fontName) + public function setFontName(string $fontName): self { $this->fontName = $fontName; $this->hasSetFontName = true; @@ -345,29 +289,30 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetFontName() + public function hasSetFontName(): bool { return $this->hasSetFontName; } - /** - * @return string - */ - public function getCellAlignment() + public function getCellAlignment(): string { return $this->cellAlignment; } + public function getCellVerticalAlignment(): string + { + return $this->cellVerticalAlignment; + } + /** * @param string $cellAlignment The cell alignment - * - * @return Style */ - public function setCellAlignment($cellAlignment) + public function setCellAlignment(string $cellAlignment): self { + if (!CellAlignment::isValid($cellAlignment)) { + throw new InvalidArgumentException('Invalid cell alignment value'); + } + $this->cellAlignment = $cellAlignment; $this->hasSetCellAlignment = true; $this->shouldApplyCellAlignment = true; @@ -377,35 +322,54 @@ class Style } /** - * @return bool + * @param string $cellVerticalAlignment The cell vertical alignment */ - public function hasSetCellAlignment() + public function setCellVerticalAlignment(string $cellVerticalAlignment): self + { + if (!CellVerticalAlignment::isValid($cellVerticalAlignment)) { + throw new InvalidArgumentException('Invalid cell vertical alignment value'); + } + + $this->cellVerticalAlignment = $cellVerticalAlignment; + $this->hasSetCellVerticalAlignment = true; + $this->shouldApplyCellVerticalAlignment = true; + $this->isEmpty = false; + + return $this; + } + + public function hasSetCellAlignment(): bool { return $this->hasSetCellAlignment; } + public function hasSetCellVerticalAlignment(): bool + { + return $this->hasSetCellVerticalAlignment; + } + /** * @return bool Whether specific cell alignment should be applied */ - public function shouldApplyCellAlignment() + public function shouldApplyCellAlignment(): bool { return $this->shouldApplyCellAlignment; } - /** - * @return bool - */ - public function shouldWrapText() + public function shouldApplyCellVerticalAlignment(): bool + { + return $this->shouldApplyCellVerticalAlignment; + } + + public function shouldWrapText(): bool { return $this->shouldWrapText; } /** * @param bool $shouldWrap Should the text be wrapped - * - * @return Style */ - public function setShouldWrapText($shouldWrap = true) + public function setShouldWrapText(bool $shouldWrap = true): self { $this->shouldWrapText = $shouldWrap; $this->hasSetWrapText = true; @@ -414,10 +378,7 @@ class Style return $this; } - /** - * @return bool - */ - public function hasSetWrapText() + public function hasSetWrapText(): bool { return $this->hasSetWrapText; } @@ -425,7 +386,7 @@ class Style /** * @return bool Whether specific font properties should be applied */ - public function shouldApplyFont() + public function shouldApplyFont(): bool { return $this->shouldApplyFont; } @@ -434,66 +395,36 @@ class Style * Sets the background color. * * @param string $color ARGB color (@see Color) - * - * @return Style */ - public function setBackgroundColor($color) + public function setBackgroundColor(string $color): self { - $this->hasSetBackgroundColor = true; $this->backgroundColor = $color; $this->isEmpty = false; return $this; } - /** - * @return null|string - */ - public function getBackgroundColor() + public function getBackgroundColor(): ?string { return $this->backgroundColor; } - /** - * @return bool Whether the background color should be applied - */ - public function shouldApplyBackgroundColor() - { - return $this->hasSetBackgroundColor; - } - /** * Sets format. - * - * @param string $format - * - * @return Style */ - public function setFormat($format) + public function setFormat(string $format): self { - $this->hasSetFormat = true; $this->format = $format; $this->isEmpty = false; return $this; } - /** - * @return null|string - */ - public function getFormat() + public function getFormat(): ?string { return $this->format; } - /** - * @return bool Whether format should be applied - */ - public function shouldApplyFormat() - { - return $this->hasSetFormat; - } - public function isRegistered(): bool { return $this->isRegistered; @@ -505,12 +436,6 @@ class Style $this->isRegistered = true; } - public function unmarkAsRegistered(): void - { - $this->setId(0); - $this->isRegistered = false; - } - public function isEmpty(): bool { return $this->isEmpty; @@ -518,12 +443,8 @@ class Style /** * Sets should shrink to fit. - * - * @param bool $shrinkToFit - * - * @return Style */ - public function setShouldShrinkToFit($shrinkToFit = true) + public function setShouldShrinkToFit(bool $shrinkToFit = true): self { $this->hasSetShrinkToFit = true; $this->shouldShrinkToFit = $shrinkToFit; @@ -534,15 +455,12 @@ class Style /** * @return bool Whether format should be applied */ - public function shouldShrinkToFit() + public function shouldShrinkToFit(): bool { return $this->shouldShrinkToFit; } - /** - * @return bool - */ - public function hasSetShrinkToFit() + public function hasSetShrinkToFit(): bool { return $this->hasSetShrinkToFit; } diff --git a/lib/openspout/src/Common/Exception/EncodingConversionException.php b/lib/openspout/src/Common/Exception/EncodingConversionException.php index ef0cdc6d80b..0a0fce8847b 100644 --- a/lib/openspout/src/Common/Exception/EncodingConversionException.php +++ b/lib/openspout/src/Common/Exception/EncodingConversionException.php @@ -1,7 +1,9 @@ Map representing the encodings supporting BOMs (key) and their associated BOM (value) */ + private array $supportedEncodingsWithBom; - /** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */ - protected $supportedEncodingsWithBom; + private bool $canUseIconv; - /** - * @param \OpenSpout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper - */ - public function __construct($globalFunctionsHelper) + private bool $canUseMbString; + + public function __construct(bool $canUseIconv, bool $canUseMbString) { - $this->globalFunctionsHelper = $globalFunctionsHelper; + $this->canUseIconv = $canUseIconv; + $this->canUseMbString = $canUseMbString; $this->supportedEncodingsWithBom = [ self::ENCODING_UTF8 => self::BOM_UTF8, @@ -45,6 +51,14 @@ class EncodingHelper ]; } + public static function factory(): self + { + return new self( + \function_exists('iconv'), + \function_exists('mb_convert_encoding'), + ); + } + /** * Returns the number of bytes to use as offset in order to skip the BOM. * @@ -53,7 +67,7 @@ class EncodingHelper * * @return int Bytes offset to apply to skip the BOM (0 means no BOM) */ - public function getBytesOffsetToSkipBOM($filePointer, $encoding) + public function getBytesOffsetToSkipBOM($filePointer, string $encoding): int { $byteOffsetToSkipBom = 0; @@ -73,11 +87,11 @@ class EncodingHelper * @param string $string Non UTF-8 string to be converted * @param string $sourceEncoding The encoding used to encode the source string * - * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed - * * @return string The converted, UTF-8 string + * + * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed */ - public function attemptConversionToUTF8($string, $sourceEncoding) + public function attemptConversionToUTF8(?string $string, string $sourceEncoding): ?string { return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8); } @@ -88,11 +102,11 @@ class EncodingHelper * @param string $string UTF-8 string to be converted * @param string $targetEncoding The encoding the string should be re-encoded into * - * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed - * * @return string The converted string, encoded with the given encoding + * + * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed */ - public function attemptConversionFromUTF8($string, $targetEncoding) + public function attemptConversionFromUTF8(?string $string, string $targetEncoding): ?string { return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding); } @@ -105,17 +119,17 @@ class EncodingHelper * * @return bool TRUE if the file has a BOM, FALSE otherwise */ - protected function hasBOM($filePointer, $encoding) + private function hasBOM($filePointer, string $encoding): bool { $hasBOM = false; - $this->globalFunctionsHelper->rewind($filePointer); + rewind($filePointer); if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) { $potentialBom = $this->supportedEncodingsWithBom[$encoding]; $numBytesInBom = \strlen($potentialBom); - $hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom); + $hasBOM = (fgets($filePointer, $numBytesInBom + 1) === $potentialBom); } return $hasBOM; @@ -129,23 +143,45 @@ class EncodingHelper * @param string $sourceEncoding The encoding used to encode the source string * @param string $targetEncoding The encoding the string should be re-encoded into * - * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed - * * @return string The converted string, encoded with the given encoding + * + * @throws \OpenSpout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed */ - protected function attemptConversion($string, $sourceEncoding, $targetEncoding) + private function attemptConversion(?string $string, string $sourceEncoding, string $targetEncoding): ?string { // if source and target encodings are the same, it's a no-op - if ($sourceEncoding === $targetEncoding) { + if (null === $string || $sourceEncoding === $targetEncoding) { return $string; } $convertedString = null; - if ($this->canUseIconv()) { - $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding); - } elseif ($this->canUseMbString()) { - $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding); + if ($this->canUseIconv) { + set_error_handler(static function (): bool { + return true; + }); + + $convertedString = iconv($sourceEncoding, $targetEncoding, $string); + + restore_error_handler(); + } elseif ($this->canUseMbString) { + $errorMessage = null; + set_error_handler(static function ($nr, $message) use (&$errorMessage): bool { + $errorMessage = $message; // @codeCoverageIgnore + + return true; // @codeCoverageIgnore + }); + + try { + $convertedString = mb_convert_encoding($string, $targetEncoding, $sourceEncoding); + } catch (Error $error) { + $errorMessage = $error->getMessage(); + } + + restore_error_handler(); + if (null !== $errorMessage) { + $convertedString = false; + } } else { throw new EncodingConversionException("The conversion from {$sourceEncoding} to {$targetEncoding} is not supported. Please install \"iconv\" or \"PHP Intl\"."); } @@ -156,25 +192,4 @@ class EncodingHelper return $convertedString; } - - /** - * Returns whether "iconv" can be used. - * - * @return bool TRUE if "iconv" is available and can be used, FALSE otherwise - */ - protected function canUseIconv() - { - return $this->globalFunctionsHelper->function_exists('iconv'); - } - - /** - * Returns whether "mb_string" functions can be used. - * These functions come with the PHP Intl package. - * - * @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise - */ - protected function canUseMbString() - { - return $this->globalFunctionsHelper->function_exists('mb_convert_encoding'); - } } diff --git a/lib/openspout/src/Common/Helper/Escaper/CSV.php b/lib/openspout/src/Common/Helper/Escaper/CSV.php deleted file mode 100644 index d68199af89c..00000000000 --- a/lib/openspout/src/Common/Helper/Escaper/CSV.php +++ /dev/null @@ -1,37 +0,0 @@ -', '&') as well as - // single/double quotes (for XML attributes) need to be encoded. - if (\defined('ENT_DISALLOWED')) { - /** - * 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced. - * Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor. - * - * @see https://github.com/box/spout/issues/329 - */ - $replacedString = htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8'); - } else { - // We are on hhvm or any other engine that does not support ENT_DISALLOWED. - $escapedString = htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); - - // control characters values are from 0 to 1F (hex values) in the ASCII table - // some characters should not be escaped though: "\t", "\r" and "\n". - $regexPattern = '[\x00-\x08'. - // skipping "\t" (0x9) and "\n" (0xA) - '\x0B-\x0C'. - // skipping "\r" (0xD) - '\x0E-\x1F]'; - $replacedString = preg_replace("/{$regexPattern}/", '�', $escapedString); - } - - return $replacedString; + /* + * 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced. + * Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor. + * + * @see https://github.com/box/spout/issues/329 + */ + return htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8'); } /** @@ -50,7 +34,7 @@ class ODS implements EscaperInterface * * @return string The unescaped string */ - public function unescape($string) + public function unescape(string $string): string { // ============== // = WARNING = diff --git a/lib/openspout/src/Common/Helper/Escaper/XLSX.php b/lib/openspout/src/Common/Helper/Escaper/XLSX.php index 16bb162a38f..130ee5f4973 100644 --- a/lib/openspout/src/Common/Helper/Escaper/XLSX.php +++ b/lib/openspout/src/Common/Helper/Escaper/XLSX.php @@ -1,23 +1,25 @@ initIfNeeded(); @@ -43,7 +45,7 @@ class XLSX implements EscaperInterface * * @return string The unescaped string */ - public function unescape($string) + public function unescape(string $string): string { $this->initIfNeeded(); @@ -59,7 +61,7 @@ class XLSX implements EscaperInterface /** * Initializes the control characters if not already done. */ - protected function initIfNeeded() + private function initIfNeeded(): void { if (!$this->isAlreadyInitialized) { $this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern(); @@ -73,7 +75,7 @@ class XLSX implements EscaperInterface /** * @return string Regex pattern containing all escapable control characters */ - protected function getEscapableControlCharactersPattern() + private function getEscapableControlCharactersPattern(): string { // control characters values are from 0 to 1F (hex values) in the ASCII table // some characters should not be escaped though: "\t", "\r" and "\n". @@ -95,14 +97,14 @@ class XLSX implements EscaperInterface * * @return string[] */ - protected function getControlCharactersEscapingMap() + private function getControlCharactersEscapingMap(): array { $controlCharactersEscapingMap = []; // control characters values are from 0 to 1F (hex values) in the ASCII table for ($charValue = 0x00; $charValue <= 0x1F; ++$charValue) { $character = \chr($charValue); - if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) { + if (1 === preg_match("/{$this->escapableControlCharactersPattern}/", $character)) { $charHexValue = dechex($charValue); $escapedChar = '_x'.sprintf('%04s', strtoupper($charHexValue)).'_'; $controlCharactersEscapingMap[$escapedChar] = $character; @@ -124,15 +126,13 @@ class XLSX implements EscaperInterface * @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 * * @param string $string String to escape - * - * @return string */ - protected function escapeControlCharacters($string) + private function escapeControlCharacters(string $string): string { $escapedString = $this->escapeEscapeCharacter($string); // if no control characters - if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) { + if (1 !== preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) { return $escapedString; } @@ -148,7 +148,7 @@ class XLSX implements EscaperInterface * * @return string The escaped string */ - protected function escapeEscapeCharacter($string) + private function escapeEscapeCharacter(string $string): string { return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string); } @@ -165,10 +165,8 @@ class XLSX implements EscaperInterface * @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 * * @param string $string String to unescape - * - * @return string */ - protected function unescapeControlCharacters($string) + private function unescapeControlCharacters(string $string): string { $unescapedString = $string; @@ -187,7 +185,7 @@ class XLSX implements EscaperInterface * * @return string The unescaped string */ - protected function unescapeEscapeCharacter($string) + private function unescapeEscapeCharacter(string $string): string { return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string); } diff --git a/lib/openspout/src/Common/Helper/FileSystemHelper.php b/lib/openspout/src/Common/Helper/FileSystemHelper.php index 130f1f86e47..0fb223bb274 100644 --- a/lib/openspout/src/Common/Helper/FileSystemHelper.php +++ b/lib/openspout/src/Common/Helper/FileSystemHelper.php @@ -1,24 +1,34 @@ baseFolderRealPath = realpath($baseFolderPath); + $realpath = realpath($baseFolderPath); + \assert(false !== $realpath); + $this->baseFolderRealPath = $realpath; + } + + public function getBaseFolderRealPath(): string + { + return $this->baseFolderRealPath; } /** @@ -27,19 +37,27 @@ class FileSystemHelper implements FileSystemHelperInterface * @param string $parentFolderPath The parent folder path under which the folder is going to be created * @param string $folderName The name of the folder to create * - * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder - * * @return string Path of the created folder + * + * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder */ - public function createFolder($parentFolderPath, $folderName) + public function createFolder(string $parentFolderPath, string $folderName): string { $this->throwIfOperationNotInBaseFolder($parentFolderPath); - $folderPath = $parentFolderPath.'/'.$folderName; + $folderPath = $parentFolderPath.\DIRECTORY_SEPARATOR.$folderName; + $errorMessage = ''; + set_error_handler(static function ($nr, $message) use (&$errorMessage): bool { + $errorMessage = $message; + + return true; + }); $wasCreationSuccessful = mkdir($folderPath, 0777, true); + restore_error_handler(); + if (!$wasCreationSuccessful) { - throw new IOException("Unable to create folder: {$folderPath}"); + throw new IOException("Unable to create folder: {$folderPath} - {$errorMessage}"); } return $folderPath; @@ -53,19 +71,27 @@ class FileSystemHelper implements FileSystemHelperInterface * @param string $fileName The name of the file to create * @param string $fileContents The contents of the file to create * - * @throws \OpenSpout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder - * * @return string Path of the created file + * + * @throws \OpenSpout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder */ - public function createFileWithContents($parentFolderPath, $fileName, $fileContents) + public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string { $this->throwIfOperationNotInBaseFolder($parentFolderPath); - $filePath = $parentFolderPath.'/'.$fileName; + $filePath = $parentFolderPath.\DIRECTORY_SEPARATOR.$fileName; + $errorMessage = ''; + set_error_handler(static function ($nr, $message) use (&$errorMessage): bool { + $errorMessage = $message; + + return true; + }); $wasCreationSuccessful = file_put_contents($filePath, $fileContents); + restore_error_handler(); + if (false === $wasCreationSuccessful) { - throw new IOException("Unable to create file: {$filePath}"); + throw new IOException("Unable to create file: {$filePath} - {$errorMessage}"); } return $filePath; @@ -78,7 +104,7 @@ class FileSystemHelper implements FileSystemHelperInterface * * @throws \OpenSpout\Common\Exception\IOException If the file path is not inside of the base folder */ - public function deleteFile($filePath) + public function deleteFile(string $filePath): void { $this->throwIfOperationNotInBaseFolder($filePath); @@ -94,13 +120,13 @@ class FileSystemHelper implements FileSystemHelperInterface * * @throws \OpenSpout\Common\Exception\IOException If the folder path is not inside of the base folder */ - public function deleteFolderRecursively($folderPath) + public function deleteFolderRecursively(string $folderPath): void { $this->throwIfOperationNotInBaseFolder($folderPath); - $itemIterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST + $itemIterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST ); foreach ($itemIterator as $item) { @@ -124,13 +150,13 @@ class FileSystemHelper implements FileSystemHelperInterface * @throws \OpenSpout\Common\Exception\IOException If the folder where the I/O operation should occur * is not inside the base folder or the base folder does not exist */ - protected function throwIfOperationNotInBaseFolder(string $operationFolderPath) + private function throwIfOperationNotInBaseFolder(string $operationFolderPath): void { $operationFolderRealPath = realpath($operationFolderPath); - if (!$this->baseFolderRealPath) { - throw new IOException("The base folder path is invalid: {$this->baseFolderRealPath}"); + if (false === $operationFolderRealPath) { + throw new IOException("Folder not found: {$operationFolderRealPath}"); } - $isInBaseFolder = (0 === strpos($operationFolderRealPath, $this->baseFolderRealPath)); + $isInBaseFolder = str_starts_with($operationFolderRealPath, $this->baseFolderRealPath); if (!$isInBaseFolder) { throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}"); } diff --git a/lib/openspout/src/Common/Helper/FileSystemHelperInterface.php b/lib/openspout/src/Common/Helper/FileSystemHelperInterface.php index 90082d28bf9..a6d12d89fa6 100644 --- a/lib/openspout/src/Common/Helper/FileSystemHelperInterface.php +++ b/lib/openspout/src/Common/Helper/FileSystemHelperInterface.php @@ -1,10 +1,11 @@ = 70400 ? '' : "\0"; - - return fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter); - } - - /** - * Wrapper around global function fputcsv(). - * - * @see fputcsv() - * - * @param resource $handle - * @param null|string $delimiter - * @param null|string $enclosure - * - * @return false|int - */ - public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null) - { - /** - * PHP uses '\' as the default escape character. This is not RFC-4180 compliant... - * To fix that, simply disable the escape character. - * - * @see https://bugs.php.net/bug.php?id=43225 - * @see http://tools.ietf.org/html/rfc4180 - */ - $escapeCharacter = \PHP_VERSION_ID >= 70400 ? '' : "\0"; - - return fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter); - } - - /** - * Wrapper around global function fwrite(). - * - * @see fwrite() - * - * @param resource $handle - * @param string $string - * - * @return int - */ - public function fwrite($handle, $string) - { - return fwrite($handle, $string); - } - - /** - * Wrapper around global function fclose(). - * - * @see fclose() - * - * @param resource $handle - * - * @return bool - */ - public function fclose($handle) - { - return fclose($handle); - } - - /** - * Wrapper around global function rewind(). - * - * @see rewind() - * - * @param resource $handle - * - * @return bool - */ - public function rewind($handle) - { - return rewind($handle); - } - - /** - * Wrapper around global function file_exists(). - * - * @see file_exists() - * - * @param string $fileName - * - * @return bool - */ - public function file_exists($fileName) - { - return file_exists($fileName); - } - - /** - * Wrapper around global function file_get_contents(). - * - * @see file_get_contents() - * - * @param string $filePath - * - * @return string - */ - public function file_get_contents($filePath) - { - $realFilePath = $this->convertToUseRealPath($filePath); - - return file_get_contents($realFilePath); - } - - /** - * Wrapper around global function feof(). - * - * @see feof() - * - * @param resource $handle - * - * @return bool - */ - public function feof($handle) - { - return feof($handle); - } - - /** - * Wrapper around global function is_readable(). - * - * @see is_readable() - * - * @param string $fileName - * - * @return bool - */ - public function is_readable($fileName) - { - return is_readable($fileName); - } - - /** - * Wrapper around global function basename(). - * - * @see basename() - * - * @param string $path - * @param string $suffix - * - * @return string - */ - public function basename($path, $suffix = '') - { - return basename($path, $suffix); - } - - /** - * Wrapper around global function header(). - * - * @see header() - * - * @param string $string - */ - public function header($string) - { - header($string); - } - - /** - * Wrapper around global function ob_end_clean(). - * - * @see ob_end_clean() - */ - public function ob_end_clean() - { - if (ob_get_length() > 0) { - ob_end_clean(); - } - } - - /** - * Wrapper around global function iconv(). - * - * @see iconv() - * - * @param string $string The string to be converted - * @param string $sourceEncoding The encoding of the source string - * @param string $targetEncoding The encoding the source string should be converted to - * - * @return bool|string the converted string or FALSE on failure - */ - public function iconv($string, $sourceEncoding, $targetEncoding) - { - return iconv($sourceEncoding, $targetEncoding, $string); - } - - /** - * Wrapper around global function mb_convert_encoding(). - * - * @see mb_convert_encoding() - * - * @param string $string The string to be converted - * @param string $sourceEncoding The encoding of the source string - * @param string $targetEncoding The encoding the source string should be converted to - * - * @return bool|string the converted string or FALSE on failure - */ - public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding) - { - return mb_convert_encoding($string, $targetEncoding, $sourceEncoding); - } - - /** - * Wrapper around global function stream_get_wrappers(). - * - * @see stream_get_wrappers() - * - * @return array - */ - public function stream_get_wrappers() - { - return stream_get_wrappers(); - } - - /** - * Wrapper around global function function_exists(). - * - * @see function_exists() - * - * @param string $functionName - * - * @return bool - */ - public function function_exists($functionName) - { - return \function_exists($functionName); - } - - /** - * Updates the given file path to use a real path. - * This is to avoid issues on some Windows setup. - * - * @param string $filePath File path - * - * @return string The file path using a real path - */ - protected function convertToUseRealPath($filePath) - { - $realFilePath = $filePath; - - if ($this->isZipStream($filePath)) { - if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) { - $documentPath = $matches[1]; - $documentInsideZipPath = $matches[2]; - $realFilePath = 'zip://'.realpath($documentPath).'#'.$documentInsideZipPath; - } - } else { - $realFilePath = realpath($filePath); - } - - return $realFilePath; - } - - /** - * Returns whether the given path is a zip stream. - * - * @param string $path Path pointing to a document - * - * @return bool TRUE if path is a zip stream, FALSE otherwise - */ - protected function isZipStream($path) - { - return 0 === strpos($path, 'zip://'); - } -} diff --git a/lib/openspout/src/Common/Helper/StringHelper.php b/lib/openspout/src/Common/Helper/StringHelper.php index 96e7f2c3a1c..310d4243042 100644 --- a/lib/openspout/src/Common/Helper/StringHelper.php +++ b/lib/openspout/src/Common/Helper/StringHelper.php @@ -1,28 +1,25 @@ hasMbstringSupport = \extension_loaded('mbstring'); - $this->isRunningPhp7OrOlder = version_compare(PHP_VERSION, '8.0.0') < 0; - $this->localeInfo = localeconv(); + $this->hasMbstringSupport = $hasMbstringSupport; + } + + public static function factory(): self + { + return new self(\function_exists('mb_strlen')); } /** @@ -31,14 +28,13 @@ class StringHelper * * @see strlen * @see mb_strlen - * - * @param string $string - * - * @return int */ - public function getStringLength($string) + public function getStringLength(string $string): int { - return $this->hasMbstringSupport ? mb_strlen($string) : \strlen($string); + return $this->hasMbstringSupport + ? mb_strlen($string) + : \strlen($string) // @codeCoverageIgnore + ; } /** @@ -53,9 +49,12 @@ class StringHelper * * @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found */ - public function getCharFirstOccurrencePosition($char, $string) + public function getCharFirstOccurrencePosition(string $char, string $string): int { - $position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char); + $position = $this->hasMbstringSupport + ? mb_strpos($string, $char) + : strpos($string, $char) // @codeCoverageIgnore + ; return (false !== $position) ? $position : -1; } @@ -72,37 +71,13 @@ class StringHelper * * @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found */ - public function getCharLastOccurrencePosition($char, $string) + public function getCharLastOccurrencePosition(string $char, string $string): int { - $position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char); + $position = $this->hasMbstringSupport + ? mb_strrpos($string, $char) + : strrpos($string, $char) // @codeCoverageIgnore + ; return (false !== $position) ? $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 float|int $numericValue - * - * @return float|int|string - */ - public function formatNumericValue($numericValue) - { - if ($this->isRunningPhp7OrOlder && \is_float($numericValue)) { - return str_replace( - [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']], - ['', '.'], - (string) $numericValue - ); - } - - return $numericValue; - } } diff --git a/lib/openspout/src/Common/Manager/OptionsManagerAbstract.php b/lib/openspout/src/Common/Manager/OptionsManagerAbstract.php deleted file mode 100644 index 3bfce395ccf..00000000000 --- a/lib/openspout/src/Common/Manager/OptionsManagerAbstract.php +++ /dev/null @@ -1,82 +0,0 @@ - OPTION_VALUE] */ - private $options = []; - - /** - * OptionsManagerAbstract constructor. - */ - public function __construct() - { - $this->supportedOptions = $this->getSupportedOptions(); - $this->setDefaultOptions(); - } - - /** - * Sets the given option, if this option is supported. - * - * @param string $optionName - * @param mixed $optionValue - */ - public function setOption($optionName, $optionValue) - { - if (\in_array($optionName, $this->supportedOptions, true)) { - $this->options[$optionName] = $optionValue; - } - } - - /** - * Add an option to the internal list of options - * Used only for mergeCells() for now. - * - * @param mixed $optionName - * @param mixed $optionValue - */ - public function addOption($optionName, $optionValue) - { - if (\in_array($optionName, $this->supportedOptions, true)) { - if (!isset($this->options[$optionName])) { - $this->options[$optionName] = []; - } elseif (!\is_array($this->options[$optionName])) { - $this->options[$optionName] = [$this->options[$optionName]]; - } - $this->options[$optionName][] = $optionValue; - } - } - - /** - * @param string $optionName - * - * @return null|mixed The set option or NULL if no option with given name found - */ - public function getOption($optionName) - { - $optionValue = null; - - if (isset($this->options[$optionName])) { - $optionValue = $this->options[$optionName]; - } - - return $optionValue; - } - - /** - * @return array List of supported options - */ - abstract protected function getSupportedOptions(); - - /** - * Sets the default options. - * To be overriden by child classes. - */ - abstract protected function setDefaultOptions(); -} diff --git a/lib/openspout/src/Common/Manager/OptionsManagerInterface.php b/lib/openspout/src/Common/Manager/OptionsManagerInterface.php deleted file mode 100644 index 7913017a50c..00000000000 --- a/lib/openspout/src/Common/Manager/OptionsManagerInterface.php +++ /dev/null @@ -1,31 +0,0 @@ -tempFolder = $tempFolder; + } + + final public function getTempFolder(): string + { + if (!isset($this->tempFolder)) { + $this->setTempFolder(sys_get_temp_dir()); + } + + return $this->tempFolder; + } +} diff --git a/lib/openspout/src/Common/Type.php b/lib/openspout/src/Common/Type.php deleted file mode 100644 index 9c1573d4d59..00000000000 --- a/lib/openspout/src/Common/Type.php +++ /dev/null @@ -1,13 +0,0 @@ - + */ +abstract class AbstractReader implements ReaderInterface { /** @var bool Indicates whether the stream is currently open */ - protected $isStreamOpened = false; - - /** @var InternalEntityFactoryInterface Factory to create entities */ - protected $entityFactory; - - /** @var \OpenSpout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ - protected $globalFunctionsHelper; - - /** @var OptionsManagerInterface Writer options manager */ - protected $optionsManager; - - public function __construct( - OptionsManagerInterface $optionsManager, - GlobalFunctionsHelper $globalFunctionsHelper, - InternalEntityFactoryInterface $entityFactory - ) { - $this->optionsManager = $optionsManager; - $this->globalFunctionsHelper = $globalFunctionsHelper; - $this->entityFactory = $entityFactory; - } - - /** - * Sets whether date/time values should be returned as PHP objects or be formatted as strings. - * - * @param bool $shouldFormatDates - * - * @return ReaderAbstract - */ - public function setShouldFormatDates($shouldFormatDates) - { - $this->optionsManager->setOption(Options::SHOULD_FORMAT_DATES, $shouldFormatDates); - - return $this; - } - - /** - * Sets whether empty rows should be returned or skipped. - * - * @param bool $shouldPreserveEmptyRows - * - * @return ReaderAbstract - */ - public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows) - { - $this->optionsManager->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, $shouldPreserveEmptyRows); - - return $this; - } + private bool $isStreamOpened = false; /** * Prepares the reader to read the given file. It also makes sure @@ -69,7 +26,7 @@ abstract class ReaderAbstract implements ReaderInterface * * @throws \OpenSpout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted */ - public function open($filePath) + public function open(string $filePath): void { if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) { throw new IOException("Could not open {$filePath} for reading! Stream wrapper used is not supported for this type of file."); @@ -77,10 +34,10 @@ abstract class ReaderAbstract implements ReaderInterface if (!$this->isPhpStream($filePath)) { // we skip the checks if the provided file path points to a PHP stream - if (!$this->globalFunctionsHelper->file_exists($filePath)) { + if (!file_exists($filePath)) { throw new IOException("Could not open {$filePath} for reading! File does not exist."); } - if (!$this->globalFunctionsHelper->is_readable($filePath)) { + if (!is_readable($filePath)) { throw new IOException("Could not open {$filePath} for reading! File is not readable."); } } @@ -89,86 +46,66 @@ abstract class ReaderAbstract implements ReaderInterface $fileRealPath = $this->getFileRealPath($filePath); $this->openReader($fileRealPath); $this->isStreamOpened = true; - } catch (\Exception $exception) { - throw new IOException("Could not open {$filePath} for reading! ({$exception->getMessage()})"); + } catch (ReaderException $exception) { + throw new IOException( + "Could not open {$filePath} for reading!", + 0, + $exception + ); } } - /** - * Returns an iterator to iterate over sheets. - * - * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException If called before opening the reader - * - * @return SheetIteratorInterface To iterate over sheets - */ - public function getSheetIterator() - { - if (!$this->isStreamOpened) { - throw new ReaderNotOpenedException('Reader should be opened first.'); - } - - return $this->getConcreteSheetIterator(); - } - /** * Closes the reader, preventing any additional reading. */ - public function close() + final public function close(): void { if ($this->isStreamOpened) { $this->closeReader(); - $sheetIterator = $this->getConcreteSheetIterator(); - if (null !== $sheetIterator) { - $sheetIterator->end(); - } - $this->isStreamOpened = false; } } /** * Returns whether stream wrappers are supported. - * - * @return bool */ - abstract protected function doesSupportStreamWrapper(); + abstract protected function doesSupportStreamWrapper(): bool; /** * Opens the file at the given file path to make it ready to be read. * * @param string $filePath Path of the file to be read */ - abstract protected function openReader($filePath); - - /** - * Returns an iterator to iterate over sheets. - * - * @return SheetIteratorInterface To iterate over sheets - */ - abstract protected function getConcreteSheetIterator(); + abstract protected function openReader(string $filePath): void; /** * Closes the reader. To be used after reading the file. */ - abstract protected function closeReader(); + abstract protected function closeReader(): void; + + final protected function ensureStreamOpened(): void + { + if (!$this->isStreamOpened) { + throw new ReaderNotOpenedException('Reader should be opened first.'); + } + } /** * Returns the real path of the given path. * If the given path is a valid stream wrapper, returns the path unchanged. - * - * @param string $filePath - * - * @return string */ - protected function getFileRealPath($filePath) + private function getFileRealPath(string $filePath): string { if ($this->isSupportedStreamWrapper($filePath)) { return $filePath; } // Need to use realpath to fix "Can't open file" on some Windows setup - return realpath($filePath); + $realpath = realpath($filePath); + \assert(false !== $realpath); + + return $realpath; } /** @@ -179,10 +116,10 @@ abstract class ReaderAbstract implements ReaderInterface * * @return null|string The stream wrapper scheme or NULL if not a stream wrapper */ - protected function getStreamWrapperScheme($filePath) + private function getStreamWrapperScheme(string $filePath): ?string { $streamScheme = null; - if (preg_match('/^(\w+):\/\//', $filePath, $matches)) { + if (1 === preg_match('/^(\w+):\/\//', $filePath, $matches)) { $streamScheme = $matches[1]; } @@ -197,7 +134,7 @@ abstract class ReaderAbstract implements ReaderInterface * * @return bool Whether the given path is an unsupported stream wrapper */ - protected function isStreamWrapper($filePath) + private function isStreamWrapper(string $filePath): bool { return null !== $this->getStreamWrapperScheme($filePath); } @@ -211,13 +148,11 @@ abstract class ReaderAbstract implements ReaderInterface * * @return bool Whether the given path is an supported stream wrapper */ - protected function isSupportedStreamWrapper($filePath) + private function isSupportedStreamWrapper(string $filePath): bool { $streamScheme = $this->getStreamWrapperScheme($filePath); - return (null !== $streamScheme) ? - \in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers(), true) : - true; + return null === $streamScheme || \in_array($streamScheme, stream_get_wrappers(), true); } /** @@ -227,7 +162,7 @@ abstract class ReaderAbstract implements ReaderInterface * * @return bool Whether the given path maps to a PHP stream */ - protected function isPhpStream($filePath) + private function isPhpStream(string $filePath): bool { $streamScheme = $this->getStreamWrapperScheme($filePath); diff --git a/lib/openspout/src/Reader/CSV/Creator/InternalEntityFactory.php b/lib/openspout/src/Reader/CSV/Creator/InternalEntityFactory.php deleted file mode 100644 index 8b3562d19a4..00000000000 --- a/lib/openspout/src/Reader/CSV/Creator/InternalEntityFactory.php +++ /dev/null @@ -1,98 +0,0 @@ -helperFactory = $helperFactory; - } - - /** - * @param resource $filePointer Pointer to the CSV file to read - * @param OptionsManagerInterface $optionsManager - * @param GlobalFunctionsHelper $globalFunctionsHelper - * - * @return SheetIterator - */ - public function createSheetIterator($filePointer, $optionsManager, $globalFunctionsHelper) - { - $rowIterator = $this->createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper); - $sheet = $this->createSheet($rowIterator); - - return new SheetIterator($sheet); - } - - /** - * @param Cell[] $cells - * - * @return Row - */ - public function createRow(array $cells = []) - { - return new Row($cells, null); - } - - /** - * @param mixed $cellValue - * - * @return Cell - */ - public function createCell($cellValue) - { - return new Cell($cellValue); - } - - /** - * @return Row - */ - public function createRowFromArray(array $cellValues = []) - { - $cells = array_map(function ($cellValue) { - return $this->createCell($cellValue); - }, $cellValues); - - return $this->createRow($cells); - } - - /** - * @param RowIterator $rowIterator - * - * @return Sheet - */ - private function createSheet($rowIterator) - { - return new Sheet($rowIterator); - } - - /** - * @param resource $filePointer Pointer to the CSV file to read - * @param OptionsManagerInterface $optionsManager - * @param GlobalFunctionsHelper $globalFunctionsHelper - * - * @return RowIterator - */ - private function createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper) - { - $encodingHelper = $this->helperFactory->createEncodingHelper($globalFunctionsHelper); - - return new RowIterator($filePointer, $optionsManager, $encodingHelper, $this, $globalFunctionsHelper); - } -} diff --git a/lib/openspout/src/Reader/CSV/Manager/OptionsManager.php b/lib/openspout/src/Reader/CSV/Manager/OptionsManager.php deleted file mode 100644 index 9772a43815c..00000000000 --- a/lib/openspout/src/Reader/CSV/Manager/OptionsManager.php +++ /dev/null @@ -1,39 +0,0 @@ -setOption(Options::SHOULD_FORMAT_DATES, false); - $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); - $this->setOption(Options::FIELD_DELIMITER, ','); - $this->setOption(Options::FIELD_ENCLOSURE, '"'); - $this->setOption(Options::ENCODING, EncodingHelper::ENCODING_UTF8); - } -} diff --git a/lib/openspout/src/Reader/CSV/Options.php b/lib/openspout/src/Reader/CSV/Options.php new file mode 100644 index 00000000000..e470915f13d --- /dev/null +++ b/lib/openspout/src/Reader/CSV/Options.php @@ -0,0 +1,15 @@ + */ -class Reader extends ReaderAbstract +final class Reader extends AbstractReader { /** @var resource Pointer to the file to be written */ - protected $filePointer; + private $filePointer; /** @var SheetIterator To iterator over the CSV unique "sheet" */ - protected $sheetIterator; + private SheetIterator $sheetIterator; /** @var string Original value for the "auto_detect_line_endings" INI value */ - protected $originalAutoDetectLineEndings; + private string $originalAutoDetectLineEndings; /** @var bool Whether the code is running with PHP >= 8.1 */ - private $isRunningAtLeastPhp81; + private bool $isRunningAtLeastPhp81; + + private Options $options; + private EncodingHelper $encodingHelper; public function __construct( - OptionsManagerInterface $optionsManager, - GlobalFunctionsHelper $globalFunctionsHelper, - InternalEntityFactoryInterface $entityFactory + ?Options $options = null, + ?EncodingHelper $encodingHelper = null ) { - parent::__construct($optionsManager, $globalFunctionsHelper, $entityFactory); - $this->isRunningAtLeastPhp81 = version_compare(PHP_VERSION, '8.1.0') >= 0; + $this->options = $options ?? new Options(); + $this->encodingHelper = $encodingHelper ?? EncodingHelper::factory(); + $this->isRunningAtLeastPhp81 = \PHP_VERSION_ID >= 80100; } - /** - * Sets the field delimiter for the CSV. - * Needs to be called before opening the reader. - * - * @param string $fieldDelimiter Character that delimits fields - * - * @return Reader - */ - public function setFieldDelimiter($fieldDelimiter) + public function getSheetIterator(): SheetIterator { - $this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter); + $this->ensureStreamOpened(); - return $this; - } - - /** - * Sets the field enclosure for the CSV. - * Needs to be called before opening the reader. - * - * @param string $fieldEnclosure Character that enclose fields - * - * @return Reader - */ - public function setFieldEnclosure($fieldEnclosure) - { - $this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure); - - return $this; - } - - /** - * Sets the encoding of the CSV file to be read. - * Needs to be called before opening the reader. - * - * @param string $encoding Encoding of the CSV file to be read - * - * @return Reader - */ - public function setEncoding($encoding) - { - $this->optionsManager->setOption(Options::ENCODING, $encoding); - - return $this; + return $this->sheetIterator; } /** * Returns whether stream wrappers are supported. - * - * @return bool */ - protected function doesSupportStreamWrapper() + protected function doesSupportStreamWrapper(): bool { return true; } @@ -99,51 +59,45 @@ class Reader extends ReaderAbstract * * @throws \OpenSpout\Common\Exception\IOException */ - protected function openReader($filePath) + protected function openReader(string $filePath): void { // "auto_detect_line_endings" is deprecated in PHP 8.1 if (!$this->isRunningAtLeastPhp81) { - $this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings'); + // @codeCoverageIgnoreStart + $originalAutoDetectLineEndings = \ini_get('auto_detect_line_endings'); + \assert(false !== $originalAutoDetectLineEndings); + $this->originalAutoDetectLineEndings = $originalAutoDetectLineEndings; ini_set('auto_detect_line_endings', '1'); + // @codeCoverageIgnoreEnd } - $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r'); - if (!$this->filePointer) { - throw new IOException("Could not open file {$filePath} for reading."); - } + $resource = fopen($filePath, 'r'); + \assert(false !== $resource); + $this->filePointer = $resource; - /** @var InternalEntityFactory $entityFactory */ - $entityFactory = $this->entityFactory; - - $this->sheetIterator = $entityFactory->createSheetIterator( - $this->filePointer, - $this->optionsManager, - $this->globalFunctionsHelper + $this->sheetIterator = new SheetIterator( + new Sheet( + new RowIterator( + $this->filePointer, + $this->options, + $this->encodingHelper + ) + ) ); } - /** - * Returns an iterator to iterate over sheets. - * - * @return SheetIterator To iterate over sheets - */ - protected function getConcreteSheetIterator() - { - return $this->sheetIterator; - } - /** * Closes the reader. To be used after reading the file. */ - protected function closeReader() + protected function closeReader(): void { - if (\is_resource($this->filePointer)) { - $this->globalFunctionsHelper->fclose($this->filePointer); - } + fclose($this->filePointer); // "auto_detect_line_endings" is deprecated in PHP 8.1 if (!$this->isRunningAtLeastPhp81) { + // @codeCoverageIgnoreStart ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings); + // @codeCoverageIgnoreEnd } } } diff --git a/lib/openspout/src/Reader/CSV/RowIterator.php b/lib/openspout/src/Reader/CSV/RowIterator.php index 497d26637ae..024711b8e1b 100644 --- a/lib/openspout/src/Reader/CSV/RowIterator.php +++ b/lib/openspout/src/Reader/CSV/RowIterator.php @@ -1,76 +1,52 @@ filePointer = $filePointer; - $this->fieldDelimiter = $optionsManager->getOption(Options::FIELD_DELIMITER); - $this->fieldEnclosure = $optionsManager->getOption(Options::FIELD_ENCLOSURE); - $this->encoding = $optionsManager->getOption(Options::ENCODING); - $this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS); + $this->options = $options; $this->encodingHelper = $encodingHelper; - $this->entityFactory = $entityFactory; - $this->globalFunctionsHelper = $globalFunctionsHelper; } /** @@ -78,7 +54,6 @@ class RowIterator implements RowIteratorInterface * * @see http://php.net/manual/en/iterator.rewind.php */ - #[\ReturnTypeWillChange] public function rewind(): void { $this->rewindAndSkipBom(); @@ -94,10 +69,9 @@ class RowIterator implements RowIteratorInterface * * @see http://php.net/manual/en/iterator.valid.php */ - #[\ReturnTypeWillChange] public function valid(): bool { - return $this->filePointer && !$this->hasReachedEndOfFile; + return null !== $this->filePointer && !$this->hasReachedEndOfFile; } /** @@ -107,10 +81,9 @@ class RowIterator implements RowIteratorInterface * * @throws \OpenSpout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 */ - #[\ReturnTypeWillChange] public function next(): void { - $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); + $this->hasReachedEndOfFile = feof($this->filePointer); if (!$this->hasReachedEndOfFile) { $this->readDataForNextRow(); @@ -122,7 +95,6 @@ class RowIterator implements RowIteratorInterface * * @see http://php.net/manual/en/iterator.current.php */ - #[\ReturnTypeWillChange] public function current(): ?Row { return $this->rowBuffer; @@ -133,37 +105,27 @@ class RowIterator implements RowIteratorInterface * * @see http://php.net/manual/en/iterator.key.php */ - #[\ReturnTypeWillChange] public function key(): int { return $this->numReadRows; } - /** - * Cleans up what was created to iterate over the object. - */ - #[\ReturnTypeWillChange] - public function end(): void - { - // do nothing - } - /** * This rewinds and skips the BOM if inserted at the beginning of the file * by moving the file pointer after it, so that it is not read. */ - protected function rewindAndSkipBom() + private function rewindAndSkipBom(): void { - $byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding); + $byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->options->ENCODING); // sets the cursor after the BOM (0 means no BOM, so rewind it) - $this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom); + fseek($this->filePointer, $byteOffsetToSkipBom); } /** * @throws \OpenSpout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 */ - protected function readDataForNextRow() + private function readDataForNextRow(): void { do { $rowData = $this->getNextUTF8EncodedRow(); @@ -171,8 +133,10 @@ class RowIterator implements RowIteratorInterface if (false !== $rowData) { // array_map will replace NULL values by empty strings - $rowDataBufferAsArray = array_map(function ($value) { return (string) $value; }, $rowData); - $this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray); + $rowDataBufferAsArray = array_map('\\strval', $rowData); + $this->rowBuffer = new Row(array_map(static function ($cellValue) { + return Cell::fromValue($cellValue); + }, $rowDataBufferAsArray), null); ++$this->numReadRows; } else { // If we reach this point, it means end of file was reached. @@ -182,19 +146,19 @@ class RowIterator implements RowIteratorInterface } /** - * @param array|bool $currentRowData + * @param array|bool $currentRowData * * @return bool Whether the data for the current row can be returned or if we need to keep reading */ - protected function shouldReadNextRow($currentRowData) + private function shouldReadNextRow($currentRowData): bool { $hasSuccessfullyFetchedRowData = (false !== $currentRowData); - $hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); + $hasNowReachedEndOfFile = feof($this->filePointer); $isEmptyLine = $this->isEmptyLine($currentRowData); return (!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) - || (!$this->shouldPreserveEmptyRows && $isEmptyLine) + || (!$this->options->SHOULD_PRESERVE_EMPTY_ROWS && $isEmptyLine) ; } @@ -203,19 +167,25 @@ class RowIterator implements RowIteratorInterface * As fgetcsv() does not manage correctly encoding for non UTF-8 data, * we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes). * - * @throws \OpenSpout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 + * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read * - * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read + * @throws \OpenSpout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 */ - protected function getNextUTF8EncodedRow() + private function getNextUTF8EncodedRow(): array|false { - $encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure); + $encodedRowData = fgetcsv( + $this->filePointer, + self::MAX_READ_BYTES_PER_LINE, + $this->options->FIELD_DELIMITER, + $this->options->FIELD_ENCLOSURE, + '' + ); if (false === $encodedRowData) { return false; } foreach ($encodedRowData as $cellIndex => $cellValue) { - switch ($this->encoding) { + switch ($this->options->ENCODING) { case EncodingHelper::ENCODING_UTF16_LE: case EncodingHelper::ENCODING_UTF32_LE: // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data @@ -231,18 +201,18 @@ class RowIterator implements RowIteratorInterface break; } - $encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding); + $encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->options->ENCODING); } return $encodedRowData; } /** - * @param array|bool $lineData Array containing the cells value for the line + * @param array|bool $lineData Array containing the cells value for the line * * @return bool Whether the given line is empty */ - protected function isEmptyLine($lineData) + private function isEmptyLine($lineData): bool { return \is_array($lineData) && 1 === \count($lineData) && null === $lineData[0]; } diff --git a/lib/openspout/src/Reader/CSV/Sheet.php b/lib/openspout/src/Reader/CSV/Sheet.php index f64b2ee6d86..f4e1be1c697 100644 --- a/lib/openspout/src/Reader/CSV/Sheet.php +++ b/lib/openspout/src/Reader/CSV/Sheet.php @@ -1,13 +1,18 @@ + */ +final class Sheet implements SheetInterface { - /** @var \OpenSpout\Reader\CSV\RowIterator To iterate over the CSV's rows */ - protected $rowIterator; + /** @var RowIterator To iterate over the CSV's rows */ + private RowIterator $rowIterator; /** * @param RowIterator $rowIterator Corresponding row iterator @@ -17,10 +22,7 @@ class Sheet implements SheetInterface $this->rowIterator = $rowIterator; } - /** - * @return \OpenSpout\Reader\CSV\RowIterator - */ - public function getRowIterator() + public function getRowIterator(): RowIterator { return $this->rowIterator; } @@ -28,7 +30,7 @@ class Sheet implements SheetInterface /** * @return int Index of the sheet */ - public function getIndex() + public function getIndex(): int { return 0; } @@ -36,7 +38,7 @@ class Sheet implements SheetInterface /** * @return string Name of the sheet - empty string since CSV does not support that */ - public function getName() + public function getName(): string { return ''; } @@ -44,15 +46,7 @@ class Sheet implements SheetInterface /** * @return bool Always TRUE as there is only one sheet */ - public function isActive() - { - return true; - } - - /** - * @return bool Always TRUE as the only sheet is always visible - */ - public function isVisible() + public function isActive(): bool { return true; } diff --git a/lib/openspout/src/Reader/CSV/SheetIterator.php b/lib/openspout/src/Reader/CSV/SheetIterator.php index cbcf7c12f64..7bba3aa09b6 100644 --- a/lib/openspout/src/Reader/CSV/SheetIterator.php +++ b/lib/openspout/src/Reader/CSV/SheetIterator.php @@ -1,24 +1,26 @@ */ -class SheetIterator implements SheetIteratorInterface +final class SheetIterator implements SheetIteratorInterface { /** @var Sheet The CSV unique "sheet" */ - protected $sheet; + private Sheet $sheet; /** @var bool Whether the unique "sheet" has already been read */ - protected $hasReadUniqueSheet = false; + private bool $hasReadUniqueSheet = false; /** * @param Sheet $sheet Corresponding unique sheet */ - public function __construct($sheet) + public function __construct(Sheet $sheet) { $this->sheet = $sheet; } @@ -28,7 +30,6 @@ class SheetIterator implements SheetIteratorInterface * * @see http://php.net/manual/en/iterator.rewind.php */ - #[\ReturnTypeWillChange] public function rewind(): void { $this->hasReadUniqueSheet = false; @@ -39,7 +40,6 @@ class SheetIterator implements SheetIteratorInterface * * @see http://php.net/manual/en/iterator.valid.php */ - #[\ReturnTypeWillChange] public function valid(): bool { return !$this->hasReadUniqueSheet; @@ -50,7 +50,6 @@ class SheetIterator implements SheetIteratorInterface * * @see http://php.net/manual/en/iterator.next.php */ - #[\ReturnTypeWillChange] public function next(): void { $this->hasReadUniqueSheet = true; @@ -61,7 +60,6 @@ class SheetIterator implements SheetIteratorInterface * * @see http://php.net/manual/en/iterator.current.php */ - #[\ReturnTypeWillChange] public function current(): Sheet { return $this->sheet; @@ -72,18 +70,8 @@ class SheetIterator implements SheetIteratorInterface * * @see http://php.net/manual/en/iterator.key.php */ - #[\ReturnTypeWillChange] public function key(): int { return 1; } - - /** - * Cleans up what was created to iterate over the object. - */ - #[\ReturnTypeWillChange] - public function end(): void - { - // do nothing - } } diff --git a/lib/openspout/src/Reader/Common/ColumnWidth.php b/lib/openspout/src/Reader/Common/ColumnWidth.php new file mode 100644 index 00000000000..1350a10e2ea --- /dev/null +++ b/lib/openspout/src/Reader/Common/ColumnWidth.php @@ -0,0 +1,22 @@ + new CSVReader(), + 'xlsx' => new XLSXReader(), + 'ods' => new ODSReader(), + default => throw new UnsupportedTypeException('No readers supporting the given type: '.$extension), + }; } /** - * This creates an instance of the appropriate reader, given the type of the file to be read. + * Creates a reader by mime type. * - * @param string $readerType Type of the reader to instantiate + * @param string $path the path to the spreadsheet file * * @throws \OpenSpout\Common\Exception\UnsupportedTypeException - * - * @return ReaderInterface + * @throws \OpenSpout\Common\Exception\IOException */ - public static function createFromType($readerType) + public static function createFromFileByMimeType(string $path): ReaderInterface { - switch ($readerType) { - case Type::CSV: return self::createCSVReader(); - - case Type::XLSX: return self::createXLSXReader(); - - case Type::ODS: return self::createODSReader(); - - default: - throw new UnsupportedTypeException('No readers supporting the given type: '.$readerType); + if (!file_exists($path)) { + throw new IOException("Could not open {$path} for reading! File does not exist."); } - } - /** - * @return CSVReader - */ - private static function createCSVReader() - { - $optionsManager = new CSVOptionsManager(); - $helperFactory = new HelperFactory(); - $entityFactory = new CSVInternalEntityFactory($helperFactory); - $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper(); + $mime_type = mime_content_type($path); - return new CSVReader($optionsManager, $globalFunctionsHelper, $entityFactory); - } - - /** - * @return XLSXReader - */ - private static function createXLSXReader() - { - $optionsManager = new XLSXOptionsManager(); - $helperFactory = new XLSXHelperFactory(); - $managerFactory = new XLSXManagerFactory($helperFactory, new CachingStrategyFactory()); - $entityFactory = new XLSXInternalEntityFactory($managerFactory, $helperFactory); - $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper(); - - return new XLSXReader($optionsManager, $globalFunctionsHelper, $entityFactory, $managerFactory); - } - - /** - * @return ODSReader - */ - private static function createODSReader() - { - $optionsManager = new ODSOptionsManager(); - $helperFactory = new ODSHelperFactory(); - $managerFactory = new ODSManagerFactory(); - $entityFactory = new ODSInternalEntityFactory($helperFactory, $managerFactory); - $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper(); - - return new ODSReader($optionsManager, $globalFunctionsHelper, $entityFactory); + return match ($mime_type) { + 'application/csv', 'text/csv', 'text/plain' => new CSVReader(), + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => new XLSXReader(), + 'application/vnd.oasis.opendocument.spreadsheet' => new ODSReader(), + default => throw new UnsupportedTypeException('No readers supporting the given type: '.$mime_type), + }; } } diff --git a/lib/openspout/src/Reader/Common/Entity/Options.php b/lib/openspout/src/Reader/Common/Entity/Options.php deleted file mode 100644 index 48e15a1903d..00000000000 --- a/lib/openspout/src/Reader/Common/Entity/Options.php +++ /dev/null @@ -1,22 +0,0 @@ -entityFactory = $entityFactory; - } - - /** - * Detect whether a row is considered empty. - * An empty row has all of its cells empty. - * - * @return bool - */ - public function isEmpty(Row $row) - { - foreach ($row->getCells() as $cell) { - if (!$cell->isEmpty()) { - return false; - } - } - - return true; - } - /** * Fills the missing indexes of a row with empty cells. - * - * @return Row */ - public function fillMissingIndexesWithEmptyCells(Row $row) + public function fillMissingIndexesWithEmptyCells(Row $row): void { $numCells = $row->getNumCells(); if (0 === $numCells) { - return $row; + return; } $rowCells = $row->getCells(); @@ -62,7 +37,7 @@ class RowManager for ($cellIndex = 0; $cellIndex < $maxCellIndex; ++$cellIndex) { if (!isset($rowCells[$cellIndex])) { - $row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex); + $row->setCellAtIndex(Cell::fromValue(''), $cellIndex); $needsSorting = true; } } @@ -72,7 +47,5 @@ class RowManager ksort($rowCells); $row->setCells($rowCells); } - - return $row; } } diff --git a/lib/openspout/src/Reader/Common/XMLProcessor.php b/lib/openspout/src/Reader/Common/XMLProcessor.php index 967c0ffc6da..85dff666eba 100644 --- a/lib/openspout/src/Reader/Common/XMLProcessor.php +++ b/lib/openspout/src/Reader/Common/XMLProcessor.php @@ -1,13 +1,16 @@ Registered callbacks */ + private array $callbacks = []; /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object + * @param XMLReader $xmlReader XMLReader object */ - public function __construct($xmlReader) + public function __construct(XMLReader $xmlReader) { $this->xmlReader = $xmlReader; } @@ -39,10 +42,8 @@ class XMLProcessor * @param string $nodeName A callback may be triggered when a node with this name is read * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] * @param callable $callback Callback to execute when the read node has the given name and type - * - * @return XMLProcessor */ - public function registerCallback($nodeName, $nodeType, $callback) + public function registerCallback(string $nodeName, int $nodeType, $callback): self { $callbackKey = $this->getCallbackKey($nodeName, $nodeType); $this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback); @@ -56,7 +57,7 @@ class XMLProcessor * * @throws \OpenSpout\Reader\Exception\XMLProcessingException */ - public function readUntilStopped() + public function readUntilStopped(): void { while ($this->xmlReader->read()) { $nodeType = $this->xmlReader->nodeType; @@ -82,7 +83,7 @@ class XMLProcessor * * @return string Key used to store the associated callback */ - private function getCallbackKey($nodeName, $nodeType) + private function getCallbackKey(string $nodeName, int $nodeType): string { return "{$nodeName}{$nodeType}"; } @@ -95,13 +96,13 @@ class XMLProcessor * * @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME] * - * @return array Associative array containing the elements needed to invoke the callback using Reflection + * @return array{reflectionMethod: ReflectionMethod, reflectionObject: object} Associative array containing the elements needed to invoke the callback using Reflection */ - private function getInvokableCallbackData($callback) + private function getInvokableCallbackData($callback): array { $callbackObject = $callback[0]; $callbackMethodName = $callback[1]; - $reflectionMethod = new \ReflectionMethod(\get_class($callbackObject), $callbackMethodName); + $reflectionMethod = new ReflectionMethod($callbackObject, $callbackMethodName); $reflectionMethod->setAccessible(true); return [ @@ -115,9 +116,9 @@ class XMLProcessor * @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] * - * @return null|array Callback data to be used for execution when a node of the given name/type is read or NULL if none found + * @return null|array{reflectionMethod: ReflectionMethod, reflectionObject: object} Callback data to be used for execution when a node of the given name/type is read or NULL if none found */ - private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType) + private function getRegisteredCallbackData(string $nodeNamePossiblyWithPrefix, string $nodeNameWithoutPrefix, int $nodeType): ?array { // With prefixed nodes, we should match if (by order of preference): // 1. the callback was registered with the prefixed node name (e.g. "x:worksheet") @@ -136,12 +137,12 @@ class XMLProcessor } /** - * @param array $callbackData Associative array containing data to invoke the callback using Reflection - * @param array $args Arguments to pass to the callback + * @param array{reflectionMethod: ReflectionMethod, reflectionObject: object} $callbackData Associative array containing data to invoke the callback using Reflection + * @param XMLReader[] $args Arguments to pass to the callback * * @return int Callback response */ - private function invokeCallback($callbackData, $args) + private function invokeCallback(array $callbackData, array $args): int { $reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD]; $callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT]; diff --git a/lib/openspout/src/Reader/Exception/InvalidValueException.php b/lib/openspout/src/Reader/Exception/InvalidValueException.php index 9bbcebdf03b..3f5c4b04215 100644 --- a/lib/openspout/src/Reader/Exception/InvalidValueException.php +++ b/lib/openspout/src/Reader/Exception/InvalidValueException.php @@ -1,29 +1,22 @@ invalidValue = $invalidValue; parent::__construct($message, $code, $previous); } - /** - * @return mixed - */ - public function getInvalidValue() + public function getInvalidValue(): string { return $this->invalidValue; } diff --git a/lib/openspout/src/Reader/Exception/IteratorNotRewindableException.php b/lib/openspout/src/Reader/Exception/IteratorNotRewindableException.php index 06aac22621c..80bff913fbd 100644 --- a/lib/openspout/src/Reader/Exception/IteratorNotRewindableException.php +++ b/lib/openspout/src/Reader/Exception/IteratorNotRewindableException.php @@ -1,7 +1,9 @@ createStringsEscaper(); - - return new CellValueFormatter($shouldFormatDates, $escaper); - } - - /** - * @param InternalEntityFactory $entityFactory - * - * @return SettingsHelper - */ - public function createSettingsHelper($entityFactory) - { - return new SettingsHelper($entityFactory); - } - - /** - * @return \OpenSpout\Common\Helper\Escaper\ODS - */ - public function createStringsEscaper() - { - // @noinspection PhpUnnecessaryFullyQualifiedNameInspection - return new \OpenSpout\Common\Helper\Escaper\ODS(); - } -} diff --git a/lib/openspout/src/Reader/ODS/Creator/InternalEntityFactory.php b/lib/openspout/src/Reader/ODS/Creator/InternalEntityFactory.php deleted file mode 100644 index 960fec9e207..00000000000 --- a/lib/openspout/src/Reader/ODS/Creator/InternalEntityFactory.php +++ /dev/null @@ -1,124 +0,0 @@ -helperFactory = $helperFactory; - $this->managerFactory = $managerFactory; - } - - /** - * @param string $filePath Path of the file to be read - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager - * - * @return SheetIterator - */ - public function createSheetIterator($filePath, $optionsManager) - { - $escaper = $this->helperFactory->createStringsEscaper(); - $settingsHelper = $this->helperFactory->createSettingsHelper($this); - - return new SheetIterator($filePath, $optionsManager, $escaper, $settingsHelper, $this); - } - - /** - * @param XMLReader $xmlReader XML Reader - * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) - * @param string $sheetName Name of the sheet - * @param bool $isSheetActive Whether the sheet was defined as active - * @param bool $isSheetVisible Whether the sheet is visible - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager - * - * @return Sheet - */ - public function createSheet($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible, $optionsManager) - { - $rowIterator = $this->createRowIterator($xmlReader, $optionsManager); - - return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible); - } - - /** - * @param Cell[] $cells - * - * @return Row - */ - public function createRow(array $cells = []) - { - return new Row($cells, null); - } - - /** - * @param mixed $cellValue - * - * @return Cell - */ - public function createCell($cellValue) - { - return new Cell($cellValue); - } - - /** - * @return XMLReader - */ - public function createXMLReader() - { - return new XMLReader(); - } - - /** - * @return \ZipArchive - */ - public function createZipArchive() - { - return new \ZipArchive(); - } - - /** - * @param XMLReader $xmlReader XML Reader - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager - * - * @return RowIterator - */ - private function createRowIterator($xmlReader, $optionsManager) - { - $shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES); - $cellValueFormatter = $this->helperFactory->createCellValueFormatter($shouldFormatDates); - $xmlProcessor = $this->createXMLProcessor($xmlReader); - $rowManager = $this->managerFactory->createRowManager($this); - - return new RowIterator($xmlReader, $optionsManager, $cellValueFormatter, $xmlProcessor, $rowManager, $this); - } - - /** - * @param XMLReader $xmlReader - * - * @return XMLProcessor - */ - private function createXMLProcessor($xmlReader) - { - return new XMLProcessor($xmlReader); - } -} diff --git a/lib/openspout/src/Reader/ODS/Creator/ManagerFactory.php b/lib/openspout/src/Reader/ODS/Creator/ManagerFactory.php deleted file mode 100644 index 546069e0fe8..00000000000 --- a/lib/openspout/src/Reader/ODS/Creator/ManagerFactory.php +++ /dev/null @@ -1,21 +0,0 @@ - ' ', self::XML_NODE_TEXT_TAB => "\t", self::XML_NODE_TEXT_LINE_BREAK => "\n", ]; + /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ + private bool $shouldFormatDates; + + /** @var ODS Used to unescape XML data */ + private ODS $escaper; + /** - * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings - * @param \OpenSpout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings + * @param ODS $escaper Used to unescape XML data */ - public function __construct($shouldFormatDates, $escaper) + public function __construct(bool $shouldFormatDates, ODS $escaper) { $this->shouldFormatDates = $shouldFormatDates; $this->escaper = $escaper; @@ -64,52 +81,32 @@ class CellValueFormatter * * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13 * - * @param \DOMElement $node + * @return bool|DateInterval|DateTimeImmutable|float|int|string The value associated with the cell, empty string if cell's type is void/undefined * * @throws InvalidValueException If the node value is not valid - * - * @return bool|\DateInterval|\DateTime|float|int|string The value associated with the cell, empty string if cell's type is void/undefined */ - public function extractAndFormatNodeValue($node) + public function extractAndFormatNodeValue(DOMElement $node): bool|DateInterval|DateTimeImmutable|float|int|string { $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE); - switch ($cellType) { - case self::CELL_TYPE_STRING: - return $this->formatStringCellValue($node); - - case self::CELL_TYPE_FLOAT: - return $this->formatFloatCellValue($node); - - case self::CELL_TYPE_BOOLEAN: - return $this->formatBooleanCellValue($node); - - case self::CELL_TYPE_DATE: - return $this->formatDateCellValue($node); - - case self::CELL_TYPE_TIME: - return $this->formatTimeCellValue($node); - - case self::CELL_TYPE_CURRENCY: - return $this->formatCurrencyCellValue($node); - - case self::CELL_TYPE_PERCENTAGE: - return $this->formatPercentageCellValue($node); - - case self::CELL_TYPE_VOID: - default: - return ''; - } + return match ($cellType) { + self::CELL_TYPE_STRING => $this->formatStringCellValue($node), + self::CELL_TYPE_FLOAT => $this->formatFloatCellValue($node), + self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($node), + self::CELL_TYPE_DATE => $this->formatDateCellValue($node), + self::CELL_TYPE_TIME => $this->formatTimeCellValue($node), + self::CELL_TYPE_CURRENCY => $this->formatCurrencyCellValue($node), + self::CELL_TYPE_PERCENTAGE => $this->formatPercentageCellValue($node), + default => '', + }; } /** * Returns the cell String value. * - * @param \DOMElement $node - * * @return string The value associated with the cell */ - protected function formatStringCellValue($node) + private function formatStringCellValue(DOMElement $node): string { $pNodeValues = []; $pNodes = $node->getElementsByTagName(self::XML_NODE_P); @@ -126,11 +123,9 @@ class CellValueFormatter /** * Returns the cell Numeric value from the given node. * - * @param \DOMElement $node - * * @return float|int The value associated with the cell */ - protected function formatFloatCellValue($node) + private function formatFloatCellValue(DOMElement $node): float|int { $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); @@ -143,27 +138,19 @@ class CellValueFormatter /** * Returns the cell Boolean value from the given node. * - * @param \DOMElement $node - * * @return bool The value associated with the cell */ - protected function formatBooleanCellValue($node) + private function formatBooleanCellValue(DOMElement $node): bool { - $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE); - - return (bool) $nodeValue; + return (bool) $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE); } /** * Returns the cell Date value from the given node. * - * @param \DOMElement $node - * * @throws InvalidValueException If the value is not a valid date - * - * @return \DateTime|string The value associated with the cell */ - protected function formatDateCellValue($node) + private function formatDateCellValue(DOMElement $node): string|DateTimeImmutable { // The XML node looks like this: // @@ -179,9 +166,9 @@ class CellValueFormatter $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); try { - $cellValue = new \DateTime($nodeValue); - } catch (\Exception $e) { - throw new InvalidValueException($nodeValue); + $cellValue = new DateTimeImmutable($nodeValue); + } catch (Exception $previous) { + throw new InvalidValueException($nodeValue, '', 0, $previous); } } @@ -191,13 +178,11 @@ class CellValueFormatter /** * Returns the cell Time value from the given node. * - * @param \DOMElement $node + * @return DateInterval|string The value associated with the cell * * @throws InvalidValueException If the value is not a valid time - * - * @return \DateInterval|string The value associated with the cell */ - protected function formatTimeCellValue($node) + private function formatTimeCellValue(DOMElement $node): DateInterval|string { // The XML node looks like this: // @@ -213,9 +198,9 @@ class CellValueFormatter $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); try { - $cellValue = new \DateInterval($nodeValue); - } catch (\Exception $e) { - throw new InvalidValueException($nodeValue); + $cellValue = new DateInterval($nodeValue); + } catch (Exception $previous) { + throw new InvalidValueException($nodeValue, '', 0, $previous); } } @@ -225,11 +210,9 @@ class CellValueFormatter /** * Returns the cell Currency value from the given node. * - * @param \DOMElement $node - * * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR") */ - protected function formatCurrencyCellValue($node) + private function formatCurrencyCellValue(DOMElement $node): string { $value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); $currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY); @@ -240,29 +223,22 @@ class CellValueFormatter /** * Returns the cell Percentage value from the given node. * - * @param \DOMElement $node - * * @return float|int The value associated with the cell */ - protected function formatPercentageCellValue($node) + private function formatPercentageCellValue(DOMElement $node): float|int { // percentages are formatted like floats return $this->formatFloatCellValue($node); } - /** - * @param \DOMNode $pNode - * - * @return string - */ - private function extractTextValueFromNode($pNode) + private function extractTextValueFromNode(DOMNode $pNode): string { $textValue = ''; foreach ($pNode->childNodes as $childNode) { - if ($childNode instanceof \DOMText) { + if ($childNode instanceof DOMText) { $textValue .= $childNode->nodeValue; - } elseif ($this->isWhitespaceNode($childNode->nodeName)) { + } elseif ($this->isWhitespaceNode($childNode->nodeName) && $childNode instanceof DOMElement) { $textValue .= $this->transformWhitespaceNode($childNode); } elseif (self::XML_NODE_TEXT_A === $childNode->nodeName || self::XML_NODE_TEXT_SPAN === $childNode->nodeName) { $textValue .= $this->extractTextValueFromNode($childNode); @@ -277,14 +253,10 @@ class CellValueFormatter * - * - * - . - * - * @param string $nodeName - * - * @return bool */ - private function isWhitespaceNode($nodeName) + private function isWhitespaceNode(string $nodeName): bool { - return isset(self::$WHITESPACE_XML_NODES[$nodeName]); + return isset(self::WHITESPACE_XML_NODES[$nodeName]); } /** @@ -297,15 +269,15 @@ class CellValueFormatter * * @see https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415200_253892949 * - * @param \DOMElement $node The XML node representing a whitespace + * @param DOMElement $node The XML node representing a whitespace * * @return string The corresponding whitespace value */ - private function transformWhitespaceNode($node) + private function transformWhitespaceNode(DOMElement $node): string { $countAttribute = $node->getAttribute(self::XML_ATTRIBUTE_C); // only defined for "" - $numWhitespaces = (!empty($countAttribute)) ? (int) $countAttribute : 1; + $numWhitespaces = '' !== $countAttribute ? (int) $countAttribute : 1; - return str_repeat(self::$WHITESPACE_XML_NODES[$node->nodeName], $numWhitespaces); + return str_repeat(self::WHITESPACE_XML_NODES[$node->nodeName], $numWhitespaces); } } diff --git a/lib/openspout/src/Reader/ODS/Helper/SettingsHelper.php b/lib/openspout/src/Reader/ODS/Helper/SettingsHelper.php index 4463eeb3e91..9925e88a08f 100644 --- a/lib/openspout/src/Reader/ODS/Helper/SettingsHelper.php +++ b/lib/openspout/src/Reader/ODS/Helper/SettingsHelper.php @@ -1,41 +1,34 @@ entityFactory = $entityFactory; - } - /** * @param string $filePath Path of the file to be read * * @return null|string Name of the sheet that was defined as active or NULL if none found */ - public function getActiveSheetName($filePath) + public function getActiveSheetName(string $filePath): ?string { - $xmlReader = $this->entityFactory->createXMLReader(); + $xmlReader = new XMLReader(); if (false === $xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH)) { return null; } @@ -50,7 +43,7 @@ class SettingsHelper break; } } - } catch (XMLProcessingException $exception) { + } catch (XMLProcessingException $exception) { // @codeCoverageIgnore // do nothing } diff --git a/lib/openspout/src/Reader/ODS/Manager/OptionsManager.php b/lib/openspout/src/Reader/ODS/Manager/OptionsManager.php deleted file mode 100644 index e13c5446dc5..00000000000 --- a/lib/openspout/src/Reader/ODS/Manager/OptionsManager.php +++ /dev/null @@ -1,32 +0,0 @@ -setOption(Options::SHOULD_FORMAT_DATES, false); - $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); - } -} diff --git a/lib/openspout/src/Reader/ODS/Options.php b/lib/openspout/src/Reader/ODS/Options.php new file mode 100644 index 00000000000..f7641eabe67 --- /dev/null +++ b/lib/openspout/src/Reader/ODS/Options.php @@ -0,0 +1,11 @@ + */ -class Reader extends ReaderAbstract +final class Reader extends AbstractReader { - /** @var \ZipArchive */ - protected $zip; + private ZipArchive $zip; + + private Options $options; /** @var SheetIterator To iterator over the ODS sheets */ - protected $sheetIterator; + private SheetIterator $sheetIterator; + + public function __construct(?Options $options = null) + { + $this->options = $options ?? new Options(); + } + + public function getSheetIterator(): SheetIterator + { + $this->ensureStreamOpened(); + + return $this->sheetIterator; + } /** * Returns whether stream wrappers are supported. - * - * @return bool */ - protected function doesSupportStreamWrapper() + protected function doesSupportStreamWrapper(): bool { return false; } @@ -35,39 +50,22 @@ class Reader extends ReaderAbstract * @throws \OpenSpout\Common\Exception\IOException If the file at the given path or its content cannot be read * @throws \OpenSpout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ - protected function openReader($filePath) + protected function openReader(string $filePath): void { - /** @var InternalEntityFactory $entityFactory */ - $entityFactory = $this->entityFactory; + $this->zip = new ZipArchive(); - $this->zip = $entityFactory->createZipArchive(); - - if (true === $this->zip->open($filePath)) { - /** @var InternalEntityFactory $entityFactory */ - $entityFactory = $this->entityFactory; - $this->sheetIterator = $entityFactory->createSheetIterator($filePath, $this->optionsManager); - } else { + if (true !== $this->zip->open($filePath)) { throw new IOException("Could not open {$filePath} for reading."); } - } - /** - * Returns an iterator to iterate over sheets. - * - * @return SheetIterator To iterate over sheets - */ - protected function getConcreteSheetIterator() - { - return $this->sheetIterator; + $this->sheetIterator = new SheetIterator($filePath, $this->options, new ODS(), new SettingsHelper()); } /** * Closes the reader. To be used after reading the file. */ - protected function closeReader() + protected function closeReader(): void { - if (null !== $this->zip) { - $this->zip->close(); - } + $this->zip->close(); } } diff --git a/lib/openspout/src/Reader/ODS/RowIterator.php b/lib/openspout/src/Reader/ODS/RowIterator.php index a3ab7aa4fd0..caac67eee85 100644 --- a/lib/openspout/src/Reader/ODS/RowIterator.php +++ b/lib/openspout/src/Reader/ODS/RowIterator.php @@ -1,103 +1,79 @@ " element - * @param OptionsManagerInterface $optionsManager Reader's options manager - * @param CellValueFormatter $cellValueFormatter Helper to format cell values - * @param XMLProcessor $xmlProcessor Helper to process XML files - * @param RowManager $rowManager Manages rows - * @param InternalEntityFactory $entityFactory Factory to create entities - */ public function __construct( - XMLReader $xmlReader, - OptionsManagerInterface $optionsManager, + Options $options, CellValueFormatter $cellValueFormatter, - XMLProcessor $xmlProcessor, - RowManager $rowManager, - InternalEntityFactory $entityFactory + XMLProcessor $xmlProcessor ) { - $this->xmlReader = $xmlReader; - $this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS); $this->cellValueFormatter = $cellValueFormatter; - $this->entityFactory = $entityFactory; - $this->rowManager = $rowManager; // Register all callbacks to process different nodes when reading the XML file $this->xmlProcessor = $xmlProcessor; @@ -105,6 +81,7 @@ class RowIterator implements IteratorInterface $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']); + $this->options = $options; } /** @@ -115,7 +92,6 @@ class RowIterator implements IteratorInterface * * @throws \OpenSpout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once */ - #[\ReturnTypeWillChange] public function rewind(): void { // Because sheet and row data is located in the file, we can't rewind both the @@ -139,7 +115,6 @@ class RowIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.valid.php */ - #[\ReturnTypeWillChange] public function valid(): bool { return !$this->hasReachedEndOfFile; @@ -153,7 +128,6 @@ class RowIterator implements IteratorInterface * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML */ - #[\ReturnTypeWillChange] public function next(): void { if ($this->doesNeedDataForNextRowToBeProcessed()) { @@ -168,7 +142,6 @@ class RowIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.current.php */ - #[\ReturnTypeWillChange] public function current(): Row { return $this->rowBuffer; @@ -179,21 +152,11 @@ class RowIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.key.php */ - #[\ReturnTypeWillChange] public function key(): int { return $this->lastRowIndexProcessed; } - /** - * Cleans up what was created to iterate over the object. - */ - #[\ReturnTypeWillChange] - public function end(): void - { - $this->xmlReader->close(); - } - /** * Returns whether we need data for the next row to be processed. * We DO need to read data if: @@ -203,7 +166,7 @@ class RowIterator implements IteratorInterface * * @return bool whether we need data for the next row to be processed */ - protected function doesNeedDataForNextRowToBeProcessed() + private function doesNeedDataForNextRowToBeProcessed(): bool { $hasReadAtLeastOneRow = (0 !== $this->lastRowIndexProcessed); @@ -217,25 +180,21 @@ class RowIterator implements IteratorInterface * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML */ - protected function readDataForNextRow() + private function readDataForNextRow(): void { - $this->currentlyProcessedRow = $this->entityFactory->createRow(); + $this->currentlyProcessedRow = new Row([], null); - try { - $this->xmlProcessor->readUntilStopped(); - } catch (XMLProcessingException $exception) { - throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]"); - } + $this->xmlProcessor->readUntilStopped(); $this->rowBuffer = $this->currentlyProcessedRow; } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node * * @return int A return code that indicates what action should the processor take next */ - protected function processRowStartingNode($xmlReader) + private function processRowStartingNode(XMLReader $xmlReader): int { // Reset data from current row $this->hasAlreadyReadOneCellInCurrentRow = false; @@ -247,16 +206,16 @@ class RowIterator implements IteratorInterface } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node * * @return int A return code that indicates what action should the processor take next */ - protected function processCellStartingNode($xmlReader) + private function processCellStartingNode(XMLReader $xmlReader): int { $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader); // NOTE: expand() will automatically decode all XML entities of the child nodes - /** @var \DOMElement $node */ + /** @var DOMElement $node */ $node = $xmlReader->expand(); $currentCell = $this->getCell($node); @@ -277,12 +236,12 @@ class RowIterator implements IteratorInterface /** * @return int A return code that indicates what action should the processor take next */ - protected function processRowEndingNode() + private function processRowEndingNode(): int { $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRow, $this->lastProcessedCell); // if the fetched row is empty and we don't want to preserve it... - if (!$this->shouldPreserveEmptyRows && $isEmptyRow) { + if (!$this->options->SHOULD_PRESERVE_EMPTY_ROWS && $isEmptyRow) { // ... skip it return XMLProcessor::PROCESSING_CONTINUE; } @@ -315,7 +274,7 @@ class RowIterator implements IteratorInterface /** * @return int A return code that indicates what action should the processor take next */ - protected function processTableEndingNode() + private function processTableEndingNode(): int { // The closing "" marks the end of the file $this->hasReachedEndOfFile = true; @@ -324,11 +283,11 @@ class RowIterator implements IteratorInterface } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node * * @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing */ - protected function getNumRowsRepeatedForCurrentNode($xmlReader) + private function getNumRowsRepeatedForCurrentNode(XMLReader $xmlReader): int { $numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED); @@ -336,11 +295,11 @@ class RowIterator implements IteratorInterface } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node * * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing */ - protected function getNumColumnsRepeatedForCurrentNode($xmlReader) + private function getNumColumnsRepeatedForCurrentNode(XMLReader $xmlReader): int { $numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED); @@ -350,18 +309,15 @@ class RowIterator implements IteratorInterface /** * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node. * - * @param \DOMElement $node - * * @return Cell The cell set with the associated with the cell */ - protected function getCell($node) + private function getCell(DOMElement $node): Cell { try { $cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node); - $cell = $this->entityFactory->createCell($cellValue); + $cell = Cell::fromValue($cellValue); } catch (InvalidValueException $exception) { - $cell = $this->entityFactory->createCell($exception->getInvalidValue()); - $cell->setType(Cell::TYPE_ERROR); + $cell = new Cell\ErrorCell($exception->getInvalidValue(), null); } return $cell; @@ -373,16 +329,15 @@ class RowIterator implements IteratorInterface * After finishing processing each cell, the last read cell is not part of the * row data yet (as we still need to apply the "num-columns-repeated" attribute). * - * @param Row $currentRow * @param null|Cell $lastReadCell The last read cell * * @return bool Whether the row is empty */ - protected function isEmptyRow($currentRow, $lastReadCell) + private function isEmptyRow(Row $currentRow, ?Cell $lastReadCell): bool { return - $this->rowManager->isEmpty($currentRow) - && (!isset($lastReadCell) || $lastReadCell->isEmpty()) + $currentRow->isEmpty() + && (null === $lastReadCell || $lastReadCell instanceof Cell\EmptyCell) ; } } diff --git a/lib/openspout/src/Reader/ODS/Sheet.php b/lib/openspout/src/Reader/ODS/Sheet.php index 306f2688834..7f71d0a09e0 100644 --- a/lib/openspout/src/Reader/ODS/Sheet.php +++ b/lib/openspout/src/Reader/ODS/Sheet.php @@ -1,31 +1,30 @@ */ -class Sheet implements SheetInterface +final class Sheet implements SheetWithVisibilityInterface { - /** @var \OpenSpout\Reader\ODS\RowIterator To iterate over sheet's rows */ - protected $rowIterator; - - /** @var int ID of the sheet */ - protected $id; + /** @var RowIterator To iterate over sheet's rows */ + private RowIterator $rowIterator; /** @var int Index of the sheet, based on order in the workbook (zero-based) */ - protected $index; + private int $index; /** @var string Name of the sheet */ - protected $name; + private string $name; /** @var bool Whether the sheet was the active one */ - protected $isActive; + private bool $isActive; /** @var bool Whether the sheet is visible */ - protected $isVisible; + private bool $isVisible; /** * @param RowIterator $rowIterator The corresponding row iterator @@ -34,7 +33,7 @@ class Sheet implements SheetInterface * @param bool $isSheetActive Whether the sheet was defined as active * @param bool $isSheetVisible Whether the sheet is visible */ - public function __construct($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible) + public function __construct(RowIterator $rowIterator, int $sheetIndex, string $sheetName, bool $isSheetActive, bool $isSheetVisible) { $this->rowIterator = $rowIterator; $this->index = $sheetIndex; @@ -43,10 +42,7 @@ class Sheet implements SheetInterface $this->isVisible = $isSheetVisible; } - /** - * @return \OpenSpout\Reader\ODS\RowIterator - */ - public function getRowIterator() + public function getRowIterator(): RowIterator { return $this->rowIterator; } @@ -54,7 +50,7 @@ class Sheet implements SheetInterface /** * @return int Index of the sheet, based on order in the workbook (zero-based) */ - public function getIndex() + public function getIndex(): int { return $this->index; } @@ -62,7 +58,7 @@ class Sheet implements SheetInterface /** * @return string Name of the sheet */ - public function getName() + public function getName(): string { return $this->name; } @@ -70,7 +66,7 @@ class Sheet implements SheetInterface /** * @return bool Whether the sheet was defined as active */ - public function isActive() + public function isActive(): bool { return $this->isActive; } @@ -78,7 +74,7 @@ class Sheet implements SheetInterface /** * @return bool Whether the sheet is visible */ - public function isVisible() + public function isVisible(): bool { return $this->isVisible; } diff --git a/lib/openspout/src/Reader/ODS/SheetIterator.php b/lib/openspout/src/Reader/ODS/SheetIterator.php index 5240e3873d2..d0d40f508f3 100644 --- a/lib/openspout/src/Reader/ODS/SheetIterator.php +++ b/lib/openspout/src/Reader/ODS/SheetIterator.php @@ -1,24 +1,31 @@ */ -class SheetIterator implements IteratorInterface +final class SheetIterator implements SheetIteratorInterface { public const CONTENT_XML_FILE_PATH = 'content.xml'; public const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'; - /** Definition of XML nodes name and attribute used to parse sheet data */ + /** + * Definition of XML nodes name and attribute used to parse sheet data. + */ public const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles'; public const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties'; public const XML_NODE_TABLE = 'table:table'; @@ -28,45 +35,37 @@ class SheetIterator implements IteratorInterface public const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display'; /** @var string Path of the file to be read */ - protected $filePath; + private string $filePath; - /** @var \OpenSpout\Common\Manager\OptionsManagerInterface Reader's options manager */ - protected $optionsManager; - - /** @var InternalEntityFactory Factory to create entities */ - protected $entityFactory; + private Options $options; /** @var XMLReader The XMLReader object that will help read sheet's XML data */ - protected $xmlReader; + private XMLReader $xmlReader; - /** @var \OpenSpout\Common\Helper\Escaper\ODS Used to unescape XML data */ - protected $escaper; + /** @var ODS Used to unescape XML data */ + private ODS $escaper; /** @var bool Whether there are still at least a sheet to be read */ - protected $hasFoundSheet; + private bool $hasFoundSheet; /** @var int The index of the sheet being read (zero-based) */ - protected $currentSheetIndex; + private int $currentSheetIndex; /** @var string The name of the sheet that was defined as active */ - protected $activeSheetName; + private ?string $activeSheetName; - /** @var array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */ - protected $sheetsVisibility; + /** @var array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */ + private array $sheetsVisibility; - /** - * @param string $filePath Path of the file to be read - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager - * @param \OpenSpout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data - * @param SettingsHelper $settingsHelper Helper to get data from "settings.xml" - * @param InternalEntityFactory $entityFactory Factory to create entities - */ - public function __construct($filePath, $optionsManager, $escaper, $settingsHelper, $entityFactory) - { + public function __construct( + string $filePath, + Options $options, + ODS $escaper, + SettingsHelper $settingsHelper + ) { $this->filePath = $filePath; - $this->optionsManager = $optionsManager; - $this->entityFactory = $entityFactory; - $this->xmlReader = $entityFactory->createXMLReader(); + $this->options = $options; + $this->xmlReader = new XMLReader(); $this->escaper = $escaper; $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath); } @@ -78,8 +77,7 @@ class SheetIterator implements IteratorInterface * * @throws \OpenSpout\Common\Exception\IOException If unable to open the XML file containing sheets' data */ - #[\ReturnTypeWillChange] - public function rewind() + public function rewind(): void { $this->xmlReader->close(); @@ -103,13 +101,15 @@ class SheetIterator implements IteratorInterface * Checks if current position is valid. * * @see http://php.net/manual/en/iterator.valid.php - * - * @return bool */ - #[\ReturnTypeWillChange] - public function valid() + public function valid(): bool { - return $this->hasFoundSheet; + $valid = $this->hasFoundSheet; + if (!$valid) { + $this->xmlReader->close(); + } + + return $valid; } /** @@ -117,8 +117,7 @@ class SheetIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.next.php */ - #[\ReturnTypeWillChange] - public function next() + public function next(): void { $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); @@ -131,27 +130,29 @@ class SheetIterator implements IteratorInterface * Return the current element. * * @see http://php.net/manual/en/iterator.current.php - * - * @return \OpenSpout\Reader\ODS\Sheet */ - #[\ReturnTypeWillChange] - public function current() + public function current(): Sheet { $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME); + \assert(null !== $escapedSheetName); $sheetName = $this->escaper->unescape($escapedSheetName); $isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName); $sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME); + \assert(null !== $sheetStyleName); $isSheetVisible = $this->isSheetVisible($sheetStyleName); - return $this->entityFactory->createSheet( - $this->xmlReader, + return new Sheet( + new RowIterator( + $this->options, + new CellValueFormatter($this->options->SHOULD_FORMAT_DATES, new ODS()), + new XMLProcessor($this->xmlReader) + ), $this->currentSheetIndex, $sheetName, $isSheetActive, - $isSheetVisible, - $this->optionsManager + $isSheetVisible ); } @@ -159,44 +160,33 @@ class SheetIterator implements IteratorInterface * Return the key of the current element. * * @see http://php.net/manual/en/iterator.key.php - * - * @return int */ - #[\ReturnTypeWillChange] - public function key() + public function key(): int { return $this->currentSheetIndex + 1; } - /** - * Cleans up what was created to iterate over the object. - */ - #[\ReturnTypeWillChange] - public function end() - { - $this->xmlReader->close(); - } - /** * Extracts the visibility of the sheets. * - * @return array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] + * @return array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */ - private function readSheetsVisibility() + private function readSheetsVisibility(): array { $sheetsVisibility = []; $this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES); - /** @var \DOMElement $automaticStylesNode */ + $automaticStylesNode = $this->xmlReader->expand(); + \assert($automaticStylesNode instanceof DOMElement); $tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES); - /** @var \DOMElement $tableStyleNode */ foreach ($tableStyleNodes as $tableStyleNode) { $isSheetVisible = ('false' !== $tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY)); $parentStyleNode = $tableStyleNode->parentNode; + \assert($parentStyleNode instanceof DOMElement); $styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME); $sheetsVisibility[$styleName] = $isSheetVisible; @@ -214,7 +204,7 @@ class SheetIterator implements IteratorInterface * * @return bool Whether the current sheet was defined as the active one */ - private function isSheetActive($sheetName, $sheetIndex, $activeSheetName) + private function isSheetActive(string $sheetName, int $sheetIndex, ?string $activeSheetName): bool { // The given sheet is active if its name matches the defined active sheet's name // or if no information about the active sheet was found, it defaults to the first sheet. @@ -231,7 +221,7 @@ class SheetIterator implements IteratorInterface * * @return bool Whether the current sheet is visible */ - private function isSheetVisible($sheetStyleName) + private function isSheetVisible(string $sheetStyleName): bool { return $this->sheetsVisibility[$sheetStyleName] ?? true; diff --git a/lib/openspout/src/Reader/ReaderInterface.php b/lib/openspout/src/Reader/ReaderInterface.php index a1d9f74d73e..146bdf88c62 100644 --- a/lib/openspout/src/Reader/ReaderInterface.php +++ b/lib/openspout/src/Reader/ReaderInterface.php @@ -1,9 +1,11 @@ + */ +interface RowIteratorInterface extends Iterator { - /** - * Cleans up what was created to iterate over the object. - */ - #[\ReturnTypeWillChange] - public function end(); - - /** - * @return null|Row - */ - #[\ReturnTypeWillChange] - public function current(); + public function current(): ?Row; } diff --git a/lib/openspout/src/Reader/SheetInterface.php b/lib/openspout/src/Reader/SheetInterface.php index 985b136e75c..ebd41e3b128 100644 --- a/lib/openspout/src/Reader/SheetInterface.php +++ b/lib/openspout/src/Reader/SheetInterface.php @@ -1,34 +1,31 @@ */ -interface SheetIteratorInterface extends IteratorInterface +interface SheetIteratorInterface extends Iterator { /** - * Cleans up what was created to iterate over the object. + * @return T of SheetInterface */ - #[\ReturnTypeWillChange] - public function end(); - - /** - * @return null|SheetInterface - */ - #[\ReturnTypeWillChange] - public function current(); + public function current(): SheetInterface; } diff --git a/lib/openspout/src/Reader/SheetWithVisibilityInterface.php b/lib/openspout/src/Reader/SheetWithVisibilityInterface.php new file mode 100644 index 00000000000..e4566045762 --- /dev/null +++ b/lib/openspout/src/Reader/SheetWithVisibilityInterface.php @@ -0,0 +1,18 @@ + + */ +interface SheetWithVisibilityInterface extends SheetInterface +{ + /** + * @return bool Whether the sheet is visible + */ + public function isVisible(): bool; +} diff --git a/lib/openspout/src/Reader/Wrapper/XMLInternalErrorsHelper.php b/lib/openspout/src/Reader/Wrapper/XMLInternalErrorsHelper.php index 788e732a5bb..971750c5c7e 100644 --- a/lib/openspout/src/Reader/Wrapper/XMLInternalErrorsHelper.php +++ b/lib/openspout/src/Reader/Wrapper/XMLInternalErrorsHelper.php @@ -1,22 +1,24 @@ initialUseInternalErrorsValue = libxml_use_internal_errors(true); @@ -28,7 +30,7 @@ trait XMLInternalErrorsHelper * * @throws \OpenSpout\Reader\Exception\XMLProcessingException */ - protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured() + private function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(): void { if ($this->hasXMLErrorOccured()) { $this->resetXMLInternalErrorsSetting(); @@ -39,7 +41,7 @@ trait XMLInternalErrorsHelper $this->resetXMLInternalErrorsSetting(); } - protected function resetXMLInternalErrorsSetting() + private function resetXMLInternalErrorsSetting(): void { libxml_use_internal_errors($this->initialUseInternalErrorsValue); } @@ -49,7 +51,7 @@ trait XMLInternalErrorsHelper * * @return bool TRUE if an error occured, FALSE otherwise */ - private function hasXMLErrorOccured() + private function hasXMLErrorOccured(): bool { return false !== libxml_get_last_error(); } @@ -59,11 +61,11 @@ trait XMLInternalErrorsHelper * * @see libxml_get_last_error * - * @return null|string Last XML error message or null if no error + * @return string Last XML error message or null if no error */ - private function getLastXMLErrorMessage() + private function getLastXMLErrorMessage(): string { - $errorMessage = null; + $errorMessage = ''; $error = libxml_get_last_error(); if (false !== $error) { diff --git a/lib/openspout/src/Reader/Wrapper/XMLReader.php b/lib/openspout/src/Reader/Wrapper/XMLReader.php index 946ca27f632..8fa9663f12c 100644 --- a/lib/openspout/src/Reader/Wrapper/XMLReader.php +++ b/lib/openspout/src/Reader/Wrapper/XMLReader.php @@ -1,13 +1,15 @@ getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath); // We need to check first that the file we are trying to read really exist because: // - PHP emits a warning when trying to open a file that does not exist. - // - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779) if ($this->fileExistsWithinZip($realPathURI)) { $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); } @@ -45,7 +46,7 @@ class XMLReader extends \XMLReader * * @return string The real path URI */ - public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath) + public function getRealPathURIForFileInZip(string $zipFilePath, string $fileInsideZipPath): string { // The file path should not start with a '/', otherwise it won't be found $fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/'); @@ -59,11 +60,8 @@ class XMLReader extends \XMLReader * @see \XMLReader::read * * @throws \OpenSpout\Reader\Exception\XMLProcessingException If an error/warning occurred - * - * @return bool TRUE on success or FALSE on failure */ - #[\ReturnTypeWillChange] - public function read() + public function read(): bool { $this->useXMLInternalErrors(); @@ -79,11 +77,11 @@ class XMLReader extends \XMLReader * * @param string $nodeName Name of the node to find * - * @throws \OpenSpout\Reader\Exception\XMLProcessingException If an error/warning occurred - * * @return bool TRUE on success or FALSE on failure + * + * @throws \OpenSpout\Reader\Exception\XMLProcessingException If an error/warning occurred */ - public function readUntilNodeFound($nodeName) + public function readUntilNodeFound(string $nodeName): bool { do { $wasReadSuccessful = $this->read(); @@ -101,11 +99,8 @@ class XMLReader extends \XMLReader * @param null|string $localName The name of the next node to move to * * @throws \OpenSpout\Reader\Exception\XMLProcessingException If an error/warning occurred - * - * @return bool TRUE on success or FALSE on failure */ - #[\ReturnTypeWillChange] - public function next($localName = null) + public function next($localName = null): bool { $this->useXMLInternalErrors(); @@ -117,21 +112,17 @@ class XMLReader extends \XMLReader } /** - * @param string $nodeName - * * @return bool Whether the XML Reader is currently positioned on the starting node with given name */ - public function isPositionedOnStartingNode($nodeName) + public function isPositionedOnStartingNode(string $nodeName): bool { return $this->isPositionedOnNode($nodeName, self::ELEMENT); } /** - * @param string $nodeName - * * @return bool Whether the XML Reader is currently positioned on the ending node with given name */ - public function isPositionedOnEndingNode($nodeName) + public function isPositionedOnEndingNode(string $nodeName): bool { return $this->isPositionedOnNode($nodeName, self::END_ELEMENT); } @@ -139,7 +130,7 @@ class XMLReader extends \XMLReader /** * @return string The name of the current node, un-prefixed */ - public function getCurrentNodeName() + public function getCurrentNodeName(): string { return $this->localName; } @@ -151,16 +142,16 @@ class XMLReader extends \XMLReader * * @return bool TRUE if the file exists, FALSE otherwise */ - protected function fileExistsWithinZip($zipStreamURI) + private function fileExistsWithinZip(string $zipStreamURI): bool { $doesFileExists = false; $pattern = '/zip:\/\/([^#]+)#(.*)/'; - if (preg_match($pattern, $zipStreamURI, $matches)) { + if (1 === preg_match($pattern, $zipStreamURI, $matches)) { $zipFilePath = $matches[1]; $innerFilePath = $matches[2]; - $zip = new \ZipArchive(); + $zip = new ZipArchive(); if (true === $zip->open($zipFilePath)) { $doesFileExists = (false !== $zip->locateName($innerFilePath)); $zip->close(); @@ -171,12 +162,9 @@ class XMLReader extends \XMLReader } /** - * @param string $nodeName - * @param int $nodeType - * * @return bool Whether the XML Reader is currently positioned on the node with given name and type */ - private function isPositionedOnNode($nodeName, $nodeType) + private function isPositionedOnNode(string $nodeName, int $nodeType): bool { /** * In some cases, the node has a prefix (for instance, "" can also be ""). @@ -184,7 +172,7 @@ class XMLReader extends \XMLReader * * @see https://github.com/box/spout/issues/233 */ - $hasPrefix = (false !== strpos($nodeName, ':')); + $hasPrefix = str_contains($nodeName, ':'); $currentNodeName = ($hasPrefix) ? $this->name : $this->localName; return $this->nodeType === $nodeType && $currentNodeName === $nodeName; diff --git a/lib/openspout/src/Reader/XLSX/Creator/HelperFactory.php b/lib/openspout/src/Reader/XLSX/Creator/HelperFactory.php deleted file mode 100644 index 3528b23e6e8..00000000000 --- a/lib/openspout/src/Reader/XLSX/Creator/HelperFactory.php +++ /dev/null @@ -1,38 +0,0 @@ -createStringsEscaper(); - - return new CellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper); - } - - /** - * @return Escaper\XLSX - */ - public function createStringsEscaper() - { - // @noinspection PhpUnnecessaryFullyQualifiedNameInspection - return new Escaper\XLSX(); - } -} diff --git a/lib/openspout/src/Reader/XLSX/Creator/InternalEntityFactory.php b/lib/openspout/src/Reader/XLSX/Creator/InternalEntityFactory.php deleted file mode 100644 index 33b18df6fb3..00000000000 --- a/lib/openspout/src/Reader/XLSX/Creator/InternalEntityFactory.php +++ /dev/null @@ -1,163 +0,0 @@ -managerFactory = $managerFactory; - $this->helperFactory = $helperFactory; - } - - /** - * @param string $filePath Path of the file to be read - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager - * @param SharedStringsManager $sharedStringsManager Manages shared strings - * - * @return SheetIterator - */ - public function createSheetIterator($filePath, $optionsManager, $sharedStringsManager) - { - $sheetManager = $this->managerFactory->createSheetManager( - $filePath, - $optionsManager, - $sharedStringsManager, - $this - ); - - return new SheetIterator($sheetManager); - } - - /** - * @param string $filePath Path of the XLSX file being read - * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml - * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) - * @param string $sheetName Name of the sheet - * @param bool $isSheetActive Whether the sheet was defined as active - * @param bool $isSheetVisible Whether the sheet is visible - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager - * @param SharedStringsManager $sharedStringsManager Manages shared strings - * - * @return Sheet - */ - public function createSheet( - $filePath, - $sheetDataXMLFilePath, - $sheetIndex, - $sheetName, - $isSheetActive, - $isSheetVisible, - $optionsManager, - $sharedStringsManager - ) { - $rowIterator = $this->createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager); - - return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible); - } - - /** - * @param Cell[] $cells - * - * @return Row - */ - public function createRow(array $cells = []) - { - return new Row($cells, null); - } - - /** - * @param mixed $cellValue - * - * @return Cell - */ - public function createCell($cellValue) - { - return new Cell($cellValue); - } - - /** - * @return \ZipArchive - */ - public function createZipArchive() - { - return new \ZipArchive(); - } - - /** - * @return XMLReader - */ - public function createXMLReader() - { - return new XMLReader(); - } - - /** - * @param XMLReader $xmlReader - * - * @return XMLProcessor - */ - public function createXMLProcessor($xmlReader) - { - return new XMLProcessor($xmlReader); - } - - /** - * @param string $filePath Path of the XLSX file being read - * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager - * @param SharedStringsManager $sharedStringsManager Manages shared strings - * - * @return RowIterator - */ - private function createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager) - { - $xmlReader = $this->createXMLReader(); - $xmlProcessor = $this->createXMLProcessor($xmlReader); - - $styleManager = $this->managerFactory->createStyleManager($filePath, $this); - $rowManager = $this->managerFactory->createRowManager($this); - $shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES); - $shouldUse1904Dates = $optionsManager->getOption(Options::SHOULD_USE_1904_DATES); - - $cellValueFormatter = $this->helperFactory->createCellValueFormatter( - $sharedStringsManager, - $styleManager, - $shouldFormatDates, - $shouldUse1904Dates - ); - - $shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS); - - return new RowIterator( - $filePath, - $sheetDataXMLFilePath, - $shouldPreserveEmptyRows, - $xmlReader, - $xmlProcessor, - $cellValueFormatter, - $rowManager, - $this - ); - } -} diff --git a/lib/openspout/src/Reader/XLSX/Creator/ManagerFactory.php b/lib/openspout/src/Reader/XLSX/Creator/ManagerFactory.php deleted file mode 100644 index 10a8833a4e9..00000000000 --- a/lib/openspout/src/Reader/XLSX/Creator/ManagerFactory.php +++ /dev/null @@ -1,109 +0,0 @@ -helperFactory = $helperFactory; - $this->cachingStrategyFactory = $cachingStrategyFactory; - } - - /** - * @param string $filePath Path of the XLSX file being read - * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored - * @param InternalEntityFactory $entityFactory Factory to create entities - * - * @return SharedStringsManager - */ - public function createSharedStringsManager($filePath, $tempFolder, $entityFactory) - { - $workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory); - - return new SharedStringsManager( - $filePath, - $tempFolder, - $workbookRelationshipsManager, - $entityFactory, - $this->helperFactory, - $this->cachingStrategyFactory - ); - } - - /** - * @param string $filePath Path of the XLSX file being read - * @param \OpenSpout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager - * @param \OpenSpout\Reader\XLSX\Manager\SharedStringsManager $sharedStringsManager Manages shared strings - * @param InternalEntityFactory $entityFactory Factory to create entities - * - * @return SheetManager - */ - public function createSheetManager($filePath, $optionsManager, $sharedStringsManager, $entityFactory) - { - $escaper = $this->helperFactory->createStringsEscaper(); - - return new SheetManager($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory); - } - - /** - * @param string $filePath Path of the XLSX file being read - * @param InternalEntityFactory $entityFactory Factory to create entities - * - * @return StyleManager - */ - public function createStyleManager($filePath, $entityFactory) - { - $workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory); - - return new StyleManager($filePath, $workbookRelationshipsManager, $entityFactory); - } - - /** - * @param InternalEntityFactory $entityFactory Factory to create entities - * - * @return RowManager - */ - public function createRowManager($entityFactory) - { - return new RowManager($entityFactory); - } - - /** - * @param string $filePath Path of the XLSX file being read - * @param InternalEntityFactory $entityFactory Factory to create entities - * - * @return WorkbookRelationshipsManager - */ - private function createWorkbookRelationshipsManager($filePath, $entityFactory) - { - if (!isset($this->cachedWorkbookRelationshipsManager)) { - $this->cachedWorkbookRelationshipsManager = new WorkbookRelationshipsManager($filePath, $entityFactory); - } - - return $this->cachedWorkbookRelationshipsManager; - } -} diff --git a/lib/openspout/src/Reader/XLSX/Helper/CellHelper.php b/lib/openspout/src/Reader/XLSX/Helper/CellHelper.php index 827d728ba57..19c4c248df2 100644 --- a/lib/openspout/src/Reader/XLSX/Helper/CellHelper.php +++ b/lib/openspout/src/Reader/XLSX/Helper/CellHelper.php @@ -1,16 +1,18 @@ 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, @@ -26,10 +28,8 @@ class CellHelper * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) * * @throws \OpenSpout\Common\Exception\InvalidArgumentException When the given cell index is invalid - * - * @return int */ - public static function getColumnIndexFromCellIndex($cellIndex) + public static function getColumnIndexFromCellIndex(string $cellIndex): int { if (!self::isValidCellIndex($cellIndex)) { throw new InvalidArgumentException('Cannot get column index from an invalid cell index.'); @@ -48,21 +48,21 @@ class CellHelper // Also, not using the pow() function because it's slooooow... switch ($columnLength) { case 1: - $columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]); + $columnIndex = self::columnLetterToIndexMapping[$columnLetters]; break; case 2: - $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26; - $secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]]; + $firstLetterIndex = (self::columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26; + $secondLetterIndex = self::columnLetterToIndexMapping[$columnLetters[1]]; $columnIndex = $firstLetterIndex + $secondLetterIndex; break; case 3: - $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676; - $secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26; - $thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]]; + $firstLetterIndex = (self::columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676; + $secondLetterIndex = (self::columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26; + $thirdLetterIndex = self::columnLetterToIndexMapping[$columnLetters[2]]; $columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex; break; @@ -77,10 +77,8 @@ class CellHelper * There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'. * * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) - * - * @return bool */ - protected static function isValidCellIndex($cellIndex) + private static function isValidCellIndex(string $cellIndex): bool { return 1 === preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex); } diff --git a/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php b/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php index 1734fb5aab2..9f15fb48091 100644 --- a/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php +++ b/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php @@ -1,17 +1,25 @@ sharedStringsManager = $sharedStringsManager; $this->styleManager = $styleManager; $this->shouldFormatDates = $shouldFormatDates; @@ -65,13 +84,11 @@ class CellValueFormatter /** * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * - * @param \DOMElement $node + * @return bool|DateTimeImmutable|float|int|string The value associated with the cell * * @throws InvalidValueException If the value is not valid - * - * @return bool|\DateTime|float|int|string The value associated with the cell */ - public function extractAndFormatNodeValue($node) + public function extractAndFormatNodeValue(DOMElement $node): bool|DateTimeImmutable|float|int|string { // Default cell type is "n" $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC; @@ -82,54 +99,37 @@ class CellValueFormatter return $vNodeValue; } - switch ($cellType) { - case self::CELL_TYPE_INLINE_STRING: - return $this->formatInlineStringCellValue($node); - - case self::CELL_TYPE_SHARED_STRING: - return $this->formatSharedStringCellValue($vNodeValue); - - case self::CELL_TYPE_STR: - return $this->formatStrCellValue($vNodeValue); - - case self::CELL_TYPE_BOOLEAN: - return $this->formatBooleanCellValue($vNodeValue); - - case self::CELL_TYPE_NUMERIC: - return $this->formatNumericCellValue($vNodeValue, $cellStyleId); - - case self::CELL_TYPE_DATE: - return $this->formatDateCellValue($vNodeValue); - - default: - throw new InvalidValueException($vNodeValue); - } + return match ($cellType) { + self::CELL_TYPE_INLINE_STRING => $this->formatInlineStringCellValue($node), + self::CELL_TYPE_SHARED_STRING => $this->formatSharedStringCellValue($vNodeValue), + self::CELL_TYPE_STR => $this->formatStrCellValue($vNodeValue), + self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($vNodeValue), + self::CELL_TYPE_NUMERIC => $this->formatNumericCellValue($vNodeValue, $cellStyleId), + self::CELL_TYPE_DATE => $this->formatDateCellValue($vNodeValue), + default => throw new InvalidValueException($vNodeValue), + }; } /** * Returns the cell's string value from a node's nested value node. * - * @param \DOMElement $node - * * @return string The value associated with the cell */ - protected function getVNodeValue($node) + private function getVNodeValue(DOMElement $node): string { // for cell types having a "v" tag containing the value. // if not, the returned value should be empty string. $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0); - return (null !== $vNode) ? $vNode->nodeValue : ''; + return (string) $vNode?->nodeValue; } /** * Returns the cell String value where string is inline. * - * @param \DOMElement $node - * * @return string The value associated with the cell */ - protected function formatInlineStringCellValue($node) + private function formatInlineStringCellValue(DOMElement $node): string { // inline strings are formatted this way (they can contain any number of nodes): // [INLINE_STRING][INLINE_STRING_2] @@ -137,8 +137,9 @@ class CellValueFormatter $cellValue = ''; for ($i = 0; $i < $tNodes->count(); ++$i) { - $tNode = $tNodes->item($i); - $cellValue .= $this->escaper->unescape($tNode->nodeValue); + $nodeValue = $tNodes->item($i)->nodeValue; + \assert(null !== $nodeValue); + $cellValue .= $this->escaper->unescape($nodeValue); } return $cellValue; @@ -147,11 +148,9 @@ class CellValueFormatter /** * Returns the cell String value from shared-strings file using nodeValue index. * - * @param string $nodeValue - * * @return string The value associated with the cell */ - protected function formatSharedStringCellValue($nodeValue) + private function formatSharedStringCellValue(string $nodeValue): string { // shared strings are formatted this way: // [SHARED_STRING_INDEX] @@ -164,11 +163,9 @@ class CellValueFormatter /** * Returns the cell String value, where string is stored in value node. * - * @param string $nodeValue - * * @return string The value associated with the cell */ - protected function formatStrCellValue($nodeValue) + private function formatStrCellValue(string $nodeValue): string { $escapedCellValue = trim($nodeValue); @@ -179,12 +176,9 @@ class CellValueFormatter * Returns the cell Numeric value from string of nodeValue. * The value can also represent a timestamp and a DateTime will be returned. * - * @param string $nodeValue - * @param int $cellStyleId 0 being the default style - * - * @return \DateTime|float|int The value associated with the cell + * @param int $cellStyleId 0 being the default style */ - protected function formatNumericCellValue($nodeValue, $cellStyleId) + private function formatNumericCellValue(int|float|string $nodeValue, int $cellStyleId): DateTimeImmutable|float|int|string { // Numeric values can represent numbers as well as timestamps. // We need to look at the style of the cell to determine whether it is one or the other. @@ -207,36 +201,27 @@ class CellValueFormatter * Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting. * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1. * - * @see ECMA-376 Part 1 - §18.17.4 - * - * @param float $nodeValue - * @param int $cellStyleId 0 being the default style + * @param int $cellStyleId 0 being the default style * * @throws InvalidValueException If the value is not a valid timestamp * - * @return \DateTime The value associated with the cell + * @see ECMA-376 Part 1 - §18.17.4 */ - protected function formatExcelTimestampValue($nodeValue, $cellStyleId) + private function formatExcelTimestampValue(float $nodeValue, int $cellStyleId): string|DateTimeImmutable { - if ($this->isValidTimestampValue($nodeValue)) { - $cellValue = $this->formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId); - } else { - throw new InvalidValueException($nodeValue); + if (!$this->isValidTimestampValue($nodeValue)) { + throw new InvalidValueException((string) $nodeValue); } - return $cellValue; + return $this->formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId); } /** * Returns whether the given timestamp is supported by SpreadsheetML. * * @see ECMA-376 Part 1 - §18.17.4 - this specifies the timestamp boundaries. - * - * @param float $timestampValue - * - * @return bool */ - protected function isValidTimestampValue($timestampValue) + private function isValidTimestampValue(float $timestampValue): bool { // @NOTE: some versions of Excel don't support negative dates (e.g. Excel for Mac 2011) return @@ -250,22 +235,32 @@ class CellValueFormatter * Only the time value matters. The date part is set to the base Excel date: * Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting. * - * @param float $nodeValue - * @param int $cellStyleId 0 being the default style - * - * @return \DateTime|string The value associated with the cell + * @param int $cellStyleId 0 being the default style */ - protected function formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId) + private function formatExcelTimestampValueAsDateTimeValue(float $nodeValue, int $cellStyleId): string|DateTimeImmutable { $baseDate = $this->shouldUse1904Dates ? '1904-01-01' : '1899-12-30'; $daysSinceBaseDate = (int) $nodeValue; + $daysSign = '+'; + if ($daysSinceBaseDate < 0) { + $daysSinceBaseDate = abs($daysSinceBaseDate); + $daysSign = '-'; + } $timeRemainder = fmod($nodeValue, 1); $secondsRemainder = round($timeRemainder * self::NUM_SECONDS_IN_ONE_DAY, 0); + $secondsSign = '+'; + if ($secondsRemainder < 0) { + $secondsRemainder = abs($secondsRemainder); + $secondsSign = '-'; + } - $dateObj = \DateTime::createFromFormat('|Y-m-d', $baseDate); - $dateObj->modify('+'.$daysSinceBaseDate.'days'); - $dateObj->modify('+'.$secondsRemainder.'seconds'); + $dateObj = DateTimeImmutable::createFromFormat('|Y-m-d', $baseDate); + \assert(false !== $dateObj); + $dateObj = $dateObj->modify($daysSign.$daysSinceBaseDate.'days'); + \assert(false !== $dateObj); + $dateObj = $dateObj->modify($secondsSign.$secondsRemainder.'seconds'); + \assert(false !== $dateObj); if ($this->shouldFormatDates) { $styleNumberFormatCode = $this->styleManager->getNumberFormatCode($cellStyleId); @@ -281,11 +276,9 @@ class CellValueFormatter /** * Returns the cell Boolean value from a specific node's Value. * - * @param string $nodeValue - * * @return bool The value associated with the cell */ - protected function formatBooleanCellValue($nodeValue) + private function formatBooleanCellValue(string $nodeValue): bool { return (bool) $nodeValue; } @@ -298,16 +291,14 @@ class CellValueFormatter * @param string $nodeValue ISO 8601 Date string * * @throws InvalidValueException If the value is not a valid date - * - * @return \DateTime|string The value associated with the cell */ - protected function formatDateCellValue($nodeValue) + private function formatDateCellValue(string $nodeValue): string|DateTimeImmutable { // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php) try { - $cellValue = ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue); - } catch (\Exception $e) { - throw new InvalidValueException($nodeValue); + $cellValue = ($this->shouldFormatDates) ? $nodeValue : new DateTimeImmutable($nodeValue); + } catch (Exception $exception) { + throw new InvalidValueException($nodeValue, '', 0, $exception); } return $cellValue; diff --git a/lib/openspout/src/Reader/XLSX/Helper/DateFormatHelper.php b/lib/openspout/src/Reader/XLSX/Helper/DateFormatHelper.php index 78fc4f43cb2..5b6fba79b97 100644 --- a/lib/openspout/src/Reader/XLSX/Helper/DateFormatHelper.php +++ b/lib/openspout/src/Reader/XLSX/Helper/DateFormatHelper.php @@ -1,11 +1,13 @@ [ // Time 'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem @@ -57,12 +58,13 @@ class DateFormatHelper * * @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php) */ - public static function toPHPDateFormat($excelDateFormat) + public static function toPHPDateFormat(string $excelDateFormat): string { // Remove brackets potentially present at the beginning of the format string // and text portion of the format at the end of it (starting with ";") // See §18.8.31 of ECMA-376 for more detail. $dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat); + \assert(null !== $dateFormat); // Double quotes are used to escape characters that must not be interpreted. // For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y" @@ -83,13 +85,13 @@ class DateFormatHelper $transformedPart = str_replace('\\', '', $transformedPart); // Apply general transformation first... - $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]); + $transformedPart = strtr($transformedPart, self::excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]); // ... then apply hour transformation, for 12-hour or 24-hour format if (self::has12HourFormatMarker($dateFormatPart)) { - $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]); + $transformedPart = strtr($transformedPart, self::excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]); } else { - $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]); + $transformedPart = strtr($transformedPart, self::excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]); } // overwrite the parts array with the new transformed part @@ -102,9 +104,10 @@ class DateFormatHelper // Finally, to have the date format compatible with the DateTime::format() function, we need to escape // all characters that are inside double quotes (and double quotes must be removed). // For instance, ["Day " dd] should become [\D\a\y\ dd] - return preg_replace_callback('/"(.+?)"/', function ($matches) { + return preg_replace_callback('/"(.+?)"/', static function ($matches): string { $stringToEscape = $matches[1]; $letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY); + \assert(false !== $letters); return '\\'.implode('\\', $letters); }, $phpDateFormat); @@ -115,7 +118,7 @@ class DateFormatHelper * * @return bool Whether the given date format has the 12-hour format marker */ - private static function has12HourFormatMarker($excelDateFormat) + private static function has12HourFormatMarker(string $excelDateFormat): bool { return false !== stripos($excelDateFormat, 'am/pm'); } diff --git a/lib/openspout/src/Reader/XLSX/Manager/OptionsManager.php b/lib/openspout/src/Reader/XLSX/Manager/OptionsManager.php deleted file mode 100644 index b04b92c816b..00000000000 --- a/lib/openspout/src/Reader/XLSX/Manager/OptionsManager.php +++ /dev/null @@ -1,36 +0,0 @@ -setOption(Options::TEMP_FOLDER, sys_get_temp_dir()); - $this->setOption(Options::SHOULD_FORMAT_DATES, false); - $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); - $this->setOption(Options::SHOULD_USE_1904_DATES, false); - } -} diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php index c2f8c9fd52f..3ec42cdc434 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php @@ -1,10 +1,13 @@ memoryLimit = $memoryLimit; + } + /** * Returns the best caching strategy, given the number of unique shared strings * and the amount of memory available. * - * @param null|int $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) - * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored - * @param HelperFactory $helperFactory Factory to create helpers + * @param null|int $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) + * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored * * @return CachingStrategyInterface The best caching strategy */ - public function createBestCachingStrategy($sharedStringsUniqueCount, $tempFolder, $helperFactory) + public function createBestCachingStrategy(?int $sharedStringsUniqueCount, string $tempFolder): CachingStrategyInterface { if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) { return new InMemoryStrategy($sharedStringsUniqueCount); } - return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE, $helperFactory); + return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE); } /** @@ -71,17 +80,15 @@ class CachingStrategyFactory * and the amount of memory available. * * @param null|int $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) - * - * @return bool */ - protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount) + private function isInMemoryStrategyUsageSafe(?int $sharedStringsUniqueCount): bool { // if the number of shared strings in unknown, do not use "in memory" strategy if (null === $sharedStringsUniqueCount) { return false; } - $memoryAvailable = $this->getMemoryLimitInKB(); + $memoryAvailable = $this->memoryLimit->getMemoryLimitInKB(); if (-1 === (int) $memoryAvailable) { // if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe @@ -93,49 +100,4 @@ class CachingStrategyFactory return $isInMemoryStrategyUsageSafe; } - - /** - * Returns the PHP "memory_limit" in Kilobytes. - * - * @return float - */ - protected function getMemoryLimitInKB() - { - $memoryLimitFormatted = $this->getMemoryLimitFromIni(); - $memoryLimitFormatted = strtolower(trim($memoryLimitFormatted)); - - // No memory limit - if ('-1' === $memoryLimitFormatted) { - return -1; - } - - if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) { - $amount = (int) ($matches[1]); - $unit = $matches[2]; - - switch ($unit) { - case 'b': return $amount / 1024; - - case 'k': return $amount; - - case 'm': return $amount * 1024; - - case 'g': return $amount * 1024 * 1024; - - case 't': return $amount * 1024 * 1024 * 1024; - } - } - - return -1; - } - - /** - * Returns the formatted "memory_limit" value. - * - * @return string - */ - protected function getMemoryLimitFromIni() - { - return ini_get('memory_limit'); - } } diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php index b3148274e3a..e120c9595a8 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php @@ -1,9 +1,11 @@ fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder); + $this->fileSystemHelper = new FileSystemHelper($tempFolder); $this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, uniqid('sharedstrings')); $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile; - $this->globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper(); $this->tempFilePointer = null; } @@ -70,33 +71,35 @@ class FileBasedStrategy implements CachingStrategyInterface * @param string $sharedString The string to be added to the cache * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file */ - public function addStringForIndex($sharedString, $sharedStringIndex) + public function addStringForIndex(string $sharedString, int $sharedStringIndex): void { $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); - if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { - if ($this->tempFilePointer) { - $this->globalFunctionsHelper->fclose($this->tempFilePointer); + if (!file_exists($tempFilePath)) { + if (null !== $this->tempFilePointer) { + fclose($this->tempFilePointer); } - $this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w'); + $resource = fopen($tempFilePath, 'w'); + \assert(false !== $resource); + $this->tempFilePointer = $resource; } // The shared string retrieval logic expects each cell data to be on one line only // Encoding the line feed character allows to preserve this assumption $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString); - $this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString.PHP_EOL); + fwrite($this->tempFilePointer, $lineFeedEncodedSharedString.PHP_EOL); } /** * Closes the cache after the last shared string was added. * This prevents any additional string from being added to the cache. */ - public function closeCache() + public function closeCache(): void { // close pointer to the last temp file that was written - if ($this->tempFilePointer) { - $this->globalFunctionsHelper->fclose($this->tempFilePointer); + if (null !== $this->tempFilePointer) { + fclose($this->tempFilePointer); } } @@ -105,21 +108,25 @@ class FileBasedStrategy implements CachingStrategyInterface * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * - * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index - * * @return string The shared string at the given index + * + * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index */ - public function getStringAtIndex($sharedStringIndex) + public function getStringAtIndex(int $sharedStringIndex): string { $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile; - if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { + if (!file_exists($tempFilePath)) { throw new SharedStringNotFoundException("Shared string temp file not found: {$tempFilePath} ; for index: {$sharedStringIndex}"); } if ($this->inMemoryTempFilePath !== $tempFilePath) { - $this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath)); + $tempFilePath = realpath($tempFilePath); + \assert(false !== $tempFilePath); + $contents = file_get_contents($tempFilePath); + \assert(false !== $contents); + $this->inMemoryTempFileContents = explode(PHP_EOL, $contents); $this->inMemoryTempFilePath = $tempFilePath; } @@ -141,11 +148,9 @@ class FileBasedStrategy implements CachingStrategyInterface /** * Destroys the cache, freeing memory and removing any created artifacts. */ - public function clearCache() + public function clearCache(): void { - if ($this->tempFolder) { - $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder); - } + $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder); } /** @@ -155,7 +160,7 @@ class FileBasedStrategy implements CachingStrategyInterface * * @return string The temp file path for the given index */ - protected function getSharedStringTempFilePath($sharedStringIndex) + private function getSharedStringTempFilePath(int $sharedStringIndex): string { $numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile); @@ -164,24 +169,16 @@ class FileBasedStrategy implements CachingStrategyInterface /** * Escapes the line feed characters (\n). - * - * @param string $unescapedString - * - * @return string */ - private function escapeLineFeed($unescapedString) + private function escapeLineFeed(string $unescapedString): string { return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString); } /** * Unescapes the line feed characters (\n). - * - * @param string $escapedString - * - * @return string */ - private function unescapeLineFeed($escapedString) + private function unescapeLineFeed(string $escapedString): string { return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString); } diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/InMemoryStrategy.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/InMemoryStrategy.php index 04312ad21f5..48858101664 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/InMemoryStrategy.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/InMemoryStrategy.php @@ -1,27 +1,33 @@ Array used to cache the shared strings */ + private SplFixedArray $inMemoryCache; /** @var bool Whether the cache has been closed */ - protected $isCacheClosed; + private bool $isCacheClosed; /** * @param int $sharedStringsUniqueCount Number of unique shared strings */ - public function __construct($sharedStringsUniqueCount) + public function __construct(int $sharedStringsUniqueCount) { - $this->inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount); + $this->inMemoryCache = new SplFixedArray($sharedStringsUniqueCount); $this->isCacheClosed = false; } @@ -31,7 +37,7 @@ class InMemoryStrategy implements CachingStrategyInterface * @param string $sharedString The string to be added to the cache * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file */ - public function addStringForIndex($sharedString, $sharedStringIndex) + public function addStringForIndex(string $sharedString, int $sharedStringIndex): void { if (!$this->isCacheClosed) { $this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString); @@ -42,7 +48,7 @@ class InMemoryStrategy implements CachingStrategyInterface * Closes the cache after the last shared string was added. * This prevents any additional string from being added to the cache. */ - public function closeCache() + public function closeCache(): void { $this->isCacheClosed = true; } @@ -52,15 +58,15 @@ class InMemoryStrategy implements CachingStrategyInterface * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * - * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index - * * @return string The shared string at the given index + * + * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index */ - public function getStringAtIndex($sharedStringIndex) + public function getStringAtIndex(int $sharedStringIndex): string { try { return $this->inMemoryCache->offsetGet($sharedStringIndex); - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { throw new SharedStringNotFoundException("Shared string not found for index: {$sharedStringIndex}"); } } @@ -68,9 +74,9 @@ class InMemoryStrategy implements CachingStrategyInterface /** * Destroys the cache, freeing memory and removing any created artifacts. */ - public function clearCache() + public function clearCache(): void { - $this->inMemoryCache = new \SplFixedArray(0); + $this->inMemoryCache = new SplFixedArray(0); $this->isCacheClosed = false; } } diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php new file mode 100644 index 00000000000..e9d76286c5e --- /dev/null +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php @@ -0,0 +1,50 @@ +memoryLimit = $memoryLimit; + } + + /** + * Returns the PHP "memory_limit" in Kilobytes. + */ + public function getMemoryLimitInKB(): float + { + $memoryLimitFormatted = strtolower(trim($this->memoryLimit)); + + // No memory limit + if ('-1' === $memoryLimitFormatted) { + return -1; + } + + if (1 === preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) { + $amount = (int) $matches[1]; + $unit = $matches[2]; + + switch ($unit) { + case 'b': return $amount / 1024; + + case 'k': return $amount; + + case 'm': return $amount * 1024; + + case 'g': return $amount * 1024 * 1024; + + case 't': return $amount * 1024 * 1024 * 1024; + } + } + + return -1; + } +} diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsManager.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsManager.php index 120ce6871c3..499559c8470 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsManager.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsManager.php @@ -1,83 +1,68 @@ filePath = $filePath; - $this->tempFolder = $tempFolder; + $this->options = $options; $this->workbookRelationshipsManager = $workbookRelationshipsManager; - $this->entityFactory = $entityFactory; - $this->helperFactory = $helperFactory; $this->cachingStrategyFactory = $cachingStrategyFactory; } /** * Returns whether the XLSX file contains a shared strings XML file. - * - * @return bool */ - public function hasSharedStrings() + public function hasSharedStrings(): bool { return $this->workbookRelationshipsManager->hasSharedStringsXMLFile(); } @@ -94,10 +79,10 @@ class SharedStringsManager * * @throws \OpenSpout\Common\Exception\IOException If shared strings XML file can't be read */ - public function extractSharedStrings() + public function extractSharedStrings(): void { $sharedStringsXMLFilePath = $this->workbookRelationshipsManager->getSharedStringsXMLFilePath(); - $xmlReader = $this->entityFactory->createXMLReader(); + $xmlReader = new XMLReader(); $sharedStringIndex = 0; if (false === $xmlReader->openFileInZip($this->filePath, $sharedStringsXMLFilePath)) { @@ -131,11 +116,11 @@ class SharedStringsManager * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * - * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index - * * @return string The shared string at the given index + * + * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index */ - public function getStringAtIndex($sharedStringIndex) + public function getStringAtIndex(int $sharedStringIndex): string { return $this->cachingStrategy->getStringAtIndex($sharedStringIndex); } @@ -143,9 +128,9 @@ class SharedStringsManager /** * Destroys the cache, freeing memory and removing any created artifacts. */ - public function cleanup() + public function cleanup(): void { - if (null !== $this->cachingStrategy) { + if (isset($this->cachingStrategy)) { $this->cachingStrategy->clearCache(); } } @@ -153,13 +138,13 @@ class SharedStringsManager /** * Returns the shared strings unique count, as specified in tag. * - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance - * - * @throws \OpenSpout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read + * @param XMLReader $xmlReader XMLReader instance * * @return null|int Number of unique shared strings in the sharedStrings.xml file + * + * @throws \OpenSpout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read */ - protected function getSharedStringsUniqueCount($xmlReader) + private function getSharedStringsUniqueCount(XMLReader $xmlReader): ?int { $xmlReader->next(self::XML_NODE_SST); @@ -183,37 +168,39 @@ class SharedStringsManager * Returns the best shared strings caching strategy. * * @param null|int $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) - * - * @return CachingStrategyInterface */ - protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount) + private function getBestSharedStringsCachingStrategy(?int $sharedStringsUniqueCount): CachingStrategyInterface { return $this->cachingStrategyFactory - ->createBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder, $this->helperFactory) + ->createBestCachingStrategy($sharedStringsUniqueCount, $this->options->getTempFolder()) ; } /** * Processes the shared strings item XML node which the given XML reader is positioned on. * - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on a "" node - * @param int $sharedStringIndex Index of the processed shared strings item + * @param XMLReader $xmlReader XML Reader positioned on a "" node + * @param int $sharedStringIndex Index of the processed shared strings item */ - protected function processSharedStringsItem($xmlReader, $sharedStringIndex) + private function processSharedStringsItem(XMLReader $xmlReader, int $sharedStringIndex): void { $sharedStringValue = ''; // NOTE: expand() will automatically decode all XML entities of the child nodes - /** @var \DOMElement $siNode */ $siNode = $xmlReader->expand(); + \assert($siNode instanceof DOMElement); $textNodes = $siNode->getElementsByTagName(self::XML_NODE_T); foreach ($textNodes as $textNode) { if ($this->shouldExtractTextNodeValue($textNode)) { $textNodeValue = $textNode->nodeValue; + \assert(null !== $textNodeValue); $shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode); - $sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : trim($textNodeValue); + $sharedStringValue .= $shouldPreserveWhitespace + ? $textNodeValue + : trim($textNodeValue) + ; } } @@ -225,13 +212,15 @@ class SharedStringsManager * Some text nodes are part of a node describing the pronunciation for instance. * We'll only consider the nodes whose parents are "" or "". * - * @param \DOMElement $textNode Text node to check + * @param DOMElement $textNode Text node to check * * @return bool Whether the given text node's value must be extracted */ - protected function shouldExtractTextNodeValue($textNode) + private function shouldExtractTextNodeValue(DOMElement $textNode): bool { - $parentTagName = $textNode->parentNode->localName; + $parentNode = $textNode->parentNode; + \assert(null !== $parentNode); + $parentTagName = $parentNode->localName; return self::XML_NODE_SI === $parentTagName || self::XML_NODE_R === $parentTagName; } @@ -239,11 +228,11 @@ class SharedStringsManager /** * If the text node has the attribute 'xml:space="preserve"', then preserve whitespace. * - * @param \DOMElement $textNode The text node element () whose whitespace may be preserved + * @param DOMElement $textNode The text node element () whose whitespace may be preserved * * @return bool Whether whitespace should be preserved */ - protected function shouldPreserveWhitespace($textNode) + private function shouldPreserveWhitespace(DOMElement $textNode): bool { $spaceValue = $textNode->getAttribute(self::XML_ATTRIBUTE_XML_SPACE); diff --git a/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php b/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php index e2e173e87b1..296cba3f371 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php @@ -1,29 +1,42 @@ filePath = $filePath; - $this->optionsManager = $optionsManager; + $this->options = $options; $this->sharedStringsManager = $sharedStringsManager; $this->escaper = $escaper; - $this->entityFactory = $entityFactory; } /** @@ -85,14 +88,14 @@ class SheetManager * * @return Sheet[] Sheets within the XLSX file */ - public function getSheets() + public function getSheets(): array { $this->sheets = []; $this->currentSheetIndex = 0; $this->activeSheetIndex = 0; // By default, the first sheet is active - $xmlReader = $this->entityFactory->createXMLReader(); - $xmlProcessor = $this->entityFactory->createXMLProcessor($xmlReader); + $xmlReader = new XMLReader(); + $xmlProcessor = new XMLProcessor($xmlReader); $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_PROPERTIES, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookPropertiesStartingNode']); $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_VIEW, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookViewStartingNode']); @@ -112,12 +115,12 @@ class SheetManager * * @return int A return code that indicates what action should the processor take next */ - protected function processWorkbookPropertiesStartingNode($xmlReader) + private function processWorkbookPropertiesStartingNode(XMLReader $xmlReader): int { // Using "filter_var($x, FILTER_VALIDATE_BOOLEAN)" here because the value of the "date1904" attribute // may be the string "false", that is not mapped to the boolean "false" by default... $shouldUse1904Dates = filter_var($xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904), FILTER_VALIDATE_BOOLEAN); - $this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates); + $this->options->SHOULD_USE_1904_DATES = $shouldUse1904Dates; return XMLProcessor::PROCESSING_CONTINUE; } @@ -127,7 +130,7 @@ class SheetManager * * @return int A return code that indicates what action should the processor take next */ - protected function processWorkbookViewStartingNode($xmlReader) + private function processWorkbookViewStartingNode(XMLReader $xmlReader): int { // The "workbookView" node is located before "sheet" nodes, ensuring that // the active sheet is known before parsing sheets data. @@ -141,7 +144,7 @@ class SheetManager * * @return int A return code that indicates what action should the processor take next */ - protected function processSheetStartingNode($xmlReader) + private function processSheetStartingNode(XMLReader $xmlReader): int { $isSheetActive = ($this->currentSheetIndex === $this->activeSheetIndex); $this->sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $this->currentSheetIndex, $isSheetActive); @@ -153,7 +156,7 @@ class SheetManager /** * @return int A return code that indicates what action should the processor take next */ - protected function processSheetsEndingNode() + private function processSheetsEndingNode(): int { return XMLProcessor::PROCESSING_STOP; } @@ -169,27 +172,27 @@ class SheetManager * * @return \OpenSpout\Reader\XLSX\Sheet Sheet instance */ - protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive) + private function getSheetFromSheetXMLNode(XMLReader $xmlReaderOnSheetNode, int $sheetIndexZeroBased, bool $isSheetActive): Sheet { $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID); + \assert(null !== $sheetId); $sheetState = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_STATE); $isSheetVisible = (self::SHEET_STATE_HIDDEN !== $sheetState); $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME); + \assert(null !== $escapedSheetName); $sheetName = $this->escaper->unescape($escapedSheetName); $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId); - return $this->entityFactory->createSheet( - $this->filePath, - $sheetDataXMLFilePath, + return new Sheet( + $this->createRowIterator($this->filePath, $sheetDataXMLFilePath, $this->options, $this->sharedStringsManager), + $this->createSheetHeaderReader($this->filePath, $sheetDataXMLFilePath), $sheetIndexZeroBased, $sheetName, $isSheetActive, - $isSheetVisible, - $this->optionsManager, - $this->sharedStringsManager + $isSheetVisible ); } @@ -198,12 +201,12 @@ class SheetManager * * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID */ - protected function getSheetDataXMLFilePathForSheetId($sheetId) + private function getSheetDataXMLFilePathForSheetId(string $sheetId): string { $sheetDataXMLFilePath = ''; // find the file path of the sheet, by looking at the "workbook.xml.res" file - $xmlReader = $this->entityFactory->createXMLReader(); + $xmlReader = new XMLReader(); if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) { @@ -213,9 +216,10 @@ class SheetManager // In workbook.xml.rels, it is only "worksheets/sheet1.xml" // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET); + \assert(null !== $sheetDataXMLFilePath); // sometimes, the sheet data file path already contains "/xl/"... - if (0 !== strpos($sheetDataXMLFilePath, '/xl/')) { + if (!str_starts_with($sheetDataXMLFilePath, '/xl/')) { $sheetDataXMLFilePath = '/xl/'.$sheetDataXMLFilePath; break; @@ -229,4 +233,53 @@ class SheetManager return $sheetDataXMLFilePath; } + + private function createRowIterator( + string $filePath, + string $sheetDataXMLFilePath, + Options $options, + SharedStringsManager $sharedStringsManager + ): RowIterator { + $xmlReader = new XMLReader(); + + $workbookRelationshipsManager = new WorkbookRelationshipsManager($filePath); + $styleManager = new StyleManager( + $filePath, + $workbookRelationshipsManager->hasStylesXMLFile() + ? $workbookRelationshipsManager->getStylesXMLFilePath() + : null + ); + + $cellValueFormatter = new CellValueFormatter( + $sharedStringsManager, + $styleManager, + $options->SHOULD_FORMAT_DATES, + $options->SHOULD_USE_1904_DATES, + new XLSX() + ); + + return new RowIterator( + $filePath, + $sheetDataXMLFilePath, + $options->SHOULD_PRESERVE_EMPTY_ROWS, + $xmlReader, + new XMLProcessor($xmlReader), + $cellValueFormatter, + new RowManager() + ); + } + + private function createSheetHeaderReader( + string $filePath, + string $sheetDataXMLFilePath + ): SheetHeaderReader { + $xmlReader = new XMLReader(); + + return new SheetHeaderReader( + $filePath, + $sheetDataXMLFilePath, + $xmlReader, + new XMLProcessor($xmlReader) + ); + } } diff --git a/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php b/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php index 4eb90468bb0..6c56ea8f781 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php +++ b/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php @@ -1,36 +1,42 @@ 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy' 15 => 'd-mmm-yy', 16 => 'd-mmm', @@ -46,56 +52,33 @@ class StyleManager ]; /** @var string Path of the XLSX file being read */ - protected $filePath; - - /** @var bool Whether the XLSX file contains a styles XML file */ - protected $hasStylesXMLFile; + private string $filePath; /** @var null|string Path of the styles XML file */ - protected $stylesXMLFilePath; + private ?string $stylesXMLFilePath; - /** @var InternalEntityFactory Factory to create entities */ - protected $entityFactory; + /** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */ + private array $customNumberFormats; - /** @var array Array containing the IDs of built-in number formats indicating a date */ - protected $builtinNumFmtIdIndicatingDates; + /** @var array> Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */ + private array $stylesAttributes; - /** @var null|array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */ - protected $customNumberFormats; - - /** @var null|array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */ - protected $stylesAttributes; - - /** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */ - protected $numFmtIdToIsDateFormatCache = []; + /** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */ + private array $numFmtIdToIsDateFormatCache = []; /** - * @param string $filePath Path of the XLSX file being read - * @param WorkbookRelationshipsManager $workbookRelationshipsManager Helps retrieving workbook relationships - * @param InternalEntityFactory $entityFactory Factory to create entities + * @param string $filePath Path of the XLSX file being read + * @param ?string $stylesXMLFilePath */ - public function __construct($filePath, $workbookRelationshipsManager, $entityFactory) + public function __construct(string $filePath, ?string $stylesXMLFilePath) { $this->filePath = $filePath; - $this->entityFactory = $entityFactory; - $this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping); - $this->hasStylesXMLFile = $workbookRelationshipsManager->hasStylesXMLFile(); - if ($this->hasStylesXMLFile) { - $this->stylesXMLFilePath = $workbookRelationshipsManager->getStylesXMLFilePath(); - } + $this->stylesXMLFilePath = $stylesXMLFilePath; } - /** - * Returns whether the style with the given ID should consider - * numeric values as timestamps and format the cell as a date. - * - * @param int $styleId Zero-based style ID - * - * @return bool Whether the cell with the given cell should display a date instead of a numeric value - */ - public function shouldFormatNumericValueAsDate($styleId) + public function shouldFormatNumericValueAsDate(int $styleId): bool { - if (!$this->hasStylesXMLFile) { + if (null === $this->stylesXMLFilePath) { return false; } @@ -113,22 +96,15 @@ class StyleManager return $this->doesStyleIndicateDate($styleAttributes); } - /** - * Returns the format as defined in "styles.xml" of the given style. - * NOTE: It is assumed that the style DOES have a number format associated to it. - * - * @param int $styleId Zero-based style ID - * - * @return string The number format code associated with the given style - */ - public function getNumberFormatCode($styleId) + public function getNumberFormatCode(int $styleId): string { $stylesAttributes = $this->getStylesAttributes(); $styleAttributes = $stylesAttributes[$styleId]; $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; + \assert(\is_int($numFmtId)); if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) { - $numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId]; + $numberFormatCode = self::builtinNumFmtIdToNumFormatMapping[$numFmtId]; } else { $customNumberFormats = $this->getCustomNumberFormats(); $numberFormatCode = $customNumberFormats[$numFmtId]; @@ -137,19 +113,44 @@ class StyleManager return $numberFormatCode; } + /** + * @return array The custom number formats + */ + protected function getCustomNumberFormats(): array + { + if (!isset($this->customNumberFormats)) { + $this->extractRelevantInfo(); + } + + return $this->customNumberFormats; + } + + /** + * @return array> The styles attributes + */ + protected function getStylesAttributes(): array + { + if (!isset($this->stylesAttributes)) { + $this->extractRelevantInfo(); + } + + return $this->stylesAttributes; + } + /** * Reads the styles.xml file and extract the relevant information from the file. */ - protected function extractRelevantInfo() + private function extractRelevantInfo(): void { $this->customNumberFormats = []; $this->stylesAttributes = []; - $xmlReader = $this->entityFactory->createXMLReader(); + $xmlReader = new XMLReader(); if ($xmlReader->openFileInZip($this->filePath, $this->stylesXMLFilePath)) { while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) { + if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS) + && '0' !== $xmlReader->getAttribute(self::XML_ATTRIBUTE_COUNT)) { $this->extractNumberFormats($xmlReader); } elseif ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) { $this->extractStyleAttributes($xmlReader); @@ -167,12 +168,13 @@ class StyleManager * * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node */ - protected function extractNumberFormats($xmlReader) + private function extractNumberFormats(XMLReader $xmlReader): void { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) { - $numFmtId = (int) ($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)); + $numFmtId = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID); $formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE); + \assert(null !== $formatCode); $this->customNumberFormats[$numFmtId] = $formatCode; } elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) { // Once done reading "numFmts" node's children @@ -188,7 +190,7 @@ class StyleManager * * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node */ - protected function extractStyleAttributes($xmlReader) + private function extractStyleAttributes(XMLReader $xmlReader): void { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) { @@ -210,35 +212,11 @@ class StyleManager } /** - * @return array The custom number formats - */ - protected function getCustomNumberFormats() - { - if (!isset($this->customNumberFormats)) { - $this->extractRelevantInfo(); - } - - return $this->customNumberFormats; - } - - /** - * @return array The styles attributes - */ - protected function getStylesAttributes() - { - if (!isset($this->stylesAttributes)) { - $this->extractRelevantInfo(); - } - - return $this->stylesAttributes; - } - - /** - * @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId") + * @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId") * * @return bool Whether the style with the given attributes indicates that the number is a date */ - protected function doesStyleIndicateDate($styleAttributes) + private function doesStyleIndicateDate(array $styleAttributes): bool { $applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT]; $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; @@ -248,7 +226,7 @@ class StyleManager // - "numFmtId" attribute set // This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format, // but this is not necessarily a date. - if (false === $applyNumberFormat || null === $numFmtId) { + if (false === $applyNumberFormat || !\is_int($numFmtId)) { return false; } @@ -260,11 +238,9 @@ class StyleManager * The result is cached to avoid recomputing the same thing over and over, as * "numFmtId" attributes can be shared between multiple styles. * - * @param int $numFmtId - * * @return bool Whether the number format ID indicates that the number is a date */ - protected function doesNumFmtIdIndicateDate($numFmtId) + private function doesNumFmtIdIndicateDate(int $numFmtId): bool { if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) { $formatCode = $this->getFormatCodeForNumFmtId($numFmtId); @@ -279,11 +255,9 @@ class StyleManager } /** - * @param int $numFmtId - * * @return null|string The custom number format or NULL if none defined for the given numFmtId */ - protected function getFormatCodeForNumFmtId($numFmtId) + private function getFormatCodeForNumFmtId(int $numFmtId): ?string { $customNumberFormats = $this->getCustomNumberFormats(); @@ -292,21 +266,17 @@ class StyleManager } /** - * @param int $numFmtId - * * @return bool Whether the number format ID indicates that the number is a date */ - protected function isNumFmtIdBuiltInDateFormat($numFmtId) + private function isNumFmtIdBuiltInDateFormat(int $numFmtId): bool { - return \in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates, true); + return \array_key_exists($numFmtId, self::builtinNumFmtIdToNumFormatMapping); } /** - * @param null|string $formatCode - * * @return bool Whether the given format code indicates that the number is a date */ - protected function isFormatCodeCustomDateFormat($formatCode) + private function isFormatCodeCustomDateFormat(?string $formatCode): bool { // if no associated format code or if using the default "General" format if (null === $formatCode || 0 === strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL)) { @@ -317,15 +287,14 @@ class StyleManager } /** - * @param string $formatCode - * * @return bool Whether the given format code matches a date format pattern */ - protected function isFormatCodeMatchingDateFormatPattern($formatCode) + private function isFormatCodeMatchingDateFormatPattern(string $formatCode): bool { // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\") $pattern = '((? [FILE_NAME] */ - private $cachedWorkbookRelationships; + /** @var array Cache of the already read workbook relationships: [TYPE] => [FILE_NAME] */ + private array $cachedWorkbookRelationships; /** - * @param string $filePath Path of the XLSX file being read - * @param InternalEntityFactory $entityFactory Factory to create entities + * @param string $filePath Path of the XLSX file being read */ - public function __construct($filePath, $entityFactory) + public function __construct(string $filePath) { $this->filePath = $filePath; - $this->entityFactory = $entityFactory; } /** * @return string The path of the shared string XML file */ - public function getSharedStringsXMLFilePath() + public function getSharedStringsXMLFilePath(): string { $workbookRelationships = $this->getWorkbookRelationships(); $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 = (false !== strpos($sharedStringsXMLFilePath, self::BASE_PATH)); + $doesContainBasePath = str_contains($sharedStringsXMLFilePath, self::BASE_PATH); if (!$doesContainBasePath) { // make sure we return an absolute file path $sharedStringsXMLFilePath = self::BASE_PATH.$sharedStringsXMLFilePath; @@ -68,7 +70,7 @@ class WorkbookRelationshipsManager /** * @return bool Whether the XLSX file contains a shared string XML file */ - public function hasSharedStringsXMLFile() + public function hasSharedStringsXMLFile(): bool { $workbookRelationships = $this->getWorkbookRelationships(); @@ -79,7 +81,7 @@ class WorkbookRelationshipsManager /** * @return bool Whether the XLSX file contains a styles XML file */ - public function hasStylesXMLFile() + public function hasStylesXMLFile(): bool { $workbookRelationships = $this->getWorkbookRelationships(); @@ -90,14 +92,14 @@ class WorkbookRelationshipsManager /** * @return string The path of the styles XML file */ - public function getStylesXMLFilePath() + public function getStylesXMLFilePath(): string { $workbookRelationships = $this->getWorkbookRelationships(); $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 = (false !== strpos($stylesXMLFilePath, self::BASE_PATH)); + $doesContainBasePath = str_contains($stylesXMLFilePath, self::BASE_PATH); if (!$doesContainBasePath) { // make sure we return a full path $stylesXMLFilePath = self::BASE_PATH.$stylesXMLFilePath; @@ -110,14 +112,14 @@ class WorkbookRelationshipsManager * Reads the workbook.xml.rels and extracts the filename associated to the different types. * It caches the result so that the file is read only once. * - * @throws \OpenSpout\Common\Exception\IOException If workbook.xml.rels can't be read + * @return array * - * @return array + * @throws \OpenSpout\Common\Exception\IOException If workbook.xml.rels can't be read */ - private function getWorkbookRelationships() + private function getWorkbookRelationships(): array { if (!isset($this->cachedWorkbookRelationships)) { - $xmlReader = $this->entityFactory->createXMLReader(); + $xmlReader = new XMLReader(); if (false === $xmlReader->openFileInZip($this->filePath, self::WORKBOOK_RELS_XML_FILE_PATH)) { throw new IOException('Could not open "'.self::WORKBOOK_RELS_XML_FILE_PATH.'".'); @@ -135,13 +137,12 @@ class WorkbookRelationshipsManager /** * Extracts and store the data of the current workbook relationship. - * - * @param XMLReader $xmlReader */ - private function processWorkbookRelationship($xmlReader) + private function processWorkbookRelationship(XMLReader $xmlReader): void { $type = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TYPE); $target = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET); + \assert(null !== $target); // @NOTE: if a type is defined more than once, we overwrite the previous value // To be changed if we want to get the file paths of sheet XML files for instance. diff --git a/lib/openspout/src/Reader/XLSX/Options.php b/lib/openspout/src/Reader/XLSX/Options.php new file mode 100644 index 00000000000..1c8331f827a --- /dev/null +++ b/lib/openspout/src/Reader/XLSX/Options.php @@ -0,0 +1,16 @@ + */ -class Reader extends ReaderAbstract +final class Reader extends AbstractReader { - /** @var ManagerFactory */ - protected $managerFactory; + private ZipArchive $zip; - /** @var \ZipArchive */ - protected $zip; - - /** @var \OpenSpout\Reader\XLSX\Manager\SharedStringsManager Manages shared strings */ - protected $sharedStringsManager; + /** @var SharedStringsManager Manages shared strings */ + private SharedStringsManager $sharedStringsManager; /** @var SheetIterator To iterator over the XLSX sheets */ - protected $sheetIterator; + private SheetIterator $sheetIterator; + + private Options $options; + private CachingStrategyFactory $cachingStrategyFactory; public function __construct( - OptionsManagerInterface $optionsManager, - GlobalFunctionsHelper $globalFunctionsHelper, - InternalEntityFactoryInterface $entityFactory, - ManagerFactory $managerFactory + ?Options $options = null, + ?CachingStrategyFactory $cachingStrategyFactory = null ) { - parent::__construct($optionsManager, $globalFunctionsHelper, $entityFactory); - $this->managerFactory = $managerFactory; + $this->options = $options ?? new Options(); + + if (null === $cachingStrategyFactory) { + $memoryLimit = \ini_get('memory_limit'); + \assert(false !== $memoryLimit); + + $cachingStrategyFactory = new CachingStrategyFactory(new MemoryLimit($memoryLimit)); + } + $this->cachingStrategyFactory = $cachingStrategyFactory; } - /** - * @param string $tempFolder Temporary folder where the temporary files will be created - * - * @return Reader - */ - public function setTempFolder($tempFolder) + public function getSheetIterator(): SheetIterator { - $this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder); + $this->ensureStreamOpened(); - return $this; + return $this->sheetIterator; } /** * Returns whether stream wrappers are supported. - * - * @return bool */ - protected function doesSupportStreamWrapper() + protected function doesSupportStreamWrapper(): bool { return false; } @@ -70,53 +70,42 @@ class Reader extends ReaderAbstract * @throws \OpenSpout\Common\Exception\IOException If the file at the given path or its content cannot be read * @throws \OpenSpout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ - protected function openReader($filePath) + protected function openReader(string $filePath): void { - /** @var InternalEntityFactory $entityFactory */ - $entityFactory = $this->entityFactory; + $this->zip = new ZipArchive(); - $this->zip = $entityFactory->createZipArchive(); - - if (true === $this->zip->open($filePath)) { - $tempFolder = $this->optionsManager->getOption(Options::TEMP_FOLDER); - $this->sharedStringsManager = $this->managerFactory->createSharedStringsManager($filePath, $tempFolder, $entityFactory); - - if ($this->sharedStringsManager->hasSharedStrings()) { - // Extracts all the strings from the sheets for easy access in the future - $this->sharedStringsManager->extractSharedStrings(); - } - - $this->sheetIterator = $entityFactory->createSheetIterator( - $filePath, - $this->optionsManager, - $this->sharedStringsManager - ); - } else { + if (true !== $this->zip->open($filePath)) { throw new IOException("Could not open {$filePath} for reading."); } - } - /** - * Returns an iterator to iterate over sheets. - * - * @return SheetIterator To iterate over sheets - */ - protected function getConcreteSheetIterator() - { - return $this->sheetIterator; + $this->sharedStringsManager = new SharedStringsManager( + $filePath, + $this->options, + new WorkbookRelationshipsManager($filePath), + $this->cachingStrategyFactory + ); + + if ($this->sharedStringsManager->hasSharedStrings()) { + // Extracts all the strings from the sheets for easy access in the future + $this->sharedStringsManager->extractSharedStrings(); + } + + $this->sheetIterator = new SheetIterator( + new SheetManager( + $filePath, + $this->options, + $this->sharedStringsManager, + new XLSX() + ) + ); } /** * Closes the reader. To be used after reading the file. */ - protected function closeReader() + protected function closeReader(): void { - if (null !== $this->zip) { - $this->zip->close(); - } - - if (null !== $this->sharedStringsManager) { - $this->sharedStringsManager->cleanup(); - } + $this->zip->close(); + $this->sharedStringsManager->cleanup(); } } diff --git a/lib/openspout/src/Reader/XLSX/RowIterator.php b/lib/openspout/src/Reader/XLSX/RowIterator.php index de383706629..e1c7d2d920c 100644 --- a/lib/openspout/src/Reader/XLSX/RowIterator.php +++ b/lib/openspout/src/Reader/XLSX/RowIterator.php @@ -1,105 +1,105 @@ filePath = $filePath; $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath); @@ -107,7 +107,6 @@ class RowIterator implements IteratorInterface $this->xmlReader = $xmlReader; $this->cellValueFormatter = $cellValueFormatter; $this->rowManager = $rowManager; - $this->entityFactory = $entityFactory; // Register all callbacks to process different nodes when reading the XML file $this->xmlProcessor = $xmlProcessor; @@ -127,7 +126,6 @@ class RowIterator implements IteratorInterface * * @throws \OpenSpout\Common\Exception\IOException If the sheet data XML cannot be read */ - #[\ReturnTypeWillChange] public function rewind(): void { $this->xmlReader->close(); @@ -151,10 +149,14 @@ class RowIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.valid.php */ - #[\ReturnTypeWillChange] public function valid(): bool { - return !$this->hasReachedEndOfFile; + $valid = !$this->hasReachedEndOfFile; + if (!$valid) { + $this->xmlReader->close(); + } + + return $valid; } /** @@ -165,7 +167,6 @@ class RowIterator implements IteratorInterface * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML */ - #[\ReturnTypeWillChange] public function next(): void { ++$this->nextRowIndexToBeProcessed; @@ -180,8 +181,7 @@ class RowIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.current.php */ - #[\ReturnTypeWillChange] - public function current(): ?Row + public function current(): Row { $rowToBeProcessed = $this->rowBuffer; @@ -193,10 +193,12 @@ class RowIterator implements IteratorInterface if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) { // return empty row if mismatch between last processed row // and the row that needs to be returned - $rowToBeProcessed = $this->entityFactory->createRow(); + $rowToBeProcessed = new Row([], null); } } + \assert(null !== $rowToBeProcessed); + return $rowToBeProcessed; } @@ -205,7 +207,6 @@ class RowIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.key.php */ - #[\ReturnTypeWillChange] public function key(): int { // TODO: This should return $this->nextRowIndexToBeProcessed @@ -216,22 +217,13 @@ class RowIterator implements IteratorInterface $this->numReadRows; } - /** - * Cleans up what was created to iterate over the object. - */ - #[\ReturnTypeWillChange] - public function end(): void - { - $this->xmlReader->close(); - } - /** * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml * * @return string path of the XML file containing the sheet data, * without the leading slash */ - protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath) + private function normalizeSheetDataXMLFilePath(string $sheetDataXMLFilePath): string { return ltrim($sheetDataXMLFilePath, '/'); } @@ -248,7 +240,7 @@ class RowIterator implements IteratorInterface * * @return bool whether we need data for the next row to be processed */ - protected function doesNeedDataForNextRowToBeProcessed() + private function doesNeedDataForNextRowToBeProcessed(): bool { $hasReadAtLeastOneRow = (0 !== $this->lastRowIndexProcessed); @@ -263,29 +255,26 @@ class RowIterator implements IteratorInterface * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML */ - protected function readDataForNextRow() + private function readDataForNextRow(): void { - $this->currentlyProcessedRow = $this->entityFactory->createRow(); + $this->currentlyProcessedRow = new Row([], null); - try { - $this->xmlProcessor->readUntilStopped(); - } catch (XMLProcessingException $exception) { - throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]"); - } + $this->xmlProcessor->readUntilStopped(); $this->rowBuffer = $this->currentlyProcessedRow; } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node * * @return int A return code that indicates what action should the processor take next */ - protected function processDimensionStartingNode($xmlReader) + private function processDimensionStartingNode(XMLReader $xmlReader): int { // Read dimensions of the sheet $dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet) - if (preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) { + \assert(null !== $dimensionRef); + if (1 === preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) { $this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1; } @@ -293,11 +282,11 @@ class RowIterator implements IteratorInterface } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node * * @return int A return code that indicates what action should the processor take next */ - protected function processRowStartingNode($xmlReader) + private function processRowStartingNode(XMLReader $xmlReader): int { // Reset index of the last processed column $this->lastColumnIndexProcessed = -1; @@ -308,28 +297,28 @@ class RowIterator implements IteratorInterface // Read spans info if present $numberOfColumnsForRow = $this->numColumns; $spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance - if ($spans) { + if (null !== $spans) { [, $numberOfColumnsForRow] = explode(':', $spans); $numberOfColumnsForRow = (int) $numberOfColumnsForRow; } - $cells = array_fill(0, $numberOfColumnsForRow, $this->entityFactory->createCell('')); + $cells = array_fill(0, $numberOfColumnsForRow, Cell::fromValue('')); $this->currentlyProcessedRow->setCells($cells); return XMLProcessor::PROCESSING_CONTINUE; } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node * * @return int A return code that indicates what action should the processor take next */ - protected function processCellStartingNode($xmlReader) + private function processCellStartingNode(XMLReader $xmlReader): int { $currentColumnIndex = $this->getColumnIndex($xmlReader); // NOTE: expand() will automatically decode all XML entities of the child nodes - /** @var \DOMElement $node */ + /** @var DOMElement $node */ $node = $xmlReader->expand(); $cell = $this->getCell($node); @@ -342,10 +331,10 @@ class RowIterator implements IteratorInterface /** * @return int A return code that indicates what action should the processor take next */ - protected function processRowEndingNode() + private function processRowEndingNode(): int { // if the fetched row is empty and we don't want to preserve it.., - if (!$this->shouldPreserveEmptyRows && $this->rowManager->isEmpty($this->currentlyProcessedRow)) { + if (!$this->shouldPreserveEmptyRows && $this->currentlyProcessedRow->isEmpty()) { // ... skip it return XMLProcessor::PROCESSING_CONTINUE; } @@ -354,7 +343,7 @@ class RowIterator implements IteratorInterface // If needed, we fill the empty cells if (0 === $this->numColumns) { - $this->currentlyProcessedRow = $this->rowManager->fillMissingIndexesWithEmptyCells($this->currentlyProcessedRow); + $this->rowManager->fillMissingIndexesWithEmptyCells($this->currentlyProcessedRow); } // at this point, we have all the data we need for the row @@ -365,7 +354,7 @@ class RowIterator implements IteratorInterface /** * @return int A return code that indicates what action should the processor take next */ - protected function processWorksheetEndingNode() + private function processWorksheetEndingNode(): int { // The closing "" marks the end of the file $this->hasReachedEndOfFile = true; @@ -374,13 +363,13 @@ class RowIterator implements IteratorInterface } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node - * - * @throws \OpenSpout\Common\Exception\InvalidArgumentException When the given cell index is invalid + * @param XMLReader $xmlReader XMLReader object, positioned on a "" node * * @return int Row index + * + *@throws \OpenSpout\Common\Exception\InvalidArgumentException When the given cell index is invalid */ - protected function getRowIndex($xmlReader) + private function getRowIndex(XMLReader $xmlReader): int { // Get "r" attribute if present (from something like $currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX); @@ -391,13 +380,13 @@ class RowIterator implements IteratorInterface } /** - * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node - * - * @throws \OpenSpout\Common\Exception\InvalidArgumentException When the given cell index is invalid + * @param XMLReader $xmlReader XMLReader object, positioned on a "" node * * @return int Column index + * + *@throws \OpenSpout\Common\Exception\InvalidArgumentException When the given cell index is invalid */ - protected function getColumnIndex($xmlReader) + private function getColumnIndex(XMLReader $xmlReader): int { // Get "r" attribute if present (from something like $currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX); @@ -410,18 +399,15 @@ class RowIterator implements IteratorInterface /** * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node. * - * @param \DOMElement $node - * * @return Cell The cell set with the associated with the cell */ - protected function getCell($node) + private function getCell(DOMElement $node): Cell { try { $cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node); - $cell = $this->entityFactory->createCell($cellValue); + $cell = Cell::fromValue($cellValue); } catch (InvalidValueException $exception) { - $cell = $this->entityFactory->createCell($exception->getInvalidValue()); - $cell->setType(Cell::TYPE_ERROR); + $cell = new Cell\ErrorCell($exception->getInvalidValue(), null); } return $cell; diff --git a/lib/openspout/src/Reader/XLSX/Sheet.php b/lib/openspout/src/Reader/XLSX/Sheet.php index 64d76374b37..dc671234f75 100644 --- a/lib/openspout/src/Reader/XLSX/Sheet.php +++ b/lib/openspout/src/Reader/XLSX/Sheet.php @@ -1,28 +1,34 @@ */ -class Sheet implements SheetInterface +final class Sheet implements SheetWithVisibilityInterface { - /** @var \OpenSpout\Reader\XLSX\RowIterator To iterate over sheet's rows */ - protected $rowIterator; + /** @var RowIterator To iterate over sheet's rows */ + private RowIterator $rowIterator; + + /** @var SheetHeaderReader To read the header of the sheet, containing for instance the col widths */ + private SheetHeaderReader $headerReader; /** @var int Index of the sheet, based on order in the workbook (zero-based) */ - protected $index; + private int $index; /** @var string Name of the sheet */ - protected $name; + private string $name; /** @var bool Whether the sheet was the active one */ - protected $isActive; + private bool $isActive; /** @var bool Whether the sheet is visible */ - protected $isVisible; + private bool $isVisible; /** * @param RowIterator $rowIterator The corresponding row iterator @@ -31,27 +37,33 @@ class Sheet implements SheetInterface * @param bool $isSheetActive Whether the sheet was defined as active * @param bool $isSheetVisible Whether the sheet is visible */ - public function __construct($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible) + public function __construct(RowIterator $rowIterator, SheetHeaderReader $headerReader, int $sheetIndex, string $sheetName, bool $isSheetActive, bool $isSheetVisible) { $this->rowIterator = $rowIterator; + $this->headerReader = $headerReader; $this->index = $sheetIndex; $this->name = $sheetName; $this->isActive = $isSheetActive; $this->isVisible = $isSheetVisible; } - /** - * @return \OpenSpout\Reader\XLSX\RowIterator - */ - public function getRowIterator() + public function getRowIterator(): RowIterator { return $this->rowIterator; } + /** + * @return ColumnWidth[] a list of column-widths + */ + public function getColumnWidths(): array + { + return $this->headerReader->getColumnWidths(); + } + /** * @return int Index of the sheet, based on order in the workbook (zero-based) */ - public function getIndex() + public function getIndex(): int { return $this->index; } @@ -59,7 +71,7 @@ class Sheet implements SheetInterface /** * @return string Name of the sheet */ - public function getName() + public function getName(): string { return $this->name; } @@ -67,7 +79,7 @@ class Sheet implements SheetInterface /** * @return bool Whether the sheet was defined as active */ - public function isActive() + public function isActive(): bool { return $this->isActive; } @@ -75,7 +87,7 @@ class Sheet implements SheetInterface /** * @return bool Whether the sheet is visible */ - public function isVisible() + public function isVisible(): bool { return $this->isVisible; } diff --git a/lib/openspout/src/Reader/XLSX/SheetHeaderReader.php b/lib/openspout/src/Reader/XLSX/SheetHeaderReader.php new file mode 100644 index 00000000000..ea4be2bb749 --- /dev/null +++ b/lib/openspout/src/Reader/XLSX/SheetHeaderReader.php @@ -0,0 +1,119 @@ +filePath = $filePath; + $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath); + $this->xmlReader = $xmlReader; + + // Register all callbacks to process different nodes when reading the XML file + $this->xmlProcessor = $xmlProcessor; + $this->xmlProcessor->registerCallback(self::XML_NODE_COL, XMLProcessor::NODE_TYPE_START, [$this, 'processColStartingNode']); + $this->xmlProcessor->registerCallback(self::XML_NODE_SHEETDATA, XMLProcessor::NODE_TYPE_START, [$this, 'processSheetDataStartingNode']); + + // The reader should be unused, but we close to be sure + $this->xmlReader->close(); + + if (false === $this->xmlReader->openFileInZip($this->filePath, $this->sheetDataXMLFilePath)) { + throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\"."); + } + + // Now read the entire header of the sheet, until we reach the element + $this->xmlProcessor->readUntilStopped(); + + // We don't need the reader anymore, so we close it + $this->xmlReader->close(); + } + + /** + * @internal + * + * @return ColumnWidth[] + */ + public function getColumnWidths(): array + { + return $this->columnWidths; + } + + /** + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * + * @return int A return code that indicates what action should the processor take next + */ + private function processColStartingNode(XMLReader $xmlReader): int + { + $min = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_MIN); + $max = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_MAX); + $width = (float) $xmlReader->getAttribute(self::XML_ATTRIBUTE_WIDTH); + + \assert($min > 0); + \assert($max > 0); + + $columnwidth = new ColumnWidth($min, $max, $width); + $this->columnWidths[] = $columnwidth; + + return XMLProcessor::PROCESSING_CONTINUE; + } + + /** + * @return int A return code that indicates what action should the processor take next + */ + private function processSheetDataStartingNode(): int + { + // The opening "" marks the end of the file + return XMLProcessor::PROCESSING_STOP; + } + + /** + * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml + * + * @return string path of the XML file containing the sheet data, + * without the leading slash + */ + private function normalizeSheetDataXMLFilePath(string $sheetDataXMLFilePath): string + { + return ltrim($sheetDataXMLFilePath, '/'); + } +} diff --git a/lib/openspout/src/Reader/XLSX/SheetIterator.php b/lib/openspout/src/Reader/XLSX/SheetIterator.php index f58fc672cc0..33b26f912be 100644 --- a/lib/openspout/src/Reader/XLSX/SheetIterator.php +++ b/lib/openspout/src/Reader/XLSX/SheetIterator.php @@ -1,28 +1,30 @@ */ -class SheetIterator implements IteratorInterface +final class SheetIterator implements SheetIteratorInterface { - /** @var \OpenSpout\Reader\XLSX\Sheet[] The list of sheet present in the file */ - protected $sheets; + /** @var Sheet[] The list of sheet present in the file */ + private array $sheets; /** @var int The index of the sheet being read (zero-based) */ - protected $currentSheetIndex; + private int $currentSheetIndex = 0; /** * @param SheetManager $sheetManager Manages sheets * - * @throws \OpenSpout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file + * @throws NoSheetsFoundException If there are no sheets in the file */ - public function __construct($sheetManager) + public function __construct(SheetManager $sheetManager) { // Fetch all available sheets $this->sheets = $sheetManager->getSheets(); @@ -37,8 +39,7 @@ class SheetIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.rewind.php */ - #[\ReturnTypeWillChange] - public function rewind() + public function rewind(): void { $this->currentSheetIndex = 0; } @@ -47,11 +48,8 @@ class SheetIterator implements IteratorInterface * Checks if current position is valid. * * @see http://php.net/manual/en/iterator.valid.php - * - * @return bool */ - #[\ReturnTypeWillChange] - public function valid() + public function valid(): bool { return $this->currentSheetIndex < \count($this->sheets); } @@ -61,27 +59,17 @@ class SheetIterator implements IteratorInterface * * @see http://php.net/manual/en/iterator.next.php */ - #[\ReturnTypeWillChange] - public function next() + public function next(): void { - // Using isset here because it is way faster than array_key_exists... - if (isset($this->sheets[$this->currentSheetIndex])) { - $currentSheet = $this->sheets[$this->currentSheetIndex]; - $currentSheet->getRowIterator()->end(); - - ++$this->currentSheetIndex; - } + ++$this->currentSheetIndex; } /** * Return the current element. * * @see http://php.net/manual/en/iterator.current.php - * - * @return \OpenSpout\Reader\XLSX\Sheet */ - #[\ReturnTypeWillChange] - public function current() + public function current(): Sheet { return $this->sheets[$this->currentSheetIndex]; } @@ -90,24 +78,9 @@ class SheetIterator implements IteratorInterface * Return the key of the current element. * * @see http://php.net/manual/en/iterator.key.php - * - * @return int */ - #[\ReturnTypeWillChange] - public function key() + public function key(): int { return $this->currentSheetIndex + 1; } - - /** - * Cleans up what was created to iterate over the object. - */ - #[\ReturnTypeWillChange] - public function end() - { - // make sure we are not leaking memory in case the iteration stopped before the end - foreach ($this->sheets as $sheet) { - $sheet->getRowIterator()->end(); - } - } } diff --git a/lib/openspout/src/Writer/AbstractWriter.php b/lib/openspout/src/Writer/AbstractWriter.php new file mode 100644 index 00000000000..9f0633a5ea6 --- /dev/null +++ b/lib/openspout/src/Writer/AbstractWriter.php @@ -0,0 +1,175 @@ +outputFilePath = $outputFilePath; + + $errorMessage = null; + set_error_handler(static function ($nr, $message) use (&$errorMessage): bool { + $errorMessage = $message; + + return true; + }); + + $resource = fopen($this->outputFilePath, 'w'); + restore_error_handler(); + if (null !== $errorMessage) { + throw new IOException("Unable to open file {$this->outputFilePath}: {$errorMessage}"); + } + \assert(false !== $resource); + $this->filePointer = $resource; + + $this->openWriter(); + $this->isWriterOpened = true; + } + + /** + * @codeCoverageIgnore + * {@inheritdoc} + */ + final public function openToBrowser($outputFileName): void + { + $this->outputFilePath = basename($outputFileName); + + $resource = fopen('php://output', 'w'); + \assert(false !== $resource); + $this->filePointer = $resource; + + // Clear any previous output (otherwise the generated file will be corrupted) + // @see https://github.com/box/spout/issues/241 + if (ob_get_length() > 0) { + ob_end_clean(); + } + + /* + * 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 + */ + header('Content-Type: '.static::$headerContentType); + header( + 'Content-Disposition: attachment; '. + 'filename="'.rawurlencode($this->outputFilePath).'"; '. + 'filename*=UTF-8\'\''.rawurlencode($this->outputFilePath) + ); + + /* + * When forcing the download of a file over SSL,IE8 and lower browsers fail + * if the Cache-Control and Pragma headers are not set. + * + * @see http://support.microsoft.com/KB/323308 + * @see https://github.com/liuggio/ExcelBundle/issues/45 + */ + header('Cache-Control: max-age=0'); + header('Pragma: public'); + + $this->openWriter(); + $this->isWriterOpened = true; + } + + /** + * {@inheritdoc} + */ + final public function addRow(Row $row): void + { + if (!$this->isWriterOpened) { + throw new WriterNotOpenedException('The writer needs to be opened before adding row.'); + } + + $this->addRowToWriter($row); + ++$this->writtenRowCount; + } + + /** + * {@inheritdoc} + */ + final public function addRows(array $rows): void + { + foreach ($rows as $row) { + $this->addRow($row); + } + } + + /** + * {@inheritdoc} + */ + final public function getWrittenRowCount(): int + { + return $this->writtenRowCount; + } + + /** + * {@inheritdoc} + */ + final public function close(): void + { + if (!$this->isWriterOpened) { + return; + } + + $this->closeWriter(); + + fclose($this->filePointer); + + $this->isWriterOpened = false; + } + + /** + * Opens the streamer and makes it ready to accept data. + * + * @throws IOException If the writer cannot be opened + */ + abstract protected function openWriter(): void; + + /** + * Adds a row to the currently opened writer. + * + * @param Row $row The row containing cells and styles + * + * @throws WriterNotOpenedException If the workbook is not created yet + * @throws IOException If unable to write data + */ + abstract protected function addRowToWriter(Row $row): void; + + /** + * Closes the streamer, preventing any additional writing. + */ + abstract protected function closeWriter(): void; +} diff --git a/lib/openspout/src/Writer/AbstractWriterMultiSheets.php b/lib/openspout/src/Writer/AbstractWriterMultiSheets.php new file mode 100644 index 00000000000..be61aa07011 --- /dev/null +++ b/lib/openspout/src/Writer/AbstractWriterMultiSheets.php @@ -0,0 +1,129 @@ +throwIfWorkbookIsNotAvailable(); + + $externalSheets = []; + $worksheets = $this->workbookManager->getWorksheets(); + + foreach ($worksheets as $worksheet) { + $externalSheets[] = $worksheet->getExternalSheet(); + } + + return $externalSheets; + } + + /** + * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. + * + * @return Sheet The created sheet + * + * @throws IOException + * @throws WriterNotOpenedException If the writer has not been opened yet + */ + final public function addNewSheetAndMakeItCurrent(): Sheet + { + $this->throwIfWorkbookIsNotAvailable(); + $worksheet = $this->workbookManager->addNewSheetAndMakeItCurrent(); + + return $worksheet->getExternalSheet(); + } + + /** + * Returns the current sheet. + * + * @return Sheet The current sheet + * + * @throws WriterNotOpenedException If the writer has not been opened yet + */ + final public function getCurrentSheet(): Sheet + { + $this->throwIfWorkbookIsNotAvailable(); + + return $this->workbookManager->getCurrentWorksheet()->getExternalSheet(); + } + + /** + * Sets the given sheet as the current one. New data will be written to this sheet. + * The writing will resume where it stopped (i.e. data won't be truncated). + * + * @param Sheet $sheet The sheet to set as current + * + * @throws SheetNotFoundException If the given sheet does not exist in the workbook + * @throws WriterNotOpenedException If the writer has not been opened yet + */ + final public function setCurrentSheet(Sheet $sheet): void + { + $this->throwIfWorkbookIsNotAvailable(); + $this->workbookManager->setCurrentSheet($sheet); + } + + abstract protected function createWorkbookManager(): WorkbookManagerInterface; + + /** + * {@inheritdoc} + */ + protected function openWriter(): void + { + if (!isset($this->workbookManager)) { + $this->workbookManager = $this->createWorkbookManager(); + $this->workbookManager->addNewSheetAndMakeItCurrent(); + } + } + + /** + * {@inheritdoc} + * + * @throws Exception\WriterException + */ + protected function addRowToWriter(Row $row): void + { + $this->throwIfWorkbookIsNotAvailable(); + $this->workbookManager->addRowToCurrentWorksheet($row); + } + + /** + * {@inheritdoc} + */ + protected function closeWriter(): void + { + if (isset($this->workbookManager)) { + $this->workbookManager->close($this->filePointer); + } + } + + /** + * Checks if the workbook has been created. Throws an exception if not created yet. + * + * @throws WriterNotOpenedException If the workbook is not created yet + */ + private function throwIfWorkbookIsNotAvailable(): void + { + if (!isset($this->workbookManager)) { + throw new WriterNotOpenedException('The writer must be opened before performing this action.'); + } + } +} diff --git a/lib/openspout/src/Writer/AutoFilter.php b/lib/openspout/src/Writer/AutoFilter.php new file mode 100644 index 00000000000..b9796446457 --- /dev/null +++ b/lib/openspout/src/Writer/AutoFilter.php @@ -0,0 +1,25 @@ +setOption(Options::FIELD_DELIMITER, ','); - $this->setOption(Options::FIELD_ENCLOSURE, '"'); - $this->setOption(Options::SHOULD_ADD_BOM, true); - } -} diff --git a/lib/openspout/src/Writer/CSV/Options.php b/lib/openspout/src/Writer/CSV/Options.php new file mode 100644 index 00000000000..6a0eb510e95 --- /dev/null +++ b/lib/openspout/src/Writer/CSV/Options.php @@ -0,0 +1,15 @@ +optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter); - - return $this; + $this->options = $options ?? new Options(); } - /** - * Sets the field enclosure for the CSV. - * - * @param string $fieldEnclosure Character that enclose fields - * - * @return Writer - */ - public function setFieldEnclosure($fieldEnclosure) + public function getOptions(): Options { - $this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure); - - return $this; - } - - /** - * Set if a BOM has to be added to the file. - * - * @param bool $shouldAddBOM - * - * @return Writer - */ - public function setShouldAddBOM($shouldAddBOM) - { - $this->optionsManager->setOption(Options::SHOULD_ADD_BOM, (bool) $shouldAddBOM); - - return $this; + return $this->options; } /** * Opens the CSV streamer and makes it ready to accept data. */ - protected function openWriter() + protected function openWriter(): void { - if ($this->optionsManager->getOption(Options::SHOULD_ADD_BOM)) { + if ($this->options->SHOULD_ADD_BOM) { // Adds UTF-8 BOM for Unicode compatibility - $this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8); + fwrite($this->filePointer, EncodingHelper::BOM_UTF8); } } @@ -82,19 +47,30 @@ class Writer extends WriterAbstract * * @throws IOException If unable to write data */ - protected function addRowToWriter(Row $row) + protected function addRowToWriter(Row $row): void { - $fieldDelimiter = $this->optionsManager->getOption(Options::FIELD_DELIMITER); - $fieldEnclosure = $this->optionsManager->getOption(Options::FIELD_ENCLOSURE); + $cells = array_map(static function (Cell\BooleanCell|Cell\EmptyCell|Cell\NumericCell|Cell\StringCell|Cell\FormulaCell $value): string { + if ($value instanceof Cell\BooleanCell) { + return (string) (int) $value->getValue(); + } - $wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $row->getCells(), $fieldDelimiter, $fieldEnclosure); + return (string) $value->getValue(); + }, $row->getCells()); + + $wasWriteSuccessful = fputcsv( + $this->filePointer, + $cells, + $this->options->FIELD_DELIMITER, + $this->options->FIELD_ENCLOSURE, + '' + ); if (false === $wasWriteSuccessful) { - throw new IOException('Unable to write data'); + throw new IOException('Unable to write data'); // @codeCoverageIgnore } ++$this->lastWrittenRowIndex; - if (0 === $this->lastWrittenRowIndex % self::FLUSH_THRESHOLD) { - $this->globalFunctionsHelper->fflush($this->filePointer); + if (0 === $this->lastWrittenRowIndex % $this->options->FLUSH_THRESHOLD) { + fflush($this->filePointer); } } @@ -102,7 +78,7 @@ class Writer extends WriterAbstract * Closes the CSV streamer, preventing any additional writing. * If set, sets the headers and redirects output to the browser. */ - protected function closeWriter() + protected function closeWriter(): void { $this->lastWrittenRowIndex = 0; } diff --git a/lib/openspout/src/Writer/Common/AbstractOptions.php b/lib/openspout/src/Writer/Common/AbstractOptions.php new file mode 100644 index 00000000000..0b6651d84a9 --- /dev/null +++ b/lib/openspout/src/Writer/Common/AbstractOptions.php @@ -0,0 +1,67 @@ +DEFAULT_ROW_STYLE = new Style(); + } + + /** + * @param positive-int ...$columns One or more columns with this width + */ + final public function setColumnWidth(float $width, int ...$columns): void + { + // Gather sequences + $sequence = []; + foreach ($columns as $column) { + $sequenceLength = \count($sequence); + if ($sequenceLength > 0) { + $previousValue = $sequence[$sequenceLength - 1]; + if ($column !== $previousValue + 1) { + $this->setColumnWidthForRange($width, $sequence[0], $previousValue); + $sequence = []; + } + } + $sequence[] = $column; + } + $this->setColumnWidthForRange($width, $sequence[0], $sequence[\count($sequence) - 1]); + } + + /** + * @param float $width The width to set + * @param positive-int $start First column index of the range + * @param positive-int $end Last column index of the range + */ + final public function setColumnWidthForRange(float $width, int $start, int $end): void + { + $this->COLUMN_WIDTHS[] = new ColumnWidth($start, $end, $width); + } + + /** + * @internal + * + * @return ColumnWidth[] + */ + final public function getColumnWidths(): array + { + return $this->COLUMN_WIDTHS; + } +} diff --git a/lib/openspout/src/Writer/Common/ColumnWidth.php b/lib/openspout/src/Writer/Common/ColumnWidth.php new file mode 100644 index 00000000000..ba7b8020578 --- /dev/null +++ b/lib/openspout/src/Writer/Common/ColumnWidth.php @@ -0,0 +1,22 @@ +border = new Border(); - } - - /** - * @param string $color Border A RGB color code - * @param string $width Border width @see BorderPart::allowedWidths - * @param string $style Border style @see BorderPart::allowedStyles - * - * @return BorderBuilder - */ - public function setBorderTop($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::TOP, $color, $width, $style)); - - return $this; - } - - /** - * @param string $color Border A RGB color code - * @param string $width Border width @see BorderPart::allowedWidths - * @param string $style Border style @see BorderPart::allowedStyles - * - * @return BorderBuilder - */ - public function setBorderRight($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::RIGHT, $color, $width, $style)); - - return $this; - } - - /** - * @param string $color Border A RGB color code - * @param string $width Border width @see BorderPart::allowedWidths - * @param string $style Border style @see BorderPart::allowedStyles - * - * @return BorderBuilder - */ - public function setBorderBottom($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::BOTTOM, $color, $width, $style)); - - return $this; - } - - /** - * @param string $color Border A RGB color code - * @param string $width Border width @see BorderPart::allowedWidths - * @param string $style Border style @see BorderPart::allowedStyles - * - * @return BorderBuilder - */ - public function setBorderLeft($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::LEFT, $color, $width, $style)); - - return $this; - } - - /** - * @return Border - */ - public function build() - { - return $this->border; - } -} diff --git a/lib/openspout/src/Writer/Common/Creator/Style/StyleBuilder.php b/lib/openspout/src/Writer/Common/Creator/Style/StyleBuilder.php deleted file mode 100644 index 2ef0d151667..00000000000 --- a/lib/openspout/src/Writer/Common/Creator/Style/StyleBuilder.php +++ /dev/null @@ -1,214 +0,0 @@ -style = new Style(); - } - - /** - * Makes the font bold. - * - * @return StyleBuilder - */ - public function setFontBold() - { - $this->style->setFontBold(); - - return $this; - } - - /** - * Makes the font italic. - * - * @return StyleBuilder - */ - public function setFontItalic() - { - $this->style->setFontItalic(); - - return $this; - } - - /** - * Makes the font underlined. - * - * @return StyleBuilder - */ - public function setFontUnderline() - { - $this->style->setFontUnderline(); - - return $this; - } - - /** - * Makes the font struck through. - * - * @return StyleBuilder - */ - public function setFontStrikethrough() - { - $this->style->setFontStrikethrough(); - - return $this; - } - - /** - * Sets the font size. - * - * @param int $fontSize Font size, in pixels - * - * @return StyleBuilder - */ - public function setFontSize($fontSize) - { - $this->style->setFontSize($fontSize); - - return $this; - } - - /** - * Sets the font color. - * - * @param string $fontColor ARGB color (@see Color) - * - * @return StyleBuilder - */ - public function setFontColor($fontColor) - { - $this->style->setFontColor($fontColor); - - return $this; - } - - /** - * Sets the font name. - * - * @param string $fontName Name of the font to use - * - * @return StyleBuilder - */ - public function setFontName($fontName) - { - $this->style->setFontName($fontName); - - return $this; - } - - /** - * Makes the text wrap in the cell if requested. - * - * @param bool $shouldWrap Should the text be wrapped - * - * @return StyleBuilder - */ - public function setShouldWrapText($shouldWrap = true) - { - $this->style->setShouldWrapText($shouldWrap); - - return $this; - } - - /** - * Sets the cell alignment. - * - * @param string $cellAlignment The cell alignment - * - * @throws InvalidArgumentException If the given cell alignment is not valid - * - * @return StyleBuilder - */ - public function setCellAlignment($cellAlignment) - { - if (!CellAlignment::isValid($cellAlignment)) { - throw new InvalidArgumentException('Invalid cell alignment value'); - } - - $this->style->setCellAlignment($cellAlignment); - - return $this; - } - - /** - * Set a border. - * - * @return $this - */ - public function setBorder(Border $border) - { - $this->style->setBorder($border); - - return $this; - } - - /** - * Sets a background color. - * - * @param string $color ARGB color (@see Color) - * - * @return StyleBuilder - */ - public function setBackgroundColor($color) - { - $this->style->setBackgroundColor($color); - - return $this; - } - - /** - * Sets a format. - * - * @param string $format Format - * - * @return StyleBuilder - * - * @api - */ - public function setFormat($format) - { - $this->style->setFormat($format); - - return $this; - } - - /** - * Set should shrink to fit. - * - * @param bool $shrinkToFit - * - * @return StyleBuilder - * - * @api - */ - public function setShouldShrinkToFit($shrinkToFit = true) - { - $this->style->setShouldShrinkToFit($shrinkToFit); - - return $this; - } - - /** - * Returns the configured style. The style is cached and can be reused. - * - * @return Style - */ - public function build() - { - return $this->style; - } -} diff --git a/lib/openspout/src/Writer/Common/Creator/WriterEntityFactory.php b/lib/openspout/src/Writer/Common/Creator/WriterEntityFactory.php deleted file mode 100644 index da6a43ac2ef..00000000000 --- a/lib/openspout/src/Writer/Common/Creator/WriterEntityFactory.php +++ /dev/null @@ -1,121 +0,0 @@ - new CSVWriter(), + 'xlsx' => new XLSXWriter(), + 'ods' => new ODSWriter(), + default => throw new UnsupportedTypeException('No writers supporting the given type: '.$extension), + }; } } diff --git a/lib/openspout/src/Writer/Common/Entity/Options.php b/lib/openspout/src/Writer/Common/Entity/Options.php deleted file mode 100644 index bcf1936e22a..00000000000 --- a/lib/openspout/src/Writer/Common/Entity/Options.php +++ /dev/null @@ -1,30 +0,0 @@ -index = $sheetIndex; $this->associatedWorkbookId = $associatedWorkbookId; @@ -48,17 +59,14 @@ class Sheet } /** - * @return int Index of the sheet, based on order in the workbook (zero-based) + * @return 0|positive-int Index of the sheet, based on order in the workbook (zero-based) */ - public function getIndex() + public function getIndex(): int { return $this->index; } - /** - * @return string - */ - public function getAssociatedWorkbookId() + public function getAssociatedWorkbookId(): string { return $this->associatedWorkbookId; } @@ -66,7 +74,7 @@ class Sheet /** * @return string Name of the sheet */ - public function getName() + public function getName(): string { return $this->name; } @@ -81,10 +89,8 @@ class Sheet * @param string $name Name of the sheet * * @throws \OpenSpout\Writer\Exception\InvalidSheetNameException if the sheet's name is invalid - * - * @return Sheet */ - public function setName($name) + public function setName(string $name): self { $this->sheetManager->throwIfNameIsInvalid($name, $this); @@ -98,40 +104,105 @@ class Sheet /** * @return bool isVisible Visibility of the sheet */ - public function isVisible() + public function isVisible(): bool { return $this->isVisible; } /** * @param bool $isVisible Visibility of the sheet - * - * @return Sheet */ - public function setIsVisible($isVisible) + public function setIsVisible(bool $isVisible): self { $this->isVisible = $isVisible; return $this; } + /** + * @return $this + */ + public function setSheetView(SheetView $sheetView): self + { + $this->sheetView = $sheetView; + + return $this; + } + public function getSheetView(): ?SheetView { return $this->sheetView; } + /** + * @internal + */ + public function incrementWrittenRowCount(): void + { + ++$this->writtenRowCount; + } + + /** + * @return 0|positive-int + */ + public function getWrittenRowCount(): int + { + return $this->writtenRowCount; + } + /** * @return $this */ - public function setSheetView(SheetView $sheetView) + public function setAutoFilter(?AutoFilter $autoFilter): self { - $this->sheetView = $sheetView; + $this->autoFilter = $autoFilter; return $this; } - public function hasSheetView(): bool + public function getAutoFilter(): ?AutoFilter { - return $this->sheetView instanceof SheetView; + return $this->autoFilter; + } + + /** + * @param positive-int ...$columns One or more columns with this width + */ + public function setColumnWidth(float $width, int ...$columns): void + { + // Gather sequences + $sequence = []; + foreach ($columns as $column) { + $sequenceLength = \count($sequence); + if ($sequenceLength > 0) { + $previousValue = $sequence[$sequenceLength - 1]; + if ($column !== $previousValue + 1) { + $this->setColumnWidthForRange($width, $sequence[0], $previousValue); + $sequence = []; + } + } + $sequence[] = $column; + } + $this->setColumnWidthForRange($width, $sequence[0], $sequence[\count($sequence) - 1]); + } + + /** + * @param float $width The width to set + * @param positive-int $start First column index of the range + * @param positive-int $end Last column index of the range + */ + public function setColumnWidthForRange(float $width, int $start, int $end): void + { + $this->COLUMN_WIDTHS[] = new ColumnWidth($start, $end, $width); + } + + /** + * @internal + * + * @return ColumnWidth[] + */ + public function getColumnWidths(): array + { + return $this->COLUMN_WIDTHS; } } diff --git a/lib/openspout/src/Writer/Common/Entity/Workbook.php b/lib/openspout/src/Writer/Common/Entity/Workbook.php index 152ded82b8c..e179c513b76 100644 --- a/lib/openspout/src/Writer/Common/Entity/Workbook.php +++ b/lib/openspout/src/Writer/Common/Entity/Workbook.php @@ -1,17 +1,19 @@ worksheets; } @@ -32,15 +34,12 @@ class Workbook /** * @param Worksheet[] $worksheets */ - public function setWorksheets($worksheets) + public function setWorksheets(array $worksheets): void { $this->worksheets = $worksheets; } - /** - * @return string - */ - public function getInternalId() + public function getInternalId(): string { return $this->internalId; } diff --git a/lib/openspout/src/Writer/Common/Entity/Worksheet.php b/lib/openspout/src/Writer/Common/Entity/Worksheet.php index 0263429e746..6fc6a2f659c 100644 --- a/lib/openspout/src/Writer/Common/Entity/Worksheet.php +++ b/lib/openspout/src/Writer/Common/Entity/Worksheet.php @@ -1,49 +1,42 @@ filePath = $worksheetFilePath; $this->filePointer = null; $this->externalSheet = $externalSheet; $this->maxNumColumns = 0; $this->lastWrittenRowIndex = 0; - $this->sheetDataStarted = false; } - /** - * @return string - */ - public function getFilePath() + public function getFilePath(): string { return $this->filePath; } @@ -53,53 +46,40 @@ class Worksheet */ public function getFilePointer() { + \assert(null !== $this->filePointer); + return $this->filePointer; } /** * @param resource $filePointer */ - public function setFilePointer($filePointer) + public function setFilePointer($filePointer): void { $this->filePointer = $filePointer; } - /** - * @return Sheet - */ - public function getExternalSheet() + public function getExternalSheet(): Sheet { return $this->externalSheet; } - /** - * @return int - */ - public function getMaxNumColumns() + public function getMaxNumColumns(): int { return $this->maxNumColumns; } - /** - * @param int $maxNumColumns - */ - public function setMaxNumColumns($maxNumColumns) + public function setMaxNumColumns(int $maxNumColumns): void { $this->maxNumColumns = $maxNumColumns; } - /** - * @return int - */ - public function getLastWrittenRowIndex() + public function getLastWrittenRowIndex(): int { return $this->lastWrittenRowIndex; } - /** - * @param int $lastWrittenRowIndex - */ - public function setLastWrittenRowIndex($lastWrittenRowIndex) + public function setLastWrittenRowIndex(int $lastWrittenRowIndex): void { $this->lastWrittenRowIndex = $lastWrittenRowIndex; } @@ -107,25 +87,9 @@ class Worksheet /** * @return int The ID of the worksheet */ - public function getId() + public function getId(): int { // sheet index is zero-based, while ID is 1-based return $this->externalSheet->getIndex() + 1; } - - /** - * @return bool - */ - public function getSheetDataStarted() - { - return $this->sheetDataStarted; - } - - /** - * @param bool $sheetDataStarted - */ - public function setSheetDataStarted($sheetDataStarted) - { - $this->sheetDataStarted = $sheetDataStarted; - } } diff --git a/lib/openspout/src/Writer/Common/Helper/CellHelper.php b/lib/openspout/src/Writer/Common/Helper/CellHelper.php index 400e82ca2e3..e70978d8193 100644 --- a/lib/openspout/src/Writer/Common/Helper/CellHelper.php +++ b/lib/openspout/src/Writer/Common/Helper/CellHelper.php @@ -1,14 +1,16 @@ column letters */ - private static $columnIndexToColumnLettersCache = []; + /** @var array Cache containing the mapping column index => column letters */ + private static array $columnIndexToColumnLettersCache = []; /** * Returns the column letters (base 26) associated to the base 10 column index. @@ -20,7 +22,7 @@ class CellHelper * * @return string The associated cell index ('A', 'BC', ...) */ - public static function getColumnLettersFromColumnIndex($columnIndexZeroBased) + public static function getColumnLettersFromColumnIndex(int $columnIndexZeroBased): string { $originalColumnIndex = $columnIndexZeroBased; diff --git a/lib/openspout/src/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php b/lib/openspout/src/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php index f571ebf1104..afd365b12cc 100644 --- a/lib/openspout/src/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php +++ b/lib/openspout/src/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php @@ -1,12 +1,13 @@ entityFactory = $entityFactory; - } - /** * Returns a new ZipArchive instance pointing at the given path. * * @param string $tmpFolderPath Path of the temp folder where the zip file will be created - * - * @return \ZipArchive */ - public function createZip($tmpFolderPath) + public function createZip(string $tmpFolderPath): ZipArchive { - $zip = $this->entityFactory->createZipArchive(); + $zip = new ZipArchive(); $zipFilePath = $tmpFolderPath.self::ZIP_EXTENSION; - $zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); + $zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE); return $zip; } /** - * @param \ZipArchive $zip An opened zip archive object + * @param ZipArchive $zip An opened zip archive object * * @return string Path where the zip file of the given folder will be created */ - public function getZipFilePath(\ZipArchive $zip) + public function getZipFilePath(ZipArchive $zip): string { return $zip->filename; } @@ -61,19 +55,19 @@ class ZipHelper * addFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml'); * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' * - * @param \ZipArchive $zip An opened zip archive object - * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree - * @param string $localFilePath Path of the file to be added, under the root folder - * @param string $existingFileMode Controls what to do when trying to add an existing file + * @param ZipArchive $zip An opened zip archive object + * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree + * @param string $localFilePath Path of the file to be added, under the root folder + * @param string $existingFileMode Controls what to do when trying to add an existing file */ - public function addFileToArchive($zip, $rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) + public function addFileToArchive(ZipArchive $zip, string $rootFolderPath, string $localFilePath, string $existingFileMode = self::EXISTING_FILES_OVERWRITE): void { $this->addFileToArchiveWithCompressionMethod( $zip, $rootFolderPath, $localFilePath, $existingFileMode, - \ZipArchive::CM_DEFAULT + ZipArchive::CM_DEFAULT ); } @@ -85,42 +79,37 @@ class ZipHelper * addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml'); * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' * - * @param \ZipArchive $zip An opened zip archive object - * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree - * @param string $localFilePath Path of the file to be added, under the root folder - * @param string $existingFileMode Controls what to do when trying to add an existing file + * @param ZipArchive $zip An opened zip archive object + * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree + * @param string $localFilePath Path of the file to be added, under the root folder + * @param string $existingFileMode Controls what to do when trying to add an existing file */ - public function addUncompressedFileToArchive($zip, $rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) + public function addUncompressedFileToArchive(ZipArchive $zip, string $rootFolderPath, string $localFilePath, string $existingFileMode = self::EXISTING_FILES_OVERWRITE): void { $this->addFileToArchiveWithCompressionMethod( $zip, $rootFolderPath, $localFilePath, $existingFileMode, - \ZipArchive::CM_STORE + ZipArchive::CM_STORE ); } /** - * @return bool Whether it is possible to choose the desired compression method to be used + * @param ZipArchive $zip An opened zip archive object + * @param string $folderPath Path to the folder to be zipped + * @param string $existingFileMode Controls what to do when trying to add an existing file */ - public static function canChooseCompressionMethod() - { - // setCompressionName() is a PHP7+ method... - return method_exists(new \ZipArchive(), 'setCompressionName'); - } - - /** - * @param \ZipArchive $zip An opened zip archive object - * @param string $folderPath Path to the folder to be zipped - * @param string $existingFileMode Controls what to do when trying to add an existing file - */ - public function addFolderToArchive($zip, $folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) + public function addFolderToArchive(ZipArchive $zip, string $folderPath, string $existingFileMode = self::EXISTING_FILES_OVERWRITE): void { $folderRealPath = $this->getNormalizedRealPath($folderPath).'/'; - $itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); + $itemIterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); foreach ($itemIterator as $itemInfo) { + \assert($itemInfo instanceof SplFileInfo); $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname()); $itemLocalPath = str_replace($folderRealPath, '', $itemRealPath); @@ -133,10 +122,10 @@ class ZipHelper /** * Closes the archive and copies it into the given stream. * - * @param \ZipArchive $zip An opened zip archive object - * @param resource $streamPointer Pointer to the stream to copy the zip + * @param ZipArchive $zip An opened zip archive object + * @param resource $streamPointer Pointer to the stream to copy the zip */ - public function closeArchiveAndCopyToStream($zip, $streamPointer) + public function closeArchiveAndCopyToStream(ZipArchive $zip, $streamPointer): void { $zipFilePath = $zip->filename; $zip->close(); @@ -152,32 +141,27 @@ class ZipHelper * addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml'); * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' * - * @param \ZipArchive $zip An opened zip archive object - * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree - * @param string $localFilePath Path of the file to be added, under the root folder - * @param string $existingFileMode Controls what to do when trying to add an existing file - * @param int $compressionMethod The compression method + * @param ZipArchive $zip An opened zip archive object + * @param string $rootFolderPath path of the root folder that will be ignored in the archive tree + * @param string $localFilePath Path of the file to be added, under the root folder + * @param string $existingFileMode Controls what to do when trying to add an existing file + * @param int $compressionMethod The compression method */ - protected function addFileToArchiveWithCompressionMethod($zip, $rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod) + private function addFileToArchiveWithCompressionMethod(ZipArchive $zip, string $rootFolderPath, string $localFilePath, string $existingFileMode, int $compressionMethod): void { - if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) { - $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath.'/'.$localFilePath); - $zip->addFile($normalizedFullFilePath, $localFilePath); + $normalizedLocalFilePath = str_replace('\\', '/', $localFilePath); + if (!$this->shouldSkipFile($zip, $normalizedLocalFilePath, $existingFileMode)) { + $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath.'/'.$normalizedLocalFilePath); + $zip->addFile($normalizedFullFilePath, $normalizedLocalFilePath); - if (self::canChooseCompressionMethod()) { - $zip->setCompressionName($localFilePath, $compressionMethod); - } + $zip->setCompressionName($normalizedLocalFilePath, $compressionMethod); } } /** - * @param \ZipArchive $zip - * @param string $itemLocalPath - * @param string $existingFileMode - * * @return bool Whether the file should be added to the archive or skipped */ - protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode) + private function shouldSkipFile(ZipArchive $zip, string $itemLocalPath, string $existingFileMode): bool { // Skip files if: // - EXISTING_FILES_SKIP mode chosen @@ -192,9 +176,10 @@ class ZipHelper * * @return string Normalized and canonicalized path */ - protected function getNormalizedRealPath($path) + private function getNormalizedRealPath(string $path): string { $realPath = realpath($path); + \assert(false !== $realPath); return str_replace(\DIRECTORY_SEPARATOR, '/', $realPath); } @@ -205,9 +190,10 @@ class ZipHelper * @param string $zipFilePath Path of the zip file * @param resource $pointer Pointer to the stream to copy the zip */ - protected function copyZipToStream($zipFilePath, $pointer) + private function copyZipToStream(string $zipFilePath, $pointer): void { $zipFilePointer = fopen($zipFilePath, 'r'); + \assert(false !== $zipFilePointer); stream_copy_to_stream($zipFilePointer, $pointer); fclose($zipFilePointer); } diff --git a/lib/openspout/src/Writer/Common/Manager/WorkbookManagerAbstract.php b/lib/openspout/src/Writer/Common/Manager/AbstractWorkbookManager.php similarity index 60% rename from lib/openspout/src/Writer/Common/Manager/WorkbookManagerAbstract.php rename to lib/openspout/src/Writer/Common/Manager/AbstractWorkbookManager.php index 8ffd7e91c67..3411b140bb3 100644 --- a/lib/openspout/src/Writer/Common/Manager/WorkbookManagerAbstract.php +++ b/lib/openspout/src/Writer/Common/Manager/AbstractWorkbookManager.php @@ -1,13 +1,13 @@ workbook = $workbook; - $this->optionsManager = $optionsManager; + $this->options = $options; $this->worksheetManager = $worksheetManager; $this->styleManager = $styleManager; $this->styleMerger = $styleMerger; $this->fileSystemHelper = $fileSystemHelper; - $this->entityFactory = $entityFactory; - $this->managerFactory = $managerFactory; - } - - /** - * @return null|Workbook - */ - public function getWorkbook() - { - return $this->workbook; } /** @@ -82,7 +62,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * * @return Worksheet The created sheet */ - public function addNewSheetAndMakeItCurrent() + final public function addNewSheetAndMakeItCurrent(): Worksheet { $worksheet = $this->addNewSheet(); $this->setCurrentWorksheet($worksheet); @@ -93,7 +73,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** * @return Worksheet[] All the workbook's sheets */ - public function getWorksheets() + final public function getWorksheets(): array { return $this->workbook->getWorksheets(); } @@ -103,21 +83,11 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * * @return Worksheet The current sheet */ - public function getCurrentWorksheet() + final public function getCurrentWorksheet(): Worksheet { return $this->currentWorksheet; } - /** - * Starts the current sheet and opens the file pointer. - * - * @throws IOException - */ - public function startCurrentSheet() - { - $this->worksheetManager->startSheet($this->getCurrentWorksheet()); - } - /** * Sets the given sheet as the current one. New data will be written to this sheet. * The writing will resume where it stopped (i.e. data won't be truncated). @@ -126,7 +96,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * * @throws SheetNotFoundException If the given sheet does not exist in the workbook */ - public function setCurrentSheet(Sheet $sheet) + final public function setCurrentSheet(Sheet $sheet): void { $worksheet = $this->getWorksheetFromExternalSheet($sheet); if (null !== $worksheet) { @@ -146,51 +116,19 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * @throws IOException If trying to create a new sheet and unable to open the sheet for writing * @throws \OpenSpout\Common\Exception\InvalidArgumentException */ - public function addRowToCurrentWorksheet(Row $row) + final public function addRowToCurrentWorksheet(Row $row): void { $currentWorksheet = $this->getCurrentWorksheet(); - $hasReachedMaxRows = $this->hasCurrentWorksheetReachedMaxRows(); - - // if we reached the maximum number of rows for the current sheet... - if ($hasReachedMaxRows) { - // ... continue writing in a new sheet if option set - if ($this->optionsManager->getOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY)) { - $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); - - $this->addRowToWorksheet($currentWorksheet, $row); + if ($this->hasCurrentWorksheetReachedMaxRows()) { + if (!$this->options->SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY) { + return; } - // otherwise, do nothing as the data won't be written anyways - } else { - $this->addRowToWorksheet($currentWorksheet, $row); + + $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); } - } - public function setDefaultColumnWidth(float $width) - { - $this->worksheetManager->setDefaultColumnWidth($width); - } - - public function setDefaultRowHeight(float $height) - { - $this->worksheetManager->setDefaultRowHeight($height); - } - - /** - * @param int ...$columns One or more columns with this width - */ - public function setColumnWidth(float $width, ...$columns) - { - $this->worksheetManager->setColumnWidth($width, ...$columns); - } - - /** - * @param float $width The width to set - * @param int $start First column index of the range - * @param int $end Last column index of the range - */ - public function setColumnWidthForRange(float $width, int $start, int $end) - { - $this->worksheetManager->setColumnWidthForRange($width, $start, $end); + $this->addRowToWorksheet($currentWorksheet, $row); + $currentWorksheet->getExternalSheet()->incrementWrittenRowCount(); } /** @@ -200,7 +138,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * * @param resource $finalFilePointer Pointer to the spreadsheet that will be created */ - public function close($finalFilePointer) + final public function close($finalFilePointer): void { $this->closeAllWorksheets(); $this->closeRemainingObjects(); @@ -211,17 +149,12 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** * @return int Maximum number of rows/columns a sheet can contain */ - abstract protected function getMaxRowsPerWorksheet(); - - /** - * @return string The file path where the data for the given sheet will be stored - */ - abstract protected function getWorksheetFilePath(Sheet $sheet); + abstract protected function getMaxRowsPerWorksheet(): int; /** * Closes custom objects that are still opened. */ - protected function closeRemainingObjects() + protected function closeRemainingObjects(): void { // do nothing by default } @@ -231,12 +164,22 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * * @param resource $finalFilePointer Pointer to the spreadsheet that will be created */ - abstract protected function writeAllFilesToDiskAndZipThem($finalFilePointer); + abstract protected function writeAllFilesToDiskAndZipThem($finalFilePointer): void; + + /** + * @return string The file path where the data for the given sheet will be stored + */ + private function getWorksheetFilePath(Sheet $sheet): string + { + $sheetsContentTempFolder = $this->fileSystemHelper->getSheetsContentTempFolder(); + + return $sheetsContentTempFolder.\DIRECTORY_SEPARATOR.'sheet'.(1 + $sheet->getIndex()).'.xml'; + } /** * Deletes the root folder created in the temp folder and all its contents. */ - protected function cleanupTempFolder() + private function cleanupTempFolder(): void { $rootFolder = $this->fileSystemHelper->getRootFolder(); $this->fileSystemHelper->deleteFolderRecursively($rootFolder); @@ -245,20 +188,20 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** * Creates a new sheet in the workbook. The current sheet remains unchanged. * - * @throws \OpenSpout\Common\Exception\IOException If unable to open the sheet for writing - * * @return Worksheet The created sheet + * + * @throws \OpenSpout\Common\Exception\IOException If unable to open the sheet for writing */ - private function addNewSheet() + private function addNewSheet(): Worksheet { $worksheets = $this->getWorksheets(); $newSheetIndex = \count($worksheets); - $sheetManager = $this->managerFactory->createSheetManager(); - $sheet = $this->entityFactory->createSheet($newSheetIndex, $this->workbook->getInternalId(), $sheetManager); + $sheetManager = new SheetManager(StringHelper::factory()); + $sheet = new Sheet($newSheetIndex, $this->workbook->getInternalId(), $sheetManager); $worksheetFilePath = $this->getWorksheetFilePath($sheet); - $worksheet = $this->entityFactory->createWorksheet($worksheetFilePath, $sheet); + $worksheet = new Worksheet($worksheetFilePath, $sheet); $this->worksheetManager->startSheet($worksheet); @@ -268,10 +211,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface return $worksheet; } - /** - * @param Worksheet $worksheet - */ - private function setCurrentWorksheet($worksheet) + private function setCurrentWorksheet(Worksheet $worksheet): void { $this->currentWorksheet = $worksheet; } @@ -279,11 +219,9 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** * Returns the worksheet associated to the given external sheet. * - * @param Sheet $sheet - * * @return null|Worksheet the worksheet associated to the given external sheet or null if not found */ - private function getWorksheetFromExternalSheet($sheet) + private function getWorksheetFromExternalSheet(Sheet $sheet): ?Worksheet { $worksheetFound = null; @@ -301,7 +239,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** * @return bool whether the current worksheet has reached the maximum number of rows per sheet */ - private function hasCurrentWorksheetReachedMaxRows() + private function hasCurrentWorksheetReachedMaxRows(): bool { $currentWorksheet = $this->getCurrentWorksheet(); @@ -317,7 +255,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * @throws IOException * @throws \OpenSpout\Common\Exception\InvalidArgumentException */ - private function addRowToWorksheet(Worksheet $worksheet, Row $row) + private function addRowToWorksheet(Worksheet $worksheet, Row $row): void { $this->applyDefaultRowStyle($row); $this->worksheetManager->addRow($worksheet, $row); @@ -328,20 +266,19 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface $worksheet->setMaxNumColumns(max($currentMaxNumColumns, $cellsCount)); } - private function applyDefaultRowStyle(Row $row) + private function applyDefaultRowStyle(Row $row): void { - $defaultRowStyle = $this->optionsManager->getOption(Options::DEFAULT_ROW_STYLE); - - if (null !== $defaultRowStyle) { - $mergedStyle = $this->styleMerger->merge($row->getStyle(), $defaultRowStyle); - $row->setStyle($mergedStyle); - } + $mergedStyle = $this->styleMerger->merge( + $row->getStyle(), + $this->options->DEFAULT_ROW_STYLE + ); + $row->setStyle($mergedStyle); } /** * Closes all workbook's associated sheets. */ - private function closeAllWorksheets() + private function closeAllWorksheets(): void { $worksheets = $this->getWorksheets(); diff --git a/lib/openspout/src/Writer/Common/Manager/CellManager.php b/lib/openspout/src/Writer/Common/Manager/CellManager.php deleted file mode 100644 index 5ce70d4b2b4..00000000000 --- a/lib/openspout/src/Writer/Common/Manager/CellManager.php +++ /dev/null @@ -1,29 +0,0 @@ -styleMerger = $styleMerger; - } - - /** - * Merges a Style into a cell's Style. - */ - public function applyStyle(Cell $cell, Style $style) - { - $mergedStyle = $this->styleMerger->merge($cell->getStyle(), $style); - $cell->setStyle($mergedStyle); - } -} diff --git a/lib/openspout/src/Writer/Common/Manager/ManagesCellSize.php b/lib/openspout/src/Writer/Common/Manager/ManagesCellSize.php deleted file mode 100644 index eb8c4a4f0ba..00000000000 --- a/lib/openspout/src/Writer/Common/Manager/ManagesCellSize.php +++ /dev/null @@ -1,62 +0,0 @@ -defaultColumnWidth = $width; - } - - /** - * @param null|float $height - */ - public function setDefaultRowHeight($height) - { - $this->defaultRowHeight = $height; - } - - /** - * @param int ...$columns One or more columns with this width - */ - public function setColumnWidth(float $width, ...$columns) - { - // Gather sequences - $sequence = []; - foreach ($columns as $i) { - $sequenceLength = \count($sequence); - if ($sequenceLength > 0) { - $previousValue = $sequence[$sequenceLength - 1]; - if ($i !== $previousValue + 1) { - $this->setColumnWidthForRange($width, $sequence[0], $previousValue); - $sequence = []; - } - } - $sequence[] = $i; - } - $this->setColumnWidthForRange($width, $sequence[0], $sequence[\count($sequence) - 1]); - } - - /** - * @param float $width The width to set - * @param int $start First column index of the range - * @param int $end Last column index of the range - */ - public function setColumnWidthForRange(float $width, int $start, int $end) - { - $this->columnWidths[] = [$start, $end, $width]; - } -} diff --git a/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php b/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php index d3ef877ae8d..1144cee528b 100644 --- a/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php +++ b/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php @@ -1,23 +1,21 @@ getCells() as $cell) { - if (!$cell->isEmpty()) { - return false; - } - } - - return true; - } -} diff --git a/lib/openspout/src/Writer/Common/Manager/SheetManager.php b/lib/openspout/src/Writer/Common/Manager/SheetManager.php index 4d6a6c236c1..937093f4cb1 100644 --- a/lib/openspout/src/Writer/Common/Manager/SheetManager.php +++ b/lib/openspout/src/Writer/Common/Manager/SheetManager.php @@ -1,5 +1,7 @@ [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */ - private static $SHEETS_NAME_USED = []; + /** @var array> Associative array [WORKBOOK_ID] => [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */ + private static array $SHEETS_NAME_USED = []; - /** @var StringHelper */ - private $stringHelper; + private StringHelper $stringHelper; /** * SheetManager constructor. @@ -36,40 +41,30 @@ class SheetManager * * @see Sheet::setName for validity rules. * - * @param string $name - * @param Sheet $sheet The sheet whose future name is checked + * @param Sheet $sheet The sheet whose future name is checked * * @throws \OpenSpout\Writer\Exception\InvalidSheetNameException if the sheet's name is invalid */ - public function throwIfNameIsInvalid($name, Sheet $sheet) + public function throwIfNameIsInvalid(string $name, Sheet $sheet): void { - if (!\is_string($name)) { - $actualType = \gettype($name); - $errorMessage = "The sheet's name is invalid. It must be a string ({$actualType} given)."; - - throw new InvalidSheetNameException($errorMessage); - } - $failedRequirements = []; $nameLength = $this->stringHelper->getStringLength($name); if (!$this->isNameUnique($name, $sheet)) { $failedRequirements[] = 'It should be unique'; + } elseif (0 === $nameLength) { + $failedRequirements[] = 'It should not be blank'; } else { - if (0 === $nameLength) { - $failedRequirements[] = 'It should not be blank'; - } else { - if ($nameLength > self::MAX_LENGTH_SHEET_NAME) { - $failedRequirements[] = 'It should not exceed 31 characters'; - } + if ($nameLength > self::MAX_LENGTH_SHEET_NAME) { + $failedRequirements[] = 'It should not exceed 31 characters'; + } - if ($this->doesContainInvalidCharacters($name)) { - $failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]'; - } + if ($this->doesContainInvalidCharacters($name)) { + $failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]'; + } - if ($this->doesStartOrEndWithSingleQuote($name)) { - $failedRequirements[] = 'It should not start or end with a single quote'; - } + if ($this->doesStartOrEndWithSingleQuote($name)) { + $failedRequirements[] = 'It should not start or end with a single quote'; } } @@ -84,14 +79,14 @@ class SheetManager /** * @param string $workbookId Workbook ID associated to a Sheet */ - public function markWorkbookIdAsUsed($workbookId) + public function markWorkbookIdAsUsed(string $workbookId): void { if (!isset(self::$SHEETS_NAME_USED[$workbookId])) { self::$SHEETS_NAME_USED[$workbookId] = []; } } - public function markSheetNameAsUsed(Sheet $sheet) + public function markSheetNameAsUsed(Sheet $sheet): void { self::$SHEETS_NAME_USED[$sheet->getAssociatedWorkbookId()][$sheet->getIndex()] = $sheet->getName(); } @@ -99,25 +94,19 @@ class SheetManager /** * Returns whether the given name contains at least one invalid character. * - * @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list. - * - * @param string $name - * * @return bool TRUE if the name contains invalid characters, FALSE otherwise */ - private function doesContainInvalidCharacters($name) + private function doesContainInvalidCharacters(string $name): bool { - return str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name; + return str_replace(self::INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name; } /** * Returns whether the given name starts or ends with a single quote. * - * @param string $name - * * @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise */ - private function doesStartOrEndWithSingleQuote($name) + private function doesStartOrEndWithSingleQuote(string $name): bool { $startsWithSingleQuote = (0 === $this->stringHelper->getCharFirstOccurrencePosition('\'', $name)); $endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1)); @@ -128,12 +117,11 @@ class SheetManager /** * Returns whether the given name is unique. * - * @param string $name - * @param Sheet $sheet The sheet whose future name is checked + * @param Sheet $sheet The sheet whose future name is checked * * @return bool TRUE if the name is unique, FALSE otherwise */ - private function isNameUnique($name, Sheet $sheet) + private function isNameUnique(string $name, Sheet $sheet): bool { foreach (self::$SHEETS_NAME_USED[$sheet->getAssociatedWorkbookId()] as $sheetIndex => $sheetName) { if ($sheetIndex !== $sheet->getIndex() && $sheetName === $name) { diff --git a/lib/openspout/src/Writer/Common/Manager/Style/StyleManager.php b/lib/openspout/src/Writer/Common/Manager/Style/AbstractStyleManager.php similarity index 73% rename from lib/openspout/src/Writer/Common/Manager/Style/StyleManager.php rename to lib/openspout/src/Writer/Common/Manager/Style/AbstractStyleManager.php index 47f4d34cf3f..f9c1624c793 100644 --- a/lib/openspout/src/Writer/Common/Manager/Style/StyleManager.php +++ b/lib/openspout/src/Writer/Common/Manager/Style/AbstractStyleManager.php @@ -1,19 +1,21 @@ styleRegistry = $styleRegistry; } @@ -26,7 +28,7 @@ class StyleManager implements StyleManagerInterface * * @return Style the registered style, updated with an internal ID */ - public function registerStyle($style) + final public function registerStyle(Style $style): Style { return $this->styleRegistry->registerStyle($style); } @@ -37,7 +39,7 @@ class StyleManager implements StyleManagerInterface * * @return PossiblyUpdatedStyle The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell): PossiblyUpdatedStyle + final public function applyExtraStylesIfNeeded(Cell $cell): PossiblyUpdatedStyle { return $this->applyWrapTextIfCellContainsNewLine($cell); } @@ -47,7 +49,7 @@ class StyleManager implements StyleManagerInterface * * @return Style Default style */ - protected function getDefaultStyle() + final protected function getDefaultStyle(): Style { // By construction, the default style has ID 0 return $this->styleRegistry->getRegisteredStyles()[0]; @@ -66,12 +68,12 @@ class StyleManager implements StyleManagerInterface * * @return PossiblyUpdatedStyle The eventually updated style */ - protected function applyWrapTextIfCellContainsNewLine(Cell $cell): PossiblyUpdatedStyle + private function applyWrapTextIfCellContainsNewLine(Cell $cell): PossiblyUpdatedStyle { $cellStyle = $cell->getStyle(); // if the "wrap text" option is already set, no-op - if (!$cellStyle->hasSetWrapText() && $cell->isString() && false !== strpos($cell->getValue(), "\n")) { + if (!$cellStyle->hasSetWrapText() && $cell instanceof Cell\StringCell && str_contains($cell->getValue(), "\n")) { $cellStyle->setShouldWrapText(); return new PossiblyUpdatedStyle($cellStyle, true); diff --git a/lib/openspout/src/Writer/Common/Manager/Style/StyleRegistry.php b/lib/openspout/src/Writer/Common/Manager/Style/AbstractStyleRegistry.php similarity index 66% rename from lib/openspout/src/Writer/Common/Manager/Style/StyleRegistry.php rename to lib/openspout/src/Writer/Common/Manager/Style/AbstractStyleRegistry.php index b3782a02cc1..433c6f544df 100644 --- a/lib/openspout/src/Writer/Common/Manager/Style/StyleRegistry.php +++ b/lib/openspout/src/Writer/Common/Manager/Style/AbstractStyleRegistry.php @@ -1,19 +1,21 @@ [STYLE_ID] mapping table, keeping track of the registered styles */ - protected $serializedStyleToStyleIdMappingTable = []; + /** @var array [SERIALIZED_STYLE] => [STYLE_ID] mapping table, keeping track of the registered styles */ + private array $serializedStyleToStyleIdMappingTable = []; - /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ - protected $styleIdToStyleMappingTable = []; + /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ + private array $styleIdToStyleMappingTable = []; public function __construct(Style $defaultStyle) { @@ -29,7 +31,7 @@ class StyleRegistry * * @return Style the registered style, updated with an internal ID */ - public function registerStyle(Style $style) + public function registerStyle(Style $style): Style { $serializedStyle = $this->serialize($style); @@ -47,17 +49,12 @@ class StyleRegistry /** * @return Style[] List of registered styles */ - public function getRegisteredStyles() + final public function getRegisteredStyles(): array { return array_values($this->styleIdToStyleMappingTable); } - /** - * @param int $styleId - * - * @return Style - */ - public function getStyleFromStyleId($styleId) + final public function getStyleFromStyleId(int $styleId): Style { return $this->styleIdToStyleMappingTable[$styleId]; } @@ -69,27 +66,17 @@ class StyleRegistry * * @return string The serialized style */ - public function serialize(Style $style) + final public function serialize(Style $style): string { - // In order to be able to properly compare style, set static ID value and reset registration - $currentId = $style->getId(); - $style->unmarkAsRegistered(); - - $serializedStyle = serialize($style); - - $style->markAsRegistered($currentId); - - return $serializedStyle; + return serialize($style); } /** * Returns whether the serialized style has already been registered. * * @param string $serializedStyle The serialized style - * - * @return bool */ - protected function hasSerializedStyleAlreadyBeenRegistered(string $serializedStyle) + private function hasSerializedStyleAlreadyBeenRegistered(string $serializedStyle): bool { // Using isset here because it is way faster than array_key_exists... return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); @@ -99,10 +86,8 @@ class StyleRegistry * Returns the registered style associated to the given serialization. * * @param string $serializedStyle The serialized style from which the actual style should be fetched from - * - * @return Style */ - protected function getStyleFromSerializedStyle($serializedStyle) + private function getStyleFromSerializedStyle(string $serializedStyle): Style { $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle]; diff --git a/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php b/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php index d78dd4b0b0f..c147c35aa6d 100644 --- a/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php +++ b/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php @@ -1,17 +1,18 @@ hasSetFontBold() && $baseStyle->isFontBold()) { $styleToUpdate->setFontBold(); @@ -52,7 +54,7 @@ class StyleMerger /** * @param Style $styleToUpdate Style to update (passed as reference) */ - private function mergeOtherFontProperties(Style $styleToUpdate, Style $style, Style $baseStyle) + private function mergeOtherFontProperties(Style $styleToUpdate, Style $style, Style $baseStyle): void { if (!$style->hasSetFontSize() && Style::DEFAULT_FONT_SIZE !== $baseStyle->getFontSize()) { $styleToUpdate->setFontSize($baseStyle->getFontSize()); @@ -68,10 +70,10 @@ class StyleMerger /** * @param Style $styleToUpdate Style to update (passed as reference) */ - private function mergeCellProperties(Style $styleToUpdate, Style $style, Style $baseStyle) + private function mergeCellProperties(Style $styleToUpdate, Style $style, Style $baseStyle): void { - if (!$style->hasSetWrapText() && $baseStyle->shouldWrapText()) { - $styleToUpdate->setShouldWrapText(); + if (!$style->hasSetWrapText() && $baseStyle->hasSetWrapText()) { + $styleToUpdate->setShouldWrapText($baseStyle->shouldWrapText()); } if (!$style->hasSetShrinkToFit() && $baseStyle->shouldShrinkToFit()) { $styleToUpdate->setShouldShrinkToFit(); @@ -79,14 +81,17 @@ class StyleMerger if (!$style->hasSetCellAlignment() && $baseStyle->shouldApplyCellAlignment()) { $styleToUpdate->setCellAlignment($baseStyle->getCellAlignment()); } - if (null === $style->getBorder() && $baseStyle->shouldApplyBorder()) { - $styleToUpdate->setBorder($baseStyle->getBorder()); + if (!$style->hasSetCellVerticalAlignment() && $baseStyle->shouldApplyCellVerticalAlignment()) { + $styleToUpdate->setCellVerticalAlignment($baseStyle->getCellVerticalAlignment()); } - if (null === $style->getFormat() && $baseStyle->shouldApplyFormat()) { - $styleToUpdate->setFormat($baseStyle->getFormat()); + if (null === $style->getBorder() && null !== ($border = $baseStyle->getBorder())) { + $styleToUpdate->setBorder($border); } - if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) { - $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor()); + if (null === $style->getFormat() && null !== ($format = $baseStyle->getFormat())) { + $styleToUpdate->setFormat($format); + } + if (null === $style->getBackgroundColor() && null !== ($bgColor = $baseStyle->getBackgroundColor())) { + $styleToUpdate->setBackgroundColor($bgColor); } } } diff --git a/lib/openspout/src/Writer/Common/Manager/WorkbookManagerInterface.php b/lib/openspout/src/Writer/Common/Manager/WorkbookManagerInterface.php index 209abc6dc88..13082835be3 100644 --- a/lib/openspout/src/Writer/Common/Manager/WorkbookManagerInterface.php +++ b/lib/openspout/src/Writer/Common/Manager/WorkbookManagerInterface.php @@ -1,5 +1,7 @@ getOption(Options::TEMP_FOLDER); - $zipHelper = $this->createZipHelper($entityFactory); - - return new FileSystemHelper($tempFolder, $zipHelper); - } - - /** - * @return Escaper\ODS - */ - public function createStringsEscaper() - { - return new Escaper\ODS(); - } - - /** - * @return StringHelper - */ - public function createStringHelper() - { - return new StringHelper(); - } - - /** - * @param InternalEntityFactory $entityFactory - * - * @return ZipHelper - */ - private function createZipHelper($entityFactory) - { - return new ZipHelper($entityFactory); - } -} diff --git a/lib/openspout/src/Writer/ODS/Creator/ManagerFactory.php b/lib/openspout/src/Writer/ODS/Creator/ManagerFactory.php deleted file mode 100644 index a514286de54..00000000000 --- a/lib/openspout/src/Writer/ODS/Creator/ManagerFactory.php +++ /dev/null @@ -1,107 +0,0 @@ -entityFactory = $entityFactory; - $this->helperFactory = $helperFactory; - } - - /** - * @return WorkbookManager - */ - public function createWorkbookManager(OptionsManagerInterface $optionsManager) - { - $workbook = $this->entityFactory->createWorkbook(); - - $fileSystemHelper = $this->helperFactory->createSpecificFileSystemHelper($optionsManager, $this->entityFactory); - $fileSystemHelper->createBaseFilesAndFolders(); - - $styleMerger = $this->createStyleMerger(); - $styleManager = $this->createStyleManager($optionsManager); - $worksheetManager = $this->createWorksheetManager($styleManager, $styleMerger); - - return new WorkbookManager( - $workbook, - $optionsManager, - $worksheetManager, - $styleManager, - $styleMerger, - $fileSystemHelper, - $this->entityFactory, - $this - ); - } - - /** - * @return SheetManager - */ - public function createSheetManager() - { - $stringHelper = $this->helperFactory->createStringHelper(); - - return new SheetManager($stringHelper); - } - - /** - * @return WorksheetManager - */ - private function createWorksheetManager(StyleManager $styleManager, StyleMerger $styleMerger) - { - $stringsEscaper = $this->helperFactory->createStringsEscaper(); - $stringsHelper = $this->helperFactory->createStringHelper(); - - return new WorksheetManager($styleManager, $styleMerger, $stringsEscaper, $stringsHelper); - } - - /** - * @return StyleManager - */ - private function createStyleManager(OptionsManagerInterface $optionsManager) - { - $styleRegistry = $this->createStyleRegistry($optionsManager); - - return new StyleManager($styleRegistry, $optionsManager); - } - - /** - * @return StyleRegistry - */ - private function createStyleRegistry(OptionsManagerInterface $optionsManager) - { - $defaultRowStyle = $optionsManager->getOption(Options::DEFAULT_ROW_STYLE); - - return new StyleRegistry($defaultRowStyle); - } - - /** - * @return StyleMerger - */ - private function createStyleMerger() - { - return new StyleMerger(); - } -} diff --git a/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php b/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php index caaf9544e22..f043f762079 100644 --- a/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php +++ b/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php @@ -1,5 +1,7 @@ + * + * @internal */ -class BorderHelper +final class BorderHelper { /** * Width mappings. - * - * @var array */ - protected static $widthMap = [ + public const widthMap = [ Border::WIDTH_THIN => '0.75pt', Border::WIDTH_MEDIUM => '1.75pt', Border::WIDTH_THICK => '2.5pt', @@ -32,20 +34,15 @@ class BorderHelper /** * Style mapping. - * - * @var array */ - protected static $styleMap = [ + public const styleMap = [ Border::STYLE_SOLID => 'solid', Border::STYLE_DASHED => 'dashed', Border::STYLE_DOTTED => 'dotted', Border::STYLE_DOUBLE => 'double', ]; - /** - * @return string - */ - public static function serializeBorderPart(BorderPart $borderPart) + public static function serializeBorderPart(BorderPart $borderPart): string { $definition = 'fo:border-%s="%s"'; @@ -53,8 +50,8 @@ class BorderHelper $borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none'); } else { $attributes = [ - self::$widthMap[$borderPart->getWidth()], - self::$styleMap[$borderPart->getStyle()], + self::widthMap[$borderPart->getWidth()], + self::styleMap[$borderPart->getStyle()], '#'.$borderPart->getColor(), ]; $borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes)); diff --git a/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php b/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php index b3280b6a303..12f07ac4658 100644 --- a/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php +++ b/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php @@ -1,7 +1,11 @@ baseFileSystemHelper = new CommonFileSystemHelper($baseFolderPath); + $this->baseFolderRealPath = $this->baseFileSystemHelper->getBaseFolderRealPath(); $this->zipHelper = $zipHelper; } - /** - * @return string - */ - public function getRootFolder() + public function createFolder(string $parentFolderPath, string $folderName): string + { + return $this->baseFileSystemHelper->createFolder($parentFolderPath, $folderName); + } + + public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string + { + return $this->baseFileSystemHelper->createFileWithContents($parentFolderPath, $fileName, $fileContents); + } + + public function deleteFile(string $filePath): void + { + $this->baseFileSystemHelper->deleteFile($filePath); + } + + public function deleteFolderRecursively(string $folderPath): void + { + $this->baseFileSystemHelper->deleteFolderRecursively($folderPath); + } + + public function getRootFolder(): string { return $this->rootFolder; } - /** - * @return string - */ - public function getSheetsContentTempFolder() + public function getSheetsContentTempFolder(): string { return $this->sheetsContentTempFolder; } @@ -69,7 +89,7 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * * @throws \OpenSpout\Common\Exception\IOException If unable to create at least one of the base folders */ - public function createBaseFilesAndFolders() + public function createBaseFilesAndFolders(): void { $this ->createRootFolder() @@ -83,13 +103,9 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme /** * Creates the "content.xml" file under the root folder. * - * @param WorksheetManager $worksheetManager - * @param StyleManager $styleManager - * @param Worksheet[] $worksheets - * - * @return FileSystemHelper + * @param Worksheet[] $worksheets */ - public function createContentFile($worksheetManager, $styleManager, $worksheets) + public function createContentFile(WorksheetManager $worksheetManager, StyleManager $styleManager, array $worksheets): self { $contentXmlFileContents = <<<'EOD' @@ -101,11 +117,20 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme $contentXmlFileContents .= ''; - $this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents); + $topContentTempFile = uniqid(self::CONTENT_XML_FILE_NAME); + $this->createFileWithContents($this->rootFolder, $topContentTempFile, $contentXmlFileContents); // Append sheets content to "content.xml" - $contentXmlFilePath = $this->rootFolder.'/'.self::CONTENT_XML_FILE_NAME; - $contentXmlHandle = fopen($contentXmlFilePath, 'a'); + $contentXmlFilePath = $this->rootFolder.\DIRECTORY_SEPARATOR.self::CONTENT_XML_FILE_NAME; + $contentXmlHandle = fopen($contentXmlFilePath, 'w'); + \assert(false !== $contentXmlHandle); + + $topContentTempPathname = $this->rootFolder.\DIRECTORY_SEPARATOR.$topContentTempFile; + $topContentTempHandle = fopen($topContentTempPathname, 'r'); + \assert(false !== $topContentTempHandle); + stream_copy_to_stream($topContentTempHandle, $contentXmlHandle); + fclose($topContentTempHandle); + unlink($topContentTempPathname); foreach ($worksheets as $worksheet) { // write the "" node, with the final sheet's name @@ -117,6 +142,17 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme fwrite($contentXmlHandle, ''); } + // add AutoFilter + $databaseRanges = ''; + foreach ($worksheets as $worksheet) { + $databaseRanges .= $worksheetManager->getTableDatabaseRangeElementAsString($worksheet); + } + if ('' !== $databaseRanges) { + fwrite($contentXmlHandle, ''); + fwrite($contentXmlHandle, $databaseRanges); + fwrite($contentXmlHandle, ''); + } + $contentXmlFileContents = ''; fwrite($contentXmlHandle, $contentXmlFileContents); @@ -127,10 +163,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme /** * Deletes the temporary folder where sheets content was stored. - * - * @return FileSystemHelper */ - public function deleteWorksheetTempFolder() + public function deleteWorksheetTempFolder(): self { $this->deleteFolderRecursively($this->sheetsContentTempFolder); @@ -140,12 +174,9 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme /** * Creates the "styles.xml" file under the root folder. * - * @param StyleManager $styleManager - * @param int $numWorksheets Number of created worksheets - * - * @return FileSystemHelper + * @param int $numWorksheets Number of created worksheets */ - public function createStylesFile($styleManager, $numWorksheets) + public function createStylesFile(StyleManager $styleManager, int $numWorksheets): self { $stylesXmlFileContents = $styleManager->getStylesXMLFileContent($numWorksheets); $this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); @@ -158,7 +189,7 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * * @param resource $streamPointer Pointer to the stream to copy the zip */ - public function zipRootFolderAndCopyToStream($streamPointer) + public function zipRootFolderAndCopyToStream($streamPointer): void { $zip = $this->zipHelper->createZip($this->rootFolder); @@ -180,10 +211,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the folder that will be used as root. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder - * - * @return FileSystemHelper */ - protected function createRootFolder() + private function createRootFolder(): self { $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods')); @@ -194,10 +223,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file - * - * @return FileSystemHelper */ - protected function createMetaInfoFolderAndFile() + private function createMetaInfoFolderAndFile(): self { $this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME); @@ -210,10 +237,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "manifest.xml" file under the "META-INF" folder (under root). * * @throws \OpenSpout\Common\Exception\IOException If unable to create the file - * - * @return FileSystemHelper */ - protected function createManifestFile() + private function createManifestFile(): self { $manifestXmlFileContents = <<<'EOD' @@ -235,12 +260,10 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * This folder is not part of the final ODS file and is only used to be able to jump between sheets. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder - * - * @return FileSystemHelper */ - protected function createSheetsContentTempFolder() + private function createSheetsContentTempFolder(): self { - $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME); + $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, 'worksheets-temp'); return $this; } @@ -249,13 +272,11 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "meta.xml" file under the root folder. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the file - * - * @return FileSystemHelper */ - protected function createMetaFile() + private function createMetaFile(): self { $appName = self::APP_NAME; - $createdDate = (new \DateTime())->format(\DateTime::W3C); + $createdDate = (new DateTimeImmutable())->format(DateTimeImmutable::W3C); $metaXmlFileContents = << @@ -277,10 +298,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "mimetype" file under the root folder. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the file - * - * @return FileSystemHelper */ - protected function createMimetypeFile() + private function createMimetypeFile(): self { $this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE); @@ -295,9 +314,10 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * @param string $sourceFilePath Path of the file whose content will be copied * @param resource $targetResource Target resource that will receive the content */ - protected function copyFileContentsToTarget($sourceFilePath, $targetResource) + private function copyFileContentsToTarget(string $sourceFilePath, $targetResource): void { $sourceHandle = fopen($sourceFilePath, 'r'); + \assert(false !== $sourceHandle); stream_copy_to_stream($sourceHandle, $targetResource); fclose($sourceHandle); } diff --git a/lib/openspout/src/Writer/ODS/Manager/OptionsManager.php b/lib/openspout/src/Writer/ODS/Manager/OptionsManager.php deleted file mode 100644 index d098abf96b1..00000000000 --- a/lib/openspout/src/Writer/ODS/Manager/OptionsManager.php +++ /dev/null @@ -1,50 +0,0 @@ -styleBuilder = $styleBuilder; - parent::__construct(); - } - - /** - * {@inheritdoc} - */ - protected function getSupportedOptions() - { - return [ - Options::TEMP_FOLDER, - Options::DEFAULT_ROW_STYLE, - Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, - Options::DEFAULT_COLUMN_WIDTH, - Options::DEFAULT_ROW_HEIGHT, - Options::COLUMN_WIDTHS, - ]; - } - - /** - * {@inheritdoc} - */ - protected function setDefaultOptions() - { - $this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir()); - $this->setOption(Options::DEFAULT_ROW_STYLE, $this->styleBuilder->build()); - $this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true); - } -} diff --git a/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php b/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php index e1d99764b6f..967d33dea72 100644 --- a/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php +++ b/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php @@ -1,41 +1,41 @@ setDefaultColumnWidth($optionsManager->getOption(Options::DEFAULT_COLUMN_WIDTH)); - $this->setDefaultRowHeight($optionsManager->getOption(Options::DEFAULT_ROW_HEIGHT)); - $this->columnWidths = $optionsManager->getOption(Options::COLUMN_WIDTHS) ?? []; + $this->options = $options; } /** * Returns the content of the "styles.xml" file, given a list of styles. * * @param int $numWorksheets Number of worksheets created - * - * @return string */ - public function getStylesXMLFileContent($numWorksheets) + public function getStylesXMLFileContent(int $numWorksheets): string { $content = <<<'EOD' @@ -56,10 +56,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the contents of the "" section, inside "content.xml" file. - * - * @return string */ - public function getContentXmlFontFaceSectionContent() + public function getContentXmlFontFaceSectionContent(): string { $content = ''; foreach ($this->styleRegistry->getUsedFonts() as $fontName) { @@ -74,10 +72,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager * Returns the contents of the "" section, inside "content.xml" file. * * @param Worksheet[] $worksheets - * - * @return string */ - public function getContentXmlAutomaticStylesSectionContent($worksheets) + public function getContentXmlAutomaticStylesSectionContent(array $worksheets): string { $content = ''; @@ -85,9 +81,9 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager $content .= $this->getStyleSectionContent($style); } - $useOptimalRowHeight = empty($this->defaultRowHeight) ? 'true' : 'false'; - $defaultRowHeight = empty($this->defaultRowHeight) ? '15pt' : "{$this->defaultRowHeight}pt"; - $defaultColumnWidth = empty($this->defaultColumnWidth) ? '' : "style:column-width=\"{$this->defaultColumnWidth}pt\""; + $useOptimalRowHeight = null === $this->options->DEFAULT_ROW_HEIGHT ? 'true' : 'false'; + $defaultRowHeight = null === $this->options->DEFAULT_ROW_HEIGHT ? '15pt' : "{$this->options->DEFAULT_ROW_HEIGHT}pt"; + $defaultColumnWidth = null === $this->options->DEFAULT_COLUMN_WIDTH ? '' : "style:column-width=\"{$this->options->DEFAULT_COLUMN_WIDTH}pt\""; $content .= << @@ -110,12 +106,9 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager } // Sort column widths since ODS cares about order - usort($this->columnWidths, function ($a, $b) { - if ($a[0] === $b[0]) { - return 0; - } - - return ($a[0] < $b[0]) ? -1 : 1; + $columnWidths = $this->options->getColumnWidths(); + usort($columnWidths, static function (ColumnWidth $a, ColumnWidth $b): int { + return $a->start <=> $b->start; }); $content .= $this->getTableColumnStylesXMLContent(); @@ -126,15 +119,15 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager public function getTableColumnStylesXMLContent(): string { - if (empty($this->columnWidths)) { + if ([] === $this->options->getColumnWidths()) { return ''; } $content = ''; - foreach ($this->columnWidths as $styleIndex => $entry) { + foreach ($this->options->getColumnWidths() as $styleIndex => $columnWidth) { $content .= << - + EOD; } @@ -144,46 +137,29 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager public function getStyledTableColumnXMLContent(int $maxNumColumns): string { - if (empty($this->columnWidths)) { + if ([] === $this->options->getColumnWidths()) { return ''; } $content = ''; - foreach ($this->columnWidths as $styleIndex => $entry) { - $numCols = $entry[1] - $entry[0] + 1; + foreach ($this->options->getColumnWidths() as $styleIndex => $columnWidth) { + $numCols = $columnWidth->end - $columnWidth->start + 1; $content .= << EOD; } + \assert(isset($columnWidth)); // Note: This assumes the column widths are contiguous and default width is // only applied to columns after the last custom column with a custom width - $content .= ''; - - return $content; - } - - /** - * Returns the content of the "" section, inside "styles.xml" file. - * - * @return string - */ - protected function getFontFaceSectionContent() - { - $content = ''; - foreach ($this->styleRegistry->getUsedFonts() as $fontName) { - $content .= ''; - } - $content .= ''; + $content .= ''; return $content; } /** * Returns the content of the "" section, inside "styles.xml" file. - * - * @return string */ - protected function getStylesSectionContent() + private function getStylesSectionContent(): string { $defaultStyle = $this->getDefaultStyle(); @@ -202,40 +178,12 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager EOD; } - /** - * Returns the content of the "" section, inside "styles.xml" file. - * - * @param int $numWorksheets Number of worksheets created - * - * @return string - */ - protected function getAutomaticStylesSectionContent($numWorksheets) - { - $content = ''; - - for ($i = 1; $i <= $numWorksheets; ++$i) { - $content .= << - - - - - EOD; - } - - $content .= ''; - - return $content; - } - /** * Returns the content of the "" section, inside "styles.xml" file. * * @param int $numWorksheets Number of worksheets created - * - * @return string */ - protected function getMasterStylesSectionContent($numWorksheets) + private function getMasterStylesSectionContent(int $numWorksheets): string { $content = ''; @@ -256,13 +204,47 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager } /** - * Returns the contents of the "" section, inside "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string + * Returns the content of the "" section, inside "styles.xml" file. */ - protected function getStyleSectionContent($style) + private function getFontFaceSectionContent(): string + { + $content = ''; + foreach ($this->styleRegistry->getUsedFonts() as $fontName) { + $content .= ''; + } + $content .= ''; + + return $content; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @param int $numWorksheets Number of worksheets created + */ + private function getAutomaticStylesSectionContent(int $numWorksheets): string + { + $content = ''; + + for ($i = 1; $i <= $numWorksheets; ++$i) { + $content .= << + + + + + EOD; + } + + $content .= ''; + + return $content; + } + + /** + * Returns the contents of the "" section, inside "" section. + */ + private function getStyleSectionContent(Style $style): string { $styleIndex = $style->getId() + 1; // 1-based @@ -279,12 +261,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the contents of the "" section, inside "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string */ - private function getTextPropertiesSectionContent($style) + private function getTextPropertiesSectionContent(Style $style): string { if (!$style->shouldApplyFont()) { return ''; @@ -297,12 +275,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the contents of the fonts definition section, inside "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string */ - private function getFontSectionContent($style) + private function getFontSectionContent(Style $style): string { $defaultStyle = $this->getDefaultStyle(); $content = ''; @@ -340,81 +314,91 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the contents of the "" section, inside "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string */ - private function getParagraphPropertiesSectionContent($style) + private function getParagraphPropertiesSectionContent(Style $style): string { - if (!$style->shouldApplyCellAlignment()) { + if (!$style->shouldApplyCellAlignment() && !$style->shouldApplyCellVerticalAlignment()) { return ''; } return 'getCellAlignmentSectionContent($style) + .$this->getCellVerticalAlignmentSectionContent($style) .'/>'; } /** * Returns the contents of the cell alignment definition for the "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string */ - private function getCellAlignmentSectionContent($style) + private function getCellAlignmentSectionContent(Style $style): string { + if (!$style->hasSetCellAlignment()) { + return ''; + } + return sprintf( ' fo:text-align="%s" ', $this->transformCellAlignment($style->getCellAlignment()) ); } + /** + * Returns the contents of the cell vertical alignment definition for the "" section. + */ + private function getCellVerticalAlignmentSectionContent(Style $style): string + { + if (!$style->hasSetCellVerticalAlignment()) { + return ''; + } + + return sprintf( + ' fo:vertical-align="%s" ', + $this->transformCellVerticalAlignment($style->getCellVerticalAlignment()) + ); + } + /** * Even though "left" and "right" alignments are part of the spec, and interpreted * respectively as "start" and "end", using the recommended values increase compatibility * with software that will read the created ODS file. - * - * @param string $cellAlignment - * - * @return string */ - private function transformCellAlignment($cellAlignment) + private function transformCellAlignment(string $cellAlignment): string { - switch ($cellAlignment) { - case CellAlignment::LEFT: - return 'start'; + return match ($cellAlignment) { + CellAlignment::LEFT => 'start', + CellAlignment::RIGHT => 'end', + default => $cellAlignment, + }; + } - case CellAlignment::RIGHT: - return 'end'; - - default: - return $cellAlignment; - } + /** + * Spec uses 'middle' rather than 'center' + * http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1420236_253892949. + */ + private function transformCellVerticalAlignment(string $cellVerticalAlignment): string + { + return (CellVerticalAlignment::CENTER === $cellVerticalAlignment) + ? 'middle' + : $cellVerticalAlignment; } /** * Returns the contents of the "" section, inside "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string */ - private function getTableCellPropertiesSectionContent($style) + private function getTableCellPropertiesSectionContent(Style $style): string { $content = 'shouldWrapText()) { - $content .= $this->getWrapTextXMLContent(); + if ($style->hasSetWrapText()) { + $content .= $this->getWrapTextXMLContent($style->shouldWrapText()); } - if ($style->shouldApplyBorder()) { - $content .= $this->getBorderXMLContent($style); + if (null !== ($border = $style->getBorder())) { + $content .= $this->getBorderXMLContent($border); } - if ($style->shouldApplyBackgroundColor()) { - $content .= $this->getBackgroundColorXMLContent($style); + if (null !== ($bgColor = $style->getBackgroundColor())) { + $content .= $this->getBackgroundColorXMLContent($bgColor); } $content .= '/>'; @@ -424,39 +408,29 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the contents of the wrap text definition for the "" section. - * - * @return string */ - private function getWrapTextXMLContent() + private function getWrapTextXMLContent(bool $shouldWrapText): string { - return ' fo:wrap-option="wrap" style:vertical-align="automatic" '; + return ' fo:wrap-option="'.($shouldWrapText ? '' : 'no-').'wrap" style:vertical-align="automatic" '; } /** * Returns the contents of the borders definition for the "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string */ - private function getBorderXMLContent($style) + private function getBorderXMLContent(Border $border): string { - $borders = array_map(function (BorderPart $borderPart) { + $borders = array_map(static function (BorderPart $borderPart) { return BorderHelper::serializeBorderPart($borderPart); - }, $style->getBorder()->getParts()); + }, $border->getParts()); return sprintf(' %s ', implode(' ', $borders)); } /** * Returns the contents of the background color definition for the "" section. - * - * @param \OpenSpout\Common\Entity\Style\Style $style - * - * @return string */ - private function getBackgroundColorXMLContent($style) + private function getBackgroundColorXMLContent(string $bgColor): string { - return sprintf(' fo:background-color="#%s" ', $style->getBackgroundColor()); + return sprintf(' fo:background-color="#%s" ', $bgColor); } } diff --git a/lib/openspout/src/Writer/ODS/Manager/Style/StyleRegistry.php b/lib/openspout/src/Writer/ODS/Manager/Style/StyleRegistry.php index e90dc96b986..cdba6088c29 100644 --- a/lib/openspout/src/Writer/ODS/Manager/Style/StyleRegistry.php +++ b/lib/openspout/src/Writer/ODS/Manager/Style/StyleRegistry.php @@ -1,16 +1,19 @@ [] Map whose keys contain all the fonts used */ - protected $usedFontsSet = []; + /** @var array [FONT_NAME] => [] Map whose keys contain all the fonts used */ + private array $usedFontsSet = []; /** * Registers the given style as a used style. @@ -20,7 +23,7 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry * * @return Style the registered style, updated with an internal ID */ - public function registerStyle(Style $style) + public function registerStyle(Style $style): Style { if ($style->isRegistered()) { return $style; @@ -35,7 +38,7 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry /** * @return string[] List of used fonts name */ - public function getUsedFonts() + public function getUsedFonts(): array { return array_keys($this->usedFontsSet); } diff --git a/lib/openspout/src/Writer/ODS/Manager/WorkbookManager.php b/lib/openspout/src/Writer/ODS/Manager/WorkbookManager.php index 90dc0e4ace7..0da68f458f0 100644 --- a/lib/openspout/src/Writer/ODS/Manager/WorkbookManager.php +++ b/lib/openspout/src/Writer/ODS/Manager/WorkbookManager.php @@ -1,47 +1,55 @@ fileSystemHelper->getSheetsContentTempFolder(); - - return $sheetsContentTempFolder.'/sheet'.$sheet->getIndex().'.xml'; + public function __construct( + Workbook $workbook, + Options $options, + WorksheetManager $worksheetManager, + StyleManager $styleManager, + StyleMerger $styleMerger, + FileSystemHelper $fileSystemHelper + ) { + parent::__construct( + $workbook, + $options, + $worksheetManager, + $styleManager, + $styleMerger, + $fileSystemHelper + ); } /** * @return int Maximum number of rows/columns a sheet can contain */ - protected function getMaxRowsPerWorksheet() + protected function getMaxRowsPerWorksheet(): int { return self::$maxRowsPerWorksheet; } @@ -51,7 +59,7 @@ class WorkbookManager extends WorkbookManagerAbstract * * @param resource $finalFilePointer Pointer to the spreadsheet that will be created */ - protected function writeAllFilesToDiskAndZipThem($finalFilePointer) + protected function writeAllFilesToDiskAndZipThem($finalFilePointer): void { $worksheets = $this->getWorksheets(); $numWorksheets = \count($worksheets); diff --git a/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php b/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php index 3eeca5f2728..c77f72f43bf 100644 --- a/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php +++ b/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php @@ -1,36 +1,37 @@ styleManager = $styleManager; $this->styleMerger = $styleMerger; $this->stringsEscaper = $stringsEscaper; - $this->stringHelper = $stringHelper; } /** @@ -54,10 +53,10 @@ class WorksheetManager implements WorksheetManagerInterface * * @throws \OpenSpout\Common\Exception\IOException If the sheet data file cannot be opened for writing */ - public function startSheet(Worksheet $worksheet) + public function startSheet(Worksheet $worksheet): void { $sheetFilePointer = fopen($worksheet->getFilePath(), 'w'); - $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer); + \assert(false !== $sheetFilePointer); $worksheet->setFilePointer($sheetFilePointer); } @@ -67,7 +66,7 @@ class WorksheetManager implements WorksheetManagerInterface * * @return string "" node as string */ - public function getTableElementStartAsString(Worksheet $worksheet) + public function getTableElementStartAsString(Worksheet $worksheet): string { $externalSheet = $worksheet->getExternalSheet(); $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName()); @@ -79,6 +78,31 @@ class WorksheetManager implements WorksheetManagerInterface return $tableElement; } + /** + * Returns the table:database-range XML node for AutoFilter as string. + */ + public function getTableDatabaseRangeElementAsString(Worksheet $worksheet): string + { + $externalSheet = $worksheet->getExternalSheet(); + $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName()); + $databaseRange = ''; + + if (null !== $autofilter = $externalSheet->getAutoFilter()) { + $rangeAddress = sprintf( + '\'%s\'.%s%s:\'%s\'.%s%s', + $escapedSheetName, + CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex), + $autofilter->fromRow, + $escapedSheetName, + CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex), + $autofilter->toRow + ); + $databaseRange = ''; + } + + return $databaseRange; + } + /** * Adds a row to the given worksheet. * @@ -88,7 +112,7 @@ class WorksheetManager implements WorksheetManagerInterface * @throws InvalidArgumentException If a cell value's type is not supported * @throws IOException If the data cannot be written */ - public function addRow(Worksheet $worksheet, Row $row) + public function addRow(Worksheet $worksheet, Row $row): void { $cells = $row->getCells(); $rowStyle = $row->getStyle(); @@ -101,6 +125,7 @@ class WorksheetManager implements WorksheetManagerInterface for ($i = 0; $i < $row->getNumCells(); ++$i) { /** @var Cell $cell */ $cell = $cells[$currentCellIndex]; + /** @var null|Cell $nextCell */ $nextCell = $cells[$nextCellIndex] ?? null; @@ -133,63 +158,9 @@ class WorksheetManager implements WorksheetManagerInterface /** * Closes the worksheet. */ - public function close(Worksheet $worksheet) + public function close(Worksheet $worksheet): void { - $worksheetFilePointer = $worksheet->getFilePointer(); - - if (!\is_resource($worksheetFilePointer)) { - return; - } - - fclose($worksheetFilePointer); - } - - /** - * @param null|float $width - */ - public function setDefaultColumnWidth($width) - { - $this->styleManager->setDefaultColumnWidth($width); - } - - /** - * @param null|float $height - */ - public function setDefaultRowHeight($height) - { - $this->styleManager->setDefaultRowHeight($height); - } - - /** - * @param int ...$columns One or more columns with this width - */ - public function setColumnWidth(float $width, ...$columns) - { - $this->styleManager->setColumnWidth($width, ...$columns); - } - - /** - * @param float $width The width to set - * @param int $start First column index of the range - * @param int $end Last column index of the range - */ - public function setColumnWidthForRange(float $width, int $start, int $end) - { - $this->styleManager->setColumnWidthForRange($width, $start, $end); - } - - /** - * Checks if the sheet has been sucessfully created. Throws an exception if not. - * - * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file - * - * @throws IOException If the sheet data file cannot be opened for writing - */ - private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer) - { - if (!$sheetFilePointer) { - throw new IOException('Unable to open sheet for writing.'); - } + fclose($worksheet->getFilePointer()); } /** @@ -244,11 +215,11 @@ class WorksheetManager implements WorksheetManagerInterface * @param int $styleIndex Index of the used style * @param int $numTimesValueRepeated Number of times the value is consecutively repeated * - * @throws InvalidArgumentException If a cell value's type is not supported - * * @return string The cell XML content + * + * @throws InvalidArgumentException If a cell value's type is not supported */ - private function getCellXML(Cell $cell, $styleIndex, $numTimesValueRepeated) + private function getCellXML(Cell $cell, int $styleIndex, int $numTimesValueRepeated): string { $data = 'isString()) { + if ($cell instanceof Cell\StringCell) { $data .= ' office:value-type="string" calcext:value-type="string">'; $cellValueLines = explode("\n", $cell->getValue()); @@ -265,42 +236,36 @@ class WorksheetManager implements WorksheetManagerInterface } $data .= ''; - } elseif ($cell->isBoolean()) { + } elseif ($cell instanceof Cell\BooleanCell) { $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 .= ''; - } elseif ($cell->isNumeric()) { - $cellValue = $this->stringHelper->formatNumericValue($cell->getValue()); + } elseif ($cell instanceof Cell\NumericCell) { + $cellValue = $cell->getValue(); $data .= ' office:value-type="float" calcext:value-type="float" office:value="'.$cellValue.'">'; $data .= ''.$cellValue.''; $data .= ''; - } elseif ($cell->isDate()) { - $value = $cell->getValue(); - if ($value instanceof \DateTimeInterface) { - $datevalue = substr((new \DateTimeImmutable('@'.$value->getTimestamp()))->format(\DateTimeInterface::W3C), 0, -6); - $data .= ' office:value-type="date" calcext:value-type="date" office:date-value="'.$datevalue.'Z">'; - $data .= ''.$datevalue.'Z'; - } elseif ($value instanceof \DateInterval) { - // workaround for missing DateInterval::format('c'), see https://stackoverflow.com/a/61088115/53538 - static $f = ['M0S', 'H0M', 'DT0H', 'M0D', 'Y0M', 'P0Y', 'Y0M', 'P0M']; - static $r = ['M', 'H', 'DT', 'M', 'Y0M', 'P', 'Y', 'P']; - $value = rtrim(str_replace($f, $r, $value->format('P%yY%mM%dDT%hH%iM%sS')), 'PT') ?: 'PT0S'; - $data .= ' office:value-type="time" office:time-value="'.$value.'">'; - $data .= ''.$value.''; - } else { - throw new InvalidArgumentException('Trying to add a date value with an unsupported type: '.\gettype($cell->getValue())); - } + } elseif ($cell instanceof Cell\DateTimeCell) { + $datevalue = substr((new DateTimeImmutable('@'.$cell->getValue()->getTimestamp()))->format(DateTimeInterface::W3C), 0, -6); + $data .= ' office:value-type="date" calcext:value-type="date" office:date-value="'.$datevalue.'Z">'; + $data .= ''.$datevalue.'Z'; $data .= ''; - } elseif ($cell->isError() && \is_string($cell->getValueEvenIfError())) { + } elseif ($cell instanceof Cell\DateIntervalCell) { + // workaround for missing DateInterval::format('c'), see https://stackoverflow.com/a/61088115/53538 + static $f = ['M0S', 'H0M', 'DT0H', 'M0D', 'Y0M', 'P0Y', 'Y0M', 'P0M']; + static $r = ['M', 'H', 'DT', 'M', 'Y0M', 'P', 'Y', 'P']; + $value = rtrim(str_replace($f, $r, $cell->getValue()->format('P%yY%mM%dDT%hH%iM%sS')), 'PT') ?: 'PT0S'; + $data .= ' office:value-type="time" office:time-value="'.$value.'">'; + $data .= ''.$value.''; + $data .= ''; + } elseif ($cell instanceof Cell\ErrorCell) { // only writes the error value if it's a string $data .= ' office:value-type="string" calcext:value-type="error" office:value="">'; - $data .= ''.$cell->getValueEvenIfError().''; + $data .= ''.$cell->getRawValue().''; $data .= ''; - } elseif ($cell->isEmpty()) { + } elseif ($cell instanceof Cell\EmptyCell) { $data .= '/>'; - } else { - throw new InvalidArgumentException('Trying to add a value with an unsupported type: '.\gettype($cell->getValue())); } return $data; diff --git a/lib/openspout/src/Writer/ODS/Options.php b/lib/openspout/src/Writer/ODS/Options.php new file mode 100644 index 00000000000..547ee2da805 --- /dev/null +++ b/lib/openspout/src/Writer/ODS/Options.php @@ -0,0 +1,11 @@ +throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + $this->options = $options ?? new Options(); + } - $this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder); + public function getOptions(): Options + { + return $this->options; + } - return $this; + protected function createWorkbookManager(): WorkbookManager + { + $workbook = new Workbook(); + + $fileSystemHelper = new FileSystemHelper($this->options->getTempFolder(), new ZipHelper()); + $fileSystemHelper->createBaseFilesAndFolders(); + + $styleMerger = new StyleMerger(); + $styleManager = new StyleManager(new StyleRegistry($this->options->DEFAULT_ROW_STYLE), $this->options); + $worksheetManager = new WorksheetManager($styleManager, $styleMerger, new ODS()); + + return new WorkbookManager( + $workbook, + $this->options, + $worksheetManager, + $styleManager, + $styleMerger, + $fileSystemHelper + ); } } diff --git a/lib/openspout/src/Writer/WriterAbstract.php b/lib/openspout/src/Writer/WriterAbstract.php deleted file mode 100644 index fd75617e2e0..00000000000 --- a/lib/openspout/src/Writer/WriterAbstract.php +++ /dev/null @@ -1,253 +0,0 @@ -optionsManager = $optionsManager; - $this->globalFunctionsHelper = $globalFunctionsHelper; - $this->helperFactory = $helperFactory; - } - - /** - * {@inheritdoc} - */ - public function setDefaultRowStyle(Style $defaultStyle) - { - $this->optionsManager->setOption(Options::DEFAULT_ROW_STYLE, $defaultStyle); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function openToFile($outputFilePath) - { - $this->outputFilePath = $outputFilePath; - - $this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+'); - $this->throwIfFilePointerIsNotAvailable(); - - $this->openWriter(); - $this->isWriterOpened = true; - - return $this; - } - - /** - * @codeCoverageIgnore - * {@inheritdoc} - */ - public function openToBrowser($outputFileName) - { - $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName); - - $this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w'); - $this->throwIfFilePointerIsNotAvailable(); - - // Clear any previous output (otherwise the generated file will be corrupted) - // @see https://github.com/box/spout/issues/241 - $this->globalFunctionsHelper->ob_end_clean(); - - /* - * 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="'.rawurlencode($this->outputFilePath).'"; '. - 'filename*=UTF-8\'\''.rawurlencode($this->outputFilePath) - ); - - /* - * When forcing the download of a file over SSL,IE8 and lower browsers fail - * if the Cache-Control and Pragma headers are not set. - * - * @see http://support.microsoft.com/KB/323308 - * @see https://github.com/liuggio/ExcelBundle/issues/45 - */ - $this->globalFunctionsHelper->header('Cache-Control: max-age=0'); - $this->globalFunctionsHelper->header('Pragma: public'); - - $this->openWriter(); - $this->isWriterOpened = true; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function addRow(Row $row) - { - if ($this->isWriterOpened) { - try { - $this->addRowToWriter($row); - } catch (SpoutException $e) { - // if an exception occurs while writing data, - // close the writer and remove all files created so far. - $this->closeAndAttemptToCleanupAllFiles(); - - // re-throw the exception to alert developers of the error - throw $e; - } - } else { - throw new WriterNotOpenedException('The writer needs to be opened before adding row.'); - } - - return $this; - } - - /** - * {@inheritdoc} - */ - public function addRows(array $rows) - { - foreach ($rows as $row) { - if (!$row instanceof Row) { - $this->closeAndAttemptToCleanupAllFiles(); - - throw new InvalidArgumentException('The input should be an array of Row'); - } - - $this->addRow($row); - } - - return $this; - } - - /** - * {@inheritdoc} - */ - public function close() - { - if (!$this->isWriterOpened) { - return; - } - - $this->closeWriter(); - - if (\is_resource($this->filePointer)) { - $this->globalFunctionsHelper->fclose($this->filePointer); - } - - $this->isWriterOpened = false; - } - - /** - * Opens the streamer and makes it ready to accept data. - * - * @throws IOException If the writer cannot be opened - */ - abstract protected function openWriter(); - - /** - * Adds a row to the currently opened writer. - * - * @param Row $row The row containing cells and styles - * - * @throws WriterNotOpenedException If the workbook is not created yet - * @throws IOException If unable to write data - */ - abstract protected function addRowToWriter(Row $row); - - /** - * Closes the streamer, preventing any additional writing. - */ - abstract protected function closeWriter(); - - /** - * Checks if the pointer to the file/stream to write to is available. - * Will throw an exception if not available. - * - * @throws IOException If the pointer is not available - */ - protected function throwIfFilePointerIsNotAvailable() - { - if (!\is_resource($this->filePointer)) { - throw new IOException('File pointer has not be opened'); - } - } - - /** - * Checks if the writer has already been opened, since some actions must be done before it gets opened. - * Throws an exception if already opened. - * - * @param string $message Error message - * - * @throws WriterAlreadyOpenedException if the writer was already opened and must not be - */ - protected function throwIfWriterAlreadyOpened($message) - { - if ($this->isWriterOpened) { - throw new WriterAlreadyOpenedException($message); - } - } - - /** - * Closes the writer and attempts to cleanup all files that were - * created during the writing process (temp files & final file). - */ - private function closeAndAttemptToCleanupAllFiles() - { - // close the writer, which should remove all temp files - $this->close(); - - // remove output file if it was created - if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) { - $outputFolderPath = \dirname($this->outputFilePath); - $fileSystemHelper = $this->helperFactory->createFileSystemHelper($outputFolderPath); - $fileSystemHelper->deleteFile($this->outputFilePath); - } - } -} diff --git a/lib/openspout/src/Writer/WriterInterface.php b/lib/openspout/src/Writer/WriterInterface.php index 71a298e6a73..ac43e33e4e8 100644 --- a/lib/openspout/src/Writer/WriterInterface.php +++ b/lib/openspout/src/Writer/WriterInterface.php @@ -1,13 +1,11 @@ managerFactory = $managerFactory; - } - - /** - * Sets whether new sheets should be automatically created when the max rows limit per sheet is reached. - * This must be set before opening the writer. - * - * @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached - * - * @throws WriterAlreadyOpenedException If the writer was already opened - * - * @return WriterMultiSheetsAbstract - */ - public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically) - { - $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - - $this->optionsManager->setOption( - Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, - $shouldCreateNewSheetsAutomatically - ); - - return $this; - } - - /** - * Returns all the workbook's sheets. - * - * @throws WriterNotOpenedException If the writer has not been opened yet - * - * @return Sheet[] All the workbook's sheets - */ - public function getSheets() - { - $this->throwIfWorkbookIsNotAvailable(); - - $externalSheets = []; - $worksheets = $this->workbookManager->getWorksheets(); - - foreach ($worksheets as $worksheet) { - $externalSheets[] = $worksheet->getExternalSheet(); - } - - return $externalSheets; - } - - /** - * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. - * - * @throws IOException - * @throws WriterNotOpenedException If the writer has not been opened yet - * - * @return Sheet The created sheet - */ - public function addNewSheetAndMakeItCurrent() - { - $this->throwIfWorkbookIsNotAvailable(); - $worksheet = $this->workbookManager->addNewSheetAndMakeItCurrent(); - - return $worksheet->getExternalSheet(); - } - - /** - * Returns the current sheet. - * - * @throws WriterNotOpenedException If the writer has not been opened yet - * - * @return Sheet The current sheet - */ - public function getCurrentSheet() - { - $this->throwIfWorkbookIsNotAvailable(); - - return $this->workbookManager->getCurrentWorksheet()->getExternalSheet(); - } - - /** - * Sets the given sheet as the current one. New data will be written to this sheet. - * The writing will resume where it stopped (i.e. data won't be truncated). - * - * @param Sheet $sheet The sheet to set as current - * - * @throws SheetNotFoundException If the given sheet does not exist in the workbook - * @throws WriterNotOpenedException If the writer has not been opened yet - */ - public function setCurrentSheet($sheet) - { - $this->throwIfWorkbookIsNotAvailable(); - $this->workbookManager->setCurrentSheet($sheet); - } - - /** - * @throws WriterAlreadyOpenedException - */ - public function setDefaultColumnWidth(float $width) - { - $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - $this->optionsManager->setOption( - Options::DEFAULT_COLUMN_WIDTH, - $width - ); - } - - /** - * @throws WriterAlreadyOpenedException - */ - public function setDefaultRowHeight(float $height) - { - $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - $this->optionsManager->setOption( - Options::DEFAULT_ROW_HEIGHT, - $height - ); - } - - /** - * @param null|float $width - * @param int ...$columns One or more columns with this width - * - * @throws WriterNotOpenedException - */ - public function setColumnWidth($width, ...$columns) - { - $this->throwIfWorkbookIsNotAvailable(); - $this->workbookManager->setColumnWidth($width, ...$columns); - } - - /** - * @param float $width The width to set - * @param int $start First column index of the range - * @param int $end Last column index of the range - * - * @throws WriterNotOpenedException - */ - public function setColumnWidthForRange(float $width, int $start, int $end) - { - $this->throwIfWorkbookIsNotAvailable(); - $this->workbookManager->setColumnWidthForRange($width, $start, $end); - } - - /** - * {@inheritdoc} - */ - protected function openWriter() - { - if (null === $this->workbookManager) { - $this->workbookManager = $this->managerFactory->createWorkbookManager($this->optionsManager); - $this->workbookManager->addNewSheetAndMakeItCurrent(); - } - } - - /** - * Checks if the workbook has been created. Throws an exception if not created yet. - * - * @throws WriterNotOpenedException If the workbook is not created yet - */ - protected function throwIfWorkbookIsNotAvailable() - { - if (!$this->workbookManager->getWorkbook()) { - throw new WriterNotOpenedException('The writer must be opened before performing this action.'); - } - } - - /** - * {@inheritdoc} - * - * @throws Exception\WriterException - */ - protected function addRowToWriter(Row $row) - { - $this->throwIfWorkbookIsNotAvailable(); - $this->workbookManager->addRowToCurrentWorksheet($row); - } - - /** - * {@inheritdoc} - */ - protected function closeWriter() - { - if (null !== $this->workbookManager) { - $this->workbookManager->close($this->filePointer); - } - } -} diff --git a/lib/openspout/src/Writer/XLSX/Creator/HelperFactory.php b/lib/openspout/src/Writer/XLSX/Creator/HelperFactory.php deleted file mode 100644 index 38a7f0daf6a..00000000000 --- a/lib/openspout/src/Writer/XLSX/Creator/HelperFactory.php +++ /dev/null @@ -1,53 +0,0 @@ -getOption(Options::TEMP_FOLDER); - $zipHelper = $this->createZipHelper($entityFactory); - $escaper = $this->createStringsEscaper(); - - return new FileSystemHelper($tempFolder, $zipHelper, $escaper); - } - - /** - * @return Escaper\XLSX - */ - public function createStringsEscaper() - { - return new Escaper\XLSX(); - } - - /** - * @return StringHelper - */ - public function createStringHelper() - { - return new StringHelper(); - } - - /** - * @return ZipHelper - */ - private function createZipHelper(InternalEntityFactory $entityFactory) - { - return new ZipHelper($entityFactory); - } -} diff --git a/lib/openspout/src/Writer/XLSX/Creator/ManagerFactory.php b/lib/openspout/src/Writer/XLSX/Creator/ManagerFactory.php deleted file mode 100644 index 47e60ce49b9..00000000000 --- a/lib/openspout/src/Writer/XLSX/Creator/ManagerFactory.php +++ /dev/null @@ -1,145 +0,0 @@ -entityFactory = $entityFactory; - $this->helperFactory = $helperFactory; - } - - /** - * @return WorkbookManager - */ - public function createWorkbookManager(OptionsManagerInterface $optionsManager) - { - $workbook = $this->entityFactory->createWorkbook(); - - $fileSystemHelper = $this->helperFactory->createSpecificFileSystemHelper($optionsManager, $this->entityFactory); - $fileSystemHelper->createBaseFilesAndFolders(); - - $xlFolder = $fileSystemHelper->getXlFolder(); - $sharedStringsManager = $this->createSharedStringsManager($xlFolder); - - $styleMerger = $this->createStyleMerger(); - $styleManager = $this->createStyleManager($optionsManager); - $worksheetManager = $this->createWorksheetManager($optionsManager, $styleManager, $styleMerger, $sharedStringsManager); - - return new WorkbookManager( - $workbook, - $optionsManager, - $worksheetManager, - $styleManager, - $styleMerger, - $fileSystemHelper, - $this->entityFactory, - $this - ); - } - - /** - * @return SheetManager - */ - public function createSheetManager() - { - $stringHelper = $this->helperFactory->createStringHelper(); - - return new SheetManager($stringHelper); - } - - /** - * @return RowManager - */ - public function createRowManager() - { - return new RowManager(); - } - - /** - * @return WorksheetManager - */ - private function createWorksheetManager( - OptionsManagerInterface $optionsManager, - StyleManager $styleManager, - StyleMerger $styleMerger, - SharedStringsManager $sharedStringsManager - ) { - $rowManager = $this->createRowManager(); - $stringsEscaper = $this->helperFactory->createStringsEscaper(); - $stringsHelper = $this->helperFactory->createStringHelper(); - - return new WorksheetManager( - $optionsManager, - $rowManager, - $styleManager, - $styleMerger, - $sharedStringsManager, - $stringsEscaper, - $stringsHelper - ); - } - - /** - * @return StyleManager - */ - private function createStyleManager(OptionsManagerInterface $optionsManager) - { - $styleRegistry = $this->createStyleRegistry($optionsManager); - - return new StyleManager($styleRegistry); - } - - /** - * @return StyleRegistry - */ - private function createStyleRegistry(OptionsManagerInterface $optionsManager) - { - $defaultRowStyle = $optionsManager->getOption(Options::DEFAULT_ROW_STYLE); - - return new StyleRegistry($defaultRowStyle); - } - - /** - * @return StyleMerger - */ - private function createStyleMerger() - { - return new StyleMerger(); - } - - /** - * @param string $xlFolder Path to the "xl" folder - * - * @return SharedStringsManager - */ - private function createSharedStringsManager($xlFolder) - { - $stringEscaper = $this->helperFactory->createStringsEscaper(); - - return new SharedStringsManager($xlFolder, $stringEscaper); - } -} diff --git a/lib/openspout/src/Writer/XLSX/Entity/SheetView.php b/lib/openspout/src/Writer/XLSX/Entity/SheetView.php index 71416159242..4cbd400006b 100644 --- a/lib/openspout/src/Writer/XLSX/Entity/SheetView.php +++ b/lib/openspout/src/Writer/XLSX/Entity/SheetView.php @@ -1,62 +1,31 @@ freezeRow = $freezeRow; @@ -243,16 +212,28 @@ class SheetView ''; } - protected function getSheetViewAttributes(): string + private function getSheetViewAttributes(): string { - // Get class properties - $propertyValues = get_object_vars($this); - unset($propertyValues['freezeRow'], $propertyValues['freezeColumn']); - - return $this->generateAttributes($propertyValues); + return $this->generateAttributes([ + 'showFormulas' => $this->showFormulas, + 'showGridLines' => $this->showGridLines, + 'showRowColHeaders' => $this->showRowColHeaders, + 'showZeroes' => $this->showZeroes, + 'rightToLeft' => $this->rightToLeft, + 'tabSelected' => $this->tabSelected, + 'showOutlineSymbols' => $this->showOutlineSymbols, + 'defaultGridColor' => $this->defaultGridColor, + 'view' => $this->view, + 'topLeftCell' => $this->topLeftCell, + 'colorId' => $this->colorId, + 'zoomScale' => $this->zoomScale, + 'zoomScaleNormal' => $this->zoomScaleNormal, + 'zoomScalePageLayoutView' => $this->zoomScalePageLayoutView, + 'workbookViewId' => $this->workbookViewId, + ]); } - protected function getFreezeCellPaneXml(): string + private function getFreezeCellPaneXml(): string { if ($this->freezeRow < 2 && 'A' === $this->freezeColumn) { return ''; @@ -270,12 +251,12 @@ class SheetView } /** - * @param array $data with key containing the attribute name and value containing the attribute value + * @param array $data with key containing the attribute name and value containing the attribute value */ - protected function generateAttributes(array $data): string + private function generateAttributes(array $data): string { // Create attribute for each key - $attributes = array_map(function ($key, $value) { + $attributes = array_map(static function (string $key, bool|string|int $value): string { if (\is_bool($value)) { $value = $value ? 'true' : 'false'; } diff --git a/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php b/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php index 1df43b9f105..dff73459f12 100644 --- a/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php +++ b/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php @@ -1,13 +1,18 @@ [ Border::WIDTH_THIN => 'thin', Border::WIDTH_MEDIUM => 'medium', @@ -35,14 +40,15 @@ class BorderHelper ], ]; - /** - * @return string - */ - public static function serializeBorderPart(BorderPart $borderPart) + public static function serializeBorderPart(?BorderPart $borderPart): string { + if (null === $borderPart) { + return ''; + } + $borderStyle = self::getBorderStyle($borderPart); - $colorEl = $borderPart->getColor() ? sprintf('', $borderPart->getColor()) : ''; + $colorEl = sprintf('', $borderPart->getColor()); $partEl = sprintf( '<%s style="%s">%s', $borderPart->getName(), @@ -56,11 +62,9 @@ class BorderHelper /** * Get the style definition from the style map. - * - * @return string */ - protected static function getBorderStyle(BorderPart $borderPart) + private static function getBorderStyle(BorderPart $borderPart): string { - return self::$xlsxStyleMap[$borderPart->getStyle()][$borderPart->getWidth()]; + return self::xlsxStyleMap[$borderPart->getStyle()][$borderPart->getWidth()]; } } diff --git a/lib/openspout/src/Writer/XLSX/Helper/DateHelper.php b/lib/openspout/src/Writer/XLSX/Helper/DateHelper.php index fb72df534d4..b18a9453157 100644 --- a/lib/openspout/src/Writer/XLSX/Helper/DateHelper.php +++ b/lib/openspout/src/Writer/XLSX/Helper/DateHelper.php @@ -1,15 +1,20 @@ format('Y'); $month = (int) $dateTime->format('m'); @@ -19,9 +24,9 @@ class DateHelper $seconds = (int) $dateTime->format('s'); // Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel // This affects every date following 28th February 1900 - $excel1900isLeapYear = true; + $excel1900isLeapYear = 1; if ((1900 === $year) && ($month <= 2)) { - $excel1900isLeapYear = false; + $excel1900isLeapYear = 0; } $myexcelBaseDate = 2415020; @@ -36,10 +41,18 @@ class DateHelper // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) $century = (int) substr((string) $year, 0, 2); $decade = (int) substr((string) $year, 2, 2); - $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear; + $excelDate = + floor((146097 * $century) / 4) + + floor((1461 * $decade) / 4) + + floor((153 * $month + 2) / 5) + + $day + + 1721119 + - $myexcelBaseDate + + $excel1900isLeapYear + ; $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; - return (float) $excelDate + $excelTime; + return $excelDate + $excelTime; } } diff --git a/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php b/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php index 12e4d79c140..9a1e7174144 100644 --- a/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php +++ b/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php @@ -1,21 +1,30 @@ + + EOD; - /** @var \OpenSpout\Common\Helper\Escaper\XLSX Used to escape XML data */ - private $escaper; + private string $baseFolderRealPath; + private CommonFileSystemHelper $baseFileSystemHelper; + + /** @var ZipHelper Helper to perform tasks with Zip archive */ + private ZipHelper $zipHelper; + + /** @var XLSX Used to escape XML data */ + private XLSX $escaper; /** @var string Path to the root folder inside the temp folder where the files to create the XLSX will be stored */ - private $rootFolder; + private string $rootFolder; /** @var string Path to the "_rels" folder inside the root folder */ - private $relsFolder; + private string $relsFolder; /** @var string Path to the "docProps" folder inside the root folder */ - private $docPropsFolder; + private string $docPropsFolder; /** @var string Path to the "xl" folder inside the root folder */ - private $xlFolder; + private string $xlFolder; /** @var string Path to the "_rels" folder inside the "xl" folder */ - private $xlRelsFolder; + private string $xlRelsFolder; /** @var string Path to the "worksheets" folder inside the "xl" folder */ - private $xlWorksheetsFolder; + private string $xlWorksheetsFolder; + + /** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */ + private string $sheetsContentTempFolder; /** - * @param string $baseFolderPath The path of the base folder where all the I/O can occur - * @param ZipHelper $zipHelper Helper to perform tasks with Zip archive - * @param \OpenSpout\Common\Helper\Escaper\XLSX $escaper Used to escape XML data + * @param string $baseFolderPath The path of the base folder where all the I/O can occur + * @param ZipHelper $zipHelper Helper to perform tasks with Zip archive + * @param XLSX $escaper Used to escape XML data */ - public function __construct($baseFolderPath, $zipHelper, $escaper) + public function __construct(string $baseFolderPath, ZipHelper $zipHelper, XLSX $escaper) { - parent::__construct($baseFolderPath); + $this->baseFileSystemHelper = new CommonFileSystemHelper($baseFolderPath); + $this->baseFolderRealPath = $this->baseFileSystemHelper->getBaseFolderRealPath(); $this->zipHelper = $zipHelper; $this->escaper = $escaper; } - /** - * @return string - */ - public function getRootFolder() + public function createFolder(string $parentFolderPath, string $folderName): string + { + return $this->baseFileSystemHelper->createFolder($parentFolderPath, $folderName); + } + + public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string + { + return $this->baseFileSystemHelper->createFileWithContents($parentFolderPath, $fileName, $fileContents); + } + + public function deleteFile(string $filePath): void + { + $this->baseFileSystemHelper->deleteFile($filePath); + } + + public function deleteFolderRecursively(string $folderPath): void + { + $this->baseFileSystemHelper->deleteFolderRecursively($folderPath); + } + + public function getRootFolder(): string { return $this->rootFolder; } - /** - * @return string - */ - public function getXlFolder() + public function getXlFolder(): string { return $this->xlFolder; } - /** - * @return string - */ - public function getXlWorksheetsFolder() + public function getXlWorksheetsFolder(): string { return $this->xlWorksheetsFolder; } + public function getSheetsContentTempFolder(): string + { + return $this->sheetsContentTempFolder; + } + /** * Creates all the folders needed to create a XLSX file, as well as the files that won't change. * * @throws \OpenSpout\Common\Exception\IOException If unable to create at least one of the base folders */ - public function createBaseFilesAndFolders() + public function createBaseFilesAndFolders(): void { $this ->createRootFolder() ->createRelsFolderAndFile() ->createDocPropsFolderAndFiles() ->createXlFolderAndSubFolders() + ->createSheetsContentTempFolder() ; } @@ -107,22 +145,22 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "[Content_Types].xml" file under the root folder. * * @param Worksheet[] $worksheets - * - * @return FileSystemHelper */ - public function createContentTypesFile($worksheets) + public function createContentTypesFile(array $worksheets): self { $contentTypesXmlFileContents = <<<'EOD' + EOD; /** @var Worksheet $worksheet */ foreach ($worksheets as $worksheet) { $contentTypesXmlFileContents .= ''; + $contentTypesXmlFileContents .= ''; } $contentTypesXmlFileContents .= <<<'EOD' @@ -142,10 +180,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "workbook.xml" file under the "xl" folder. * * @param Worksheet[] $worksheets - * - * @return FileSystemHelper */ - public function createWorkbookFile($worksheets) + public function createWorkbookFile(array $worksheets): self { $workbookXmlFileContents = <<<'EOD' @@ -163,6 +199,31 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme $workbookXmlFileContents .= <<<'EOD' + EOD; + + $definedNames = ''; + + /** @var Worksheet $worksheet */ + foreach ($worksheets as $worksheet) { + $sheet = $worksheet->getExternalSheet(); + if (null !== $autofilter = $sheet->getAutoFilter()) { + $worksheetName = $sheet->getName(); + $name = sprintf( + '\'%s\'!$%s$%s:$%s$%s', + $this->escaper->escape($worksheetName), + CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex), + $autofilter->fromRow, + CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex), + $autofilter->toRow + ); + $definedNames .= ''; + } + } + if ('' !== $definedNames) { + $workbookXmlFileContents .= ''.$definedNames.''; + } + + $workbookXmlFileContents .= <<<'EOD' EOD; @@ -175,10 +236,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "workbook.xml.res" file under the "xl/_res" folder. * * @param Worksheet[] $worksheets - * - * @return FileSystemHelper */ - public function createWorkbookRelsFile($worksheets) + public function createWorkbookRelsFile(array $worksheets): self { $workbookRelsXmlFileContents = <<<'EOD' @@ -201,13 +260,35 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme } /** - * Creates the "styles.xml" file under the "xl" folder. + * Create the "rels" file for a given worksheet. This contains relations to the comments.xml and drawing.vml files for this worksheet. * - * @param StyleManager $styleManager - * - * @return FileSystemHelper + * @param Worksheet[] $worksheets */ - public function createStylesFile($styleManager) + public function createWorksheetRelsFiles(array $worksheets): self + { + $this->createFolder($this->getXlWorksheetsFolder(), self::RELS_FOLDER_NAME); + + foreach ($worksheets as $worksheet) { + $worksheetId = $worksheet->getId(); + $worksheetRelsContent = ' + + + + '; + + $folder = $this->getXlWorksheetsFolder().\DIRECTORY_SEPARATOR.'_rels'; + $filename = 'sheet'.$worksheetId.'.xml.rels'; + + $this->createFileWithContents($folder, $filename, $worksheetRelsContent); + } + + return $this; + } + + /** + * Creates the "styles.xml" file under the "xl" folder. + */ + public function createStylesFile(StyleManager $styleManager): self { $stylesXmlFileContents = $styleManager->getStylesXMLFileContent(); $this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); @@ -215,12 +296,99 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme return $this; } + /** + * Creates the "content.xml" file under the root folder. + * + * @param Worksheet[] $worksheets + */ + public function createContentFiles(Options $options, array $worksheets): self + { + $allMergeCells = $options->getMergeCells(); + + foreach ($worksheets as $worksheet) { + $contentXmlFilePath = $this->getXlWorksheetsFolder().\DIRECTORY_SEPARATOR.basename($worksheet->getFilePath()); + $worksheetFilePointer = fopen($contentXmlFilePath, 'w'); + \assert(false !== $worksheetFilePointer); + + $sheet = $worksheet->getExternalSheet(); + fwrite($worksheetFilePointer, self::SHEET_XML_FILE_HEADER); + + // AutoFilter tags + $range = ''; + if (null !== $autofilter = $sheet->getAutoFilter()) { + $range = sprintf( + '%s%s:%s%s', + CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex), + $autofilter->fromRow, + CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex), + $autofilter->toRow + ); + fwrite($worksheetFilePointer, ''); + fwrite($worksheetFilePointer, sprintf('', $range)); + } + + if (null !== ($sheetView = $sheet->getSheetView())) { + fwrite($worksheetFilePointer, ''.$sheetView->getXml().''); + } + fwrite($worksheetFilePointer, $this->getXMLFragmentForDefaultCellSizing($options)); + fwrite($worksheetFilePointer, $this->getXMLFragmentForColumnWidths($options, $sheet)); + fwrite($worksheetFilePointer, ''); + + $worksheetFilePath = $worksheet->getFilePath(); + $this->copyFileContentsToTarget($worksheetFilePath, $worksheetFilePointer); + fwrite($worksheetFilePointer, ''); + + // AutoFilter tag + if ('' !== $range) { + fwrite($worksheetFilePointer, sprintf('', $range)); + } + + // create nodes for merge cells + $mergeCells = array_filter( + $allMergeCells, + static fn (MergeCell $c) => $c->sheetIndex === $worksheet->getExternalSheet()->getIndex(), + ); + if ([] !== $mergeCells) { + $mergeCellString = ''; + foreach ($mergeCells as $mergeCell) { + $topLeft = CellHelper::getColumnLettersFromColumnIndex($mergeCell->topLeftColumn).$mergeCell->topLeftRow; + $bottomRight = CellHelper::getColumnLettersFromColumnIndex($mergeCell->bottomRightColumn).$mergeCell->bottomRightRow; + $mergeCellString .= sprintf( + '', + $topLeft, + $bottomRight + ); + } + $mergeCellString .= ''; + fwrite($worksheetFilePointer, $mergeCellString); + } + + // Add the legacy drawing for comments + fwrite($worksheetFilePointer, ''); + + fwrite($worksheetFilePointer, ''); + fclose($worksheetFilePointer); + } + + return $this; + } + + /** + * Deletes the temporary folder where sheets content was stored. + */ + public function deleteWorksheetTempFolder(): self + { + $this->deleteFolderRecursively($this->sheetsContentTempFolder); + + return $this; + } + /** * Zips the root folder and streams the contents of the zip into the given stream. * * @param resource $streamPointer Pointer to the stream to copy the zip */ - public function zipRootFolderAndCopyToStream($streamPointer) + public function zipRootFolderAndCopyToStream($streamPointer): void { $zip = $this->zipHelper->createZip($this->rootFolder); @@ -230,8 +398,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme // to the zip file in a particular order. // "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first. $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME); - $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME.'/'.self::WORKBOOK_XML_FILE_NAME); - $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME.'/'.self::STYLES_XML_FILE_NAME); + $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME.\DIRECTORY_SEPARATOR.self::WORKBOOK_XML_FILE_NAME); + $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME.\DIRECTORY_SEPARATOR.self::STYLES_XML_FILE_NAME); $this->zipHelper->addFolderToArchive($zip, $this->rootFolder, ZipHelper::EXISTING_FILES_SKIP); $this->zipHelper->closeArchiveAndCopyToStream($zip, $streamPointer); @@ -240,14 +408,51 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme $this->deleteFile($zipFilePath); } + /** + * Construct column width references xml to inject into worksheet xml file. + */ + private function getXMLFragmentForColumnWidths(Options $options, Sheet $sheet): string + { + if ([] !== $sheet->getColumnWidths()) { + $widths = $sheet->getColumnWidths(); + } elseif ([] !== $options->getColumnWidths()) { + $widths = $options->getColumnWidths(); + } else { + return ''; + } + + $xml = ''; + + foreach ($widths as $columnWidth) { + $xml .= ''; + } + $xml .= ''; + + return $xml; + } + + /** + * Constructs default row height and width xml to inject into worksheet xml file. + */ + private function getXMLFragmentForDefaultCellSizing(Options $options): string + { + $rowHeightXml = null === $options->DEFAULT_ROW_HEIGHT ? '' : " defaultRowHeight=\"{$options->DEFAULT_ROW_HEIGHT}\""; + $colWidthXml = null === $options->DEFAULT_COLUMN_WIDTH ? '' : " defaultColWidth=\"{$options->DEFAULT_COLUMN_WIDTH}\""; + if ('' === $colWidthXml && '' === $rowHeightXml) { + return ''; + } + // Ensure that the required defaultRowHeight is set + $rowHeightXml = '' === $rowHeightXml ? ' defaultRowHeight="0"' : $rowHeightXml; + + return ""; + } + /** * Creates the folder that will be used as root. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder - * - * @return FileSystemHelper */ - private function createRootFolder() + private function createRootFolder(): self { $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('xlsx', true)); @@ -258,10 +463,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "_rels" folder under the root folder as well as the ".rels" file in it. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or the ".rels" file - * - * @return FileSystemHelper */ - private function createRelsFolderAndFile() + private function createRelsFolderAndFile(): self { $this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME); @@ -274,10 +477,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the ".rels" file under the "_rels" folder (under root). * * @throws \OpenSpout\Common\Exception\IOException If unable to create the file - * - * @return FileSystemHelper */ - private function createRelsFile() + private function createRelsFile(): self { $relsFileContents = <<<'EOD' @@ -297,10 +498,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or one of the files - * - * @return FileSystemHelper */ - private function createDocPropsFolderAndFiles() + private function createDocPropsFolderAndFiles(): self { $this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME); @@ -314,10 +513,8 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "app.xml" file under the "docProps" folder. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the file - * - * @return FileSystemHelper */ - private function createAppXmlFile() + private function createAppXmlFile(): self { $appName = self::APP_NAME; $appXmlFileContents = <<format(\DateTime::W3C); + $createdDate = (new DateTimeImmutable())->format(DateTimeImmutable::W3C); $coreXmlFileContents = << @@ -361,14 +556,26 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "xl" folder under the root folder as well as its subfolders. * * @throws \OpenSpout\Common\Exception\IOException If unable to create at least one of the folders - * - * @return FileSystemHelper */ - private function createXlFolderAndSubFolders() + private function createXlFolderAndSubFolders(): self { $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME); $this->createXlRelsFolder(); $this->createXlWorksheetsFolder(); + $this->createDrawingsFolder(); + + return $this; + } + + /** + * Creates the temp folder where specific sheets content will be written to. + * This folder is not part of the final ODS file and is only used to be able to jump between sheets. + * + * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder + */ + private function createSheetsContentTempFolder(): self + { + $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, 'worksheets-temp'); return $this; } @@ -377,27 +584,51 @@ class FileSystemHelper extends \OpenSpout\Common\Helper\FileSystemHelper impleme * Creates the "_rels" folder under the "xl" folder. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder - * - * @return FileSystemHelper */ - private function createXlRelsFolder() + private function createXlRelsFolder(): self { $this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME); return $this; } + /** + * Creates the "drawings" folder under the "xl" folder. + * + * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder + */ + private function createDrawingsFolder(): self + { + $this->createFolder($this->getXlFolder(), self::DRAWINGS_FOLDER_NAME); + + return $this; + } + /** * Creates the "worksheets" folder under the "xl" folder. * * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder - * - * @return FileSystemHelper */ - private function createXlWorksheetsFolder() + private function createXlWorksheetsFolder(): self { $this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME); return $this; } + + /** + * Streams the content of the file at the given path into the target resource. + * Depending on which mode the target resource was created with, it will truncate then copy + * or append the content to the target file. + * + * @param string $sourceFilePath Path of the file whose content will be copied + * @param resource $targetResource Target resource that will receive the content + */ + private function copyFileContentsToTarget(string $sourceFilePath, $targetResource): void + { + $sourceHandle = fopen($sourceFilePath, 'r'); + \assert(false !== $sourceHandle); + stream_copy_to_stream($sourceHandle, $targetResource); + fclose($sourceHandle); + } } diff --git a/lib/openspout/src/Writer/XLSX/Manager/CommentsManager.php b/lib/openspout/src/Writer/XLSX/Manager/CommentsManager.php new file mode 100644 index 00000000000..525289c3782 --- /dev/null +++ b/lib/openspout/src/Writer/XLSX/Manager/CommentsManager.php @@ -0,0 +1,225 @@ + + + Unknown + + EOD; + + public const COMMENTS_XML_FILE_FOOTER = <<<'EOD' + + + EOD; + + public const DRAWINGS_VML_FILE_HEADER = <<<'EOD' + + + + + + + + + + EOD; + + public const DRAWINGS_VML_FILE_FOOTER = <<<'EOD' + + EOD; + + /** + * File-pointers to the commentsX.xml files, where the index is the id of the worksheet. + * + * @var resource[] + */ + private array $commentsFilePointers = []; + + /** + * File-pointers to the vmlDrawingX.vml files, where the index is the id of the worksheet. + * + * @var resource[] + */ + private array $drawingFilePointers = []; + + private string $xlFolder; + + private int $shapeId = 1024; + + private Escaper\XLSX $stringsEscaper; + + /** + * @param string $xlFolder Path to the "xl" folder + */ + public function __construct(string $xlFolder, Escaper\XLSX $stringsEscaper) + { + $this->xlFolder = $xlFolder; + $this->stringsEscaper = $stringsEscaper; + } + + /** + * Create the two comment-files for the given worksheet. + */ + public function createWorksheetCommentFiles(Worksheet $sheet): void + { + $sheetId = $sheet->getId(); + $commentFp = fopen($this->getCommentsFilePath($sheet), 'w'); + \assert(false !== $commentFp); + + $drawingFp = fopen($this->getDrawingFilePath($sheet), 'w'); + \assert(false !== $drawingFp); + + fwrite($commentFp, self::COMMENTS_XML_FILE_HEADER); + fwrite($drawingFp, self::DRAWINGS_VML_FILE_HEADER); + + $this->commentsFilePointers[$sheetId] = $commentFp; + $this->drawingFilePointers[$sheetId] = $drawingFp; + } + + /** + * Close the two comment-files for the given worksheet. + */ + public function closeWorksheetCommentFiles(Worksheet $sheet): void + { + $sheetId = $sheet->getId(); + + $commentFp = $this->commentsFilePointers[$sheetId]; + $drawingFp = $this->drawingFilePointers[$sheetId]; + + fwrite($commentFp, self::COMMENTS_XML_FILE_FOOTER); + fwrite($drawingFp, self::DRAWINGS_VML_FILE_FOOTER); + + fclose($commentFp); + fclose($drawingFp); + } + + public function addComments(Worksheet $worksheet, Row $row): void + { + $rowIndexZeroBased = 0 + $worksheet->getLastWrittenRowIndex(); + foreach ($row->getCells() as $columnIndexZeroBased => $cell) { + if (null === $cell->comment) { + continue; + } + + $this->addXmlComment($worksheet->getId(), $rowIndexZeroBased, $columnIndexZeroBased, $cell->comment); + $this->addVmlComment($worksheet->getId(), $rowIndexZeroBased, $columnIndexZeroBased, $cell->comment); + } + } + + /** + * @return string The file path where the comments for the given sheet will be stored + */ + private function getCommentsFilePath(Worksheet $sheet): string + { + return $this->xlFolder.\DIRECTORY_SEPARATOR.'comments'.$sheet->getId().'.xml'; + } + + /** + * @return string The file path where the VML comments for the given sheet will be stored + */ + private function getDrawingFilePath(Worksheet $sheet): string + { + return $this->xlFolder.\DIRECTORY_SEPARATOR.'drawings'.\DIRECTORY_SEPARATOR.'vmlDrawing'.$sheet->getId().'.vml'; + } + + /** + * Add a comment to the commentsX.xml file. + * + * @param int $sheetId The id of the sheet (starting with 1) + * @param int $rowIndexZeroBased The row index, starting at 0, of the cell with the comment + * @param int $columnIndexZeroBased The column index, starting at 0, of the cell with the comment + * @param Comment $comment The actual comment + */ + private function addXmlComment(int $sheetId, int $rowIndexZeroBased, int $columnIndexZeroBased, Comment $comment): void + { + $commentsFilePointer = $this->commentsFilePointers[$sheetId]; + $rowIndexOneBased = $rowIndexZeroBased + 1; + $columnLetters = CellHelper::getColumnLettersFromColumnIndex($columnIndexZeroBased); + + $commentxml = ''; + foreach ($comment->getTextRuns() as $line) { + $commentxml .= ''; + $commentxml .= ' '; + if ($line->bold) { + $commentxml .= ' '; + } + if ($line->italic) { + $commentxml .= ' '; + } + $commentxml .= ' '; + $commentxml .= ' '; + $commentxml .= ' '; + $commentxml .= ' '; + $commentxml .= ' '; + $commentxml .= ' '.$this->stringsEscaper->escape($line->text).''; + $commentxml .= ''; + } + $commentxml .= ''; + + fwrite($commentsFilePointer, $commentxml); + } + + /** + * Add a comment to the vmlDrawingX.vml file. + * + * @param int $sheetId The id of the sheet (starting with 1) + * @param int $rowIndexZeroBased The row index, starting at 0, of the cell with the comment + * @param int $columnIndexZeroBased The column index, starting at 0, of the cell with the comment + * @param Comment $comment The actual comment + */ + private function addVmlComment(int $sheetId, int $rowIndexZeroBased, int $columnIndexZeroBased, Comment $comment): void + { + $drawingFilePointer = $this->drawingFilePointers[$sheetId]; + ++$this->shapeId; + + $style = 'position:absolute;z-index:1'; + $style .= ';margin-left:'.$comment->marginLeft; + $style .= ';margin-top:'.$comment->marginTop; + $style .= ';width:'.$comment->width; + $style .= ';height:'.$comment->height; + if (!$comment->visible) { + $style .= ';visibility:hidden'; + } + + $drawingVml = ''; + $drawingVml .= ''; + $drawingVml .= ''; + $drawingVml .= ''; + $drawingVml .= ''; + $drawingVml .= '
'; + $drawingVml .= ''; + $drawingVml .= ''; + $drawingVml .= ' '; + $drawingVml .= ' '; + $drawingVml .= ' False'; + $drawingVml .= ' '.$rowIndexZeroBased.''; + $drawingVml .= ' '.$columnIndexZeroBased.''; + $drawingVml .= ''; + $drawingVml .= ''; + + fwrite($drawingFilePointer, $drawingVml); + } +} diff --git a/lib/openspout/src/Writer/XLSX/Manager/OptionsManager.php b/lib/openspout/src/Writer/XLSX/Manager/OptionsManager.php deleted file mode 100644 index b7e7eaebd1f..00000000000 --- a/lib/openspout/src/Writer/XLSX/Manager/OptionsManager.php +++ /dev/null @@ -1,64 +0,0 @@ -styleBuilder = $styleBuilder; - parent::__construct(); - } - - /** - * {@inheritdoc} - */ - protected function getSupportedOptions() - { - return [ - Options::TEMP_FOLDER, - Options::DEFAULT_ROW_STYLE, - Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, - Options::SHOULD_USE_INLINE_STRINGS, - Options::DEFAULT_COLUMN_WIDTH, - Options::DEFAULT_ROW_HEIGHT, - Options::COLUMN_WIDTHS, - Options::MERGE_CELLS, - ]; - } - - /** - * {@inheritdoc} - */ - protected function setDefaultOptions() - { - $defaultRowStyle = $this->styleBuilder - ->setFontSize(self::DEFAULT_FONT_SIZE) - ->setFontName(self::DEFAULT_FONT_NAME) - ->build() - ; - - $this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir()); - $this->setOption(Options::DEFAULT_ROW_STYLE, $defaultRowStyle); - $this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true); - $this->setOption(Options::SHOULD_USE_INLINE_STRINGS, true); - $this->setOption(Options::MERGE_CELLS, []); - } -} diff --git a/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php b/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php index 97395561a22..3b232c7b614 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php @@ -1,14 +1,15 @@ sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w'); - - $this->throwIfSharedStringsFilePointerIsNotAvailable(); + $sharedStringsFilePath = $xlFolder.\DIRECTORY_SEPARATOR.self::SHARED_STRINGS_FILE_NAME; + $resource = fopen($sharedStringsFilePath, 'w'); + \assert(false !== $resource); + $this->sharedStringsFilePointer = $resource; // the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later $header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER.' '.self::DEFAULT_STRINGS_COUNT_PART.'>'; @@ -54,11 +55,9 @@ class SharedStringsManager * Writes the given string into the sharedStrings.xml file. * Starting and ending whitespaces are preserved. * - * @param string $string - * * @return int ID of the written shared string */ - public function writeString($string) + public function writeString(string $string): int { fwrite($this->sharedStringsFilePointer, ''.$this->stringsEscaper->escape($string).''); ++$this->numSharedStrings; @@ -70,12 +69,8 @@ class SharedStringsManager /** * Finishes writing the data in the sharedStrings.xml file and closes the file. */ - public function close() + public function close(): void { - if (!\is_resource($this->sharedStringsFilePointer)) { - return; - } - fwrite($this->sharedStringsFilePointer, ''); // Replace the default strings count with the actual number of shared strings in the file header @@ -88,16 +83,4 @@ class SharedStringsManager fclose($this->sharedStringsFilePointer); } - - /** - * Checks if the book has been created. Throws an exception if not created yet. - * - * @throws \OpenSpout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - protected function throwIfSharedStringsFilePointerIsNotAvailable() - { - if (!\is_resource($this->sharedStringsFilePointer)) { - throw new IOException('Unable to open shared strings file for writing.'); - } - } } diff --git a/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php b/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php index 5f284b9511d..3953f4dc5f8 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php @@ -1,19 +1,26 @@ styleRegistry->getFillIdForStyleId($styleId); $hasStyleCustomFill = (null !== $associatedFillId && 0 !== $associatedFillId); @@ -42,10 +50,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "styles.xml" file, given a list of styles. - * - * @return string */ - public function getStylesXMLFileContent() + public function getStylesXMLFileContent(): string { $content = <<<'EOD' @@ -69,17 +75,15 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "" section. - * - * @return string */ - protected function getFormatsSectionContent() + private function getFormatsSectionContent(): string { $tags = []; $registeredFormats = $this->styleRegistry->getRegisteredFormats(); foreach ($registeredFormats as $styleId) { $numFmtId = $this->styleRegistry->getFormatIdForStyleId($styleId); - //Built-in formats do not need to be declared, skip them + // Built-in formats do not need to be declared, skip them if ($numFmtId < 164) { continue; } @@ -98,10 +102,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "" section. - * - * @return string */ - protected function getFontsSectionContent() + private function getFontsSectionContent(): string { $registeredStyles = $this->styleRegistry->getRegisteredStyles(); @@ -138,10 +140,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "" section. - * - * @return string */ - protected function getFillsSectionContent() + private function getFillsSectionContent(): string { $registeredFills = $this->styleRegistry->getRegisteredFills(); @@ -171,10 +171,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "" section. - * - * @return string */ - protected function getBordersSectionContent() + private function getBordersSectionContent(): string { $registeredBorders = $this->styleRegistry->getRegisteredBorders(); @@ -187,20 +185,14 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager $content .= ''; foreach ($registeredBorders as $styleId) { - /** @var Style $style */ $style = $this->styleRegistry->getStyleFromStyleId($styleId); $border = $style->getBorder(); + \assert(null !== $border); $content .= ''; - /** @see https://github.com/box/spout/issues/271 */ - $sortOrder = ['left', 'right', 'top', 'bottom']; - - foreach ($sortOrder as $partName) { - if ($border->hasPart($partName)) { - /** @var BorderPart $part */ - $part = $border->getPart($partName); - $content .= BorderHelper::serializeBorderPart($part); - } + // @see https://github.com/box/spout/issues/271 + foreach (BorderPart::allowedNames as $partName) { + $content .= BorderHelper::serializeBorderPart($border->getPart($partName)); } $content .= ''; @@ -213,10 +205,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "" section. - * - * @return string */ - protected function getCellStyleXfsSectionContent() + private function getCellStyleXfsSectionContent(): string { return <<<'EOD' @@ -227,10 +217,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "" section. - * - * @return string */ - protected function getCellXfsSectionContent() + private function getCellXfsSectionContent(): string { $registeredStyles = $this->styleRegistry->getRegisteredStyles(); @@ -248,16 +236,19 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager $content .= ' applyFont="1"'; } - $content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0); + $content .= sprintf(' applyBorder="%d"', (bool) $style->getBorder()); - if ($style->shouldApplyCellAlignment() || $style->shouldWrapText() || $style->shouldShrinkToFit()) { + if ($style->shouldApplyCellAlignment() || $style->shouldApplyCellVerticalAlignment() || $style->hasSetWrapText() || $style->shouldShrinkToFit()) { $content .= ' applyAlignment="1">'; $content .= 'shouldApplyCellAlignment()) { $content .= sprintf(' horizontal="%s"', $style->getCellAlignment()); } - if ($style->shouldWrapText()) { - $content .= ' wrapText="1"'; + if ($style->shouldApplyCellVerticalAlignment()) { + $content .= sprintf(' vertical="%s"', $style->getCellVerticalAlignment()); + } + if ($style->hasSetWrapText()) { + $content .= ' wrapText="'.($style->shouldWrapText() ? '1' : '0').'"'; } if ($style->shouldShrinkToFit()) { $content .= ' shrinkToFit="true"'; @@ -277,10 +268,8 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the content of the "" section. - * - * @return string */ - protected function getCellStylesSectionContent() + private function getCellStylesSectionContent(): string { return <<<'EOD' @@ -292,51 +281,39 @@ class StyleManager extends \OpenSpout\Writer\Common\Manager\Style\StyleManager /** * Returns the fill ID associated to the given style ID. * For the default style, we don't a fill. - * - * @param int $styleId - * - * @return int */ - private function getFillIdForStyleId($styleId) + private function getFillIdForStyleId(int $styleId): int { // For the default style (ID = 0), we don't want to override the fill. // Otherwise all cells of the spreadsheet will have a background color. $isDefaultStyle = (0 === $styleId); - return $isDefaultStyle ? 0 : ($this->styleRegistry->getFillIdForStyleId($styleId) ?: 0); + return $isDefaultStyle ? 0 : ($this->styleRegistry->getFillIdForStyleId($styleId) ?? 0); } /** * Returns the fill ID associated to the given style ID. * For the default style, we don't a border. - * - * @param int $styleId - * - * @return int */ - private function getBorderIdForStyleId($styleId) + private function getBorderIdForStyleId(int $styleId): int { // For the default style (ID = 0), we don't want to override the border. // Otherwise all cells of the spreadsheet will have a border. $isDefaultStyle = (0 === $styleId); - return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?: 0); + return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?? 0); } /** * Returns the format ID associated to the given style ID. * For the default style use general format. - * - * @param int $styleId - * - * @return int */ - private function getFormatIdForStyleId($styleId) + private function getFormatIdForStyleId(int $styleId): int { // For the default style (ID = 0), we don't want to override the format. // Otherwise all cells of the spreadsheet will have a format. $isDefaultStyle = (0 === $styleId); - return $isDefaultStyle ? 0 : ($this->styleRegistry->getFormatIdForStyleId($styleId) ?: 0); + return $isDefaultStyle ? 0 : ($this->styleRegistry->getFormatIdForStyleId($styleId) ?? 0); } } diff --git a/lib/openspout/src/Writer/XLSX/Manager/Style/StyleRegistry.php b/lib/openspout/src/Writer/XLSX/Manager/Style/StyleRegistry.php index 259d4e8408d..374638d1117 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/Style/StyleRegistry.php +++ b/lib/openspout/src/Writer/XLSX/Manager/Style/StyleRegistry.php @@ -1,20 +1,23 @@ 0, '0' => 1, '0.00' => 2, @@ -64,15 +67,11 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry 't# ??/??' => 70, ]; - /** - * @var array - */ - protected $registeredFormats = []; + /** @var array */ + private array $registeredFormats = []; - /** - * @var array [STYLE_ID] => [FORMAT_ID] maps a style to a format declaration - */ - protected $styleIdToFormatsMappingTable = []; + /** @var array [STYLE_ID] => [FORMAT_ID] maps a style to a format declaration */ + private array $styleIdToFormatsMappingTable = []; /** * If the numFmtId is lower than 0xA4 (164 in decimal) @@ -81,17 +80,13 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry * * @var int the fill index counter for custom fills */ - protected $formatIndex = 164; + private int $formatIndex = 164; - /** - * @var array - */ - protected $registeredFills = []; + /** @var array */ + private array $registeredFills = []; - /** - * @var array [STYLE_ID] => [FILL_ID] maps a style to a fill declaration - */ - protected $styleIdToFillMappingTable = []; + /** @var array [STYLE_ID] => [FILL_ID] maps a style to a fill declaration */ + private array $styleIdToFillMappingTable = []; /** * Excel preserves two default fills with index 0 and 1 @@ -99,24 +94,18 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry * * @var int the fill index counter for custom fills */ - protected $fillIndex = 2; + private int $fillIndex = 2; - /** - * @var array - */ - protected $registeredBorders = []; + /** @var array */ + private array $registeredBorders = []; - /** - * @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration - */ - protected $styleIdToBorderMappingTable = []; + /** @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration */ + private array $styleIdToBorderMappingTable = []; /** * XLSX specific operations on the registered styles. - * - * @return Style */ - public function registerStyle(Style $style) + public function registerStyle(Style $style): Style { if ($style->isRegistered()) { return $style; @@ -131,59 +120,49 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry } /** - * @param int $styleId - * * @return null|int Format ID associated to the given style ID */ - public function getFormatIdForStyleId($styleId) + public function getFormatIdForStyleId(int $styleId): ?int { return $this->styleIdToFormatsMappingTable[$styleId] ?? null; } /** - * @param int $styleId - * * @return null|int Fill ID associated to the given style ID */ - public function getFillIdForStyleId($styleId) + public function getFillIdForStyleId(int $styleId): ?int { - return (isset($this->styleIdToFillMappingTable[$styleId])) ? - $this->styleIdToFillMappingTable[$styleId] : - null; + return $this->styleIdToFillMappingTable[$styleId] ?? null; } /** - * @param int $styleId - * * @return null|int Fill ID associated to the given style ID */ - public function getBorderIdForStyleId($styleId) + public function getBorderIdForStyleId(int $styleId): ?int { - return (isset($this->styleIdToBorderMappingTable[$styleId])) ? - $this->styleIdToBorderMappingTable[$styleId] : - null; + return $this->styleIdToBorderMappingTable[$styleId] ?? null; } /** - * @return array + * @return array */ - public function getRegisteredFills() + public function getRegisteredFills(): array { return $this->registeredFills; } /** - * @return array + * @return array */ - public function getRegisteredBorders() + public function getRegisteredBorders(): array { return $this->registeredBorders; } /** - * @return array + * @return array */ - public function getRegisteredFormats() + public function getRegisteredFormats(): array { return $this->registeredFormats; } @@ -191,12 +170,12 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry /** * Register a format definition. */ - protected function registerFormat(Style $style) + private function registerFormat(Style $style): void { $styleId = $style->getId(); $format = $style->getFormat(); - if ($format) { + if (null !== $format) { $isFormatRegistered = isset($this->registeredFormats[$format]); // We need to track the already registered format definitions @@ -207,7 +186,7 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry } else { $this->registeredFormats[$format] = $styleId; - $id = self::$builtinNumFormatToIdMapping[$format] ?? $this->formatIndex++; + $id = self::builtinNumFormatToIdMapping[$format] ?? $this->formatIndex++; $this->styleIdToFormatsMappingTable[$styleId] = $id; } } else { @@ -220,7 +199,7 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry /** * Register a fill definition. */ - private function registerFill(Style $style) + private function registerFill(Style $style): void { $styleId = $style->getId(); @@ -228,7 +207,7 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry // so $backgroundColor is a scalar value (RGB Color) $backgroundColor = $style->getBackgroundColor(); - if ($backgroundColor) { + if (null !== $backgroundColor) { $isBackgroundColorRegistered = isset($this->registeredFills[$backgroundColor]); // We need to track the already registered background definitions @@ -250,12 +229,11 @@ class StyleRegistry extends \OpenSpout\Writer\Common\Manager\Style\StyleRegistry /** * Register a border definition. */ - private function registerBorder(Style $style) + private function registerBorder(Style $style): void { $styleId = $style->getId(); - if ($style->shouldApplyBorder()) { - $border = $style->getBorder(); + if (null !== ($border = $style->getBorder())) { $serializedBorder = serialize($border); $isBorderAlreadyRegistered = isset($this->registeredBorders[$serializedBorder]); diff --git a/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php b/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php index 2feb331d86c..8404c47041f 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php @@ -1,47 +1,56 @@ fileSystemHelper->getXlWorksheetsFolder(); - - return $worksheetFilesFolder.'/'.strtolower($sheet->getName()).'.xml'; + public function __construct( + Workbook $workbook, + Options $options, + WorksheetManager $worksheetManager, + StyleManager $styleManager, + StyleMerger $styleMerger, + FileSystemHelper $fileSystemHelper + ) { + parent::__construct( + $workbook, + $options, + $worksheetManager, + $styleManager, + $styleMerger, + $fileSystemHelper + ); } /** * @return int Maximum number of rows/columns a sheet can contain */ - protected function getMaxRowsPerWorksheet() + protected function getMaxRowsPerWorksheet(): int { return self::$maxRowsPerWorksheet; } @@ -49,7 +58,7 @@ class WorkbookManager extends WorkbookManagerAbstract /** * Closes custom objects that are still opened. */ - protected function closeRemainingObjects() + protected function closeRemainingObjects(): void { $this->worksheetManager->getSharedStringsManager()->close(); } @@ -59,14 +68,17 @@ class WorkbookManager extends WorkbookManagerAbstract * * @param resource $finalFilePointer Pointer to the spreadsheet that will be created */ - protected function writeAllFilesToDiskAndZipThem($finalFilePointer) + protected function writeAllFilesToDiskAndZipThem($finalFilePointer): void { $worksheets = $this->getWorksheets(); $this->fileSystemHelper + ->createContentFiles($this->options, $worksheets) + ->deleteWorksheetTempFolder() ->createContentTypesFile($worksheets) ->createWorkbookFile($worksheets) ->createWorkbookRelsFile($worksheets) + ->createWorksheetRelsFiles($worksheets) ->createStylesFile($this->styleManager) ->zipRootFolderAndCopyToStream($finalFilePointer) ; diff --git a/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php b/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php index c84be5ab555..e0e448a29f5 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php @@ -1,5 +1,7 @@ - - EOD; + /** @var CommentsManager Manages comments */ + private CommentsManager $commentsManager; - /** @var bool Whether inline or shared strings should be used */ - protected $shouldUseInlineStrings; - - /** @var OptionsManagerInterface */ - private $optionsManager; - - /** @var RowManager Manages rows */ - private $rowManager; + private Options $options; /** @var StyleManager Manages styles */ - private $styleManager; + private StyleManager $styleManager; /** @var StyleMerger Helper to merge styles together */ - private $styleMerger; + private StyleMerger $styleMerger; /** @var SharedStringsManager Helper to write shared strings */ - private $sharedStringsManager; + private SharedStringsManager $sharedStringsManager; /** @var XLSXEscaper Strings escaper */ - private $stringsEscaper; + private XLSXEscaper $stringsEscaper; /** @var StringHelper String helper */ - private $stringHelper; + private StringHelper $stringHelper; /** * WorksheetManager constructor. */ public function __construct( - OptionsManagerInterface $optionsManager, - RowManager $rowManager, + Options $options, StyleManager $styleManager, StyleMerger $styleMerger, + CommentsManager $commentsManager, SharedStringsManager $sharedStringsManager, XLSXEscaper $stringsEscaper, StringHelper $stringHelper ) { - $this->optionsManager = $optionsManager; - $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS); - $this->setDefaultColumnWidth($optionsManager->getOption(Options::DEFAULT_COLUMN_WIDTH)); - $this->setDefaultRowHeight($optionsManager->getOption(Options::DEFAULT_ROW_HEIGHT)); - $this->columnWidths = $optionsManager->getOption(Options::COLUMN_WIDTHS) ?? []; - $this->rowManager = $rowManager; + $this->options = $options; $this->styleManager = $styleManager; $this->styleMerger = $styleMerger; + $this->commentsManager = $commentsManager; $this->sharedStringsManager = $sharedStringsManager; $this->stringsEscaper = $stringsEscaper; $this->stringHelper = $stringHelper; } - /** - * @return SharedStringsManager - */ - public function getSharedStringsManager() + public function getSharedStringsManager(): SharedStringsManager { return $this->sharedStringsManager; } @@ -102,127 +83,35 @@ class WorksheetManager implements WorksheetManagerInterface /** * {@inheritdoc} */ - public function startSheet(Worksheet $worksheet) + public function startSheet(Worksheet $worksheet): void { $sheetFilePointer = fopen($worksheet->getFilePath(), 'w'); - $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer); + \assert(false !== $sheetFilePointer); $worksheet->setFilePointer($sheetFilePointer); - - fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER); + $this->commentsManager->createWorksheetCommentFiles($worksheet); } /** * {@inheritdoc} */ - public function addRow(Worksheet $worksheet, Row $row) + public function addRow(Worksheet $worksheet, Row $row): void { - if (!$this->rowManager->isEmpty($row)) { + if (!$row->isEmpty()) { $this->addNonEmptyRow($worksheet, $row); + $this->commentsManager->addComments($worksheet, $row); } $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1); } - /** - * Construct column width references xml to inject into worksheet xml file. - * - * @return string - */ - public function getXMLFragmentForColumnWidths() - { - if (empty($this->columnWidths)) { - return ''; - } - $xml = ''; - foreach ($this->columnWidths as $entry) { - $xml .= '
'; - } - $xml .= ''; - - return $xml; - } - - /** - * Constructs default row height and width xml to inject into worksheet xml file. - * - * @return string - */ - public function getXMLFragmentForDefaultCellSizing() - { - $rowHeightXml = empty($this->defaultRowHeight) ? '' : " defaultRowHeight=\"{$this->defaultRowHeight}\""; - $colWidthXml = empty($this->defaultColumnWidth) ? '' : " defaultColWidth=\"{$this->defaultColumnWidth}\""; - if (empty($colWidthXml) && empty($rowHeightXml)) { - return ''; - } - // Ensure that the required defaultRowHeight is set - $rowHeightXml = empty($rowHeightXml) ? ' defaultRowHeight="0"' : $rowHeightXml; - - return ""; - } - /** * {@inheritdoc} */ - public function close(Worksheet $worksheet) + public function close(Worksheet $worksheet): void { - $worksheetFilePointer = $worksheet->getFilePointer(); - - if (!\is_resource($worksheetFilePointer)) { - return; - } - $this->ensureSheetDataStated($worksheet); - fwrite($worksheetFilePointer, ''); - - // create nodes for merge cells - if ($this->optionsManager->getOption(Options::MERGE_CELLS)) { - $mergeCellString = ''; - foreach ($this->optionsManager->getOption(Options::MERGE_CELLS) as $values) { - $output = array_map(function ($value) { - return CellHelper::getColumnLettersFromColumnIndex($value[0]).$value[1]; - }, $values); - $mergeCellString .= ''; - } - $mergeCellString .= ''; - fwrite($worksheet->getFilePointer(), $mergeCellString); - } - - fwrite($worksheetFilePointer, ''); - fclose($worksheetFilePointer); - } - - /** - * Writes the sheet data header. - * - * @param Worksheet $worksheet The worksheet to add the row to - */ - private function ensureSheetDataStated(Worksheet $worksheet) - { - if (!$worksheet->getSheetDataStarted()) { - $worksheetFilePointer = $worksheet->getFilePointer(); - $sheet = $worksheet->getExternalSheet(); - if ($sheet->hasSheetView()) { - fwrite($worksheetFilePointer, ''.$sheet->getSheetView()->getXml().''); - } - fwrite($worksheetFilePointer, $this->getXMLFragmentForDefaultCellSizing()); - fwrite($worksheetFilePointer, $this->getXMLFragmentForColumnWidths()); - fwrite($worksheetFilePointer, ''); - $worksheet->setSheetDataStarted(true); - } - } - - /** - * Checks if the sheet has been sucessfully created. Throws an exception if not. - * - * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file - * - * @throws IOException If the sheet data file cannot be opened for writing - */ - private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer) - { - if (!$sheetFilePointer) { - throw new IOException('Unable to open sheet for writing.'); - } + $this->commentsManager->closeWorksheetCommentFiles($worksheet); + fclose($worksheet->getFilePointer()); } /** @@ -234,16 +123,16 @@ class WorksheetManager implements WorksheetManagerInterface * @throws InvalidArgumentException If a cell value's type is not supported * @throws IOException If the data cannot be written */ - private function addNonEmptyRow(Worksheet $worksheet, Row $row) + private function addNonEmptyRow(Worksheet $worksheet, Row $row): void { - $this->ensureSheetDataStated($worksheet); $sheetFilePointer = $worksheet->getFilePointer(); $rowStyle = $row->getStyle(); $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1; $numCells = $row->getNumCells(); - $hasCustomHeight = $this->defaultRowHeight > 0 ? '1' : '0'; - $rowXML = ""; + $rowHeight = $row->getHeight(); + $hasCustomHeight = ($this->options->DEFAULT_ROW_HEIGHT > 0 || $rowHeight > 0) ? '1' : '0'; + $rowXML = " 0 ? "ht=\"{$rowHeight}\" " : '')."customHeight=\"{$hasCustomHeight}\">"; foreach ($row->getCells() as $columnIndexZeroBased => $cell) { $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); @@ -302,39 +191,28 @@ class WorksheetManager implements WorksheetManagerInterface /** * Builds and returns xml for a single cell. * - * @param int $rowIndexOneBased - * @param int $columnIndexZeroBased - * @param int $styleId - * * @throws InvalidArgumentException If the given value cannot be processed - * - * @return string */ - private function getCellXML($rowIndexOneBased, $columnIndexZeroBased, Cell $cell, $styleId) + private function getCellXML(int $rowIndexOneBased, int $columnIndexZeroBased, Cell $cell, ?int $styleId): string { $columnLetters = CellHelper::getColumnLettersFromColumnIndex($columnIndexZeroBased); $cellXML = 'isString()) { + if ($cell instanceof Cell\StringCell) { $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue()); - } elseif ($cell->isBoolean()) { - $cellXML .= ' t="b">'.(int) ($cell->getValue()).''; - } elseif ($cell->isNumeric()) { - $cellXML .= '>'.$this->stringHelper->formatNumericValue($cell->getValue()).''; - } elseif ($cell->isFormula()) { + } elseif ($cell instanceof Cell\BooleanCell) { + $cellXML .= ' t="b">'.(int) $cell->getValue().''; + } elseif ($cell instanceof Cell\NumericCell) { + $cellXML .= '>'.$cell->getValue().''; + } elseif ($cell instanceof Cell\FormulaCell) { $cellXML .= '>'.substr($cell->getValue(), 1).''; - } elseif ($cell->isDate()) { - $value = $cell->getValue(); - if ($value instanceof \DateTimeInterface) { - $cellXML .= '>'.(string) DateHelper::toExcel($value).''; - } else { - throw new InvalidArgumentException('Trying to add a date value with an unsupported type: '.\gettype($value)); - } - } elseif ($cell->isError() && \is_string($cell->getValueEvenIfError())) { + } elseif ($cell instanceof Cell\DateTimeCell) { + $cellXML .= '>'.DateHelper::toExcel($cell->getValue()).''; + } elseif ($cell instanceof Cell\ErrorCell) { // only writes the error value if it's a string - $cellXML .= ' t="e">'.$cell->getValueEvenIfError().''; - } elseif ($cell->isEmpty()) { + $cellXML .= ' t="e">'.$cell->getRawValue().''; + } elseif ($cell instanceof Cell\EmptyCell) { if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) { $cellXML .= '/>'; } else { @@ -342,8 +220,6 @@ class WorksheetManager implements WorksheetManagerInterface // NOTE: not appending to $cellXML is the right behavior!! $cellXML = ''; } - } else { - throw new InvalidArgumentException('Trying to add a value with an unsupported type: '.\gettype($cell->getValue())); } return $cellXML; @@ -354,17 +230,17 @@ class WorksheetManager implements WorksheetManagerInterface * * @param string $cellValue The cell value * - * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell - * * @return string The XML fragment representing the cell + * + * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell */ - private function getCellXMLFragmentForNonEmptyString($cellValue) + private function getCellXMLFragmentForNonEmptyString(string $cellValue): string { if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) { throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)'); } - if ($this->shouldUseInlineStrings) { + if ($this->options->SHOULD_USE_INLINE_STRINGS) { $cellXMLFragment = ' t="inlineStr">'.$this->stringsEscaper->escape($cellValue).''; } else { $sharedStringId = $this->sharedStringsManager->writeString($cellValue); diff --git a/lib/openspout/src/Writer/XLSX/MergeCell.php b/lib/openspout/src/Writer/XLSX/MergeCell.php new file mode 100644 index 00000000000..3d342df529f --- /dev/null +++ b/lib/openspout/src/Writer/XLSX/MergeCell.php @@ -0,0 +1,27 @@ +setFontSize(self::DEFAULT_FONT_SIZE); + $defaultRowStyle->setFontName(self::DEFAULT_FONT_NAME); + + $this->DEFAULT_ROW_STYLE = $defaultRowStyle; + } + + /** + * Row coordinates are indexed from 1, columns from 0 (A = 0), + * so a merge B2:G2 looks like $writer->mergeCells(1, 2, 6, 2);. + * + * @param 0|positive-int $topLeftColumn + * @param positive-int $topLeftRow + * @param 0|positive-int $bottomRightColumn + * @param positive-int $bottomRightRow + * @param 0|positive-int $sheetIndex + */ + public function mergeCells( + int $topLeftColumn, + int $topLeftRow, + int $bottomRightColumn, + int $bottomRightRow, + int $sheetIndex = 0, + ): void { + $this->MERGE_CELLS[] = new MergeCell( + $sheetIndex, + $topLeftColumn, + $topLeftRow, + $bottomRightColumn, + $bottomRightRow + ); + } + + /** + * @return MergeCell[] + * + * @internal + */ + public function getMergeCells(): array + { + return $this->MERGE_CELLS; + } +} diff --git a/lib/openspout/src/Writer/XLSX/Writer.php b/lib/openspout/src/Writer/XLSX/Writer.php index fa3a0548ba6..4c4410a5474 100644 --- a/lib/openspout/src/Writer/XLSX/Writer.php +++ b/lib/openspout/src/Writer/XLSX/Writer.php @@ -1,72 +1,76 @@ throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - - $this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder); - - return $this; + $this->options = $options ?? new Options(); } - /** - * Use inline string to be more memory efficient. If set to false, it will use shared strings. - * This must be set before opening the writer. - * - * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used - * - * @throws \OpenSpout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened - * - * @return Writer - */ - public function setShouldUseInlineStrings($shouldUseInlineStrings) + public function getOptions(): Options { - $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - - $this->optionsManager->setOption(Options::SHOULD_USE_INLINE_STRINGS, $shouldUseInlineStrings); - - return $this; + return $this->options; } - /** - * Merge cells. - * Row coordinates are indexed from 1, columns from 0 (A = 0), - * so a merge B2:G2 looks like $writer->mergeCells([1,2], [6, 2]);. - * - * You may use CellHelper::getColumnLettersFromColumnIndex() to convert from "B2" to "[1,2]" - * - * @param int[] $range1 - top left cell's coordinate [column, row] - * @param int[] $range2 - bottom right cell's coordinate [column, row] - * - * @return $this - */ - public function mergeCells(array $range1, array $range2) + protected function createWorkbookManager(): WorkbookManager { - $this->optionsManager->addOption(Options::MERGE_CELLS, [$range1, $range2]); + $workbook = new Workbook(); - return $this; + $fileSystemHelper = new FileSystemHelper( + $this->options->getTempFolder(), + new ZipHelper(), + new XLSX() + ); + $fileSystemHelper->createBaseFilesAndFolders(); + + $xlFolder = $fileSystemHelper->getXlFolder(); + $sharedStringsManager = new SharedStringsManager($xlFolder, new XLSX()); + + $styleMerger = new StyleMerger(); + $styleManager = new StyleManager(new StyleRegistry($this->options->DEFAULT_ROW_STYLE)); + + $commentsManager = new CommentsManager($xlFolder, new XLSX()); + + $worksheetManager = new WorksheetManager( + $this->options, + $styleManager, + $styleMerger, + $commentsManager, + $sharedStringsManager, + new XLSX(), + StringHelper::factory() + ); + + return new WorkbookManager( + $workbook, + $this->options, + $worksheetManager, + $styleManager, + $styleMerger, + $fileSystemHelper + ); } } diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 2b190aa5ce6..207b6a35f06 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -377,7 +377,7 @@ All rights reserved. openspout OpenSpout Library to read and write spreadsheet files (CSV, XLSX and ODS). - 3.7.3 + 4.13.0 MIT https://github.com/openspout/openspout