├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── UPGRADE-3.0.md ├── composer.json ├── composer.lock ├── phpstan.neon └── src └── Spout ├── Autoloader ├── Psr4Autoloader.php └── autoload.php ├── Common ├── Creator │ └── HelperFactory.php ├── Entity │ ├── Cell.php │ ├── Row.php │ └── Style │ │ ├── Border.php │ │ ├── BorderPart.php │ │ ├── CellAlignment.php │ │ ├── Color.php │ │ └── Style.php ├── Exception │ ├── EncodingConversionException.php │ ├── IOException.php │ ├── InvalidArgumentException.php │ ├── InvalidColorException.php │ ├── SpoutException.php │ └── UnsupportedTypeException.php ├── Helper │ ├── CellTypeHelper.php │ ├── EncodingHelper.php │ ├── Escaper │ │ ├── CSV.php │ │ ├── EscaperInterface.php │ │ ├── ODS.php │ │ └── XLSX.php │ ├── FileSystemHelper.php │ ├── FileSystemHelperInterface.php │ ├── GlobalFunctionsHelper.php │ └── StringHelper.php ├── Manager │ ├── OptionsManagerAbstract.php │ └── OptionsManagerInterface.php └── Type.php ├── Reader ├── CSV │ ├── Creator │ │ └── InternalEntityFactory.php │ ├── Manager │ │ └── OptionsManager.php │ ├── Reader.php │ ├── RowIterator.php │ ├── Sheet.php │ └── SheetIterator.php ├── Common │ ├── Creator │ │ ├── InternalEntityFactoryInterface.php │ │ ├── ReaderEntityFactory.php │ │ └── ReaderFactory.php │ ├── Entity │ │ └── Options.php │ ├── Manager │ │ └── RowManager.php │ └── XMLProcessor.php ├── Exception │ ├── InvalidValueException.php │ ├── IteratorNotRewindableException.php │ ├── NoSheetsFoundException.php │ ├── ReaderException.php │ ├── ReaderNotOpenedException.php │ ├── SharedStringNotFoundException.php │ └── XMLProcessingException.php ├── IteratorInterface.php ├── ODS │ ├── Creator │ │ ├── HelperFactory.php │ │ ├── InternalEntityFactory.php │ │ └── ManagerFactory.php │ ├── Helper │ │ ├── CellValueFormatter.php │ │ └── SettingsHelper.php │ ├── Manager │ │ └── OptionsManager.php │ ├── Reader.php │ ├── RowIterator.php │ ├── Sheet.php │ └── SheetIterator.php ├── ReaderAbstract.php ├── ReaderInterface.php ├── SheetInterface.php ├── Wrapper │ ├── XMLInternalErrorsHelper.php │ └── XMLReader.php └── XLSX │ ├── Creator │ ├── HelperFactory.php │ ├── InternalEntityFactory.php │ └── ManagerFactory.php │ ├── Helper │ ├── CellHelper.php │ ├── CellValueFormatter.php │ └── DateFormatHelper.php │ ├── Manager │ ├── OptionsManager.php │ ├── SharedStringsCaching │ │ ├── CachingStrategyFactory.php │ │ ├── CachingStrategyInterface.php │ │ ├── FileBasedStrategy.php │ │ └── InMemoryStrategy.php │ ├── SharedStringsManager.php │ ├── SheetManager.php │ ├── StyleManager.php │ └── WorkbookRelationshipsManager.php │ ├── Reader.php │ ├── RowIterator.php │ ├── Sheet.php │ └── SheetIterator.php └── Writer ├── CSV ├── Manager │ └── OptionsManager.php └── Writer.php ├── Common ├── Creator │ ├── InternalEntityFactory.php │ ├── ManagerFactoryInterface.php │ ├── Style │ │ ├── BorderBuilder.php │ │ └── StyleBuilder.php │ ├── WriterEntityFactory.php │ └── WriterFactory.php ├── Entity │ ├── Options.php │ ├── Sheet.php │ ├── Workbook.php │ └── Worksheet.php ├── Helper │ ├── CellHelper.php │ ├── FileSystemWithRootFolderHelperInterface.php │ └── ZipHelper.php └── Manager │ ├── CellManager.php │ ├── RegisteredStyle.php │ ├── RowManager.php │ ├── SheetManager.php │ ├── Style │ ├── PossiblyUpdatedStyle.php │ ├── StyleManager.php │ ├── StyleManagerInterface.php │ ├── StyleMerger.php │ └── StyleRegistry.php │ ├── WorkbookManagerAbstract.php │ ├── WorkbookManagerInterface.php │ └── WorksheetManagerInterface.php ├── Exception ├── Border │ ├── InvalidNameException.php │ ├── InvalidStyleException.php │ └── InvalidWidthException.php ├── InvalidSheetNameException.php ├── SheetNotFoundException.php ├── WriterAlreadyOpenedException.php ├── WriterException.php └── WriterNotOpenedException.php ├── ODS ├── Creator │ ├── HelperFactory.php │ └── ManagerFactory.php ├── Helper │ ├── BorderHelper.php │ └── FileSystemHelper.php ├── Manager │ ├── OptionsManager.php │ ├── Style │ │ ├── StyleManager.php │ │ └── StyleRegistry.php │ ├── WorkbookManager.php │ └── WorksheetManager.php └── Writer.php ├── WriterAbstract.php ├── WriterInterface.php ├── WriterMultiSheetsAbstract.php └── XLSX ├── Creator ├── HelperFactory.php └── ManagerFactory.php ├── Helper ├── BorderHelper.php └── FileSystemHelper.php ├── Manager ├── OptionsManager.php ├── SharedStringsManager.php ├── Style │ ├── StyleManager.php │ └── StyleRegistry.php ├── WorkbookManager.php └── WorksheetManager.php └── Writer.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adrilo 2 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->name('*.php') 6 | ->exclude('vendor'); 7 | 8 | $config = new PhpCsFixer\Config(); 9 | return $config 10 | ->setRiskyAllowed(true) 11 | ->setRules([ 12 | '@Symfony' => true, 13 | 'align_multiline_comment' => false, 14 | 'array_syntax' => ['syntax' => 'short'], 15 | 'binary_operator_spaces' => ['default' => null], 16 | 'blank_line_before_statement' => ['statements' => ['return']], 17 | 'combine_consecutive_unsets' => true, 18 | 'concat_space' => ['spacing' => 'one'], 19 | 'declare_equal_normalize' => ['space' => 'single'], 20 | 'heredoc_to_nowdoc' => true, 21 | 'increment_style' => ['style' => 'post'], 22 | 'is_null' => true, 23 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 24 | 'modernize_types_casting' => true, 25 | 'no_break_comment' => ['comment_text' => 'do nothing'], 26 | 'no_empty_phpdoc' => false, 27 | 'no_null_property_initialization' => true, 28 | 'echo_tag_syntax' => false, 29 | 'no_superfluous_elseif' => true, 30 | 'no_superfluous_phpdoc_tags' => false, 31 | 'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'switch_case', 'yield']], 32 | 'no_unneeded_curly_braces' => true, 33 | 'no_unneeded_final_method' => true, 34 | 'no_useless_else' => false, 35 | 'no_useless_return' => true, 36 | 'ordered_imports' => true, 37 | 'phpdoc_add_missing_param_annotation' => true, 38 | 'phpdoc_align' => false, 39 | 'phpdoc_annotation_without_dot' => false, 40 | 'phpdoc_no_empty_return' => false, 41 | 'phpdoc_order' => true, 42 | 'phpdoc_summary' => false, 43 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 44 | 'phpdoc_separation' => false, 45 | 'protected_to_private' => true, 46 | 'psr_autoloading' => true, 47 | 'return_type_declaration' => ['space_before' => 'one'], 48 | 'semicolon_after_instruction' => true, 49 | 'simplified_null_return' => false, 50 | 'single_line_comment_style' => ['comment_types' => ['hash']], 51 | 'strict_comparison' => true, 52 | 'yoda_style' => ['equal' => false, 'identical' => false], 53 | ]) 54 | ->setFinder($finder); 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spout 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout) 4 | [![Project Status](https://opensource.box.com/badges/inactive.svg)](https://opensource.box.com/badges) 5 | [![example workflow](https://github.com/box/spout/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/box/spout/actions/workflows/ci.yml?query=branch%3Amaster) 6 | [![Coverage Status](https://coveralls.io/repos/github/box/spout/badge.svg?branch=master)](https://coveralls.io/github/box/spout?branch=master) 7 | [![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) 8 | 9 | ## 🪦 Archived project 🪦 10 | 11 | This project has been archived and is no longer maintained. No bug fix and no additional features will be added.
12 | You won't be able to submit new issues or pull requests, and no additional features will be added 13 | 14 | You can still use Spout as is in your projects though :) 15 | 16 | > Thanks to everyone who contributed to this project, from a typo fix to the new cool feature.
17 | > It was great to see the involvement of this community! 18 | 19 |
20 | 21 | ## About 22 | 23 | Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. 24 | Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB). 25 | 26 | Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 27 | 28 | 29 | ## Documentation 30 | 31 | Full documentation can be found at [https://opensource.box.com/spout/](https://opensource.box.com/spout/). 32 | 33 | 34 | ## Requirements 35 | 36 | * PHP version 7.2 or higher 37 | * PHP extension `php_zip` enabled 38 | * PHP extension `php_xmlreader` enabled 39 | 40 | ## Upgrade guide 41 | 42 | 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). 43 | 44 | ## Running tests 45 | 46 | The `master` branch includes unit, functional and performance tests. 47 | If you just want to check that everything is working as expected, executing the unit and functional tests is enough. 48 | 49 | * `phpunit` - runs unit and functional tests 50 | * `phpunit --group perf-tests` - only runs the performance tests 51 | 52 | For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing). 53 | 54 | ## Support 55 | 56 | Spout is no longer actively supported. You can still ask questions, or discuss about it in the chat room:
57 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 58 | 59 | ## Copyright and License 60 | 61 | Copyright 2022 Box, Inc. All rights reserved. 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /UPGRADE-3.0.md: -------------------------------------------------------------------------------- 1 | Upgrading from 2.x to 3.0 2 | ========================= 3 | 4 | Spout 3.0 introduced several backwards-incompatible changes. The upgrade from Spout 2.x to 3.0 must therefore be done with caution. 5 | This guide is meant to ease this process. 6 | 7 | Most notable changes 8 | -------------------- 9 | In 2.x, styles were applied per row; it was therefore impossible to apply different styles to cells in the same row. 10 | With the 3.0 version, this is now possible: each cell can have its own style. 11 | 12 | Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value. 13 | 14 | Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community. 15 | 16 | Reader changes 17 | -------------- 18 | Creating a reader should now be done through the Reader `ReaderEntityFactory`, instead of using the `ReaderFactory`. 19 | Also, the `ReaderFactory::create($type)` method was removed and replaced by methods for each reader: 20 | ```php 21 | use Box\Spout\Reader\Common\Creator\ReaderEntityFactory; // namespace is no longer "Box\Spout\Reader" 22 | ... 23 | $reader = ReaderEntityFactory::createXLSXReader(); // replaces ReaderFactory::create(Type::XLSX) 24 | $reader = ReaderEntityFactory::createCSVReader(); // replaces ReaderFactory::create(Type::CSV) 25 | $reader = ReaderEntityFactory::createODSReader(); // replaces ReaderFactory::create(Type::ODS) 26 | ``` 27 | 28 | When iterating over the spreadsheet rows, Spout now returns `Row` objects, instead of an array containing row values. Accessing the row values should now be done this way: 29 | ```php 30 | ... 31 | foreach ($reader->getSheetIterator() as $sheet) { 32 | foreach ($sheet->getRowIterator() as $row) { // $row is a "Row" object, not an array 33 | $rowAsArray = $row->toArray(); // this is the 2.x equivalent 34 | // OR 35 | $cellsArray = $row->getCells(); // this can be used to get access to cells' details 36 | ... 37 | } 38 | } 39 | ``` 40 | 41 | Writer changes 42 | -------------- 43 | Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`, instead of using the `WriterFactory`. 44 | Also, the `WriterFactory::create($type)` method was removed and replaced by methods for each writer: 45 | 46 | ```php 47 | use Box\Spout\Writer\Common\Creator\WriterEntityFactory; // namespace is no longer "Box\Spout\Writer" 48 | ... 49 | $writer = WriterEntityFactory::createXLSXWriter(); // replaces WriterFactory::create(Type::XLSX) 50 | $writer = WriterEntityFactory::createCSVWriter(); // replaces WriterFactory::create(Type::CSV) 51 | $writer = WriterEntityFactory::createODSWriter(); // replaces WriterFactory::create(Type::ODS) 52 | ``` 53 | 54 | Adding rows is also done differently: instead of passing an array, the writer now takes in a `Row` object (or an array of `Row`). Creating such objects can easily be done this way: 55 | ```php 56 | // Adding a row from an array of values (2.x equivalent) 57 | $cellValues = ['foo', 12345]; 58 | $row1 = WriterEntityFactory::createRowFromArray($cellValues, $rowStyle); 59 | 60 | // Adding a row from an array of Cell 61 | $cell1 = WriterEntityFactory::createCell('foo', $cellStyle1); // this cell has its own style 62 | $cell2 = WriterEntityFactory::createCell(12345, $cellStyle2); // this cell has its own style 63 | $row2 = WriterEntityFactory::createRow([$cell1, $cell2]); 64 | 65 | $writer->addRows([$row1, $row2]); 66 | ``` 67 | 68 | Namespace changes for styles 69 | ----------------- 70 | The namespaces for styles have changed. Styles are still created by using a `builder` class. 71 | 72 | For the builder, please update your import statements to use the following namespaces: 73 | 74 | Box\Spout\Writer\Common\Creator\Style\StyleBuilder 75 | Box\Spout\Writer\Common\Creator\Style\BorderBuilder 76 | 77 | The `Style` base class and style definitions like `Border`, `BorderPart` and `Color` also have a new namespace. 78 | 79 | If your are using these classes directly via an import statement in your code, please use the following namespaces: 80 | 81 | Box\Spout\Common\Entity\Style\Border 82 | Box\Spout\Common\Entity\Style\BorderPart 83 | Box\Spout\Common\Entity\Style\Color 84 | Box\Spout\Common\Entity\Style\Style 85 | 86 | Handling of empty rows 87 | ---------------------- 88 | In 2.x, empty rows were not added to the spreadsheet. 89 | In 3.0, `addRow` now always writes a row to the spreadsheet: when the row does not contain any cells, an empty row is created in the sheet. 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "box/spout", 3 | "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way", 4 | "type": "library", 5 | "keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"], 6 | "license": "Apache-2.0", 7 | "homepage": "https://www.github.com/box/spout", 8 | "authors": [ 9 | { 10 | "name": "Adrien Loison", 11 | "email": "adrien@box.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.3.0", 16 | "ext-zip": "*", 17 | "ext-xmlreader": "*", 18 | "ext-dom": "*" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^9", 22 | "friendsofphp/php-cs-fixer": "^3", 23 | "phpstan/phpstan": "^1" 24 | }, 25 | "suggest": { 26 | "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", 27 | "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Box\\Spout\\": "src/Spout" 32 | } 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "3.1.x-dev" 37 | } 38 | }, 39 | "config": { 40 | "platform": { 41 | "php": "7.3" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 4 3 | paths: 4 | - src 5 | - tests 6 | excludePaths: 7 | # Exclude these files that are OK 8 | - src/Spout/Reader/Common/Creator/ReaderEntityFactory.php 9 | - src/Spout/Writer/Common/Creator/WriterEntityFactory.php 10 | -------------------------------------------------------------------------------- /src/Spout/Autoloader/Psr4Autoloader.php: -------------------------------------------------------------------------------- 1 | prefixes[$prefix]) === false) { 50 | $this->prefixes[$prefix] = []; 51 | } 52 | 53 | // retain the base directory for the namespace prefix 54 | if ($prepend) { 55 | \array_unshift($this->prefixes[$prefix], $baseDir); 56 | } else { 57 | \array_push($this->prefixes[$prefix], $baseDir); 58 | } 59 | } 60 | 61 | /** 62 | * Loads the class file for a given class name. 63 | * 64 | * @param string $class The fully-qualified class name. 65 | * @return mixed The mapped file name on success, or boolean false on 66 | * failure. 67 | */ 68 | public function loadClass($class) 69 | { 70 | // the current namespace prefix 71 | $prefix = $class; 72 | 73 | // work backwards through the namespace names of the fully-qualified 74 | // class name to find a mapped file name 75 | while (($pos = \strrpos($prefix, '\\')) !== false) { 76 | // retain the trailing namespace separator in the prefix 77 | $prefix = \substr($class, 0, $pos + 1); 78 | 79 | // the rest is the relative class name 80 | $relativeClass = \substr($class, $pos + 1); 81 | 82 | // try to load a mapped file for the prefix and relative class 83 | $mappedFile = $this->loadMappedFile($prefix, $relativeClass); 84 | if ($mappedFile !== false) { 85 | return $mappedFile; 86 | } 87 | 88 | // remove the trailing namespace separator for the next iteration 89 | // of strrpos() 90 | $prefix = \rtrim($prefix, '\\'); 91 | } 92 | 93 | // never found a mapped file 94 | return false; 95 | } 96 | 97 | /** 98 | * Load the mapped file for a namespace prefix and relative class. 99 | * 100 | * @param string $prefix The namespace prefix. 101 | * @param string $relativeClass The relative class name. 102 | * @return mixed Boolean false if no mapped file can be loaded, or the 103 | * name of the mapped file that was loaded. 104 | */ 105 | protected function loadMappedFile($prefix, $relativeClass) 106 | { 107 | // are there any base directories for this namespace prefix? 108 | if (isset($this->prefixes[$prefix]) === false) { 109 | return false; 110 | } 111 | 112 | // look through base directories for this namespace prefix 113 | foreach ($this->prefixes[$prefix] as $baseDir) { 114 | // replace the namespace prefix with the base directory, 115 | // replace namespace separators with directory separators 116 | // in the relative class name, append with .php 117 | $file = $baseDir 118 | . \str_replace('\\', '/', $relativeClass) 119 | . '.php'; 120 | 121 | // if the mapped file exists, require it 122 | if ($this->requireFile($file)) { 123 | // yes, we're done 124 | return $file; 125 | } 126 | } 127 | 128 | // never found it 129 | return false; 130 | } 131 | 132 | /** 133 | * If a file exists, require it from the file system. 134 | * 135 | * @param string $file The file to require. 136 | * @return bool True if the file exists, false if not. 137 | */ 138 | protected function requireFile($file) 139 | { 140 | if (\file_exists($file)) { 141 | require $file; 142 | 143 | return true; 144 | } 145 | 146 | return false; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Spout/Autoloader/autoload.php: -------------------------------------------------------------------------------- 1 | register(); 15 | $loader->addNamespace('Box\Spout', $srcBaseDirectory); 16 | -------------------------------------------------------------------------------- /src/Spout/Common/Creator/HelperFactory.php: -------------------------------------------------------------------------------- 1 | setValue($value); 74 | $this->setStyle($style); 75 | } 76 | 77 | /** 78 | * @param mixed|null $value 79 | */ 80 | public function setValue($value) 81 | { 82 | $this->value = $value; 83 | $this->type = $this->detectType($value); 84 | } 85 | 86 | /** 87 | * @return mixed|null 88 | */ 89 | public function getValue() 90 | { 91 | return !$this->isError() ? $this->value : null; 92 | } 93 | 94 | /** 95 | * @return mixed 96 | */ 97 | public function getValueEvenIfError() 98 | { 99 | return $this->value; 100 | } 101 | 102 | /** 103 | * @param Style|null $style 104 | */ 105 | public function setStyle($style) 106 | { 107 | $this->style = $style ?: new Style(); 108 | } 109 | 110 | /** 111 | * @return Style 112 | */ 113 | public function getStyle() 114 | { 115 | return $this->style; 116 | } 117 | 118 | /** 119 | * @return int|null 120 | */ 121 | public function getType() 122 | { 123 | return $this->type; 124 | } 125 | 126 | /** 127 | * @param int $type 128 | */ 129 | public function setType($type) 130 | { 131 | $this->type = $type; 132 | } 133 | 134 | /** 135 | * Get the current value type 136 | * 137 | * @param mixed|null $value 138 | * @return int 139 | */ 140 | protected function detectType($value) 141 | { 142 | if (CellTypeHelper::isBoolean($value)) { 143 | return self::TYPE_BOOLEAN; 144 | } 145 | if (CellTypeHelper::isEmpty($value)) { 146 | return self::TYPE_EMPTY; 147 | } 148 | if (CellTypeHelper::isNumeric($value)) { 149 | return self::TYPE_NUMERIC; 150 | } 151 | if (CellTypeHelper::isDateTimeOrDateInterval($value)) { 152 | return self::TYPE_DATE; 153 | } 154 | if (CellTypeHelper::isNonEmptyString($value)) { 155 | return self::TYPE_STRING; 156 | } 157 | 158 | return self::TYPE_ERROR; 159 | } 160 | 161 | /** 162 | * @return bool 163 | */ 164 | public function isBoolean() 165 | { 166 | return $this->type === self::TYPE_BOOLEAN; 167 | } 168 | 169 | /** 170 | * @return bool 171 | */ 172 | public function isEmpty() 173 | { 174 | return $this->type === self::TYPE_EMPTY; 175 | } 176 | 177 | /** 178 | * @return bool 179 | */ 180 | public function isNumeric() 181 | { 182 | return $this->type === self::TYPE_NUMERIC; 183 | } 184 | 185 | /** 186 | * @return bool 187 | */ 188 | public function isString() 189 | { 190 | return $this->type === self::TYPE_STRING; 191 | } 192 | 193 | /** 194 | * @return bool 195 | */ 196 | public function isDate() 197 | { 198 | return $this->type === self::TYPE_DATE; 199 | } 200 | 201 | /** 202 | * @return bool 203 | */ 204 | public function isError() 205 | { 206 | return $this->type === self::TYPE_ERROR; 207 | } 208 | 209 | /** 210 | * @return string 211 | */ 212 | public function __toString() 213 | { 214 | return (string) $this->getValue(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Spout/Common/Entity/Row.php: -------------------------------------------------------------------------------- 1 | setCells($cells) 30 | ->setStyle($style); 31 | } 32 | 33 | /** 34 | * @return Cell[] $cells 35 | */ 36 | public function getCells() 37 | { 38 | return $this->cells; 39 | } 40 | 41 | /** 42 | * @param Cell[] $cells 43 | * @return Row 44 | */ 45 | public function setCells(array $cells) 46 | { 47 | $this->cells = []; 48 | foreach ($cells as $cell) { 49 | $this->addCell($cell); 50 | } 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * @param Cell $cell 57 | * @param int $cellIndex 58 | * @return Row 59 | */ 60 | public function setCellAtIndex(Cell $cell, $cellIndex) 61 | { 62 | $this->cells[$cellIndex] = $cell; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @param int $cellIndex 69 | * @return Cell|null 70 | */ 71 | public function getCellAtIndex($cellIndex) 72 | { 73 | return $this->cells[$cellIndex] ?? null; 74 | } 75 | 76 | /** 77 | * @param Cell $cell 78 | * @return Row 79 | */ 80 | public function addCell(Cell $cell) 81 | { 82 | $this->cells[] = $cell; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @return int 89 | */ 90 | public function getNumCells() 91 | { 92 | // When using "setCellAtIndex", it's possible to 93 | // have "$this->cells" contain holes. 94 | if (empty($this->cells)) { 95 | return 0; 96 | } 97 | 98 | return \max(\array_keys($this->cells)) + 1; 99 | } 100 | 101 | /** 102 | * @return Style 103 | */ 104 | public function getStyle() 105 | { 106 | return $this->style; 107 | } 108 | 109 | /** 110 | * @param Style|null $style 111 | * @return Row 112 | */ 113 | public function setStyle($style) 114 | { 115 | $this->style = $style ?: new Style(); 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * @return array The row values, as array 122 | */ 123 | public function toArray() 124 | { 125 | return \array_map(function (Cell $cell) { 126 | return $cell->getValue(); 127 | }, $this->cells); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Spout/Common/Entity/Style/Border.php: -------------------------------------------------------------------------------- 1 | setParts($borderParts); 34 | } 35 | 36 | /** 37 | * @param string $name The name of the border part 38 | * @return BorderPart|null 39 | */ 40 | public function getPart($name) 41 | { 42 | return $this->hasPart($name) ? $this->parts[$name] : null; 43 | } 44 | 45 | /** 46 | * @param string $name The name of the border part 47 | * @return bool 48 | */ 49 | public function hasPart($name) 50 | { 51 | return isset($this->parts[$name]); 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | public function getParts() 58 | { 59 | return $this->parts; 60 | } 61 | 62 | /** 63 | * Set BorderParts 64 | * @param array $parts 65 | * @return void 66 | */ 67 | public function setParts($parts) 68 | { 69 | unset($this->parts); 70 | foreach ($parts as $part) { 71 | $this->addPart($part); 72 | } 73 | } 74 | 75 | /** 76 | * @param BorderPart $borderPart 77 | * @return Border 78 | */ 79 | public function addPart(BorderPart $borderPart) 80 | { 81 | $this->parts[$borderPart->getName()] = $borderPart; 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Spout/Common/Entity/Style/BorderPart.php: -------------------------------------------------------------------------------- 1 | setName($name); 76 | $this->setColor($color); 77 | $this->setWidth($width); 78 | $this->setStyle($style); 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getName() 85 | { 86 | return $this->name; 87 | } 88 | 89 | /** 90 | * @param string $name The name of the border part @see BorderPart::$allowedNames 91 | * @throws InvalidNameException 92 | * @return void 93 | */ 94 | public function setName($name) 95 | { 96 | if (!\in_array($name, self::$allowedNames)) { 97 | throw new InvalidNameException($name); 98 | } 99 | $this->name = $name; 100 | } 101 | 102 | /** 103 | * @return string 104 | */ 105 | public function getStyle() 106 | { 107 | return $this->style; 108 | } 109 | 110 | /** 111 | * @param string $style The style of the border part @see BorderPart::$allowedStyles 112 | * @throws InvalidStyleException 113 | * @return void 114 | */ 115 | public function setStyle($style) 116 | { 117 | if (!\in_array($style, self::$allowedStyles)) { 118 | throw new InvalidStyleException($style); 119 | } 120 | $this->style = $style; 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | public function getColor() 127 | { 128 | return $this->color; 129 | } 130 | 131 | /** 132 | * @param string $color The color of the border part @see Color::rgb() 133 | * @return void 134 | */ 135 | public function setColor($color) 136 | { 137 | $this->color = $color; 138 | } 139 | 140 | /** 141 | * @return string 142 | */ 143 | public function getWidth() 144 | { 145 | return $this->width; 146 | } 147 | 148 | /** 149 | * @param string $width The width of the border part @see BorderPart::$allowedWidths 150 | * @throws InvalidWidthException 151 | * @return void 152 | */ 153 | public function setWidth($width) 154 | { 155 | if (!\in_array($width, self::$allowedWidths)) { 156 | throw new InvalidWidthException($width); 157 | } 158 | $this->width = $width; 159 | } 160 | 161 | /** 162 | * @return array 163 | */ 164 | public static function getAllowedStyles() 165 | { 166 | return self::$allowedStyles; 167 | } 168 | 169 | /** 170 | * @return array 171 | */ 172 | public static function getAllowedNames() 173 | { 174 | return self::$allowedNames; 175 | } 176 | 177 | /** 178 | * @return array 179 | */ 180 | public static function getAllowedWidths() 181 | { 182 | return self::$allowedWidths; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Spout/Common/Entity/Style/CellAlignment.php: -------------------------------------------------------------------------------- 1 | 1, 18 | self::RIGHT => 1, 19 | self::CENTER => 1, 20 | self::JUSTIFY => 1, 21 | ]; 22 | 23 | /** 24 | * @param string $cellAlignment 25 | * 26 | * @return bool Whether the given cell alignment is valid 27 | */ 28 | public static function isValid($cellAlignment) 29 | { 30 | return isset(self::$VALID_ALIGNMENTS[$cellAlignment]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Spout/Common/Entity/Style/Color.php: -------------------------------------------------------------------------------- 1 | 255) { 58 | throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent"); 59 | } 60 | } 61 | 62 | /** 63 | * Converts the color component to its corresponding hexadecimal value 64 | * 65 | * @param int $colorComponent Color component, 0 - 255 66 | * @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d" 67 | */ 68 | protected static function convertColorComponentToHex($colorComponent) 69 | { 70 | return \str_pad(\dechex($colorComponent), 2, '0', STR_PAD_LEFT); 71 | } 72 | 73 | /** 74 | * Returns the ARGB color of the given RGB color, 75 | * assuming that alpha value is always 1. 76 | * 77 | * @param string $rgbColor RGB color like "FF08B2" 78 | * @return string ARGB color 79 | */ 80 | public static function toARGB($rgbColor) 81 | { 82 | return 'FF' . $rgbColor; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Spout/Common/Exception/EncodingConversionException.php: -------------------------------------------------------------------------------- 1 | ', '&') as well as 20 | // single/double quotes (for XML attributes) need to be encoded. 21 | if (\defined('ENT_DISALLOWED')) { 22 | // 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced. 23 | // Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor 24 | // @link https://github.com/box/spout/issues/329 25 | $replacedString = \htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8'); 26 | } else { 27 | // We are on hhvm or any other engine that does not support ENT_DISALLOWED. 28 | $escapedString = \htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 29 | 30 | // control characters values are from 0 to 1F (hex values) in the ASCII table 31 | // some characters should not be escaped though: "\t", "\r" and "\n". 32 | $regexPattern = '[\x00-\x08' . 33 | // skipping "\t" (0x9) and "\n" (0xA) 34 | '\x0B-\x0C' . 35 | // skipping "\r" (0xD) 36 | '\x0E-\x1F]'; 37 | $replacedString = \preg_replace("/$regexPattern/", '�', $escapedString); 38 | } 39 | 40 | return $replacedString; 41 | } 42 | 43 | /** 44 | * Unescapes the given string to make it compatible with XLSX 45 | * 46 | * @param string $string The string to unescape 47 | * @return string The unescaped string 48 | */ 49 | public function unescape($string) 50 | { 51 | // ============== 52 | // = WARNING = 53 | // ============== 54 | // It is assumed that the given string has already had its XML entities decoded. 55 | // This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation). 56 | // Therefore there is no need to call "htmlspecialchars_decode()". 57 | return $string; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Spout/Common/Helper/FileSystemHelperInterface.php: -------------------------------------------------------------------------------- 1 | hasMbstringSupport = \extension_loaded('mbstring'); 28 | $this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0; 29 | $this->localeInfo = \localeconv(); 30 | } 31 | 32 | /** 33 | * Returns the length of the given string. 34 | * It uses the multi-bytes function is available. 35 | * @see strlen 36 | * @see mb_strlen 37 | * 38 | * @param string $string 39 | * @return int 40 | */ 41 | public function getStringLength($string) 42 | { 43 | return $this->hasMbstringSupport ? \mb_strlen($string) : \strlen($string); 44 | } 45 | 46 | /** 47 | * Returns the position of the first occurrence of the given character/substring within the given string. 48 | * It uses the multi-bytes function is available. 49 | * @see strpos 50 | * @see mb_strpos 51 | * 52 | * @param string $char Needle 53 | * @param string $string Haystack 54 | * @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found 55 | */ 56 | public function getCharFirstOccurrencePosition($char, $string) 57 | { 58 | $position = $this->hasMbstringSupport ? \mb_strpos($string, $char) : \strpos($string, $char); 59 | 60 | return ($position !== false) ? $position : -1; 61 | } 62 | 63 | /** 64 | * Returns the position of the last occurrence of the given character/substring within the given string. 65 | * It uses the multi-bytes function is available. 66 | * @see strrpos 67 | * @see mb_strrpos 68 | * 69 | * @param string $char Needle 70 | * @param string $string Haystack 71 | * @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found 72 | */ 73 | public function getCharLastOccurrencePosition($char, $string) 74 | { 75 | $position = $this->hasMbstringSupport ? \mb_strrpos($string, $char) : \strrpos($string, $char); 76 | 77 | return ($position !== false) ? $position : -1; 78 | } 79 | 80 | /** 81 | * Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format. 82 | * 83 | * Formatting of float values is locale dependent in PHP < 8. 84 | * Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34). 85 | * However, float values must be formatted with no thousands separator and a "." as decimal point 86 | * to work properly. This method can be used to convert the value to the correct format before storing it. 87 | * 88 | * @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8. 89 | * 90 | * @param int|float $numericValue 91 | * @return int|float|string 92 | */ 93 | public function formatNumericValue($numericValue) 94 | { 95 | if ($this->isRunningPhp7OrOlder && is_float($numericValue)) { 96 | return str_replace( 97 | [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']], 98 | ['', '.'], 99 | $numericValue 100 | ); 101 | } 102 | 103 | return $numericValue; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Spout/Common/Manager/OptionsManagerAbstract.php: -------------------------------------------------------------------------------- 1 | OPTION_VALUE] */ 16 | private $options = []; 17 | 18 | /** 19 | * OptionsManagerAbstract constructor. 20 | */ 21 | public function __construct() 22 | { 23 | $this->supportedOptions = $this->getSupportedOptions(); 24 | $this->setDefaultOptions(); 25 | } 26 | 27 | /** 28 | * @return array List of supported options 29 | */ 30 | abstract protected function getSupportedOptions(); 31 | 32 | /** 33 | * Sets the default options. 34 | * To be overriden by child classes 35 | * 36 | * @return void 37 | */ 38 | abstract protected function setDefaultOptions(); 39 | 40 | /** 41 | * Sets the given option, if this option is supported. 42 | * 43 | * @param string $optionName 44 | * @param mixed $optionValue 45 | * @return void 46 | */ 47 | public function setOption($optionName, $optionValue) 48 | { 49 | if (\in_array($optionName, $this->supportedOptions)) { 50 | $this->options[$optionName] = $optionValue; 51 | } 52 | } 53 | 54 | /** 55 | * @param string $optionName 56 | * @return mixed|null The set option or NULL if no option with given name found 57 | */ 58 | public function getOption($optionName) 59 | { 60 | $optionValue = null; 61 | 62 | if (isset($this->options[$optionName])) { 63 | $optionValue = $this->options[$optionName]; 64 | } 65 | 66 | return $optionValue; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Spout/Common/Manager/OptionsManagerInterface.php: -------------------------------------------------------------------------------- 1 | helperFactory = $helperFactory; 30 | } 31 | 32 | /** 33 | * @param resource $filePointer Pointer to the CSV file to read 34 | * @param OptionsManagerInterface $optionsManager 35 | * @param GlobalFunctionsHelper $globalFunctionsHelper 36 | * @return SheetIterator 37 | */ 38 | public function createSheetIterator($filePointer, $optionsManager, $globalFunctionsHelper) 39 | { 40 | $rowIterator = $this->createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper); 41 | $sheet = $this->createSheet($rowIterator); 42 | 43 | return new SheetIterator($sheet); 44 | } 45 | 46 | /** 47 | * @param RowIterator $rowIterator 48 | * @return Sheet 49 | */ 50 | private function createSheet($rowIterator) 51 | { 52 | return new Sheet($rowIterator); 53 | } 54 | 55 | /** 56 | * @param resource $filePointer Pointer to the CSV file to read 57 | * @param OptionsManagerInterface $optionsManager 58 | * @param GlobalFunctionsHelper $globalFunctionsHelper 59 | * @return RowIterator 60 | */ 61 | private function createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper) 62 | { 63 | $encodingHelper = $this->helperFactory->createEncodingHelper($globalFunctionsHelper); 64 | 65 | return new RowIterator($filePointer, $optionsManager, $encodingHelper, $this, $globalFunctionsHelper); 66 | } 67 | 68 | /** 69 | * @param Cell[] $cells 70 | * @return Row 71 | */ 72 | public function createRow(array $cells = []) 73 | { 74 | return new Row($cells, null); 75 | } 76 | 77 | /** 78 | * @param mixed $cellValue 79 | * @return Cell 80 | */ 81 | public function createCell($cellValue) 82 | { 83 | return new Cell($cellValue); 84 | } 85 | 86 | /** 87 | * @param array $cellValues 88 | * @return Row 89 | */ 90 | public function createRowFromArray(array $cellValues = []) 91 | { 92 | $cells = \array_map(function ($cellValue) { 93 | return $this->createCell($cellValue); 94 | }, $cellValues); 95 | 96 | return $this->createRow($cells); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Spout/Reader/CSV/Manager/OptionsManager.php: -------------------------------------------------------------------------------- 1 | setOption(Options::SHOULD_FORMAT_DATES, false); 35 | $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); 36 | $this->setOption(Options::FIELD_DELIMITER, ','); 37 | $this->setOption(Options::FIELD_ENCLOSURE, '"'); 38 | $this->setOption(Options::ENCODING, EncodingHelper::ENCODING_UTF8); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Spout/Reader/CSV/Reader.php: -------------------------------------------------------------------------------- 1 | = 8.1 */ 29 | private $isRunningAtLeastPhp81; 30 | 31 | /** 32 | * @param OptionsManagerInterface $optionsManager 33 | * @param GlobalFunctionsHelper $globalFunctionsHelper 34 | * @param InternalEntityFactoryInterface $entityFactory 35 | */ 36 | public function __construct( 37 | OptionsManagerInterface $optionsManager, 38 | GlobalFunctionsHelper $globalFunctionsHelper, 39 | InternalEntityFactoryInterface $entityFactory 40 | ) { 41 | parent::__construct($optionsManager, $globalFunctionsHelper, $entityFactory); 42 | $this->isRunningAtLeastPhp81 = \version_compare(PHP_VERSION, '8.1.0') >= 0; 43 | } 44 | 45 | /** 46 | * Sets the field delimiter for the CSV. 47 | * Needs to be called before opening the reader. 48 | * 49 | * @param string $fieldDelimiter Character that delimits fields 50 | * @return Reader 51 | */ 52 | public function setFieldDelimiter($fieldDelimiter) 53 | { 54 | $this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Sets the field enclosure for the CSV. 61 | * Needs to be called before opening the reader. 62 | * 63 | * @param string $fieldEnclosure Character that enclose fields 64 | * @return Reader 65 | */ 66 | public function setFieldEnclosure($fieldEnclosure) 67 | { 68 | $this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure); 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Sets the encoding of the CSV file to be read. 75 | * Needs to be called before opening the reader. 76 | * 77 | * @param string $encoding Encoding of the CSV file to be read 78 | * @return Reader 79 | */ 80 | public function setEncoding($encoding) 81 | { 82 | $this->optionsManager->setOption(Options::ENCODING, $encoding); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Returns whether stream wrappers are supported 89 | * 90 | * @return bool 91 | */ 92 | protected function doesSupportStreamWrapper() 93 | { 94 | return true; 95 | } 96 | 97 | /** 98 | * Opens the file at the given path to make it ready to be read. 99 | * If setEncoding() was not called, it assumes that the file is encoded in UTF-8. 100 | * 101 | * @param string $filePath Path of the CSV file to be read 102 | * @throws \Box\Spout\Common\Exception\IOException 103 | * @return void 104 | */ 105 | protected function openReader($filePath) 106 | { 107 | // "auto_detect_line_endings" is deprecated in PHP 8.1 108 | if (!$this->isRunningAtLeastPhp81) { 109 | $this->originalAutoDetectLineEndings = \ini_get('auto_detect_line_endings'); 110 | \ini_set('auto_detect_line_endings', '1'); 111 | } 112 | 113 | $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r'); 114 | if (!$this->filePointer) { 115 | throw new IOException("Could not open file $filePath for reading."); 116 | } 117 | 118 | /** @var InternalEntityFactory $entityFactory */ 119 | $entityFactory = $this->entityFactory; 120 | 121 | $this->sheetIterator = $entityFactory->createSheetIterator( 122 | $this->filePointer, 123 | $this->optionsManager, 124 | $this->globalFunctionsHelper 125 | ); 126 | } 127 | 128 | /** 129 | * Returns an iterator to iterate over sheets. 130 | * 131 | * @return SheetIterator To iterate over sheets 132 | */ 133 | protected function getConcreteSheetIterator() 134 | { 135 | return $this->sheetIterator; 136 | } 137 | 138 | /** 139 | * Closes the reader. To be used after reading the file. 140 | * 141 | * @return void 142 | */ 143 | protected function closeReader() 144 | { 145 | if (is_resource($this->filePointer)) { 146 | $this->globalFunctionsHelper->fclose($this->filePointer); 147 | } 148 | 149 | // "auto_detect_line_endings" is deprecated in PHP 8.1 150 | if (!$this->isRunningAtLeastPhp81) { 151 | \ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Spout/Reader/CSV/Sheet.php: -------------------------------------------------------------------------------- 1 | rowIterator = $rowIterator; 21 | } 22 | 23 | /** 24 | * @return \Box\Spout\Reader\CSV\RowIterator 25 | */ 26 | public function getRowIterator() 27 | { 28 | return $this->rowIterator; 29 | } 30 | 31 | /** 32 | * @return int Index of the sheet 33 | */ 34 | public function getIndex() 35 | { 36 | return 0; 37 | } 38 | 39 | /** 40 | * @return string Name of the sheet - empty string since CSV does not support that 41 | */ 42 | public function getName() 43 | { 44 | return ''; 45 | } 46 | 47 | /** 48 | * @return bool Always TRUE as there is only one sheet 49 | */ 50 | public function isActive() 51 | { 52 | return true; 53 | } 54 | 55 | /** 56 | * @return bool Always TRUE as the only sheet is always visible 57 | */ 58 | public function isVisible() 59 | { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Spout/Reader/CSV/SheetIterator.php: -------------------------------------------------------------------------------- 1 | sheet = $sheet; 25 | } 26 | 27 | /** 28 | * Rewind the Iterator to the first element 29 | * @see http://php.net/manual/en/iterator.rewind.php 30 | * 31 | * @return void 32 | */ 33 | public function rewind() : void 34 | { 35 | $this->hasReadUniqueSheet = false; 36 | } 37 | 38 | /** 39 | * Checks if current position is valid 40 | * @see http://php.net/manual/en/iterator.valid.php 41 | * 42 | * @return bool 43 | */ 44 | public function valid() : bool 45 | { 46 | return (!$this->hasReadUniqueSheet); 47 | } 48 | 49 | /** 50 | * Move forward to next element 51 | * @see http://php.net/manual/en/iterator.next.php 52 | * 53 | * @return void 54 | */ 55 | public function next() : void 56 | { 57 | $this->hasReadUniqueSheet = true; 58 | } 59 | 60 | /** 61 | * Return the current element 62 | * @see http://php.net/manual/en/iterator.current.php 63 | * 64 | * @return Sheet 65 | */ 66 | public function current() : Sheet 67 | { 68 | return $this->sheet; 69 | } 70 | 71 | /** 72 | * Return the key of the current element 73 | * @see http://php.net/manual/en/iterator.key.php 74 | * 75 | * @return int 76 | */ 77 | public function key() : int 78 | { 79 | return 1; 80 | } 81 | 82 | /** 83 | * Cleans up what was created to iterate over the object. 84 | * 85 | * @return void 86 | */ 87 | public function end() : void 88 | { 89 | // do nothing 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Spout/Reader/Common/Creator/InternalEntityFactoryInterface.php: -------------------------------------------------------------------------------- 1 | createGlobalFunctionsHelper(); 72 | 73 | return new CSVReader($optionsManager, $globalFunctionsHelper, $entityFactory); 74 | } 75 | 76 | /** 77 | * @return XLSXReader 78 | */ 79 | private static function createXLSXReader() 80 | { 81 | $optionsManager = new XLSXOptionsManager(); 82 | $helperFactory = new XLSXHelperFactory(); 83 | $managerFactory = new XLSXManagerFactory($helperFactory, new CachingStrategyFactory()); 84 | $entityFactory = new XLSXInternalEntityFactory($managerFactory, $helperFactory); 85 | $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper(); 86 | 87 | return new XLSXReader($optionsManager, $globalFunctionsHelper, $entityFactory, $managerFactory); 88 | } 89 | 90 | /** 91 | * @return ODSReader 92 | */ 93 | private static function createODSReader() 94 | { 95 | $optionsManager = new ODSOptionsManager(); 96 | $helperFactory = new ODSHelperFactory(); 97 | $managerFactory = new ODSManagerFactory(); 98 | $entityFactory = new ODSInternalEntityFactory($helperFactory, $managerFactory); 99 | $globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper(); 100 | 101 | return new ODSReader($optionsManager, $globalFunctionsHelper, $entityFactory); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Spout/Reader/Common/Entity/Options.php: -------------------------------------------------------------------------------- 1 | entityFactory = $entityFactory; 22 | } 23 | 24 | /** 25 | * Detect whether a row is considered empty. 26 | * An empty row has all of its cells empty. 27 | * 28 | * @param Row $row 29 | * @return bool 30 | */ 31 | public function isEmpty(Row $row) 32 | { 33 | foreach ($row->getCells() as $cell) { 34 | if (!$cell->isEmpty()) { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | 42 | /** 43 | * Fills the missing indexes of a row with empty cells. 44 | * 45 | * @param Row $row 46 | * @return Row 47 | */ 48 | public function fillMissingIndexesWithEmptyCells(Row $row) 49 | { 50 | $numCells = $row->getNumCells(); 51 | 52 | if ($numCells === 0) { 53 | return $row; 54 | } 55 | 56 | $rowCells = $row->getCells(); 57 | $maxCellIndex = $numCells; 58 | 59 | // If the row has empty cells, calling "setCellAtIndex" will add the cell 60 | // but in the wrong place (the new cell is added at the end of the array). 61 | // Therefore, we need to sort the array using keys to have proper order. 62 | // @see https://github.com/box/spout/issues/740 63 | $needsSorting = false; 64 | 65 | for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) { 66 | if (!isset($rowCells[$cellIndex])) { 67 | $row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex); 68 | $needsSorting = true; 69 | } 70 | } 71 | 72 | if ($needsSorting) { 73 | $rowCells = $row->getCells(); 74 | ksort($rowCells); 75 | $row->setCells($rowCells); 76 | } 77 | 78 | return $row; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Spout/Reader/Exception/InvalidValueException.php: -------------------------------------------------------------------------------- 1 | invalidValue = $invalidValue; 24 | parent::__construct($message, $code, $previous); 25 | } 26 | 27 | /** 28 | * @return mixed 29 | */ 30 | public function getInvalidValue() 31 | { 32 | return $this->invalidValue; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Spout/Reader/Exception/IteratorNotRewindableException.php: -------------------------------------------------------------------------------- 1 | createStringsEscaper(); 21 | 22 | return new CellValueFormatter($shouldFormatDates, $escaper); 23 | } 24 | 25 | /** 26 | * @param InternalEntityFactory $entityFactory 27 | * @return SettingsHelper 28 | */ 29 | public function createSettingsHelper($entityFactory) 30 | { 31 | return new SettingsHelper($entityFactory); 32 | } 33 | 34 | /** 35 | * @return \Box\Spout\Common\Helper\Escaper\ODS 36 | */ 37 | public function createStringsEscaper() 38 | { 39 | /* @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ 40 | return new \Box\Spout\Common\Helper\Escaper\ODS(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Spout/Reader/ODS/Creator/InternalEntityFactory.php: -------------------------------------------------------------------------------- 1 | helperFactory = $helperFactory; 34 | $this->managerFactory = $managerFactory; 35 | } 36 | 37 | /** 38 | * @param string $filePath Path of the file to be read 39 | * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager 40 | * @return SheetIterator 41 | */ 42 | public function createSheetIterator($filePath, $optionsManager) 43 | { 44 | $escaper = $this->helperFactory->createStringsEscaper(); 45 | $settingsHelper = $this->helperFactory->createSettingsHelper($this); 46 | 47 | return new SheetIterator($filePath, $optionsManager, $escaper, $settingsHelper, $this); 48 | } 49 | 50 | /** 51 | * @param XMLReader $xmlReader XML Reader 52 | * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) 53 | * @param string $sheetName Name of the sheet 54 | * @param bool $isSheetActive Whether the sheet was defined as active 55 | * @param bool $isSheetVisible Whether the sheet is visible 56 | * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager 57 | * @return Sheet 58 | */ 59 | public function createSheet($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible, $optionsManager) 60 | { 61 | $rowIterator = $this->createRowIterator($xmlReader, $optionsManager); 62 | 63 | return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible); 64 | } 65 | 66 | /** 67 | * @param XMLReader $xmlReader XML Reader 68 | * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager 69 | * @return RowIterator 70 | */ 71 | private function createRowIterator($xmlReader, $optionsManager) 72 | { 73 | $shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES); 74 | $cellValueFormatter = $this->helperFactory->createCellValueFormatter($shouldFormatDates); 75 | $xmlProcessor = $this->createXMLProcessor($xmlReader); 76 | $rowManager = $this->managerFactory->createRowManager($this); 77 | 78 | return new RowIterator($xmlReader, $optionsManager, $cellValueFormatter, $xmlProcessor, $rowManager, $this); 79 | } 80 | 81 | /** 82 | * @param Cell[] $cells 83 | * @return Row 84 | */ 85 | public function createRow(array $cells = []) 86 | { 87 | return new Row($cells, null); 88 | } 89 | 90 | /** 91 | * @param mixed $cellValue 92 | * @return Cell 93 | */ 94 | public function createCell($cellValue) 95 | { 96 | return new Cell($cellValue); 97 | } 98 | 99 | /** 100 | * @return XMLReader 101 | */ 102 | public function createXMLReader() 103 | { 104 | return new XMLReader(); 105 | } 106 | 107 | /** 108 | * @param XMLReader $xmlReader 109 | * @return XMLProcessor 110 | */ 111 | private function createXMLProcessor($xmlReader) 112 | { 113 | return new XMLProcessor($xmlReader); 114 | } 115 | 116 | /** 117 | * @return \ZipArchive 118 | */ 119 | public function createZipArchive() 120 | { 121 | return new \ZipArchive(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Spout/Reader/ODS/Creator/ManagerFactory.php: -------------------------------------------------------------------------------- 1 | entityFactory = $entityFactory; 30 | } 31 | 32 | /** 33 | * @param string $filePath Path of the file to be read 34 | * @return string|null Name of the sheet that was defined as active or NULL if none found 35 | */ 36 | public function getActiveSheetName($filePath) 37 | { 38 | $xmlReader = $this->entityFactory->createXMLReader(); 39 | if ($xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) { 40 | return null; 41 | } 42 | 43 | $activeSheetName = null; 44 | 45 | try { 46 | while ($xmlReader->readUntilNodeFound(self::XML_NODE_CONFIG_ITEM)) { 47 | if ($xmlReader->getAttribute(self::XML_ATTRIBUTE_CONFIG_NAME) === self::XML_ATTRIBUTE_VALUE_ACTIVE_TABLE) { 48 | $activeSheetName = $xmlReader->readString(); 49 | break; 50 | } 51 | } 52 | } catch (XMLProcessingException $exception) { 53 | // do nothing 54 | } 55 | 56 | $xmlReader->close(); 57 | 58 | return $activeSheetName; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Spout/Reader/ODS/Manager/OptionsManager.php: -------------------------------------------------------------------------------- 1 | setOption(Options::SHOULD_FORMAT_DATES, false); 31 | $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Spout/Reader/ODS/Reader.php: -------------------------------------------------------------------------------- 1 | entityFactory; 43 | 44 | $this->zip = $entityFactory->createZipArchive(); 45 | 46 | if ($this->zip->open($filePath) === true) { 47 | /** @var InternalEntityFactory $entityFactory */ 48 | $entityFactory = $this->entityFactory; 49 | $this->sheetIterator = $entityFactory->createSheetIterator($filePath, $this->optionsManager); 50 | } else { 51 | throw new IOException("Could not open $filePath for reading."); 52 | } 53 | } 54 | 55 | /** 56 | * Returns an iterator to iterate over sheets. 57 | * 58 | * @return SheetIterator To iterate over sheets 59 | */ 60 | protected function getConcreteSheetIterator() 61 | { 62 | return $this->sheetIterator; 63 | } 64 | 65 | /** 66 | * Closes the reader. To be used after reading the file. 67 | * 68 | * @return void 69 | */ 70 | protected function closeReader() 71 | { 72 | if ($this->zip !== null) { 73 | $this->zip->close(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Spout/Reader/ODS/Sheet.php: -------------------------------------------------------------------------------- 1 | rowIterator = $rowIterator; 41 | $this->index = $sheetIndex; 42 | $this->name = $sheetName; 43 | $this->isActive = $isSheetActive; 44 | $this->isVisible = $isSheetVisible; 45 | } 46 | 47 | /** 48 | * @return \Box\Spout\Reader\ODS\RowIterator 49 | */ 50 | public function getRowIterator() 51 | { 52 | return $this->rowIterator; 53 | } 54 | 55 | /** 56 | * @return int Index of the sheet, based on order in the workbook (zero-based) 57 | */ 58 | public function getIndex() 59 | { 60 | return $this->index; 61 | } 62 | 63 | /** 64 | * @return string Name of the sheet 65 | */ 66 | public function getName() 67 | { 68 | return $this->name; 69 | } 70 | 71 | /** 72 | * @return bool Whether the sheet was defined as active 73 | */ 74 | public function isActive() 75 | { 76 | return $this->isActive; 77 | } 78 | 79 | /** 80 | * @return bool Whether the sheet is visible 81 | */ 82 | public function isVisible() 83 | { 84 | return $this->isVisible; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Spout/Reader/ReaderInterface.php: -------------------------------------------------------------------------------- 1 | initialUseInternalErrorsValue = \libxml_use_internal_errors(true); 25 | } 26 | 27 | /** 28 | * Throws an XMLProcessingException if an error occured. 29 | * It also always resets the "libxml_use_internal_errors" setting back to its initial value. 30 | * 31 | * @throws \Box\Spout\Reader\Exception\XMLProcessingException 32 | * @return void 33 | */ 34 | protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured() 35 | { 36 | if ($this->hasXMLErrorOccured()) { 37 | $this->resetXMLInternalErrorsSetting(); 38 | throw new XMLProcessingException($this->getLastXMLErrorMessage()); 39 | } 40 | 41 | $this->resetXMLInternalErrorsSetting(); 42 | } 43 | 44 | /** 45 | * Returns whether the a XML error has occured since the last time errors were cleared. 46 | * 47 | * @return bool TRUE if an error occured, FALSE otherwise 48 | */ 49 | private function hasXMLErrorOccured() 50 | { 51 | return (\libxml_get_last_error() !== false); 52 | } 53 | 54 | /** 55 | * Returns the error message for the last XML error that occured. 56 | * @see libxml_get_last_error 57 | * 58 | * @return string|null Last XML error message or null if no error 59 | */ 60 | private function getLastXMLErrorMessage() 61 | { 62 | $errorMessage = null; 63 | $error = \libxml_get_last_error(); 64 | 65 | if ($error !== false) { 66 | $errorMessage = \trim($error->message); 67 | } 68 | 69 | return $errorMessage; 70 | } 71 | 72 | /** 73 | * @return void 74 | */ 75 | protected function resetXMLInternalErrorsSetting() 76 | { 77 | \libxml_use_internal_errors($this->initialUseInternalErrorsValue); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/Creator/HelperFactory.php: -------------------------------------------------------------------------------- 1 | createStringsEscaper(); 26 | 27 | return new CellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper); 28 | } 29 | 30 | /** 31 | * @return Escaper\XLSX 32 | */ 33 | public function createStringsEscaper() 34 | { 35 | /* @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ 36 | return new Escaper\XLSX(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/Creator/ManagerFactory.php: -------------------------------------------------------------------------------- 1 | helperFactory = $helperFactory; 34 | $this->cachingStrategyFactory = $cachingStrategyFactory; 35 | } 36 | 37 | /** 38 | * @param string $filePath Path of the XLSX file being read 39 | * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored 40 | * @param InternalEntityFactory $entityFactory Factory to create entities 41 | * @return SharedStringsManager 42 | */ 43 | public function createSharedStringsManager($filePath, $tempFolder, $entityFactory) 44 | { 45 | $workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory); 46 | 47 | return new SharedStringsManager( 48 | $filePath, 49 | $tempFolder, 50 | $workbookRelationshipsManager, 51 | $entityFactory, 52 | $this->helperFactory, 53 | $this->cachingStrategyFactory 54 | ); 55 | } 56 | 57 | /** 58 | * @param string $filePath Path of the XLSX file being read 59 | * @param InternalEntityFactory $entityFactory Factory to create entities 60 | * @return WorkbookRelationshipsManager 61 | */ 62 | private function createWorkbookRelationshipsManager($filePath, $entityFactory) 63 | { 64 | if (!isset($this->cachedWorkbookRelationshipsManager)) { 65 | $this->cachedWorkbookRelationshipsManager = new WorkbookRelationshipsManager($filePath, $entityFactory); 66 | } 67 | 68 | return $this->cachedWorkbookRelationshipsManager; 69 | } 70 | 71 | /** 72 | * @param string $filePath Path of the XLSX file being read 73 | * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager 74 | * @param \Box\Spout\Reader\XLSX\Manager\SharedStringsManager $sharedStringsManager Manages shared strings 75 | * @param InternalEntityFactory $entityFactory Factory to create entities 76 | * @return SheetManager 77 | */ 78 | public function createSheetManager($filePath, $optionsManager, $sharedStringsManager, $entityFactory) 79 | { 80 | $escaper = $this->helperFactory->createStringsEscaper(); 81 | 82 | return new SheetManager($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory); 83 | } 84 | 85 | /** 86 | * @param string $filePath Path of the XLSX file being read 87 | * @param InternalEntityFactory $entityFactory Factory to create entities 88 | * @return StyleManager 89 | */ 90 | public function createStyleManager($filePath, $entityFactory) 91 | { 92 | $workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory); 93 | 94 | return new StyleManager($filePath, $workbookRelationshipsManager, $entityFactory); 95 | } 96 | 97 | /** 98 | * @param InternalEntityFactory $entityFactory Factory to create entities 99 | * @return RowManager 100 | */ 101 | public function createRowManager($entityFactory) 102 | { 103 | return new RowManager($entityFactory); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/Helper/CellHelper.php: -------------------------------------------------------------------------------- 1 | 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 16 | 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 17 | 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 18 | 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25, 19 | ]; 20 | 21 | /** 22 | * Returns the base 10 column index associated to the cell index (base 26). 23 | * Excel uses A to Z letters for column indexing, where A is the 1st column, 24 | * Z is the 26th and AA is the 27th. 25 | * The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26. 26 | * 27 | * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) 28 | * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid 29 | * @return int 30 | */ 31 | public static function getColumnIndexFromCellIndex($cellIndex) 32 | { 33 | if (!self::isValidCellIndex($cellIndex)) { 34 | throw new InvalidArgumentException('Cannot get column index from an invalid cell index.'); 35 | } 36 | 37 | $columnIndex = 0; 38 | 39 | // Remove row information 40 | $columnLetters = \preg_replace('/\d/', '', $cellIndex); 41 | 42 | // strlen() is super slow too... Using isset() is way faster and not too unreadable, 43 | // since we checked before that there are between 1 and 3 letters. 44 | $columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1; 45 | 46 | // Looping over the different letters of the column is slower than this method. 47 | // Also, not using the pow() function because it's slooooow... 48 | switch ($columnLength) { 49 | case 1: 50 | $columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]); 51 | break; 52 | case 2: 53 | $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26; 54 | $secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]]; 55 | $columnIndex = $firstLetterIndex + $secondLetterIndex; 56 | break; 57 | case 3: 58 | $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676; 59 | $secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26; 60 | $thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]]; 61 | $columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex; 62 | break; 63 | } 64 | 65 | return $columnIndex; 66 | } 67 | 68 | /** 69 | * Returns whether a cell index is valid, in an Excel world. 70 | * To be valid, the cell index should start with capital letters and be followed by numbers. 71 | * There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'. 72 | * 73 | * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) 74 | * @return bool 75 | */ 76 | protected static function isValidCellIndex($cellIndex) 77 | { 78 | return (\preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/Manager/OptionsManager.php: -------------------------------------------------------------------------------- 1 | setOption(Options::TEMP_FOLDER, \sys_get_temp_dir()); 33 | $this->setOption(Options::SHOULD_FORMAT_DATES, false); 34 | $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); 35 | $this->setOption(Options::SHOULD_USE_1904_DATES, false); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php: -------------------------------------------------------------------------------- 1 | inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount); 27 | $this->isCacheClosed = false; 28 | } 29 | 30 | /** 31 | * Adds the given string to the cache. 32 | * 33 | * @param string $sharedString The string to be added to the cache 34 | * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file 35 | * @return void 36 | */ 37 | public function addStringForIndex($sharedString, $sharedStringIndex) 38 | { 39 | if (!$this->isCacheClosed) { 40 | $this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString); 41 | } 42 | } 43 | 44 | /** 45 | * Closes the cache after the last shared string was added. 46 | * This prevents any additional string from being added to the cache. 47 | * 48 | * @return void 49 | */ 50 | public function closeCache() 51 | { 52 | $this->isCacheClosed = true; 53 | } 54 | 55 | /** 56 | * Returns the string located at the given index from the cache. 57 | * 58 | * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file 59 | * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index 60 | * @return string The shared string at the given index 61 | */ 62 | public function getStringAtIndex($sharedStringIndex) 63 | { 64 | try { 65 | return $this->inMemoryCache->offsetGet($sharedStringIndex); 66 | } catch (\RuntimeException $e) { 67 | throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); 68 | } 69 | } 70 | 71 | /** 72 | * Destroys the cache, freeing memory and removing any created artifacts 73 | * 74 | * @return void 75 | */ 76 | public function clearCache() 77 | { 78 | unset($this->inMemoryCache); 79 | $this->isCacheClosed = false; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/Reader.php: -------------------------------------------------------------------------------- 1 | managerFactory = $managerFactory; 46 | } 47 | 48 | /** 49 | * @param string $tempFolder Temporary folder where the temporary files will be created 50 | * @return Reader 51 | */ 52 | public function setTempFolder($tempFolder) 53 | { 54 | $this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Returns whether stream wrappers are supported 61 | * 62 | * @return bool 63 | */ 64 | protected function doesSupportStreamWrapper() 65 | { 66 | return false; 67 | } 68 | 69 | /** 70 | * Opens the file at the given file path to make it ready to be read. 71 | * It also parses the sharedStrings.xml file to get all the shared strings available in memory 72 | * and fetches all the available sheets. 73 | * 74 | * @param string $filePath Path of the file to be read 75 | * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read 76 | * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file 77 | * @return void 78 | */ 79 | protected function openReader($filePath) 80 | { 81 | /** @var InternalEntityFactory $entityFactory */ 82 | $entityFactory = $this->entityFactory; 83 | 84 | $this->zip = $entityFactory->createZipArchive(); 85 | 86 | if ($this->zip->open($filePath) === true) { 87 | $tempFolder = $this->optionsManager->getOption(Options::TEMP_FOLDER); 88 | $this->sharedStringsManager = $this->managerFactory->createSharedStringsManager($filePath, $tempFolder, $entityFactory); 89 | 90 | if ($this->sharedStringsManager->hasSharedStrings()) { 91 | // Extracts all the strings from the sheets for easy access in the future 92 | $this->sharedStringsManager->extractSharedStrings(); 93 | } 94 | 95 | $this->sheetIterator = $entityFactory->createSheetIterator( 96 | $filePath, 97 | $this->optionsManager, 98 | $this->sharedStringsManager 99 | ); 100 | } else { 101 | throw new IOException("Could not open $filePath for reading."); 102 | } 103 | } 104 | 105 | /** 106 | * Returns an iterator to iterate over sheets. 107 | * 108 | * @return SheetIterator To iterate over sheets 109 | */ 110 | protected function getConcreteSheetIterator() 111 | { 112 | return $this->sheetIterator; 113 | } 114 | 115 | /** 116 | * Closes the reader. To be used after reading the file. 117 | * 118 | * @return void 119 | */ 120 | protected function closeReader() 121 | { 122 | if ($this->zip !== null) { 123 | $this->zip->close(); 124 | } 125 | 126 | if ($this->sharedStringsManager !== null) { 127 | $this->sharedStringsManager->cleanup(); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/Sheet.php: -------------------------------------------------------------------------------- 1 | rowIterator = $rowIterator; 38 | $this->index = $sheetIndex; 39 | $this->name = $sheetName; 40 | $this->isActive = $isSheetActive; 41 | $this->isVisible = $isSheetVisible; 42 | } 43 | 44 | /** 45 | * @return \Box\Spout\Reader\XLSX\RowIterator 46 | */ 47 | public function getRowIterator() 48 | { 49 | return $this->rowIterator; 50 | } 51 | 52 | /** 53 | * @return int Index of the sheet, based on order in the workbook (zero-based) 54 | */ 55 | public function getIndex() 56 | { 57 | return $this->index; 58 | } 59 | 60 | /** 61 | * @return string Name of the sheet 62 | */ 63 | public function getName() 64 | { 65 | return $this->name; 66 | } 67 | 68 | /** 69 | * @return bool Whether the sheet was defined as active 70 | */ 71 | public function isActive() 72 | { 73 | return $this->isActive; 74 | } 75 | 76 | /** 77 | * @return bool Whether the sheet is visible 78 | */ 79 | public function isVisible() 80 | { 81 | return $this->isVisible; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Spout/Reader/XLSX/SheetIterator.php: -------------------------------------------------------------------------------- 1 | sheets = $sheetManager->getSheets(); 29 | 30 | if (\count($this->sheets) === 0) { 31 | throw new NoSheetsFoundException('The file must contain at least one sheet.'); 32 | } 33 | } 34 | 35 | /** 36 | * Rewind the Iterator to the first element 37 | * @see http://php.net/manual/en/iterator.rewind.php 38 | * 39 | * @return void 40 | */ 41 | public function rewind() 42 | { 43 | $this->currentSheetIndex = 0; 44 | } 45 | 46 | /** 47 | * Checks if current position is valid 48 | * @see http://php.net/manual/en/iterator.valid.php 49 | * 50 | * @return bool 51 | */ 52 | public function valid() 53 | { 54 | return ($this->currentSheetIndex < \count($this->sheets)); 55 | } 56 | 57 | /** 58 | * Move forward to next element 59 | * @see http://php.net/manual/en/iterator.next.php 60 | * 61 | * @return void 62 | */ 63 | public function next() 64 | { 65 | // Using isset here because it is way faster than array_key_exists... 66 | if (isset($this->sheets[$this->currentSheetIndex])) { 67 | $currentSheet = $this->sheets[$this->currentSheetIndex]; 68 | $currentSheet->getRowIterator()->end(); 69 | 70 | $this->currentSheetIndex++; 71 | } 72 | } 73 | 74 | /** 75 | * Return the current element 76 | * @see http://php.net/manual/en/iterator.current.php 77 | * 78 | * @return \Box\Spout\Reader\XLSX\Sheet 79 | */ 80 | public function current() 81 | { 82 | return $this->sheets[$this->currentSheetIndex]; 83 | } 84 | 85 | /** 86 | * Return the key of the current element 87 | * @see http://php.net/manual/en/iterator.key.php 88 | * 89 | * @return int 90 | */ 91 | public function key() 92 | { 93 | return $this->currentSheetIndex + 1; 94 | } 95 | 96 | /** 97 | * Cleans up what was created to iterate over the object. 98 | * 99 | * @return void 100 | */ 101 | public function end() 102 | { 103 | // make sure we are not leaking memory in case the iteration stopped before the end 104 | foreach ($this->sheets as $sheet) { 105 | $sheet->getRowIterator()->end(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Spout/Writer/CSV/Manager/OptionsManager.php: -------------------------------------------------------------------------------- 1 | setOption(Options::FIELD_DELIMITER, ','); 32 | $this->setOption(Options::FIELD_ENCLOSURE, '"'); 33 | $this->setOption(Options::SHOULD_ADD_BOM, true); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Spout/Writer/CSV/Writer.php: -------------------------------------------------------------------------------- 1 | optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter); 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * Sets the field enclosure for the CSV 41 | * 42 | * @param string $fieldEnclosure Character that enclose fields 43 | * @return Writer 44 | */ 45 | public function setFieldEnclosure($fieldEnclosure) 46 | { 47 | $this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure); 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Set if a BOM has to be added to the file 54 | * 55 | * @param bool $shouldAddBOM 56 | * @return Writer 57 | */ 58 | public function setShouldAddBOM($shouldAddBOM) 59 | { 60 | $this->optionsManager->setOption(Options::SHOULD_ADD_BOM, (bool) $shouldAddBOM); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Opens the CSV streamer and makes it ready to accept data. 67 | * 68 | * @return void 69 | */ 70 | protected function openWriter() 71 | { 72 | if ($this->optionsManager->getOption(Options::SHOULD_ADD_BOM)) { 73 | // Adds UTF-8 BOM for Unicode compatibility 74 | $this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8); 75 | } 76 | } 77 | 78 | /** 79 | * Adds a row to the currently opened writer. 80 | * 81 | * @param Row $row The row containing cells and styles 82 | * @throws IOException If unable to write data 83 | * @return void 84 | */ 85 | protected function addRowToWriter(Row $row) 86 | { 87 | $fieldDelimiter = $this->optionsManager->getOption(Options::FIELD_DELIMITER); 88 | $fieldEnclosure = $this->optionsManager->getOption(Options::FIELD_ENCLOSURE); 89 | 90 | $wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $row->getCells(), $fieldDelimiter, $fieldEnclosure); 91 | if ($wasWriteSuccessful === false) { 92 | throw new IOException('Unable to write data'); 93 | } 94 | 95 | $this->lastWrittenRowIndex++; 96 | if ($this->lastWrittenRowIndex % self::FLUSH_THRESHOLD === 0) { 97 | $this->globalFunctionsHelper->fflush($this->filePointer); 98 | } 99 | } 100 | 101 | /** 102 | * Closes the CSV streamer, preventing any additional writing. 103 | * If set, sets the headers and redirects output to the browser. 104 | * 105 | * @return void 106 | */ 107 | protected function closeWriter() 108 | { 109 | $this->lastWrittenRowIndex = 0; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Creator/InternalEntityFactory.php: -------------------------------------------------------------------------------- 1 | border = new Border(); 22 | } 23 | 24 | /** 25 | * @param string $color Border A RGB color code 26 | * @param string $width Border width @see BorderPart::allowedWidths 27 | * @param string $style Border style @see BorderPart::allowedStyles 28 | * @return BorderBuilder 29 | */ 30 | public function setBorderTop($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) 31 | { 32 | $this->border->addPart(new BorderPart(Border::TOP, $color, $width, $style)); 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * @param string $color Border A RGB color code 39 | * @param string $width Border width @see BorderPart::allowedWidths 40 | * @param string $style Border style @see BorderPart::allowedStyles 41 | * @return BorderBuilder 42 | */ 43 | public function setBorderRight($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) 44 | { 45 | $this->border->addPart(new BorderPart(Border::RIGHT, $color, $width, $style)); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @param string $color Border A RGB color code 52 | * @param string $width Border width @see BorderPart::allowedWidths 53 | * @param string $style Border style @see BorderPart::allowedStyles 54 | * @return BorderBuilder 55 | */ 56 | public function setBorderBottom($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) 57 | { 58 | $this->border->addPart(new BorderPart(Border::BOTTOM, $color, $width, $style)); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @param string $color Border A RGB color code 65 | * @param string $width Border width @see BorderPart::allowedWidths 66 | * @param string $style Border style @see BorderPart::allowedStyles 67 | * @return BorderBuilder 68 | */ 69 | public function setBorderLeft($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) 70 | { 71 | $this->border->addPart(new BorderPart(Border::LEFT, $color, $width, $style)); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @return Border 78 | */ 79 | public function build() 80 | { 81 | return $this->border; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Creator/Style/StyleBuilder.php: -------------------------------------------------------------------------------- 1 | style = new Style(); 25 | } 26 | 27 | /** 28 | * Makes the font bold. 29 | * 30 | * @return StyleBuilder 31 | */ 32 | public function setFontBold() 33 | { 34 | $this->style->setFontBold(); 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * Makes the font italic. 41 | * 42 | * @return StyleBuilder 43 | */ 44 | public function setFontItalic() 45 | { 46 | $this->style->setFontItalic(); 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Makes the font underlined. 53 | * 54 | * @return StyleBuilder 55 | */ 56 | public function setFontUnderline() 57 | { 58 | $this->style->setFontUnderline(); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Makes the font struck through. 65 | * 66 | * @return StyleBuilder 67 | */ 68 | public function setFontStrikethrough() 69 | { 70 | $this->style->setFontStrikethrough(); 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Sets the font size. 77 | * 78 | * @param int $fontSize Font size, in pixels 79 | * @return StyleBuilder 80 | */ 81 | public function setFontSize($fontSize) 82 | { 83 | $this->style->setFontSize($fontSize); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Sets the font color. 90 | * 91 | * @param string $fontColor ARGB color (@see Color) 92 | * @return StyleBuilder 93 | */ 94 | public function setFontColor($fontColor) 95 | { 96 | $this->style->setFontColor($fontColor); 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Sets the font name. 103 | * 104 | * @param string $fontName Name of the font to use 105 | * @return StyleBuilder 106 | */ 107 | public function setFontName($fontName) 108 | { 109 | $this->style->setFontName($fontName); 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Makes the text wrap in the cell if requested 116 | * 117 | * @param bool $shouldWrap Should the text be wrapped 118 | * @return StyleBuilder 119 | */ 120 | public function setShouldWrapText($shouldWrap = true) 121 | { 122 | $this->style->setShouldWrapText($shouldWrap); 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Sets the cell alignment. 129 | * 130 | * @param string $cellAlignment The cell alignment 131 | * 132 | * @throws InvalidArgumentException If the given cell alignment is not valid 133 | * @return StyleBuilder 134 | */ 135 | public function setCellAlignment($cellAlignment) 136 | { 137 | if (!CellAlignment::isValid($cellAlignment)) { 138 | throw new InvalidArgumentException('Invalid cell alignment value'); 139 | } 140 | 141 | $this->style->setCellAlignment($cellAlignment); 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Set a border 148 | * 149 | * @param Border $border 150 | * @return $this 151 | */ 152 | public function setBorder(Border $border) 153 | { 154 | $this->style->setBorder($border); 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * Sets a background color 161 | * 162 | * @param string $color ARGB color (@see Color) 163 | * @return StyleBuilder 164 | */ 165 | public function setBackgroundColor($color) 166 | { 167 | $this->style->setBackgroundColor($color); 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Sets a format 174 | * 175 | * @param string $format Format 176 | * @return StyleBuilder 177 | * @api 178 | */ 179 | public function setFormat($format) 180 | { 181 | $this->style->setFormat($format); 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Returns the configured style. The style is cached and can be reused. 188 | * 189 | * @return Style 190 | */ 191 | public function build() 192 | { 193 | return $this->style; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Creator/WriterEntityFactory.php: -------------------------------------------------------------------------------- 1 | index = $sheetIndex; 38 | $this->associatedWorkbookId = $associatedWorkbookId; 39 | 40 | $this->sheetManager = $sheetManager; 41 | $this->sheetManager->markWorkbookIdAsUsed($associatedWorkbookId); 42 | 43 | $this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1)); 44 | $this->setIsVisible(true); 45 | } 46 | 47 | /** 48 | * @return int Index of the sheet, based on order in the workbook (zero-based) 49 | */ 50 | public function getIndex() 51 | { 52 | return $this->index; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getAssociatedWorkbookId() 59 | { 60 | return $this->associatedWorkbookId; 61 | } 62 | 63 | /** 64 | * @return string Name of the sheet 65 | */ 66 | public function getName() 67 | { 68 | return $this->name; 69 | } 70 | 71 | /** 72 | * Sets the name of the sheet. Note that Excel has some restrictions on the name: 73 | * - it should not be blank 74 | * - it should not exceed 31 characters 75 | * - it should not contain these characters: \ / ? * : [ or ] 76 | * - it should be unique 77 | * 78 | * @param string $name Name of the sheet 79 | * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid. 80 | * @return Sheet 81 | */ 82 | public function setName($name) 83 | { 84 | $this->sheetManager->throwIfNameIsInvalid($name, $this); 85 | 86 | $this->name = $name; 87 | 88 | $this->sheetManager->markSheetNameAsUsed($this); 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * @return bool isVisible Visibility of the sheet 95 | */ 96 | public function isVisible() 97 | { 98 | return $this->isVisible; 99 | } 100 | 101 | /** 102 | * @param bool $isVisible Visibility of the sheet 103 | * @return Sheet 104 | */ 105 | public function setIsVisible($isVisible) 106 | { 107 | $this->isVisible = $isVisible; 108 | 109 | return $this; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Entity/Workbook.php: -------------------------------------------------------------------------------- 1 | internalId = \uniqid(); 23 | } 24 | 25 | /** 26 | * @return Worksheet[] 27 | */ 28 | public function getWorksheets() 29 | { 30 | return $this->worksheets; 31 | } 32 | 33 | /** 34 | * @param Worksheet[] $worksheets 35 | */ 36 | public function setWorksheets($worksheets) 37 | { 38 | $this->worksheets = $worksheets; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getInternalId() 45 | { 46 | return $this->internalId; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Entity/Worksheet.php: -------------------------------------------------------------------------------- 1 | filePath = $worksheetFilePath; 35 | $this->filePointer = null; 36 | $this->externalSheet = $externalSheet; 37 | $this->maxNumColumns = 0; 38 | $this->lastWrittenRowIndex = 0; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getFilePath() 45 | { 46 | return $this->filePath; 47 | } 48 | 49 | /** 50 | * @return resource 51 | */ 52 | public function getFilePointer() 53 | { 54 | return $this->filePointer; 55 | } 56 | 57 | /** 58 | * @param resource $filePointer 59 | */ 60 | public function setFilePointer($filePointer) 61 | { 62 | $this->filePointer = $filePointer; 63 | } 64 | 65 | /** 66 | * @return Sheet 67 | */ 68 | public function getExternalSheet() 69 | { 70 | return $this->externalSheet; 71 | } 72 | 73 | /** 74 | * @return int 75 | */ 76 | public function getMaxNumColumns() 77 | { 78 | return $this->maxNumColumns; 79 | } 80 | 81 | /** 82 | * @param int $maxNumColumns 83 | */ 84 | public function setMaxNumColumns($maxNumColumns) 85 | { 86 | $this->maxNumColumns = $maxNumColumns; 87 | } 88 | 89 | /** 90 | * @return int 91 | */ 92 | public function getLastWrittenRowIndex() 93 | { 94 | return $this->lastWrittenRowIndex; 95 | } 96 | 97 | /** 98 | * @param int $lastWrittenRowIndex 99 | */ 100 | public function setLastWrittenRowIndex($lastWrittenRowIndex) 101 | { 102 | $this->lastWrittenRowIndex = $lastWrittenRowIndex; 103 | } 104 | 105 | /** 106 | * @return int The ID of the worksheet 107 | */ 108 | public function getId() 109 | { 110 | // sheet index is zero-based, while ID is 1-based 111 | return $this->externalSheet->getIndex() + 1; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Helper/CellHelper.php: -------------------------------------------------------------------------------- 1 | column letters */ 12 | private static $columnIndexToColumnLettersCache = []; 13 | 14 | /** 15 | * Returns the column letters (base 26) associated to the base 10 column index. 16 | * Excel uses A to Z letters for column indexing, where A is the 1st column, 17 | * Z is the 26th and AA is the 27th. 18 | * The mapping is zero based, so that 0 maps to A, B maps to 1, Z to 25 and AA to 26. 19 | * 20 | * @param int $columnIndexZeroBased The Excel column index (0, 42, ...) 21 | * 22 | * @return string The associated cell index ('A', 'BC', ...) 23 | */ 24 | public static function getColumnLettersFromColumnIndex($columnIndexZeroBased) 25 | { 26 | $originalColumnIndex = $columnIndexZeroBased; 27 | 28 | // Using isset here because it is way faster than array_key_exists... 29 | if (!isset(self::$columnIndexToColumnLettersCache[$originalColumnIndex])) { 30 | $columnLetters = ''; 31 | $capitalAAsciiValue = \ord('A'); 32 | 33 | do { 34 | $modulus = $columnIndexZeroBased % 26; 35 | $columnLetters = \chr($capitalAAsciiValue + $modulus) . $columnLetters; 36 | 37 | // substracting 1 because it's zero-based 38 | $columnIndexZeroBased = (int) ($columnIndexZeroBased / 26) - 1; 39 | } while ($columnIndexZeroBased >= 0); 40 | 41 | self::$columnIndexToColumnLettersCache[$originalColumnIndex] = $columnLetters; 42 | } 43 | 44 | return self::$columnIndexToColumnLettersCache[$originalColumnIndex]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php: -------------------------------------------------------------------------------- 1 | styleMerger = $styleMerger; 22 | } 23 | 24 | /** 25 | * Merges a Style into a cell's Style. 26 | * 27 | * @param Cell $cell 28 | * @param Style $style 29 | * @return void 30 | */ 31 | public function applyStyle(Cell $cell, Style $style) 32 | { 33 | $mergedStyle = $this->styleMerger->merge($cell->getStyle(), $style); 34 | $cell->setStyle($mergedStyle); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Manager/RegisteredStyle.php: -------------------------------------------------------------------------------- 1 | style = $style; 26 | $this->isMatchingRowStyle = $isMatchingRowStyle; 27 | } 28 | 29 | public function getStyle() : Style 30 | { 31 | return $this->style; 32 | } 33 | 34 | public function isMatchingRowStyle() : bool 35 | { 36 | return $this->isMatchingRowStyle; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Manager/RowManager.php: -------------------------------------------------------------------------------- 1 | getCells() as $cell) { 19 | if (!$cell->isEmpty()) { 20 | return false; 21 | } 22 | } 23 | 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php: -------------------------------------------------------------------------------- 1 | style = $style; 20 | $this->isUpdated = $isUpdated; 21 | } 22 | 23 | public function getStyle() : Style 24 | { 25 | return $this->style; 26 | } 27 | 28 | public function isUpdated() : bool 29 | { 30 | return $this->isUpdated; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Manager/Style/StyleManager.php: -------------------------------------------------------------------------------- 1 | styleRegistry = $styleRegistry; 23 | } 24 | 25 | /** 26 | * Returns the default style 27 | * 28 | * @return Style Default style 29 | */ 30 | protected function getDefaultStyle() 31 | { 32 | // By construction, the default style has ID 0 33 | return $this->styleRegistry->getRegisteredStyles()[0]; 34 | } 35 | 36 | /** 37 | * Registers the given style as a used style. 38 | * Duplicate styles won't be registered more than once. 39 | * 40 | * @param Style $style The style to be registered 41 | * @return Style The registered style, updated with an internal ID. 42 | */ 43 | public function registerStyle($style) 44 | { 45 | return $this->styleRegistry->registerStyle($style); 46 | } 47 | 48 | /** 49 | * Apply additional styles if the given row needs it. 50 | * Typically, set "wrap text" if a cell contains a new line. 51 | * 52 | * @param Cell $cell 53 | * @return PossiblyUpdatedStyle The eventually updated style 54 | */ 55 | public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle 56 | { 57 | return $this->applyWrapTextIfCellContainsNewLine($cell); 58 | } 59 | 60 | /** 61 | * Set the "wrap text" option if a cell of the given row contains a new line. 62 | * 63 | * @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines 64 | * are ignored even when the "wrap text" option is set. This only occurs with 65 | * inline strings (shared strings do work fine). 66 | * A workaround would be to encode "\n" as "_x000D_" but it does not work 67 | * on the Windows version of Excel... 68 | * 69 | * @param Cell $cell The cell the style should be applied to 70 | * @return PossiblyUpdatedStyle The eventually updated style 71 | */ 72 | protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : PossiblyUpdatedStyle 73 | { 74 | $cellStyle = $cell->getStyle(); 75 | 76 | // if the "wrap text" option is already set, no-op 77 | if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) { 78 | $cellStyle->setShouldWrapText(); 79 | 80 | return new PossiblyUpdatedStyle($cellStyle, true); 81 | } 82 | 83 | return new PossiblyUpdatedStyle($cellStyle, false); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php: -------------------------------------------------------------------------------- 1 | mergeFontStyles($mergedStyle, $style, $baseStyle); 30 | $this->mergeOtherFontProperties($mergedStyle, $style, $baseStyle); 31 | $this->mergeCellProperties($mergedStyle, $style, $baseStyle); 32 | 33 | return $mergedStyle; 34 | } 35 | 36 | /** 37 | * @param Style $styleToUpdate (passed as reference) 38 | * @param Style $style 39 | * @param Style $baseStyle 40 | * @return void 41 | */ 42 | private function mergeFontStyles(Style $styleToUpdate, Style $style, Style $baseStyle) 43 | { 44 | if (!$style->hasSetFontBold() && $baseStyle->isFontBold()) { 45 | $styleToUpdate->setFontBold(); 46 | } 47 | if (!$style->hasSetFontItalic() && $baseStyle->isFontItalic()) { 48 | $styleToUpdate->setFontItalic(); 49 | } 50 | if (!$style->hasSetFontUnderline() && $baseStyle->isFontUnderline()) { 51 | $styleToUpdate->setFontUnderline(); 52 | } 53 | if (!$style->hasSetFontStrikethrough() && $baseStyle->isFontStrikethrough()) { 54 | $styleToUpdate->setFontStrikethrough(); 55 | } 56 | } 57 | 58 | /** 59 | * @param Style $styleToUpdate Style to update (passed as reference) 60 | * @param Style $style 61 | * @param Style $baseStyle 62 | * @return void 63 | */ 64 | private function mergeOtherFontProperties(Style $styleToUpdate, Style $style, Style $baseStyle) 65 | { 66 | if (!$style->hasSetFontSize() && $baseStyle->getFontSize() !== Style::DEFAULT_FONT_SIZE) { 67 | $styleToUpdate->setFontSize($baseStyle->getFontSize()); 68 | } 69 | if (!$style->hasSetFontColor() && $baseStyle->getFontColor() !== Style::DEFAULT_FONT_COLOR) { 70 | $styleToUpdate->setFontColor($baseStyle->getFontColor()); 71 | } 72 | if (!$style->hasSetFontName() && $baseStyle->getFontName() !== Style::DEFAULT_FONT_NAME) { 73 | $styleToUpdate->setFontName($baseStyle->getFontName()); 74 | } 75 | } 76 | 77 | /** 78 | * @param Style $styleToUpdate Style to update (passed as reference) 79 | * @param Style $style 80 | * @param Style $baseStyle 81 | * @return void 82 | */ 83 | private function mergeCellProperties(Style $styleToUpdate, Style $style, Style $baseStyle) 84 | { 85 | if (!$style->hasSetWrapText() && $baseStyle->shouldWrapText()) { 86 | $styleToUpdate->setShouldWrapText(); 87 | } 88 | if (!$style->hasSetCellAlignment() && $baseStyle->shouldApplyCellAlignment()) { 89 | $styleToUpdate->setCellAlignment($baseStyle->getCellAlignment()); 90 | } 91 | if ($style->getBorder() === null && $baseStyle->shouldApplyBorder()) { 92 | $styleToUpdate->setBorder($baseStyle->getBorder()); 93 | } 94 | if ($style->getFormat() === null && $baseStyle->shouldApplyFormat()) { 95 | $styleToUpdate->setFormat($baseStyle->getFormat()); 96 | } 97 | if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) { 98 | $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor()); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Manager/Style/StyleRegistry.php: -------------------------------------------------------------------------------- 1 | [STYLE_ID] mapping table, keeping track of the registered styles */ 14 | protected $serializedStyleToStyleIdMappingTable = []; 15 | 16 | /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ 17 | protected $styleIdToStyleMappingTable = []; 18 | 19 | /** 20 | * @param Style $defaultStyle 21 | */ 22 | public function __construct(Style $defaultStyle) 23 | { 24 | // This ensures that the default style is the first one to be registered 25 | $this->registerStyle($defaultStyle); 26 | } 27 | 28 | /** 29 | * Registers the given style as a used style. 30 | * Duplicate styles won't be registered more than once. 31 | * 32 | * @param Style $style The style to be registered 33 | * @return Style The registered style, updated with an internal ID. 34 | */ 35 | public function registerStyle(Style $style) 36 | { 37 | $serializedStyle = $this->serialize($style); 38 | 39 | if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) { 40 | $nextStyleId = \count($this->serializedStyleToStyleIdMappingTable); 41 | $style->markAsRegistered($nextStyleId); 42 | 43 | $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; 44 | $this->styleIdToStyleMappingTable[$nextStyleId] = $style; 45 | } 46 | 47 | return $this->getStyleFromSerializedStyle($serializedStyle); 48 | } 49 | 50 | /** 51 | * Returns whether the serialized style has already been registered. 52 | * 53 | * @param string $serializedStyle The serialized style 54 | * @return bool 55 | */ 56 | protected function hasSerializedStyleAlreadyBeenRegistered(string $serializedStyle) 57 | { 58 | // Using isset here because it is way faster than array_key_exists... 59 | return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); 60 | } 61 | 62 | /** 63 | * Returns the registered style associated to the given serialization. 64 | * 65 | * @param string $serializedStyle The serialized style from which the actual style should be fetched from 66 | * @return Style 67 | */ 68 | protected function getStyleFromSerializedStyle($serializedStyle) 69 | { 70 | $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle]; 71 | 72 | return $this->styleIdToStyleMappingTable[$styleId]; 73 | } 74 | 75 | /** 76 | * @return Style[] List of registered styles 77 | */ 78 | public function getRegisteredStyles() 79 | { 80 | return \array_values($this->styleIdToStyleMappingTable); 81 | } 82 | 83 | /** 84 | * @param int $styleId 85 | * @return Style 86 | */ 87 | public function getStyleFromStyleId($styleId) 88 | { 89 | return $this->styleIdToStyleMappingTable[$styleId]; 90 | } 91 | 92 | /** 93 | * Serializes the style for future comparison with other styles. 94 | * The ID is excluded from the comparison, as we only care about 95 | * actual style properties. 96 | * 97 | * @param Style $style 98 | * @return string The serialized style 99 | */ 100 | public function serialize(Style $style) 101 | { 102 | // In order to be able to properly compare style, set static ID value and reset registration 103 | $currentId = $style->getId(); 104 | $style->unmarkAsRegistered(); 105 | 106 | $serializedStyle = \serialize($style); 107 | 108 | $style->markAsRegistered($currentId); 109 | 110 | return $serializedStyle; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php: -------------------------------------------------------------------------------- 1 | getOption(Options::TEMP_FOLDER); 27 | $zipHelper = $this->createZipHelper($entityFactory); 28 | 29 | return new FileSystemHelper($tempFolder, $zipHelper); 30 | } 31 | 32 | /** 33 | * @param InternalEntityFactory $entityFactory 34 | * @return ZipHelper 35 | */ 36 | private function createZipHelper($entityFactory) 37 | { 38 | return new ZipHelper($entityFactory); 39 | } 40 | 41 | /** 42 | * @return Escaper\ODS 43 | */ 44 | public function createStringsEscaper() 45 | { 46 | return new Escaper\ODS(); 47 | } 48 | 49 | /** 50 | * @return StringHelper 51 | */ 52 | public function createStringHelper() 53 | { 54 | return new StringHelper(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Spout/Writer/ODS/Creator/ManagerFactory.php: -------------------------------------------------------------------------------- 1 | entityFactory = $entityFactory; 35 | $this->helperFactory = $helperFactory; 36 | } 37 | 38 | /** 39 | * @param OptionsManagerInterface $optionsManager 40 | * @return WorkbookManager 41 | */ 42 | public function createWorkbookManager(OptionsManagerInterface $optionsManager) 43 | { 44 | $workbook = $this->entityFactory->createWorkbook(); 45 | 46 | $fileSystemHelper = $this->helperFactory->createSpecificFileSystemHelper($optionsManager, $this->entityFactory); 47 | $fileSystemHelper->createBaseFilesAndFolders(); 48 | 49 | $styleMerger = $this->createStyleMerger(); 50 | $styleManager = $this->createStyleManager($optionsManager); 51 | $worksheetManager = $this->createWorksheetManager($styleManager, $styleMerger); 52 | 53 | return new WorkbookManager( 54 | $workbook, 55 | $optionsManager, 56 | $worksheetManager, 57 | $styleManager, 58 | $styleMerger, 59 | $fileSystemHelper, 60 | $this->entityFactory, 61 | $this 62 | ); 63 | } 64 | 65 | /** 66 | * @param StyleManager $styleManager 67 | * @param StyleMerger $styleMerger 68 | * @return WorksheetManager 69 | */ 70 | private function createWorksheetManager(StyleManager $styleManager, StyleMerger $styleMerger) 71 | { 72 | $stringsEscaper = $this->helperFactory->createStringsEscaper(); 73 | $stringsHelper = $this->helperFactory->createStringHelper(); 74 | 75 | return new WorksheetManager($styleManager, $styleMerger, $stringsEscaper, $stringsHelper); 76 | } 77 | 78 | /** 79 | * @return SheetManager 80 | */ 81 | public function createSheetManager() 82 | { 83 | $stringHelper = $this->helperFactory->createStringHelper(); 84 | 85 | return new SheetManager($stringHelper); 86 | } 87 | 88 | /** 89 | * @param OptionsManagerInterface $optionsManager 90 | * @return StyleManager 91 | */ 92 | private function createStyleManager(OptionsManagerInterface $optionsManager) 93 | { 94 | $styleRegistry = $this->createStyleRegistry($optionsManager); 95 | 96 | return new StyleManager($styleRegistry); 97 | } 98 | 99 | /** 100 | * @param OptionsManagerInterface $optionsManager 101 | * @return StyleRegistry 102 | */ 103 | private function createStyleRegistry(OptionsManagerInterface $optionsManager) 104 | { 105 | $defaultRowStyle = $optionsManager->getOption(Options::DEFAULT_ROW_STYLE); 106 | 107 | return new StyleRegistry($defaultRowStyle); 108 | } 109 | 110 | /** 111 | * @return StyleMerger 112 | */ 113 | private function createStyleMerger() 114 | { 115 | return new StyleMerger(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Spout/Writer/ODS/Helper/BorderHelper.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class BorderHelper 23 | { 24 | /** 25 | * Width mappings 26 | * 27 | * @var array 28 | */ 29 | protected static $widthMap = [ 30 | Border::WIDTH_THIN => '0.75pt', 31 | Border::WIDTH_MEDIUM => '1.75pt', 32 | Border::WIDTH_THICK => '2.5pt', 33 | ]; 34 | 35 | /** 36 | * Style mapping 37 | * 38 | * @var array 39 | */ 40 | protected static $styleMap = [ 41 | Border::STYLE_SOLID => 'solid', 42 | Border::STYLE_DASHED => 'dashed', 43 | Border::STYLE_DOTTED => 'dotted', 44 | Border::STYLE_DOUBLE => 'double', 45 | ]; 46 | 47 | /** 48 | * @param BorderPart $borderPart 49 | * @return string 50 | */ 51 | public static function serializeBorderPart(BorderPart $borderPart) 52 | { 53 | $definition = 'fo:border-%s="%s"'; 54 | 55 | if ($borderPart->getStyle() === Border::STYLE_NONE) { 56 | $borderPartDefinition = \sprintf($definition, $borderPart->getName(), 'none'); 57 | } else { 58 | $attributes = [ 59 | self::$widthMap[$borderPart->getWidth()], 60 | self::$styleMap[$borderPart->getStyle()], 61 | '#' . $borderPart->getColor(), 62 | ]; 63 | $borderPartDefinition = \sprintf($definition, $borderPart->getName(), \implode(' ', $attributes)); 64 | } 65 | 66 | return $borderPartDefinition; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Spout/Writer/ODS/Manager/OptionsManager.php: -------------------------------------------------------------------------------- 1 | styleBuilder = $styleBuilder; 25 | parent::__construct(); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function getSupportedOptions() 32 | { 33 | return [ 34 | Options::TEMP_FOLDER, 35 | Options::DEFAULT_ROW_STYLE, 36 | Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, 37 | ]; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function setDefaultOptions() 44 | { 45 | $this->setOption(Options::TEMP_FOLDER, \sys_get_temp_dir()); 46 | $this->setOption(Options::DEFAULT_ROW_STYLE, $this->styleBuilder->build()); 47 | $this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php: -------------------------------------------------------------------------------- 1 | [] Map whose keys contain all the fonts used */ 14 | protected $usedFontsSet = []; 15 | 16 | /** 17 | * Registers the given style as a used style. 18 | * Duplicate styles won't be registered more than once. 19 | * 20 | * @param Style $style The style to be registered 21 | * @return Style The registered style, updated with an internal ID. 22 | */ 23 | public function registerStyle(Style $style) 24 | { 25 | if ($style->isRegistered()) { 26 | return $style; 27 | } 28 | 29 | $registeredStyle = parent::registerStyle($style); 30 | $this->usedFontsSet[$style->getFontName()] = true; 31 | 32 | return $registeredStyle; 33 | } 34 | 35 | /** 36 | * @return string[] List of used fonts name 37 | */ 38 | public function getUsedFonts() 39 | { 40 | return \array_keys($this->usedFontsSet); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Spout/Writer/ODS/Manager/WorkbookManager.php: -------------------------------------------------------------------------------- 1 | fileSystemHelper->getSheetsContentTempFolder(); 46 | 47 | return $sheetsContentTempFolder . '/sheet' . $sheet->getIndex() . '.xml'; 48 | } 49 | 50 | /** 51 | * Writes all the necessary files to disk and zip them together to create the final file. 52 | * 53 | * @param resource $finalFilePointer Pointer to the spreadsheet that will be created 54 | * @return void 55 | */ 56 | protected function writeAllFilesToDiskAndZipThem($finalFilePointer) 57 | { 58 | $worksheets = $this->getWorksheets(); 59 | $numWorksheets = \count($worksheets); 60 | 61 | $this->fileSystemHelper 62 | ->createContentFile($this->worksheetManager, $this->styleManager, $worksheets) 63 | ->deleteWorksheetTempFolder() 64 | ->createStylesFile($this->styleManager, $numWorksheets) 65 | ->zipRootFolderAndCopyToStream($finalFilePointer); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Spout/Writer/ODS/Writer.php: -------------------------------------------------------------------------------- 1 | throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); 28 | 29 | $this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder); 30 | 31 | return $this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Spout/Writer/WriterInterface.php: -------------------------------------------------------------------------------- 1 | getOption(Options::TEMP_FOLDER); 27 | $zipHelper = $this->createZipHelper($entityFactory); 28 | $escaper = $this->createStringsEscaper(); 29 | 30 | return new FileSystemHelper($tempFolder, $zipHelper, $escaper); 31 | } 32 | 33 | /** 34 | * @param InternalEntityFactory $entityFactory 35 | * @return ZipHelper 36 | */ 37 | private function createZipHelper(InternalEntityFactory $entityFactory) 38 | { 39 | return new ZipHelper($entityFactory); 40 | } 41 | 42 | /** 43 | * @return Escaper\XLSX 44 | */ 45 | public function createStringsEscaper() 46 | { 47 | return new Escaper\XLSX(); 48 | } 49 | 50 | /** 51 | * @return StringHelper 52 | */ 53 | public function createStringHelper() 54 | { 55 | return new StringHelper(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Spout/Writer/XLSX/Creator/ManagerFactory.php: -------------------------------------------------------------------------------- 1 | entityFactory = $entityFactory; 37 | $this->helperFactory = $helperFactory; 38 | } 39 | 40 | /** 41 | * @param OptionsManagerInterface $optionsManager 42 | * @return WorkbookManager 43 | */ 44 | public function createWorkbookManager(OptionsManagerInterface $optionsManager) 45 | { 46 | $workbook = $this->entityFactory->createWorkbook(); 47 | 48 | $fileSystemHelper = $this->helperFactory->createSpecificFileSystemHelper($optionsManager, $this->entityFactory); 49 | $fileSystemHelper->createBaseFilesAndFolders(); 50 | 51 | $xlFolder = $fileSystemHelper->getXlFolder(); 52 | $sharedStringsManager = $this->createSharedStringsManager($xlFolder); 53 | 54 | $styleMerger = $this->createStyleMerger(); 55 | $styleManager = $this->createStyleManager($optionsManager); 56 | $worksheetManager = $this->createWorksheetManager($optionsManager, $styleManager, $styleMerger, $sharedStringsManager); 57 | 58 | return new WorkbookManager( 59 | $workbook, 60 | $optionsManager, 61 | $worksheetManager, 62 | $styleManager, 63 | $styleMerger, 64 | $fileSystemHelper, 65 | $this->entityFactory, 66 | $this 67 | ); 68 | } 69 | 70 | /** 71 | * @param OptionsManagerInterface $optionsManager 72 | * @param StyleManager $styleManager 73 | * @param StyleMerger $styleMerger 74 | * @param SharedStringsManager $sharedStringsManager 75 | * @return WorksheetManager 76 | */ 77 | private function createWorksheetManager( 78 | OptionsManagerInterface $optionsManager, 79 | StyleManager $styleManager, 80 | StyleMerger $styleMerger, 81 | SharedStringsManager $sharedStringsManager 82 | ) { 83 | $rowManager = $this->createRowManager(); 84 | $stringsEscaper = $this->helperFactory->createStringsEscaper(); 85 | $stringsHelper = $this->helperFactory->createStringHelper(); 86 | 87 | return new WorksheetManager( 88 | $optionsManager, 89 | $rowManager, 90 | $styleManager, 91 | $styleMerger, 92 | $sharedStringsManager, 93 | $stringsEscaper, 94 | $stringsHelper 95 | ); 96 | } 97 | 98 | /** 99 | * @return SheetManager 100 | */ 101 | public function createSheetManager() 102 | { 103 | $stringHelper = $this->helperFactory->createStringHelper(); 104 | 105 | return new SheetManager($stringHelper); 106 | } 107 | 108 | /** 109 | * @return RowManager 110 | */ 111 | public function createRowManager() 112 | { 113 | return new RowManager(); 114 | } 115 | 116 | /** 117 | * @param OptionsManagerInterface $optionsManager 118 | * @return StyleManager 119 | */ 120 | private function createStyleManager(OptionsManagerInterface $optionsManager) 121 | { 122 | $styleRegistry = $this->createStyleRegistry($optionsManager); 123 | 124 | return new StyleManager($styleRegistry); 125 | } 126 | 127 | /** 128 | * @param OptionsManagerInterface $optionsManager 129 | * @return StyleRegistry 130 | */ 131 | private function createStyleRegistry(OptionsManagerInterface $optionsManager) 132 | { 133 | $defaultRowStyle = $optionsManager->getOption(Options::DEFAULT_ROW_STYLE); 134 | 135 | return new StyleRegistry($defaultRowStyle); 136 | } 137 | 138 | /** 139 | * @return StyleMerger 140 | */ 141 | private function createStyleMerger() 142 | { 143 | return new StyleMerger(); 144 | } 145 | 146 | /** 147 | * @param string $xlFolder Path to the "xl" folder 148 | * @return SharedStringsManager 149 | */ 150 | private function createSharedStringsManager($xlFolder) 151 | { 152 | $stringEscaper = $this->helperFactory->createStringsEscaper(); 153 | 154 | return new SharedStringsManager($xlFolder, $stringEscaper); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Spout/Writer/XLSX/Helper/BorderHelper.php: -------------------------------------------------------------------------------- 1 | [ 12 | Border::WIDTH_THIN => 'thin', 13 | Border::WIDTH_MEDIUM => 'medium', 14 | Border::WIDTH_THICK => 'thick', 15 | ], 16 | Border::STYLE_DOTTED => [ 17 | Border::WIDTH_THIN => 'dotted', 18 | Border::WIDTH_MEDIUM => 'dotted', 19 | Border::WIDTH_THICK => 'dotted', 20 | ], 21 | Border::STYLE_DASHED => [ 22 | Border::WIDTH_THIN => 'dashed', 23 | Border::WIDTH_MEDIUM => 'mediumDashed', 24 | Border::WIDTH_THICK => 'mediumDashed', 25 | ], 26 | Border::STYLE_DOUBLE => [ 27 | Border::WIDTH_THIN => 'double', 28 | Border::WIDTH_MEDIUM => 'double', 29 | Border::WIDTH_THICK => 'double', 30 | ], 31 | Border::STYLE_NONE => [ 32 | Border::WIDTH_THIN => 'none', 33 | Border::WIDTH_MEDIUM => 'none', 34 | Border::WIDTH_THICK => 'none', 35 | ], 36 | ]; 37 | 38 | /** 39 | * @param BorderPart $borderPart 40 | * @return string 41 | */ 42 | public static function serializeBorderPart(BorderPart $borderPart) 43 | { 44 | $borderStyle = self::getBorderStyle($borderPart); 45 | 46 | $colorEl = $borderPart->getColor() ? \sprintf('', $borderPart->getColor()) : ''; 47 | $partEl = \sprintf( 48 | '<%s style="%s">%s', 49 | $borderPart->getName(), 50 | $borderStyle, 51 | $colorEl, 52 | $borderPart->getName() 53 | ); 54 | 55 | return $partEl . PHP_EOL; 56 | } 57 | 58 | /** 59 | * Get the style definition from the style map 60 | * 61 | * @param BorderPart $borderPart 62 | * @return string 63 | */ 64 | protected static function getBorderStyle(BorderPart $borderPart) 65 | { 66 | return self::$xlsxStyleMap[$borderPart->getStyle()][$borderPart->getWidth()]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Spout/Writer/XLSX/Manager/OptionsManager.php: -------------------------------------------------------------------------------- 1 | styleBuilder = $styleBuilder; 29 | parent::__construct(); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function getSupportedOptions() 36 | { 37 | return [ 38 | Options::TEMP_FOLDER, 39 | Options::DEFAULT_ROW_STYLE, 40 | Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, 41 | Options::SHOULD_USE_INLINE_STRINGS, 42 | ]; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | protected function setDefaultOptions() 49 | { 50 | $defaultRowStyle = $this->styleBuilder 51 | ->setFontSize(self::DEFAULT_FONT_SIZE) 52 | ->setFontName(self::DEFAULT_FONT_NAME) 53 | ->build(); 54 | 55 | $this->setOption(Options::TEMP_FOLDER, \sys_get_temp_dir()); 56 | $this->setOption(Options::DEFAULT_ROW_STYLE, $defaultRowStyle); 57 | $this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true); 58 | $this->setOption(Options::SHOULD_USE_INLINE_STRINGS, true); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Spout/Writer/XLSX/Manager/SharedStringsManager.php: -------------------------------------------------------------------------------- 1 | 18 | sharedStringsFilePointer = \fopen($sharedStringsFilePath, 'w'); 44 | 45 | $this->throwIfSharedStringsFilePointerIsNotAvailable(); 46 | 47 | // the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later 48 | $header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>'; 49 | \fwrite($this->sharedStringsFilePointer, $header); 50 | 51 | $this->stringsEscaper = $stringsEscaper; 52 | } 53 | 54 | /** 55 | * Checks if the book has been created. Throws an exception if not created yet. 56 | * 57 | * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing 58 | * @return void 59 | */ 60 | protected function throwIfSharedStringsFilePointerIsNotAvailable() 61 | { 62 | if (!is_resource($this->sharedStringsFilePointer)) { 63 | throw new IOException('Unable to open shared strings file for writing.'); 64 | } 65 | } 66 | 67 | /** 68 | * Writes the given string into the sharedStrings.xml file. 69 | * Starting and ending whitespaces are preserved. 70 | * 71 | * @param string $string 72 | * @return int ID of the written shared string 73 | */ 74 | public function writeString($string) 75 | { 76 | \fwrite($this->sharedStringsFilePointer, '' . $this->stringsEscaper->escape($string) . ''); 77 | $this->numSharedStrings++; 78 | 79 | // Shared string ID is zero-based 80 | return ($this->numSharedStrings - 1); 81 | } 82 | 83 | /** 84 | * Finishes writing the data in the sharedStrings.xml file and closes the file. 85 | * 86 | * @return void 87 | */ 88 | public function close() 89 | { 90 | if (!\is_resource($this->sharedStringsFilePointer)) { 91 | return; 92 | } 93 | 94 | \fwrite($this->sharedStringsFilePointer, ''); 95 | 96 | // Replace the default strings count with the actual number of shared strings in the file header 97 | $firstPartHeaderLength = \strlen(self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER); 98 | $defaultStringsCountPartLength = \strlen(self::DEFAULT_STRINGS_COUNT_PART); 99 | 100 | // Adding 1 to take into account the space between the last xml attribute and "count" 101 | \fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1); 102 | \fwrite($this->sharedStringsFilePointer, \sprintf("%-{$defaultStringsCountPartLength}s", 'count="' . $this->numSharedStrings . '" uniqueCount="' . $this->numSharedStrings . '"')); 103 | 104 | \fclose($this->sharedStringsFilePointer); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Spout/Writer/XLSX/Manager/WorkbookManager.php: -------------------------------------------------------------------------------- 1 | fileSystemHelper->getXlWorksheetsFolder(); 46 | 47 | return $worksheetFilesFolder . '/' . \strtolower($sheet->getName()) . '.xml'; 48 | } 49 | 50 | /** 51 | * Closes custom objects that are still opened 52 | * 53 | * @return void 54 | */ 55 | protected function closeRemainingObjects() 56 | { 57 | $this->worksheetManager->getSharedStringsManager()->close(); 58 | } 59 | 60 | /** 61 | * Writes all the necessary files to disk and zip them together to create the final file. 62 | * 63 | * @param resource $finalFilePointer Pointer to the spreadsheet that will be created 64 | * @return void 65 | */ 66 | protected function writeAllFilesToDiskAndZipThem($finalFilePointer) 67 | { 68 | $worksheets = $this->getWorksheets(); 69 | 70 | $this->fileSystemHelper 71 | ->createContentTypesFile($worksheets) 72 | ->createWorkbookFile($worksheets) 73 | ->createWorkbookRelsFile($worksheets) 74 | ->createStylesFile($this->styleManager) 75 | ->zipRootFolderAndCopyToStream($finalFilePointer); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Spout/Writer/XLSX/Writer.php: -------------------------------------------------------------------------------- 1 | throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); 28 | 29 | $this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder); 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Use inline string to be more memory efficient. If set to false, it will use shared strings. 36 | * This must be set before opening the writer. 37 | * 38 | * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used 39 | * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened 40 | * @return Writer 41 | */ 42 | public function setShouldUseInlineStrings($shouldUseInlineStrings) 43 | { 44 | $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); 45 | 46 | $this->optionsManager->setOption(Options::SHOULD_USE_INLINE_STRINGS, $shouldUseInlineStrings); 47 | 48 | return $this; 49 | } 50 | } 51 | --------------------------------------------------------------------------------