Compare commits

...

477 Commits

Author SHA1 Message Date
Adrien Loison
8459666841 Add "archived project" statement 2022-05-26 15:51:47 +02:00
Adrien Loison
ec8d53b13c Make builds reproducible
By committing composer.lock, we make sure that dependencies are locked to a given version.
This ensures that builds are reproducible and deterministic.

Also, fixes some phpstan issues, that appeared with the latest version of PHPStan.
2022-05-26 15:49:00 +02:00
larsbonczek
0739e044da Fix encoding of Content-Disposition header
URL-encode file name in Content-Disposition header for file download.

fixes box/spout#878
2022-03-26 16:37:15 +01:00
Rodrigo Azevedo
cc42c1d29f Use DateTimeInterface 2022-02-06 22:38:43 +01:00
Jörg Mönke
5926207012 Update GlobalFunctionsHelper.php
fix for php 8.1
2022-02-04 22:11:33 +01:00
Adrien Loison
550a6831f3 Update ci.yml
Launch CI on pull requests
2022-01-25 14:29:02 +01:00
Adrien Loison
6f1b67b39d PHPStan Level 4 2022-01-23 15:21:49 +01:00
Adrien Loison
ea0a67d283 Remove var_dump 2022-01-21 14:31:32 +01:00
Adrien Loison
e95e0eeefd PHPStan Level 3 2022-01-16 22:44:00 +01:00
Adrien Loison
64a09a748d More deprecations fixes 2022-01-13 23:04:26 +01:00
Adrien Loison
7517e5c4de Upgrade PHPUnit config 2022-01-13 23:04:26 +01:00
Adrien Loison
e75f6f7301 Fix PHP 8.1 deprecations 2022-01-13 23:04:26 +01:00
Adrien Loison
27c6845b4b Upgrade to PHP 8.1 2022-01-13 23:04:26 +01:00
Adrien Loison
2499dc46b7 Integrate Coveralls 2022-01-13 14:46:46 +01:00
Adrien Loison
6b7366bb6f Upgrade PHP and dev dependencies
/!\ Removed PHP 7.2 support /!\

- PHPUnit 8 => 9 (+ fix the tests)
- PHP-CS-Fixer 2 => 3 (+ fix the code)
- Introduced PHP stan
2022-01-12 23:38:25 +01:00
Adrien Loison
6a10ec3586 Reduce number of jobs 2022-01-11 09:05:23 +01:00
Adrien Loison
9882bf0946 Fix errors on Windows 2022-01-11 09:05:23 +01:00
Adrien Loison
75c06807af Fix PHP Stan errors 2022-01-11 09:05:23 +01:00
Adrien Loison
0345b369c7 Fix tests with no locales + Apply CSFixer fixes 2022-01-11 09:05:23 +01:00
Adrien Loison
f8595e9d63 Setup Github actions 2022-01-11 09:05:23 +01:00
Adrien Loison
9533accd73 Create FUNDING.yml 2021-06-07 12:33:43 +02:00
Adrien Loison
9bdb027d31 Update documentation with number format 2021-05-14 23:18:09 +02:00
Adrien Loison
76017f0949 Skipped cells are in wrong order
This only happens when no sheet's dimension is specified.
When filling empty cells with empty strings, we push these new cells with the correct cell index but they are added at the end of the cells array (normal PHP behavior). This means that we were going from `{[0] => 'A', [2] => 'C'}` to `{[0] => 'A', [2] => 'C', [1] => ''}`. We therefore need to sort the array to get the values in the correct order ( `{[0] => 'A', [1] => '', [2] => 'C'}`).
2021-05-14 23:01:36 +02:00
Adrien Loison
fde8a495ca Inline strings can contain multiple value nodes
We were working under the assumption that XLSX's inline strings only had a single value node (`<t>`). This is incorrect.
To get the actual value of an inline string node, we need to concatenate the value of all its child nodes.
2021-05-14 22:36:34 +02:00
Adrien Loison
69eeeff478 Remove var_dump 2021-05-14 15:17:30 +02:00
Adrien Loison
2ff515c306 Support for strict OOXML
There are 2 types of OOXML format: transitional and strict. Transitional is what's mostly used but some softwares still allow XLSX to be generated using the strict OOXML format.
In this format, namespaces of the XML files are different: `http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings` is replaced by `http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings` for instance. To support both formats, Spout needs to be able to look for both.
2021-05-13 12:16:20 +02:00
Toby Allen
0837d49c2b Remove unneeded comma 2021-05-10 10:04:14 +02:00
Toby Allen
110876e32c Some small grammar changes 2021-05-10 10:04:14 +02:00
Adrien Loison
8c1f0cc447 Floats must not be stored as locale dependent
Floats are currently stored formatted per the locale setting. This leads to different values being written whether the locale uses "." or "," for the decimal point for instance. This poses a problem as floats must be stored using "." as the decimal point to be valid.
This commit ensures that the floats are stored correctly by forcing the formatting of the value.
2021-05-05 20:43:02 +02:00
Antoine Lamirault
eb84ec9364 Rename ManagedStyle to PossiblyUpdatedStyle and add documentation 2021-03-30 19:42:21 +02:00
Antoine Lamirault
8a17d6c71f Remove rowStyle reference and replace it by new RegisteredStyle class 2021-03-30 19:42:21 +02:00
Antoine Lamirault
c6f596c776 New code review fixs 2021-03-30 19:42:21 +02:00
Antoine Lamirault
11d91e1740 Code review changes 2021-03-30 19:42:21 +02:00
Antoine Lamirault
197fb9987a Register style can be skipped when already registered 2021-03-30 19:42:21 +02:00
Antoine Lamirault
a58b340835 Empty style on cell 2021-03-30 19:42:21 +02:00
Antoine Lamirault
57b6e87a65 Begin optimize xlsx write 2021-03-30 19:42:21 +02:00
yiranzai
91f756be0b remove custom headers 2021-03-18 20:05:10 +01:00
yiranzai
03e1ce438a Fixed Code Style 2021-03-18 20:05:10 +01:00
yiranzai
df9d96366f Fixed WriterAbstract::openToBrowser meet RFC6266 2021-03-18 20:05:10 +01:00
Andrii Dembitskyi
0f20c99a7f Fix constant usage in example 2021-02-10 10:53:55 +01:00
jmsche
9ab0b10a0f Contributing: added info about code style 2021-02-09 17:39:42 +01:00
jmsche
ed9322e309 Shorter (relevant) diff by php-cs-fixer for Travis CI 2021-02-09 13:58:05 +01:00
Oded Arbel
73347517f0 added comment with spec link, as requested 2021-02-09 11:21:25 +01:00
Oded Arbel
ad913f0100 write boolean value according to http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean instead of just "1" for true or "" for false; 2021-02-09 11:21:25 +01:00
jmsche
c29d1877b8 Fixed code style (probably due to recent php-cs-fixer version) 2021-02-08 22:03:03 +01:00
Petr Skoda
816596183f Add full support for PHP 8.0
Unfortunately due to PHPUnit 8.5 dependency
this also drops support for PHP 7.1
2021-02-08 14:31:55 +01:00
madflow
ab973cab34 use existing base folder 2019-12-19 23:24:24 +01:00
madflow
b8eb2bb814 error_reporting set to -1 2019-12-16 13:42:24 +01:00
madflow
f54f7a400c add PHP 7.4 2019-12-16 13:42:24 +01:00
Adrien Loison
7964dadc21 Add support for cells in error when writing XLSX and ODS
When appending data to an existing sheet, it was possible to get cells in error when reading (DIV/0 for instance). When trying to write them back, `addRow` would throw because `Cell`s in error were not supported by the writers.
2019-12-02 22:21:41 +01:00
drowe
eb88bb4c3a Automated native_function_invocation fixes 2019-11-18 12:17:27 +01:00
madflow
94b654175c disable no_superfluous_phpdoc_tags 2019-11-18 11:20:20 +01:00
Adrien Loison
dbdf5f7f38 [ODS] Add support for whitespaces inside <text:span>
The `<text:p>` node can contain the string value directly or contain child elements. In this case, whitespaces contain in the child elements should be replaced by their XML equivalent:
 - space => `<text:s />`
 - tab => `<text:tab />`
 - line break => `<text:line-break />`

@see https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415200_253892949
2019-10-27 20:14:51 +01:00
Adrien Loison
9f4c094fa0 Cell alignment
This PR adds support for cell alignment for XLSX and ODS files.
You can now align the content of the cells this way:
```
use Box\Spout\Common\Entity\Style\CellAlignment;
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;

$style = (new StyleBuilder())
    ->setCellAlignment(CellAlignment::RIGHT)
    ->build();
...
```

Possible cell alignments are: LEFT, RIGHT, CENTER and JUSTIFY.
2019-10-27 18:58:56 +01:00
Adrien Loison
0a0b1f7196 [docs] Fix site_url - take 2 2019-10-17 13:12:40 +00:00
Adrien Loison
db32a7c7db [docs] Fix site_url 2019-10-17 11:15:55 +00:00
Adrien Loison
859b8d336e [Docs] Force using current protocol scheme for resources and links
If the docs is accessed with https, change all links to be https as well.
2019-10-17 10:32:39 +00:00
Adrien Loison
8a2dcc946b Improve docs for local dev 2019-10-17 09:29:03 +00:00
Adrien Loison
74146c6224 Switch documentation site to HTTPS
Accessing the website through HTTP may be blocked in some regions of the world.
Using HTTPS should help with this problem and is the good thing to do anyway.
2019-10-17 07:44:13 +00:00
Adrien Loison
6a6d1df9df Fix typo - Boder => Border 2019-10-17 07:37:37 +00:00
Alexander Rakushin
52312b7045 Added tests for StyleRegistry and StyleMerger 2019-10-01 20:08:54 +00:00
Alexander Rakushin
d4e12b1812 Simplification of the code 2019-10-01 20:08:54 +00:00
Alexander Rakushin
e8c6d83104 сode formatting (CS Fixer) 2019-10-01 20:08:54 +00:00
Alexander Rakushin
40aecd7b90 Added support for cell formats when writing Excel files 2019-10-01 20:08:54 +00:00
Adrien Loison
16a2f91a22 Cell indexes not being respected when rendering row
Fixes #682
When calling `Row::setCellIndex`, it's possible to create a Row with holes.
Instead of iterating over existing cells of a Row, we should instead use the cell indexes (from 0 to max cell index).
2019-09-28 14:02:23 +00:00
Ilya Troy
2d297e954b Update getting-started.md
Fix writer usage example
2019-07-30 22:14:20 +02:00
Adrien Loison
2716d7eeed [PHP 7.4] Updated way to disable the fgetcsv/fputcsv escape character
From PHP 7.4, the recommended way to disable the escape character for fgetcsv() and fputcsv() is an empty string, instead of "\0".
Discussed here: https://github.com/php/php-src/pull/3515
2019-07-21 23:15:34 +02:00
Adrien Loison
1bbfd45b82 Support for missing styles XML file in XLSX
Some files don't have a "styles.xml" file. Excel supports these files, Spout should do too.
2019-07-20 16:48:51 +02:00
Adrien Loison
6c4086cf97 Fix reading of 1904 dates option
Whether the spreadsheet is using 1904 dates or not is controlled by a XML property. Its value can be the string "false" that is not mapped to the boolean "false" but to the boolean "true"... Therefore Spout was previously using the wrong date system when this property was set.
2019-06-04 09:36:51 +02:00
Adrien Loison
a296f73a98 Fix Github icon in docs 2019-05-24 10:12:35 +02:00
Adrien Loison
0f0bf64802 Merge branch 'master' into develop_3.0 2019-05-24 09:30:41 +02:00
Adrien Loison
9d33fcdd00 Update FAQ 2019-05-24 09:24:34 +02:00
Adrien Loison
4ff9717b0a Update FAQ in docs 2019-05-23 09:00:46 +02:00
Adrien Loison
c62177f0e4 Move documentation from gh-pages branch to 'docs' folder
To prepare the migration to 3.0, we need to change the location where the documentation is generated from.
Having a gh-pages branch makes it hard to synchronize the code and the docs. Having a "docs" folder in the repo itself simplifies this.
2019-05-22 09:45:45 +02:00
Adrien Loison
3beaa32021 Fix docblock 2019-05-21 19:43:54 +02:00
Adrien Loison
5ce5a488d1 Force UTF-8 encoding in htmlspecialchars 2019-05-21 19:37:09 +02:00
Adrien Loison
69b0fb9eaf Add methods to SheetInterface
The SheetInterface was missing methods common to all Sheets (getIndex, getName, isActive, isVisible).
2019-05-17 21:38:29 +02:00
Adrien Loison
40ee386edd Add helper functions to create specific readers and writers
Removed the `ReaderEntityFactory::createReader(Type)` method and replaced it by 3 methods:
- `ReaderEntityFactory::createCSVReader()`
- `ReaderEntityFactory::createXLSXReader()`
- `ReaderEntityFactory::createODSReader()`

This has the advantage of enabling autocomplete in the IDE, as the return type is no longer the interface but the concrete type. Since readers may expose different options, this is pretty useful.

Similarly, removed the `WriterEntityFactory::createWriter(Type)` method and replaced it by 3 methods:
- `WriterEntityFactory::createCSVWriter()`
- `WriterEntityFactory::createXLSXWriter()`
- `WriterEntityFactory::createODSWriter()`

Since this is a breaking change, I also updated the Upgrade guide.
Finally, the doc is up to date too.
2019-05-17 21:22:03 +02:00
Adrien Loison
6104d41857 Add tests runs on PHP 7.3 in Travis 2019-05-17 13:37:25 +02:00
Adrien Loison
4260c46b11 Update documentation for 3.0 2019-05-17 13:25:49 +02:00
Adrien Loison
4a9d0398ad Update Reader/WriterEntityFactory
Add `WriterEntityFactory::createWriterFromFile`, working like `ReaderEntityFactory::createReaderFromFile` (guessing writer type from file name).
Use static functions when needed.
2019-05-17 13:22:27 +02:00
madflow
e8693834a0 perf tests in development branch 2019-03-24 22:53:28 +01:00
Adrien Loison
171a2fab10 Fix test failure message 2019-02-02 09:55:16 +01:00
madflow
3d577197d2 upgrade guide 2019-01-30 16:01:25 +01:00
Adrien Loison
71cf0fe339 Fix sheet name escaping
Sheet names are stored as attributes of an XML entity. We therefore need a different escaping strategy, escaping quotes.
2019-01-26 16:14:15 +01:00
Adrien Loison
ee998f7173 For PHP 7.1 for Composer
If local PHP is 7.2, we still want to download dependencies compatible with PHP 7.1
2019-01-04 19:24:28 +01:00
madflow
6c8344c025 PHP version in readme 2019-01-04 19:23:27 +01:00
Yannick ROGER
a420e3fffa Fix phpunit (#604)
* Enforce PHP 5.4 on composer update
* Ran composer update
2018-12-03 16:42:00 +01:00
madflow
e99c80b3ad create a reader by file type #569 2018-10-23 13:33:39 +02:00
madflow
8f7f106555 doc update with new classes and signatures 2018-10-12 20:16:30 +02:00
madflow
738ea30f35 use expectNotToPerformAssertions 2018-10-08 10:09:47 +02:00
madflow
8a1c48b6b0 rename EntityFactory for writers and readers #526 2018-09-03 11:15:09 +02:00
madflow
e1acdc1fc5 (docs) removed Bower, use a CDN, Docker usage, updated Readme 2018-08-09 23:52:14 +02:00
madflow
b105d15f08 some migrations to PHP 7.1 2018-06-12 18:28:04 +02:00
madflow
b05ce01d3c delete unused ReaderOptions 2018-06-12 18:27:00 +02:00
Adrien Loison
195b0d4bda Upgrade to 3.0 guide 2018-06-04 08:46:37 +02:00
Adrien Loison
f7c483adbd Better support for errored cells 2018-06-03 22:31:24 +02:00
Adrien Loison
1b64a06fbe Move ReaderFactory into Common/Creator 2018-06-03 21:13:38 +02:00
Adrien Loison
d25a4ebd6d Add docs to .gitattributes 2018-06-03 21:08:21 +02:00
Adrien Loison
e83ac423dc Force PHP 7.1 2018-06-03 20:43:49 +02:00
Adrien Loison
799ad93d23 Force UTF-8 encoding in htmlspecialchars 2018-05-09 22:56:06 +02:00
madflow
5c0030854f appveyor 2018-03-28 09:50:56 +02:00
madflow
01ad5af2c5 fix risky tests and assert true for silent tests 2018-03-25 15:35:19 +02:00
madflow
29cf6245a1 added Row::getCellAtIndex method 2018-02-14 21:32:10 +01:00
madflow
21e0e9e6b1 implement Cell:isDate() for unification 2018-01-16 14:22:37 +01:00
madflow
e135b71473 remove unused import 2018-01-16 14:22:10 +01:00
madflow
d84f5168ec RowManager constructor does not take any arguments 2018-01-16 14:22:10 +01:00
madflow
88eee3be72 wrong parameter count in method call 2018-01-16 14:22:10 +01:00
Gabriel Caruso
4c7adbb33f Refactoring tests 2017-12-15 10:09:18 +01:00
Gabriel Caruso
9f4e28b3fd Clean else 2017-12-15 10:06:19 +01:00
Gabriel Caruso
0efdf48119 Support PHPUnit 6 2017-11-27 00:24:13 +01:00
madflow
cd0831ea8e #463, move gh-pages to docs folder 2017-11-26 20:02:20 +01:00
madflow
5b1bcc1303 (chore) add PHP 7.2 2017-11-20 22:24:01 +01:00
Adrien Loison
f5168114d0 Merge Reader and Writer entities
Merged Cell/Row/Style entities
2017-11-19 02:54:17 +01:00
Adrien Loison
4d1d1c1e87 various improvements 2017-11-19 02:41:07 +01:00
Adrien Loison
102e17159c Make ODS reader return Row object 2017-11-19 01:36:18 +01:00
Adrien Loison
68a96367a8 Remove StyleMerger from RowManager 2017-11-18 21:19:31 +01:00
Adrien Loison
78b6639480 Make XLSX reader return Row objects 2017-11-18 20:53:22 +01:00
Adrien Loison
a665b974fa Make CSV reader return Row objects 2017-11-18 19:08:27 +01:00
Adrien Loison
139f7fdfb3 Remove StyleMerger from Cell entity 2017-11-18 17:37:52 +01:00
Adrien Loison
c826d15472 Fix charachters escaping with CSV reader/writer
PHP's built-in functions fputcsv and fgetcsv are not RFC-4180 compliant and include an escape character that's not defined in the spec.
This results in escaping characters that should not be escaped.
This commit disables this escaping mechanism.
2017-11-11 16:21:05 +01:00
Adrien Loison
e2b519d6f9 Fetch XML file paths from Workbook Relationships 2017-11-11 15:25:12 +01:00
Adrien Loison
0c8a53c821 Fix isEmptyRow() check 2017-11-11 12:35:03 +01:00
Adrien Loison
5a470188a9 Merge remote-tracking branch 'origin/master' into develop_3.0 2017-11-11 12:20:28 +01:00
Adrien Loison
1c69dee9c9 Sheet visibility - ODS writer and reader 2017-11-11 11:11:47 +01:00
Adrien Loison
b9206fcb4b Sheet visibility - XLSX writer and reader 2017-11-11 11:11:47 +01:00
Adrien Loison
ddfa40e8b3 StyleMerger and RowManager changes
Move DI of StyleMerger to get rid of business logic in the entities Cell and Row
Simplify RowManager
2017-11-10 22:45:57 +01:00
Adrien Loison
111f82d35f Add tests for cell styling 2017-11-10 21:35:59 +01:00
madflow
8dd6487ea3 add upgrade documentation #492 2017-11-10 11:21:57 +01:00
madflow
7367b89384 always write rows even with no cells #492 2017-11-10 11:21:57 +01:00
Adrien Loison
8aec9ea992 Apply default row style in WorkbookManager
Instead of doing it in the Writer
2017-11-05 14:13:22 +01:00
Adrien Loison
3d0f108b1d Consolidate external EntityFactory
All entities will now be created through a single factory (including the Writers).
Also, added a EntityFactory::createRowFromArray() to make it easier to create rows
2017-11-05 13:18:29 +01:00
Adrien Loison
2d2151ac8d Split Writer EntityFactory into interal and external ones 2017-11-05 13:04:03 +01:00
Adrien Loison
ca5962271e Default row style should only apply to existing cells 2017-11-05 12:17:45 +01:00
Adrien Loison
727a90fd06 Remove @api annotation 2017-11-05 02:23:26 +01:00
Adrien Loison
3851e05f83 Remove @expectedException annotation 2017-11-05 02:21:09 +01:00
Adrien Loison
7274226b75 Row objects and Cell styling
This commit introduces Row and Cell entities, that will replace the arrays passed in previously.
It also adds support for Cell styling (instead of Row styling only).
2017-11-05 02:12:28 +01:00
Adrien Loison
fec27e9056
Update .php_cs.dist
Remove "void_return" rule
2017-11-04 16:43:36 +01:00
Adrien Loison
c74c0d9127 Add support for 1904 dates
This commit adds support for dates using the 1904 calendar (starting 1904-01-01 00:00:00).
It also fixes some issues with the dates in 1900 calendar (which now correctly start at 1899-12-30 00:00:00).
Finally, it is now possible to have negative timestamps, representing dates before the base date (and up to 0000-01-01 00:00:00), as per the SpreadsheetML specs. Note that some versions of Excel don't support negative dates...
2017-11-04 16:33:46 +01:00
Adrien Loison
e1ae3c8a81 Update php-cs config: increment_style 2017-11-04 13:32:22 +01:00
madflow
0ab053dc6e (chore) remove hhvm from the test grid 2017-10-13 18:50:10 +02:00
Adrien Loison
28c1bea28c Code style should not follow Yoda style 2017-10-04 00:53:05 +02:00
Lumír Toman
3681a3421a Change Worksheet function visibility from private to protected in order to class extending. We have created small package on top of the Spout for support cell number format and per cell styles. 2017-09-25 21:44:35 +02:00
Chris Muthig
3bbff7ea7d Fix WriterInterface docblock typo 2017-09-06 10:54:21 +02:00
Adrien Loison
b968513cb9 Fix code style 2017-09-06 00:33:43 +02:00
Adrien Loison
740fcfb8c1 Fix code before applying PHP CS Fixer 2017-09-06 00:33:43 +02:00
Adrien Loison
554ebf987b Setup PHP CS fixer
- Add PHP CS Fixer as a dev dependencies.
- Add PHP CS Fixer cache file to gitignore/gitattributes
- Add custom code style config
- Update TravisCI config to check code style
2017-09-06 00:33:43 +02:00
Adrien Loison
668c10a30d Introduce Managers for readers
Some helper classes were more managers than helpers...
2017-08-27 03:56:17 +02:00
Adrien Loison
61f2addefa Favor object creation in factories (#459)
Instead of passing factories in the constructors and let objects call the factory method, create all dependencies directly in the factories.
2017-08-27 02:40:39 +02:00
Adrien Loison
4ec3a21170 Random DI improvements (#458)
* Add random DI improvements

Fixing things that were previously missed

* Split InternalFactory into Manager and Helper factories
2017-08-27 01:44:20 +02:00
Adrien Loison
b7e46740ce Refactor readers for better di (#457)
* Refactor readers to get a proper DI

Similar to what was done with writers, readers also needed to be updated to match the new way of doing things.
This commits promotes a better DI (factories, injection through constructors).

* Escapers should not be singletons

Instead, they should be proper object that can be injected where needed.
2017-08-27 00:01:17 +02:00
Adrien Loison
fd6dd46b25 Update gitignore (#453) 2017-07-27 23:40:26 +02:00
Adrien Loison
6d44cd26cc Fix prefixed shared strings XML file (#450)
A prefixed sharedStrings.xml file was not properly read, as we were comparing the un-prefixed name with the possible prefixed name.
Also, this commit contains a fix for sheets with rows not starting at column A.
2017-07-25 14:16:22 +02:00
Adrien Loison
ee5dee61c7 Fix HHVM jobs on Travis (#451) 2017-07-25 14:02:17 +02:00
madflow
4d6437fa77 merge master, resolve conflicts (#447) 2017-07-13 09:52:15 +02:00
Adrien Loison
40b4a57e6b Update README.md (#442) 2017-06-19 20:37:30 +02:00
madflow
5d4166196a HHVM is no longer supported on Ubuntu Precise (#439) 2017-06-02 08:46:48 +02:00
Adrien Loison
3bb4fd3d48 Move Sheet to Common/Entity (#438)
... and introduce a SheetManager
2017-05-30 16:49:07 +02:00
Adrien Loison
30366e6a5d Inject ZipHelper through constructor (#437) 2017-05-30 15:19:05 +02:00
Adrien Loison
762dd1573a Inject GlobalFunctionsHelper through constructor for Writers (#436) 2017-05-30 14:37:38 +02:00
Adrien Loison
7ec0f565fd Rename SharedStringsHelper to SharedStringsManager (#435) 2017-05-30 14:11:18 +02:00
Adrien Loison
cc9a0b526b Refactory Writer Styles to match new code organization (#433)
Decomposed old StyleHelper into StyleManager, StyleRegistry and StyleMerger.
2017-05-30 13:05:18 +02:00
Adrien Loison
238756ab6e Move Style classes into Common folder (#432) 2017-05-30 00:56:50 +02:00
Adrien Loison
c4e25a168e Move entities and managers back to Common (#431) 2017-05-29 23:28:10 +02:00
Adrien Loison
878c4a9c8b Rename Factory folder to Creator (#430) 2017-05-29 23:10:24 +02:00
Adrien Loison
cebffbe80c Move Cell and Options to Entity folder (#429) 2017-05-29 22:51:12 +02:00
Adrien Loison
69b091b37c Move OptionManager from Common/Manager to Manager (#428) 2017-05-29 22:34:25 +02:00
Adrien Loison
bc17311f5f Refactor writers for better DI (#427)
This commit is a big refactor that improves the code organization.
It focuses on how dependencies are injected into the different classes. This is now done via some factories.

Also, the code is now built around entities (data model that only exposes getters and setters), managers (used to manage an entity) and helpers (used by the managers to perform some specific tasks).

The refactoring is not fully complete, as some dependencies are still hidden...
2017-05-29 22:18:40 +02:00
Adrien Loison
a366d0d0af Introduce an options manager for all writers (#423)
This will improve the management of options and simplify some methods' signatures.
This commit will also help moving the code to a better state regarding Dependency Injection.
2017-05-24 13:17:50 +02:00
Adrien Loison
99816b0b8e Use openFileInZip() only (#421) 2017-05-22 14:39:26 +02:00
madflow
f9d8ad8be3 bump phpunit to the latest supported release, travis setup, ignore composer.lock (#419) 2017-05-22 10:39:03 +02:00
Adrien Loison
80553c6c52 Bump min PHP version to 5.6 (#415) 2017-05-05 15:14:47 +02:00
Adrien Loison
606103f7fc Move all documentation to Spout's website (#413)
The README became really long. All the documentation (README + Wiki) has been moved to a custom website: http://opensource.box.com/spout/.
It is now possible to remove all the duplicate content from the README.
2017-05-02 14:47:00 +02:00
madflow
4acd9ad087 Cell value objects (#383)
* first stab at cell objects, #182
* removed comment parameter, streamlined cell detection, more tests #182
* shorter constant names, missing isFormula() #182
* first batch of changes #182
* documentation #182
2017-05-01 12:09:24 +02:00
Adrien Loison
4e6db6a8a1 Update copyright year (#412) 2017-04-28 11:17:41 +02:00
Adrien Loison
048105461c Fix shared strings XML Entities auto decode (#411)
When converting an XMLReader node to a SimpleXMLElement, the conversion would automatically decode the XML entities. This resulted in a double decode.
For example: "&amp;#34;" was converted to "&#34;" when imported into a SimpleXMLElement and was again converted into " (quote).

This commit changes the way the XLSX Shared Strings file is processed. It also changes the unescaping logic for both XLSX and ODS.

Finally, it removes any usage of the SimpleXML library (yay!).
2017-04-28 02:27:33 +02:00
Adrien Loison
1eb01a3d2a Use constants instead of arbitrary strings in SheetHelper (#407) 2017-04-15 23:52:48 +02:00
Adrien Loison
9f80ece73f Minor fixes for tests (#406) 2017-04-15 23:33:50 +02:00
Adrien Loison
7f8b95b2f3 Expose Sheet::isActive() to provide info about the last active sheet (#405) 2017-04-15 21:40:19 +02:00
Adrien Loison
4a65466b61 Prevent error when close() called while writer already closed (#402) 2017-03-28 15:07:48 +02:00
Adrien Loison
742780613a Do not add space between text nodes (#401) 2017-03-28 14:36:15 +02:00
Adrien Loison
3128f86769 Remove max line length when reading CSV files (#399) 2017-03-28 13:55:06 +02:00
Adrien Loison
d898f91917 Update README.md (#398) 2017-03-27 18:28:12 +02:00
Adrien Loison
33c9d2f2ed Enforce sheet name uniqueness per workbook (#397)
Instead of across all workbooks (in case of multiple spreadsheets being created at the same time).
2017-03-27 17:58:06 +02:00
someson
36d3596f83 Fixing the Bug reading the ODS Sheetnames (#389)
incorrect sequence of arguments creating a Sheet in
Reader/ODS/SheetIterator::current()
Tests added.
2017-02-28 10:25:25 +11:00
Stian Liknes
1ce931a424 Handle empty rows without E_WARNING when filling missing array indexes (#385)
In some cases, reading an XLSX file produce E_WARNING from the max()
call in the fillMissingArray() method. This commit fix the problem
by handling empty rows.
2017-02-19 23:30:35 +13:00
Lito
6f4ddb1569 Fixed processDimensionStartingNode regular expression (#372)
* Fixed processDimensionStartingNode regular expression
* Improved processDimensionStartingNode regular expression
* Removed strict control on processDimensionStartingNode regular expression
2017-01-04 18:50:12 +01:00
Adrien Loison
521f799366 [XLSX] A cell should not contain more than 32,767 characters (#365) 2016-11-29 15:43:38 -08:00
Adrien Loison
984c9c1f67 Improve error message when invalid sheet name set (#364)
Instead of listing all the requirements, only list the requirements that actually failed to be met.
2016-11-29 14:36:57 -08:00
Adrien Loison
e255895cff Refactor ODS escaper (#363) 2016-11-28 17:03:13 -08:00
Adrien Loison
e276b4378e Fix crash when using associative array with empty row (#354) 2016-11-03 13:45:18 -07:00
Adrien Loison
9ce77405e0 Refactor Style::mergeWith() and StyleHelper::getStyleSectionContent() (#346) 2016-10-19 17:01:19 -07:00
madflow
ef4a32eb5e Fix issue 329 (#333)
* Used ENT_DISALLOWED when escaping a string.
* Added workaround for environments where ENT_DISALLOWED is not defined
2016-10-19 09:46:37 -07:00
Adrien Loison
4cb30bc36d Add Scrutinizer Code Quality badge (#345) 2016-10-18 20:29:33 -07:00
Adrien Loison
bf616dee90 Bump dev-master version (#344) 2016-10-18 20:03:15 -07:00
Adrien Loison
3a330debb3 Move ReaderCommonOptions class to Common folder (#343) 2016-10-18 16:55:05 -07:00
Adrien Loison
a19231fb68 Introduce XMLProcessor to reduce ODS,XLSX readers' complexity (#342) 2016-10-18 16:28:26 -07:00
Adrien Loison
73d5d0ea17 Remove text suffix in XLSX date formats (#341)
Some date formats have a text suffix, e.g. "mm/dd/yy;@". We should remove the ";...@" part.
2016-10-18 11:55:36 -07:00
Adrien Loison
687c321363 Refactor SharedStringsHelper::extractSharedStrings (#340) 2016-10-18 00:03:15 -07:00
Adrien Loison
2fa01cd838 Remove unused SimpleXMLElement::children() method (#339) 2016-10-17 22:49:37 -07:00
Adrien Loison
752f4bf64e Add ReaderOptions for all readers (#338)
Instead of passing every single option down the chain
2016-10-17 22:41:36 -07:00
Adrien Loison
b61323d7d2 Remove XLSX references in ODS Writer (#337) 2016-10-17 11:54:49 -07:00
Adrien Loison
179ab483d6 Empty rows do not need to be written for XLSX files (#336) 2016-10-17 11:40:52 -07:00
Adrien Loison
2fafb63115 ODS Reader should support num-rows-repeated for non empty rows (#335) 2016-10-17 10:51:12 -07:00
Adrien Loison
5ef5647558 Make getConcreteSheetIterator() protected (#334) 2016-10-17 10:26:37 -07:00
Adrien Loison
0978d340f0 Option to keep empty rows (#331)
* Add option to preserve empty rows when reading an XLSX file
* Add option to preserve empty rows when reading a CSV file
* Add option to preserve empty rows when reading an ODS file
2016-10-17 10:20:02 -07:00
Adrien Loison
77178122c3 Fix output file deletion after exception thrown on write (#328)
For relative paths, it would not work as the FileSystemHelper would not allow deleting a file that's not part of its base folder.
2016-10-12 11:15:07 -07:00
Adrien Loison
23f8cc4f05 Temp files should be deleted when an exception is thrown (#327)
If an exception is thrown while writing data, instead of letting the developer handle this situation gracefully, Spout can attempt to delete all the temporary files that were created so far, as well as the output file as it won't be completed and therefore corrupted.
2016-10-11 15:08:37 -07:00
Adrien Loison
442a9837f1 Removing license badge (#320)
As Github now provides this information in the project's header
2016-09-30 10:11:05 -07:00
Adrien Loison
cc07072cbb Better support for Date custom format (#316)
- To determine if a style should apply a date format, the presence of "applyNumberFormat" attribute on the "cellXfs" section of styles.xml is now optional. We only look at the "numFmtId" attribute (but early return if "applyNumberFormat" is set to "0").
- The format code can contain lowercase AND now uppercase characters as its pattern.
- "General" format code used as a custom format is now supported. It seems to be used by a bunch of programs...
2016-09-24 10:46:42 -07:00
Hastegan
30aa1b87e2 Fix boolean notation in PHPDoc (#314) 2016-09-16 15:12:58 -07:00
Hastegan
ddb7365a79 Add a way to disable text wrapping through the api (Fix #247) (#311)
* Add a way to disable text wrapping (Fix #247)
* Fix PHPDoc boolean notation
* Fix PHPDoc param notation
2016-09-16 11:24:36 -07:00
Adrien Loison
a07a96f523 Bump dev-master version (#309)
Bump to `2.7.x-dev` as 2.6.0 just got released
2016-09-08 09:34:43 -07:00
Adrien Loison
3e0afd858f Apply custom style to empty cells if needed (#307)
Fixes #295

If a row should be written with a custom style, the handling of empty cells should change.
Instead of being skipped entirely, empty cells will be applied the custom style, if this style has custom background color or borders.
If not, then the cell definition can still be skipped.
2016-09-07 17:04:31 -07:00
Adrien Loison
d4e57b1f0d Add consistency using getCellXML() (#306) 2016-09-06 19:47:18 -07:00
Adrien Loison
435a9a016e Improve XLSX Escaper performance (#305) 2016-09-03 11:04:25 -07:00
Stefan
5e7a1745ac Initial extraction of method getCellXml from addRow (#302) 2016-09-02 19:48:10 -07:00
madflow
277c353984 Fix #297 (#299) 2016-08-30 10:56:52 -07:00
madflow
ff2d54cc8d Fix #276, some general refinement (#289)
* Fix #276, some general refinement

* Failing test #267

* Fixed shared border definitions across different styles #267

* Fix finding the correct borderId
2016-08-23 19:57:57 -07:00
Adrien Loison
c94694cb60 API to set default row style (#290) 2016-08-16 21:18:44 -07:00
madflow
584121d478 Add background-color to styles (#211)
Removed default background color, cosmetics
Remove default background color
reuse bg colors
Cosmetics
Moved reusing fills to XLSX StyleHelper
Tests and inline doc
2016-08-10 13:11:47 -07:00
Adrien Loison
b2dc0c3fa9 Fix tests on Windows (#288) 2016-08-10 12:09:37 -07:00
Adrien Loison
7f65993c87 Spout should be able to read prefixed styles.xml (#287) 2016-08-09 20:53:40 -07:00
Adrien Loison
b75a3e34fc XLSX cells containing date values should respect shouldFormatDate option (#282)
Return the ISO 8601 date string directly if option is set
2016-07-20 20:12:00 -07:00
Adrien Loison
82605ab57b Do not return anything from constructor (#275) 2016-07-14 10:53:44 -07:00
madflow
7a613eed8c Border ordering (#273)
* Fix #271

* Minor fix for quick copy+paste

* Added link to issue #271
2016-07-13 11:45:37 -07:00
Adrien Loison
54a1a09e29 Bump dev-master version (#270) 2016-07-11 20:07:56 +02:00
Adrien Loison
a8eb7ad39c Shared strings table without uniqueCount and count should work (#269)
Use file based strategy in this case
2016-07-11 19:03:37 +02:00
Adrien Loison
ffea8871a6 Add support for missing cell reference (#268)
When describing a cell, the cell reference (r="A1") is optional.
When not present, we should just increment the index of the last processed row.
2016-07-11 18:15:55 +02:00
Marie
b02d13cd40 Set BOM as optional on CSV writer (#265) 2016-07-07 15:21:41 +02:00
rlukasz
aa25678a83 Update RowIterator.php (#263) 2016-07-04 11:31:03 +02:00
Adrien Loison
192659cb24 Update README.md (#256) 2016-06-19 20:20:49 -07:00
madflow
e30bc37448 First stab at #135 - Add borders (#200)
Fixes #135

Added border support for ODS and XLSX files.
Updated README.
2016-06-18 23:36:10 -07:00
Adrien Loison
1891c0b053 Fix XLSX reading when shared strings is missing the uniqueCount attribute (#255)
Use "count" attribute as a fallback
2016-06-16 10:06:11 -07:00
madflow
a43c13a36f Fix #252 (#253) 2016-06-14 10:02:01 -07:00
Adrien Loison
dc31d6e8c2 Update .scrutinizer.yml (#248) 2016-06-09 10:52:01 -07:00
Adrien Loison
8edd8e2401 Clear previous output when openToBrowser() called (#243)
If any character has been outputted before, the generated file will be corrupted.
2016-06-08 13:53:41 -07:00
madflow
cd38ba093e Fix #245 (#246) 2016-06-08 09:50:00 -07:00
Pavel Zyukin
70c81e809f Add the ability to pass an array with various keys to addRows() (#240) 2016-06-03 08:09:37 -07:00
Adrien Loison
1d3a9f939c Convert escapers to singletons (#239) 2016-05-30 13:55:21 -07:00
Ingmar Runge
efebfb2bc2 CellValueFormatterTest: fix expectations for 32bit PHP (#234) 2016-05-30 10:25:30 -07:00
Adrien Loison
251c0bebc1 Adding open_file_in_zip() helper function to XMLReader (#238) 2016-05-29 23:22:57 -07:00
Adrien Loison
03866a6604 Support XLSX with prefixed XML files (#237)
While the standard is not to have prefixes, some XLSX files have XML files containing a prefix.
Microsoft has a tool that generates such files: https://msdn.microsoft.com/en-us/library/office/gg278316.aspx
2016-05-29 22:16:59 -07:00
Adrien Loison
2ed30321b4 ODS Writer should accept associative arrays (#232) 2016-05-25 19:59:18 -07:00
Adrien Loison
2c80b1f23a XLSX Reader should add a space between text nodes (#229)
When a cell contains multiple text nodes, the cell value is currently obtained by concatenating the value of each text node.
Instead, values should still be concatenated but a space should be added in between.
2016-05-23 14:15:48 -07:00
Adrien Loison
a24e794177 Update README.md (#228)
Fixed the amount of memory used by Spout, as it used to take into account the memory used by PHPUnit.
2016-05-23 13:36:07 -07:00
Adrien Loison
104cd9b811 Option to return formatted dates instead of PHP objects (#226)
When reading spreadsheets, Spout should be able to return formatted dates, as shown when opened with Excel for instance.
It currently only returns DateTime/DateInterval objects, making it impossible to read + write, as the Writer does not accept objects.
2016-05-20 16:08:35 -07:00
Adrien Loison
a6a6b158de Pin PHPUnit version to keep support for PHP5.4 (#227) 2016-05-20 16:00:22 -07:00
madflow
2d923c7e46 Fix issue #218 (#222) 2016-05-20 09:32:47 -07:00
Adrien Loison
b4724906c4 Add support for cells formatted as time (#224)
Cells formatted as "time" have values between 0 and 1. These values used to be considered as invalid.
Note: this uses what was started in #202
2016-05-19 13:10:47 -07:00
Adrien Loison
bb20d2e6bb Update Scrutinizer dependency and code coverage (#223) 2016-05-19 11:25:00 -07:00
Adrien Loison
b8fd789ac0 Retrieve XLSX sheets in order of appearance (#220)
Instead of relying on the ID, sheets should be retrieved in the order they appear in the file.
Workbook.xml describes the correct order.
This allows the reader to read data in the correct order when sheets have been manually moved after creation.
2016-05-19 10:37:48 -07:00
Adrien Loison
5a7c2c1262 Handle General number format as non date (#221)
If the number format is set to General (id = 0), do no try to format the value as a date
2016-05-19 09:40:12 -07:00
Adrien Loison
e9cd7a397e Merge pull request #199 from box/check_valid_resource_on_close
Check file handle is valid before manipulating it
2016-04-12 11:03:15 -07:00
Adrien Loison
8bb42ebc23 Check file handle is valid before manipulating it
This fixes issues when something went wrong on reader/writer init and the developer wants to close the reader/writer.
The file handle may not be defined so we need to add a check for it, before actually using it.
2016-04-12 10:36:59 -07:00
Adrien Loison
71a6f6a937 Merge pull request #196 from madflow/trim-inline-strings
Fix #195
2016-04-09 13:20:48 -07:00
madflow
616925148e Renamed xlsx file, #195 2016-04-07 08:58:54 +02:00
madflow
6f0f7c9690 Fix #195 2016-04-06 22:00:47 +02:00
Adrien Loison
b69e28050b Merge pull request #187 from skeleton/issue-183
Fix line breaks on CSV reader
2016-03-23 17:02:42 -07:00
skeleton
d6e8fe4b54 Fix line breaks on CSV reader 2016-03-23 23:26:49 +01:00
Adrien Loison
e321f30c3b Merge pull request #191 from box/close_file_pointer_when_done_writing
Writers did not close written file pointer
2016-03-23 10:45:42 -07:00
Adrien Loison
c31373fb1a Writers did not close written file pointer 2016-03-23 10:21:27 -07:00
Adrien Loison
6c57125c0c Merge pull request #189 from madflow/ods-missing-values
Fixes for #184
2016-03-21 10:24:38 -07:00
madflow
30837f869d Coding style and typos 2016-03-20 08:46:30 +01:00
madflow
e60054f3c4 More explicit rule for ignoring empty placeholder cells in Excel ODS #184 2016-03-19 11:34:32 +01:00
madflow
3ee7099c95 Fix zeros treated as missing values #184 2016-03-19 11:34:32 +01:00
madflow
2b1160bb33 Tests for #184 2016-03-19 11:34:31 +01:00
Adrien Loison
049fd990b9 Merge pull request #188 from box/stream_wrapper_support
Custom stream wrapper support
2016-03-18 18:09:22 -07:00
Adrien Loison
d2ac54c578 Custom stream wrapper support
Added support for custom stream wrappers, such as "fly" or "s3".
Support is determined per reader.
2016-03-18 17:09:13 -07:00
Adrien Loison
0c90d102ef Merge pull request #179 from lichunqiang/gitattributes
Add .gitattributes
2016-02-21 18:11:03 -08:00
lichunqiang
e39dcb3847 Add .gitattributes 2016-02-22 09:48:28 +08:00
Adrien Loison
10d1140a95 Merge pull request #175 from IlyaBakhlin/master
Fixing the boolean bug.
2016-02-15 16:43:30 -08:00
Ilya Bakhlin
48debbcbca Simplifying the fix. 2016-02-15 22:11:04 +01:00
Ilya Bakhlin
f4d6fb87ee Fixing the boolean bug. 2016-02-15 15:55:46 +01:00
Adrien Loison
771afcb5f1 Merge pull request #173 from sfichera/master
Support for variable EOL for CSV
2016-02-14 00:20:48 -08:00
Sebastian Fichera
86e26632f6 Added test case for custom EOL characters... 2016-02-12 16:30:18 -06:00
Sebastian Fichera
8614f79da3 Minor fixes in order to be ok with naming conventions and code documentation... 2016-02-11 17:51:24 -06:00
Sebastian Fichera
4827e56cac Added new public function usage to docs... 2016-02-11 17:15:48 -06:00
Sebastian Fichera
03e85ffc21 Added EOL configuration support while reading CSV files...
Enhancement for #172 issue…
2016-02-11 17:12:54 -06:00
Adrien Loison
e4cc8b4eaa Merge pull request #169 from welcoMattic/patch-1
Update README.md
2016-01-27 13:26:18 -08:00
Mathieu Santo Stefano--Féron
73341d06a9 Update README.md
Fix typo
2016-01-27 18:21:06 +01:00
Adrien Loison
209372462b Merge pull request #167 from box/fix_int_float_value_formatters
Fix CellValueFormatter for numeric values
2016-01-14 11:29:01 -08:00
Adrien Loison
4a5da2ad74 Fix CellValueFormatter for numeric values
The value passed into the format() function is coming from an XML file and has never been coerced.
Therefore, when checking is_int($value), the check always returns false - because it's a string.
Changing the check fixes the issue and Spout now correctly parses large numbers.
2016-01-14 11:11:31 -08:00
Adrien Loison
c48c07db99 Merge pull request #165 from box/support_xlsx_sheets_random_order
Support XLSX that are defined in random order
2016-01-08 08:50:49 -08:00
Adrien Loison
a804be4844 Support XLSX that are defined in random order
Some software generate [Content_Types].xml file with sheets definition in random order.
Instead of having the first sheet (id = 1) defined first, it may be defined in 3rd position.
Therefore, to read the file in the correct order, sheets order need to be fixed.
2016-01-08 08:42:29 -08:00
Adrien Loison
4bfbb41c95 Merge pull request #162 from box/update_branch_alias
Update dev-master branch alias
2015-12-22 12:22:15 +01:00
Adrien Loison
61b71bf379 Update dev-master branch alias 2015-12-22 12:17:15 +01:00
Adrien Loison
3c3294061a Merge pull request #161 from garak/patch-1
support branch alias
2015-12-21 12:05:16 +01:00
Massimiliano Arione
1fde6d836a support branch alias 2015-12-21 10:54:15 +01:00
Adrien Loison
05489cda88 Merge pull request #160 from box/reference-wiki
Update README.md
2015-12-21 10:52:46 +01:00
Adrien Loison
128db45f22 Update README.md 2015-12-21 10:45:02 +01:00
Adrien Loison
9a85d84a2e Merge pull request #155 from KiNgMaR/Fix-Dates-Beyond-2037
XLSX Date Support / Fix for years beyond 2037
2015-12-17 10:59:45 -08:00
Ingmar Runge
4407cffeff XLSX Date Support / Test + Fix for years beyond 2037
This also fixes years < 1902 on 32-bit PHP systems.
2015-12-17 08:52:15 +01:00
Adrien Loison
8d27b3097d Merge pull request #154 from box/fix_font_color_check
Fix hasSetFontColor check
2015-12-08 10:35:49 -08:00
Adrien Loison
2c6cb1ffe5 Fix hasSetFontColor check 2015-12-08 10:31:16 -08:00
Adrien Loison
22daea5f9a Merge pull request #153 from box/improve_zip_for_mime_detection
Improve ZIP interface for better mime detection
2015-12-05 19:13:47 -08:00
Adrien Loison
ed0e8f79cc Improve ZIP interface for better mime detection
The ZipHelper interface is now more generic and allow single files to be added.
It supports adding uncompressed files (for PHP7+), which is required to have the mime detection magic work with ODS files.
Also fixed a few issues with the created ODS file (thanks to https://odf-validator.rhcloud.com/)
2015-12-05 18:06:13 -08:00
Adrien Loison
44d72d8245 Merge pull request #152 from box/proper_mime_type_detection
Proper mime type detection for XLSX files
2015-12-05 00:30:28 -08:00
Adrien Loison
728dd3b399 Proper mime type detection for XLSX files
Heuristics to detect proper mime type for XLSX files expect to see
certain files at the beginning of the XLSX archive. The order in which
the XML files are added therefore matters.
Specifically, "[Content_Types].xml" should be added first, followed by the
files located in the "xl" folder (at least 1 file).
2015-12-05 00:20:40 -08:00
Adrien Loison
05a9a1b60a Merge pull request #146 from box/update_perf_tests_doc
Update README.md
2015-11-13 10:36:40 -08:00
Adrien Loison
a76624a721 Update README.md 2015-11-13 10:14:10 -08:00
Adrien Loison
90cbb7b5a6 Merge pull request #145 from box/speed_up
Various speed improvements
2015-11-12 14:09:52 -08:00
Adrien Loison
f55520661e Various speed improvements 2015-11-12 13:55:25 -08:00
Adrien Loison
cb5dae22e4 Merge pull request #143 from box/xlsx_skip_empty_cells_on_write
[XLSX] Skip empty cells on write
2015-11-10 17:26:26 -08:00
Adrien Loison
2f6193ce20 [XLSX] Skip empty cells on write
Since cells are referenced by their coordinates (A2, B4...), it is not necessary to write empty cells.
This will reduce the final size of the generated XML and therefore XLSX file.
2015-11-10 17:17:54 -08:00
Adrien Loison
582da8403d Merge pull request #139 from box/fix_phpdoc
Fix PHPDoc to work with Augmented Types
2015-11-05 15:52:56 -08:00
Adrien Loison
8b666fc6cd Fix PHPDoc to work with Augmented Types 2015-11-05 15:48:26 -08:00
Adrien Loison
f8aab6eefd Merge pull request #138 from box/increase_entropy_uniqid
Increase entropy of uniqid
2015-11-05 10:44:47 -08:00
Adrien Loison
c4c6dddb20 Increase entropy of uniqid
This is to avoid conflicts if two folders are being created at the exact same time.
2015-11-05 10:40:19 -08:00
Adrien Loison
6c97141679 Merge pull request #133 from philipbrown/patch-1
Update README.md
2015-10-27 11:44:01 -07:00
Philip Brown
9c8cc05364 Update README.md 2015-10-27 18:34:58 +00:00
Adrien Loison
2a9400dfca Merge pull request #131 from box/better_date_support_xlsx
Better date support
2015-10-23 16:26:00 -07:00
Adrien Loison
8ef6bdac62 Better date support
Although Excel has a Date type, older Excel versions use numeric values to store dates.
The value represents the number of days since Jan 1st, 1900.
The only way to tell if the value is a number or a date is to look at the styles.xml and check if the cell has date formatting.
2015-10-23 16:04:38 -07:00
Adrien Loison
46c246e6a4 Merge pull request #130 from box/highlight_xlsx_over_csv
Highlight XLSX over CSV in documentation
2015-10-23 11:00:04 -07:00
Adrien Loison
8fd606ae4f Highlight XLSX over CSV in documentation 2015-10-23 10:52:12 -07:00
Adrien Loison
6767386daf Merge pull request #128 from box/increase_max_read_bytes_for_csv
Increase max read bytes per line for CSV
2015-10-22 13:39:18 -07:00
Adrien Loison
3395d3abb3 Increase max read bytes per line for CSV
Specify a bigger value than the default one to support long lines.
2015-10-22 10:54:12 -07:00
Adrien Loison
d1c4d563c1 Merge pull request #126 from box/fix_xmlreader_open_issue_windows
Fix "Cannot open file" issue with XMLReader::open on Windows
2015-10-15 10:41:12 -07:00
Adrien Loison
01cc8b3da0 Fix "Cannot open file" issue with XMLReader::open on Windows
This occurred when using relative paths. Using realpath() solves this issue.
2015-10-15 09:19:47 -07:00
Adrien Loison
45980195cd Merge pull request #121 from box/add_test_for_empty_csv
Fix infinite loop for CSV with all lines empty
2015-10-05 21:15:11 +02:00
Adrien Loison
a1a1077677 Fix infinite loop for CSV with all lines empty
Only occured with multiline CSV files
2015-10-05 21:10:41 +02:00
Adrien Loison
16d0290a17 Merge pull request #115 from box/add_api_phpdoc
Added @api tag for documentation
2015-09-04 12:52:32 -07:00
Adrien Loison
f8c39287ad Added @api tag for documentation 2015-09-04 11:43:01 -07:00
Adrien Loison
118810de22 Merge pull request #114 from box/remove_duplicate_method
Remove duplicated method
2015-09-04 11:42:52 -07:00
Adrien Loison
3720c9ea1c Remove duplicated method 2015-09-04 11:37:44 -07:00
Adrien Loison
6124528161 Merge pull request #113 from box/all_ods_cell_types
Support all ODS cell types
2015-09-02 14:30:19 -07:00
Adrien Loison
818ec2488c Support all ODS cell types
Including:
- date / time
- currency
- percentage
- void

And improved support for boolean
2015-09-02 14:03:38 -07:00
Adrien Loison
aa7978146f Merge pull request #112 from box/cell_formatters
Moved cell value formatting logic into formatters
2015-09-02 00:21:25 -07:00
Adrien Loison
d6e707c5fe Moved cell value formatting logic into formatters 2015-09-02 00:12:59 -07:00
Adrien Loison
03ae367fb3 Update composer.json 2015-09-02 00:11:36 -07:00
Adrien Loison
ac48dce334 Merge pull request #110 from box/ods_documentation
ODS documentation
2015-09-01 23:23:10 -07:00
Adrien Loison
e147c580ca ODS documentation 2015-09-01 23:11:07 -07:00
Adrien Loison
7615acf398 Random fixes 2015-09-01 23:04:57 -07:00
Adrien Loison
1d38bb00c5 Merge pull request #108 from box/remove_xml_indentation
Remove XML indentation
2015-09-01 12:52:34 -07:00
Adrien Loison
79d947a6b3 Merge pull request #107 from box/remove_is_inside_row
Remove unused isInsideRowTag
2015-09-01 11:30:48 -07:00
Adrien Loison
dd3cc5bf47 Remove XML indentation 2015-09-01 11:24:13 -07:00
Adrien Loison
0a5be41c53 Remove unused isInsideRowTag 2015-09-01 10:59:33 -07:00
Adrien Loison
d2ba6d884c Merge pull request #106 from box/ods_reader
ODS Reader
2015-09-01 10:57:59 -07:00
Adrien Loison
e4154dfdc3 ODS Reader
Spout can now read ODS files.
It's on par with the XLSX reader. The only difference is that the row iterator cannot be rewound.
It supports the different output formats from LibreOffice and Excel, skipping extra rows/cells if needed.
2015-09-01 10:53:49 -07:00
Adrien Loison
3f0016f753 Merge pull request #103 from box/use_number_columns_repeated
Use number-columns-repeated in ODS writer
2015-08-31 12:55:37 -07:00
Adrien Loison
bc009a3241 Use number-columns-repeated in ODS writer
The number-columns-repeated usage may reduce the size of the outputted XML file by merging repeated values together.
2015-08-31 12:03:28 -07:00
Adrien Loison
0f8e7a8f58 Merge pull request #102 from box/improve_ods_writer
Improve ODS Writer
2015-08-31 10:02:06 -07:00
Adrien Loison
156fd29a44 Improve ODS Writer
Remove num-columns-repeated and num-rows-repeated as it does not seem to be required (LibreOffice does not add them).
This greatly simplifies the writer and the XML output.
Added some optional attributes to help LibreOffice with cell values caching ("calcext")
2015-08-31 09:55:17 -07:00
Adrien Loison
ef171910b9 Merge pull request #99 from box/ods_writer
ODS writer
2015-08-28 20:28:08 -07:00
Adrien Loison
5949cb2442 ODS writer
Added ODS writer
Refactored XLSX writer to abstract some pieces into an abstract multi-sheets writer
Created an abstract style helper
Moved shared components around
2015-08-28 20:19:45 -07:00
Adrien Loison
108a92e259 Merge pull request #97 from box/add_bool_type_xlsx_writer
Add boolean type when writing XLSX cell
2015-08-25 23:53:46 -07:00
Adrien Loison
9fb1427944 Add boolean type when writing XLSX cell 2015-08-25 23:36:24 -07:00
Adrien Loison
a8f1fba854 Merge pull request #96 from box/throw_if_xlsx_setters_called_after_open
Throw if XLSX Writer configured after being opened
2015-08-24 10:55:59 -07:00
Adrien Loison
1812b4f996 Throw if XLSX Writer configured after being opened 2015-08-24 10:52:12 -07:00
Adrien Loison
b4ace972e7 Update README.md 2015-08-22 19:39:43 -07:00
Adrien Loison
c428daff5a Update README.md 2015-08-22 18:14:33 -07:00
Adrien Loison
5908443583 Update README.md 2015-08-22 18:13:36 -07:00
Adrien Loison
050672755d Update README.md 2015-08-22 12:37:24 -07:00
Adrien Loison
e1928eb68a Merge pull request #93 from box/font_color
Add support for font color
2015-08-21 21:04:32 -07:00
Adrien Loison
9467b5a810 Add support for font color 2015-08-21 20:58:21 -07:00
Adrien Loison
2699ffbcae Merge pull request #92 from box/invalid_sheet_names_contd
Detection of invalid sheet name - continued
2015-08-21 16:51:02 -07:00
Adrien Loison
3559bc8834 Detection of invalid sheet name - continued
Invalid names can also be triggered by:
- character ":"
- single quote at the beginning of the name
- single quote at the end of the name

Introduced a StringHelper, wrapping multibyte strings functions
2015-08-21 16:44:13 -07:00
Adrien Loison
506d682e74 Merge pull request #91 from box/invalid_sheet_name_detection
Detection of invalid sheet name
2015-08-21 15:36:25 -07:00
Adrien Loison
7efab5576d Detection of invalid sheet name
Based on Excel requirements:
 - it should not be blank
 - it should not exceed 31 characters
 - it should not contain these characters: \ / ? * [ or ]
 - it should be unique
2015-08-21 15:21:36 -07:00
Adrien Loison
366f121eb0 Update README.md 2015-08-21 10:58:11 -07:00
Adrien Loison
f8d0ac2682 Merge pull request #89 from box/fix_ios_issue
Fix OfficeImportErrorDomain issue on iOS
2015-08-20 20:32:41 -07:00
Adrien Loison
82377403ff Fix OfficeImportErrorDomain issue on iOS 2015-08-20 20:28:34 -07:00
Adrien Loison
e0c84f77b1 Merge pull request #87 from box/use_travis_cache
Use Travis cache
2015-08-18 21:56:51 -07:00
Adrien Loison
b78227c370 Use Travis cache
This is to avoid re-downloading all dependencies for every build.
2015-08-18 21:52:25 -07:00
Adrien Loison
444308d42c Merge pull request #84 from box/rename_strikethrough
Rename StrikeThrough to Strikethrough
2015-08-13 23:18:26 -07:00
Adrien Loison
353d4e86a5 Merge pull request #83 from box/wrap_text_on_multiline_strings
Set wrap text style when multiline string encountered
2015-08-13 23:18:11 -07:00
Adrien Loison
f043f8d4d0 Rename StrikeThrough to Strikethrough 2015-08-13 23:09:43 -07:00
Adrien Loison
c8ddcf5441 Set wrap text style when multiline string encountered
Fixes #10
If a cell contains a multiline string, "wrap text" style option should
automatically be set.
2015-08-13 23:03:28 -07:00
Adrien Loison
4a346cef0c Update README.md
Display style options as a table instead of bullet points
2015-08-13 23:02:37 -07:00
Adrien Loison
1c8934790d Update README.md
Added "How to style a row" section
2015-08-13 20:33:37 -07:00
Adrien Loison
2a1925bc51 Merge pull request #82 from box/fix_double_equals
Replace == with ===
2015-08-10 19:21:18 -07:00
Adrien Loison
2183ff6738 Replace == with === 2015-08-10 19:13:40 -07:00
Adrien Loison
380ee524a5 Merge pull request #80 from box/add_style_support
Add support for styling
2015-08-10 19:11:59 -07:00
Adrien Loison
21263a0730 Add support for styling
Added top level methods on the Writer:
 - addRowWithStyle()
 - addRowsWithStyle()

Added a style builder, to easily create new styles.
Each writer can specify its own default style and all styles will automatically inherit from it.

For now, the style properties supported are:
 - bold
 - italic
 - underline
 - strikethrough
 - font size
 - font name
 - wrap text (alignment)
2015-08-07 20:39:17 -07:00
Adrien Loison
0104714cbd Merge pull request #79 from jtreminio/master
getRowIterator() is on $sheet variable
2015-07-29 20:44:56 -07:00
Juan Treminio
611a091290 getRowIterator() is on $sheet variable 2015-07-29 22:04:47 -05:00
Adrien Loison
40a86c4b6c Merge pull request #77 from box/fix_csv_reader_empty_last_line
Fix CSV reader when last line is empty
2015-07-29 10:27:44 -07:00
Adrien Loison
8a3b895afc Fix CSV reader when last line is empty
If the last line was empty, it would create an infinite loop...
2015-07-29 10:17:51 -07:00
Adrien Loison
e3f7ecfa64 Merge pull request #78 from box/fix_xml_reader_open_overriding
Fix XMLReader open() overriding
2015-07-29 10:17:34 -07:00
Adrien Loison
93d7aafe8b Fix XMLReader open() overriding
This is to avoid a warning in PHP7 (and also because that's how it should be!)
2015-07-29 09:59:33 -07:00
Adrien Loison
dc53b6aa20 Update README.md
Change Travis badge from PNG to SVG
2015-07-27 22:26:36 -07:00
Adrien Loison
30aa8970d5 Update README.md
Bump "require" version in preparation of the 2.0.0 release
2015-07-27 22:21:33 -07:00
Adrien Loison
2ec12dd16b Merge pull request #76 from box/csv_multiple_encodings
Csv multiple encodings
2015-07-27 21:02:52 -07:00
Adrien Loison
fd84c6f1c8 Update README 2015-07-27 20:59:21 -07:00
Adrien Loison
5e1cfbfdbd Attempt to convert the non UTF-8 strings to UTF-8 2015-07-27 20:59:12 -07:00
Adrien Loison
d946f12951 Support for multiple BOMs depending on the selected encoding 2015-07-27 09:36:55 -07:00
Adrien Loison
03d1917080 Merge pull request #75 from box/xml_reader_wrappers
Add wrappers around XMLReader and SimpleXMLElement
2015-07-27 09:36:13 -07:00
Adrien Loison
1ba10ed2b0 Add wrappers around XMLReader and SimpleXMLElement to improve error handling 2015-07-27 00:49:43 -07:00
Adrien Loison
be3932af18 Merge pull request #73 from box/iterators
Moved readers to iterators
2015-07-27 00:26:26 -07:00
Adrien Loison
37d87a8a27 Fix various problems 2015-07-27 00:23:18 -07:00
Adrien Loison
86a4c3790a Adding more tests 2015-07-26 23:53:49 -07:00
Adrien Loison
2345a80784 Update README for iterators 2015-07-26 23:53:18 -07:00
Adrien Loison
15aab7902a Factory should return Interface 2015-07-26 23:53:17 -07:00
Adrien Loison
c672558a18 Update Writer folder structure to match Reader new structure 2015-07-26 23:53:17 -07:00
Adrien Loison
c52dd7bde8 Remove old reader files 2015-07-26 23:53:17 -07:00
Adrien Loison
ae3ee357ff Moved readers to iterators
Instead of the hasNext() / next() syntax, readers now implements the PHP iterator pattern.
It allows readers to be used with a foreach() loop.

All readers now share the same structure (CSV is treated as having exactly one sheet):
- one concrete Reader
- one SheetIterator, exposed by the Reader
- one or more Sheets, returned at every iteration
- one RowIterator, exposed by the Sheet

Introducing the concept of sheets for CSV may be kind of confusing but it makes Spout way more consistent.
Also, this confusion may be resolved by creating a wrapper around the readers if needed.

-- This commit does not delete the old files, not change the folder structure for Writers. This will be done in another commit.
2015-07-26 23:53:17 -07:00
Adrien Loison
322c3d0738 Merge pull request #74 from box/scrutinizer
Use Scrutinizer instead of Coveralls
2015-07-26 23:52:30 -07:00
Adrien Loison
0adbf439f5 Use Scrutinizer instead of Coveralls 2015-07-26 23:49:19 -07:00
Adrien Loison
ad6b881685 Update README.md 2015-07-16 23:16:31 -07:00
Adrien Loison
d410224e2d Merge pull request #71 from box/manual_autoloader
Autoloader for manual installation
2015-07-16 00:51:50 -07:00
Adrien Loison
79982a6107 Autoloader for manual installation
Added PSR4 Autoloader
Updated README with manual installation instructions
2015-07-16 00:46:15 -07:00
Adrien Loison
6ae79b63b3 Merge pull request #67 from box/caching_strategies
Caching strategies
2015-07-14 10:58:37 -07:00
Adrien Loison
277b665dad Merge pull request #68 from box/add_coverage_abstract_reader
Improve coverage AbstractReader
2015-07-14 10:55:22 -07:00
Adrien Loison
b0c7c6ca84 Improve coverage AbstractReader 2015-07-14 10:48:03 -07:00
Adrien Loison
494c506d56 Add logic to automatically select the best caching strategy
Based on the number of unique shared strings as well as the available memory amount,
one strategy will be chosen over the other.
The algorithm is based on empirical data and super safe so it may need to be tuned.
2015-07-14 02:26:01 -07:00
Adrien Loison
334f7087da Add in-memory caching strategy for shared strings
In-memory implementation using SplFixedArray
Updated code and tests to support errors when reading XML nodes (useful when reading XML files used for attacks)
Removed LIBXML_NOENT option (which DOES substitute entities...)
Added test for Quadratic Blowup attack
2015-07-13 00:29:59 -07:00
Adrien Loison
2dcb86aae9 Move shared strings caching strategy into its own component
This will help implementing different caching strategies:
- file based
- in-memory
2015-07-11 14:12:18 -07:00
Adrien Loison
3edb056286 Merge pull request #60 from Lewiscowles1986/Update-XLSX-Reader
Separated getCellValue into multiple functions
2015-07-06 10:12:35 -07:00
Lewis Cowles
edc0c22009 Update XLSX.php 2015-07-06 12:10:41 +01:00
Adrien Loison
0319e578cb Update README.md 2015-07-02 16:19:55 -07:00
Lewis
1e2452934c Additional tests for Cell Types 2015-07-02 19:35:23 +01:00
Lewis
3e1793d852 Changes made to XLSX.php 2015-07-02 17:56:55 +01:00
Lewis Cowles
c1b1bd0b76 Separated getCellValue into multiple functions
Author:    Lewis Cowles <lewiscowles@me.com>
Committer: Lewis <lewis@chromebook>
	modified:   src/Spout/Reader/XLSX.php
2015-07-02 17:41:00 +01:00
Adrien Loison
cf239905d7 Merge pull request #61 from box/coveralls_support
Adding coveralls.io support
2015-07-01 16:20:46 -07:00
Adrien Loison
7c8f9293cc Adding coveralls.io support
Updated TravisCI config, using:
- http://docs.travis-ci.com/user/languages/php/
- https://github.com/satooshi/php-coveralls#travis-ci

Removed automatic code coverage execution when running phpunit
Added coveralls.io badge
2015-07-01 16:15:58 -07:00
Adrien Loison
503ba97e9d Merge pull request #59 from box/fix_xlsx_writer_on_windows
Fix XLSX Writer on Windows plaftorms
2015-07-01 15:29:10 -07:00
Adrien Loison
b3df57d2e5 Fix XLSX Writer on Windows plaftorms
A bug was introduced, preventing Spout to create valid XLSX files on Windows.
This commits reverts the changes that introduced DIRECTORY_SEPARATOR everywhere
and fixes the original issue with the writer by normalizing paths when creating
the zipped file.
2015-07-01 15:24:58 -07:00
Adrien Loison
c6ebf115fc Merge pull request #58 from box/improve_xml_security
Prevent entity loading when reading XML
2015-07-01 14:19:49 -07:00
Adrien Loison
7d922e6776 Prevent entity loading when reading XML
Added LIBXML_NOENT option when reading a XML file
libxml_disable_entity_loader(true) cannot be used because it disables
the use of XMLReader::open()... see https://bugs.php.net/bug.php?id=62577
2015-07-01 14:07:15 -07:00
Adrien Loison
d4cf853270 Merge pull request #48 from box/add_support_for_more_cell_types
Add support for more cell types
2015-06-03 11:24:43 -07:00
Adrien Loison
8bac924d48 Add support for more cell types
Added proper support for booleans, dates, numbers, errors.
Added unescaping of the read string.
Fixed a bug when cells did not have any values => now returns empty string.
2015-06-03 11:19:21 -07:00
Adrien Loison
591d86cf07 Merge pull request #46 from box/remove_version_from_composer_json
Remove version from composer.json
2015-06-02 21:47:09 -07:00
Adrien Loison
ba4a55292d Remove version from composer.json 2015-06-02 19:48:37 -07:00
Adrien Loison
d617a50b85 Merge pull request #44 from box/bump_to_v1.0.9
Bump to version 1.0.9
2015-05-29 09:50:21 -07:00
Adrien Loison
92981fa80b Bump to version 1.0.9 2015-05-29 09:42:38 -07:00
Adrien Loison
766c733466 Update README.md
Added question about charts and formulas in the FAQ
2015-05-29 09:39:36 -07:00
Adrien Loison
46cae76546 Update README.md
Added new badges
2015-05-29 09:30:40 -07:00
Adrien Loison
03be58b84d Merge pull request #43 from box/support_files_with_formulas
Add support for files containing formulas
2015-05-29 09:08:48 -07:00
Adrien Loison
b21bb86682 Add support for files containing formulas
Formulas will be skipped on reading.
The result of the formulas will be kept though.
2015-05-29 09:01:28 -07:00
Adrien Loison
8a45cec220 Merge pull request #42 from box/support_files_without_shared_strings
Support XLSX that don't have a sharedStrings.xml file
2015-05-28 18:06:09 -07:00
Adrien Loison
04d41d7c9f Support XLSX that don't have a sharedStrings.xml file 2015-05-28 17:59:30 -07:00
Adrien Loison
f8f194bc8f Merge pull request #37 from box/bump_to_version_1.0.8
Bump to version 1.0.8
2015-05-12 21:13:03 -07:00
Adrien Loison
044ad11596 Bump to version 1.0.8 2015-05-12 21:09:22 -07:00
Adrien Loison
935ba1fb5e Merge pull request #36 from box/fix_zip_path_windows
Fix issue with directory separators for zip:// on Windows
2015-05-12 21:08:38 -07:00
Adrien Loison
fb0175d633 Fix issue with directory separators for zip:// on Windows
Replaced "/" by DIRECTORY_SEPARATOR every time it was used with zip://
2015-05-12 20:51:57 -07:00
Adrien Loison
69190cd855 Merge pull request #34 from box/bump_version_to_1.0.7
Bump version to 1.0.7
2015-04-29 13:53:38 -07:00
Adrien Loison
13a53fb43f Bump version to 1.0.7 2015-04-29 11:56:25 -07:00
Adrien Loison
a848be52de Merge pull request #33 from box/improve_code_coverage
Improve code coverage
2015-04-29 11:55:37 -07:00
Adrien Loison
52faef0168 Merge pull request #32 from box/rename_number_to_index
Rename *Number to *Index
2015-04-29 11:55:26 -07:00
Adrien Loison
3b4dfba38e Improve code coverage 2015-04-29 11:39:21 -07:00
Adrien Loison
cfd3e0ffa3 Rename *Number to *Index 2015-04-29 10:48:31 -07:00
Adrien Loison
c6e943041e Merge pull request #31 from box/bump_version_to_1.0.6
Bump version to 1.0.6
2015-04-29 01:10:43 -07:00
Adrien Loison
502dd54c81 Bump version to 1.0.6 2015-04-29 01:06:16 -07:00
Adrien Loison
c6cfc4c80f Merge pull request #30 from box/allow_custom_sheet_name_xlsx_writer
Allow custom sheet name in the XLSX writer
2015-04-29 01:04:22 -07:00
Adrien Loison
d02013c82e Allow custom sheet name in the XLSX writer
Added setter
Added test
Updated README
2015-04-29 01:01:59 -07:00
Adrien Loison
71d9c18a81 Merge pull request #29 from box/expose_sheet_xlsx_reader
Expose a Sheet object on Reader::XLSX::nextSheet()
2015-04-29 00:32:12 -07:00
Adrien Loison
e9ec4e745c Expose a Sheet object on Reader::XLSX::nextSheet()
Added Sheet class for the XLSX reader that exposes basic sheet info, such as name or ID.
When retrieving the sheet data XML, added extra XML parsing to retrieve sheet data.
Added test
2015-04-29 00:27:45 -07:00
Adrien Loison
e58284d27b Merge pull request #25 from box/bump_version_to_1.0.5
Bump version to 1.0.5
2015-04-16 15:22:08 -07:00
Adrien Loison
27a47f509f Bump version to 1.0.5 2015-04-16 15:05:41 -07:00
Adrien Loison
8aeb883ebc Merge pull request #23 from box/add_hhvm_and_nightly_in_travis
Add HHVM and PHP nighly in Travis
2015-04-16 15:04:07 -07:00
Adrien Loison
05c504e72c Add HHVM and PHP nighly in Travis 2015-04-16 14:56:07 -07:00
Adrien Loison
abbc158877 Merge pull request #24 from box/add_improve_test_coverage
Add and improve test coverage
2015-04-16 14:55:42 -07:00
Adrien Loison
3f3461b002 Add and improve test coverage 2015-04-16 14:51:48 -07:00
Adrien Loison
2074781852 Merge pull request #22 from box/bump_version_to_1.0.4
Bump version to 1.0.4
2015-04-16 13:17:24 -07:00
Adrien Loison
fd5cfc5440 Bump version to 1.0.4 2015-04-16 13:13:05 -07:00
Adrien Loison
a538813827 Merge pull request #21 from box/fix_empty_shared_string_bug
Fix empty shared string bug
2015-04-16 13:08:56 -07:00
Adrien Loison
3e5ef284a5 Fix empty shared string bug
Replaced !$sharedString by $sharedString === null to avoid the case
when $sharedString = ''
2015-04-16 13:00:02 -07:00
Adrien Loison
f770f38967 Merge pull request #19 from box/bump_version_to_1.0.3
Bump version to 1.0.3
2015-04-14 20:20:34 -07:00
Adrien Loison
0edd9c434f Merge pull request #20 from box/add_editorconfig
Add .editorconfig
2015-04-14 20:20:21 -07:00
Adrien Loison
3c9bf12a5f Add .editorconfig 2015-04-14 20:16:41 -07:00
Adrien Loison
69c1436625 Bump version to 1.0.3 2015-04-14 20:01:24 -07:00
Adrien Loison
af940069bc Merge pull request #18 from box/better_guess_cell_type
Better guess the cell type based on its value
2015-04-14 19:58:38 -07:00
Adrien Loison
d6155a4243 Better guess the cell type based on its value 2015-04-14 19:52:56 -07:00
Adrien Loison
538f6109ad Merge pull request #15 from box/add_test_for_skipping_empty_rows
Add test for skipping empty rows
2015-04-03 22:49:40 -07:00
Adrien Loison
93cdd398dd Add test for skipping empty rows 2015-04-03 22:45:09 -07:00
Adrien Loison
66e5792a7e Merge pull request #14 from box/bump_version_to_1.0.2
Bump version to 1.0.2
2015-04-03 22:29:18 -07:00
Adrien Loison
d4dbc32a53 Bump version to 1.0.2 2015-04-03 22:28:49 -07:00
Adrien Loison
09eac62c22 Merge pull request #13 from box/random_fixes
Random fixes
2015-04-03 22:24:56 -07:00
Adrien Loison
7bb5e02d2c Fix Typo in CSV Reader 2015-04-03 22:09:25 -07:00
Adrien Loison
aebdd1acd7 Empty $dataRow should not create an empty rw 2015-04-03 22:08:55 -07:00
Adrien Loison
41a449f245 Merge pull request #12 from box/fix_dir_separator_bug_on_windows
Fix DIRECTORY_SEPARATOR bug occurring on Windows
2015-04-03 21:17:49 -07:00
Adrien Loison
39c72a91b4 Fix DIRECTORY_SEPARATOR bug occurring on Windows
Fixes #3:
The sheetN.xml files location is dynamically retrieved when parsing
[Content_Types].xml. In this file, the location is like "/xl/worksheets/sheet1.xml".

Because the zip stream wrapper expects the zip entry name to be like
"xl/worksheets/sheet1/xml" (without the first "/"), this first "/"
needs to be trimmed. It was trimmed using:
  ltrim($this->dataXmlFilePath, DIRECTORY_SEPARATOR);
which obvisously does not work on Windows platform where DIRECTORY_SEPARATOR is "\".
Replacing DIRECTORY_SEPARATOR by '/' solves this issue.
2015-04-03 21:10:30 -07:00
Adrien Loison
4ca1fc5851 Merge pull request #8 from box/add_multiline_strings_support
Add support for multiline strings
2015-03-30 22:21:47 -07:00
Adrien Loison
419544a85f Update README.md 2015-03-27 17:05:17 -07:00
Adrien Loison
6e11a043c1 Add support for multiline strings
Escaped line feed characters in shared strings before processing them.
This makes every string remain on one single line and therefore allow
fast retrieval
Replaced usages of "\n" by PHP_EOL
Added test for multiline strings
2015-03-27 16:54:56 -07:00
Adrien Loison
c24cdbb9be Merge pull request #7 from box/document_utf8_use
Document the use of UTF-8 by default
2015-03-26 15:36:17 -07:00
Adrien Loison
18f6e64910 Document the use of UTF-8 by default 2015-03-26 15:13:47 -07:00
382 changed files with 1030128 additions and 3939 deletions

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
[{.travis.yml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

12
.gitattributes vendored Normal file
View File

@ -0,0 +1,12 @@
# Ignore all tests, documentation and dot files for archive
/docs export-ignore
/tests export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.php_cs.dist export-ignore
/.scrutinizer.yml export-ignore
/.travis.yml export-ignore
/CONTRIBUTING.md export-ignore
/logo.png export-ignore
/phpunit.xml export-ignore

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: adrilo

270
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,270 @@
name: Spout CI
on: [push, pull_request]
jobs:
tests-on-php-latest:
strategy:
matrix:
operating-system: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.operating-system }}
name: Tests - PHP 8.1 on ${{ matrix.operating-system }}
env:
extensions: zip, xmlreader, dom
cache-key: cache-matrix-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Test with phpunit
run: vendor/bin/phpunit --no-coverage
tests-on-older-php:
strategy:
matrix:
php-versions: ['7.3', '7.4', '8.0']
runs-on: ubuntu-latest
name: Tests - PHP ${{ matrix.php-versions }}
env:
extensions: zip, xmlreader, dom
cache-key: cache-matrix-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Test with phpunit
run: vendor/bin/phpunit --no-coverage
code-coverage:
name: Code coverage
runs-on: ubuntu-latest
env:
extensions: zip, xmlreader, dom
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run Tests with Code Coverage
run: |
mkdir -p build/logs
vendor/bin/phpunit --coverage-clover=build/logs/clover.xml
- name: Upload coverage results to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
composer global require php-coveralls/php-coveralls
php-coveralls --coverage_clover=build/logs/clover.xml -v --exclude-no-stmt
coding-style:
name: Coding Style
runs-on: ubuntu-latest
env:
extensions: zip, xmlreader, dom
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHP-CS-Fixer
run: vendor/bin/php-cs-fixer fix --verbose --diff --dry-run
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
env:
extensions: zip, xmlreader, dom
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
tools: phpstan
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Static Analysis using PHPStan
run: ./vendor/bin/phpstan analyse -c phpstan.neon --no-progress

9
.gitignore vendored
View File

@ -1,5 +1,8 @@
/tests/resources/generated
/vendor
composer.lock
/.idea /.idea
*.iml *.iml
/tests/resources/generated
/tests/coverage
/vendor
/.php-cs-fixer.cache
/.phpunit.result.cache

54
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,54 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->name('*.php')
->exclude('vendor');
$config = new PhpCsFixer\Config();
return $config
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'align_multiline_comment' => false,
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => ['default' => null],
'blank_line_before_statement' => ['statements' => ['return']],
'combine_consecutive_unsets' => true,
'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => ['space' => 'single'],
'heredoc_to_nowdoc' => true,
'increment_style' => ['style' => 'post'],
'is_null' => true,
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'modernize_types_casting' => true,
'no_break_comment' => ['comment_text' => 'do nothing'],
'no_empty_phpdoc' => false,
'no_null_property_initialization' => true,
'echo_tag_syntax' => false,
'no_superfluous_elseif' => true,
'no_superfluous_phpdoc_tags' => false,
'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'switch_case', 'yield']],
'no_unneeded_curly_braces' => true,
'no_unneeded_final_method' => true,
'no_useless_else' => false,
'no_useless_return' => true,
'ordered_imports' => true,
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_align' => false,
'phpdoc_annotation_without_dot' => false,
'phpdoc_no_empty_return' => false,
'phpdoc_order' => true,
'phpdoc_summary' => false,
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'phpdoc_separation' => false,
'protected_to_private' => true,
'psr_autoloading' => true,
'return_type_declaration' => ['space_before' => 'one'],
'semicolon_after_instruction' => true,
'simplified_null_return' => false,
'single_line_comment_style' => ['comment_types' => ['hash']],
'strict_comparison' => true,
'yoda_style' => ['equal' => false, 'identical' => false],
])
->setFinder($finder);

View File

@ -1,12 +0,0 @@
language: php
php:
- 5.4
- 5.5
- 5.6
install:
- composer self-update
- composer install --prefer-source
script: phpunit

View File

@ -6,7 +6,7 @@ All contributions are welcome to this project.
Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at: Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at:
http://opensource.box.com/cla https://opensource.box.com/cla
To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement). To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement).
@ -68,7 +68,21 @@ This will add your changes on top of what's already in upstream, minimizing merg
Make sure that all tests are passing before submitting a pull request. Make sure that all tests are passing before submitting a pull request.
### Step 8: Send the pull request ### Step 8: Fix code style
Run the following command to check the code style of your changes:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff
```
This will print a diff of proposed code style changes. To apply these suggestions, run the following command:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist
```
### Step 9: Send the pull request
Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did.

253
README.md
View File

@ -1,245 +1,64 @@
# Spout # Spout
Spout is a PHP library to read and write CSV and XLSX files, in a fast and scalable way. [![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB). [![Project Status](https://opensource.box.com/badges/inactive.svg)](https://opensource.box.com/badges)
[![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)
[![Coverage Status](https://coveralls.io/repos/github/box/spout/badge.svg?branch=master)](https://coveralls.io/github/box/spout?branch=master)
[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
[![Build Status](https://travis-ci.org/box/spout.png?branch=master)](http://travis-ci.org/box/spout) ## 🪦 Archived project 🪦
[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges)
This project has been archived and is no longer maintained. No bug fix and no additional features will be added.<br>
You won't be able to submit new issues or pull requests, and no additional features will be added
You can still use Spout as is in your projects though :)
> Thanks to everyone who contributed to this project, from a typo fix to the new cool feature.<br>
> It was great to see the involvement of this community!
<br>
## About
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB).
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)
## Installation ## Documentation
The Spout library can be installed directly from [Composer](https://getcomposer.org/). Full documentation can be found at [https://opensource.box.com/spout/](https://opensource.box.com/spout/).
Add "box/spout" as a dependency in your project's composer.json file:
```json
"require": {
"box/spout": "*"
}
```
Then run the install command from Composer:
```
php composer.phar install
```
## Requirements ## Requirements
* PHP version 5.4.0 or higher * PHP version 7.2 or higher
* PHP extension `php_zip` enabled * PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled * PHP extension `php_xmlreader` enabled
* PHP extension `php_simplexml` enabled
## Upgrade guide
## Basic usage 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).
### Reader
#### How to read a CSV file?
```php
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::CSV);
$reader->open($filePath);
while ($reader->hasNextRow()) {
$row = $reader->nextRow();
// do stuff
}
$reader->close();
```
#### How to read a XLSX file?
```php
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($filePath);
while ($reader->hasNextSheet()) {
$reader->nextSheet();
while ($reader->hasNextRow()) {
$row = $reader->nextRow();
// do stuff
}
}
$reader->close();
```
If there are multiple sheets in the file, the reader will read through all of them sequentially.
### Writer
### How to create a CSV file?
```php
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::CSV);
$writer->openToFile($filePath); // write data to a file or to a PHP stream
$writer->addRow($singleRow); // add a row at a time
$writer->close();
```
### How to create a XLSX file?
```php
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::XLSX);
$writer->openToBrowser($fileName); // stream data directly to the browser
$writer->addRows($multipleRows); // add multiple rows at a time
$writer->close();
```
For XLSX files, the number of rows per sheet is limited to 1,048,576 (see [Office OpenXML specs](http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx)). By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
## Advanced usage
### Configuring the CSV reader and writer
It is possible to configure the both the CSV reader and writer to specify the field separator as well as the field enclosure:
```php
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::CSV);
$reader->setFieldDelimiter('|');
$reader->setFieldEnclosure('@');
```
### Configuring the XLSX writer
#### Strings storage
XLSX files support different ways to store the string values:
* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored)
* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process
In order to keep the memory usage really low, Spout does not optimize strings when using shared strings. It is nevertheless possible to use this mode.
```php
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings(true); // default (and recommended) value
$writer->setShouldUseInlineStrings(false); // will use shared strings
```
#### New sheet creation
It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
```php
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
```
### Using custom temporary folder
Processing XLSX files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by sys_get_temp_dir()). It is possible to override this by explicitly setting it on the reader or writer:
```php
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::XLSX);
$writer->setTempFolder($customTempFolderPath);
```
### Playing with XLSX sheets
When creating a XLSX file, it is possible to control in which sheet the data will be written to.
At any point, you can retrieve the current sheet and set the current sheet:
```php
$firstSheet = $writer->getCurrentSheet();
$writer->addRow($rowForSheet1); // writes the row to the first sheet
$newSheet = $writer->addNewSheetAndMakeItCurrent();
$writer->addRow($rowForSheet2); // writes the row to the new sheet
$writer->setCurrentSheet($firstSheet);
$writer->addRow($anotherRowForSheet1); // append the row to the first sheet
```
It is also possible to retrieve all the sheets currently created:
```php
$sheets = $writer->getSheets();
```
### Fluent interface
Because fluent interfaces are great, you can use them with Spout:
```php
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::XLSX);
$writer->setTempFolder($customTempFolderPath)
->setShouldUseInlineStrings(true)
->openToFile($filePath)
->addRow($headerRow)
->addRows($dataRows)
->close();
```
## Running tests ## Running tests
On the `master` branch, only unit and functional tests are included. The performance requires very large files and have been excluded. The `master` branch includes unit, functional and performance tests.
If you just want to check that everything is working as expected, executing the tests of the master branch is enough. If you just want to check that everything is working as expected, executing the unit and functional tests is enough.
If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output: * `phpunit` - runs unit and functional tests
* `phpunit --group perf-tests` - only runs the performance tests
* `phpunit` - runs the whole test suite (unit + functional + performance tests)
* `phpunit --testuite no-perf-tests` - only runs the unit and functional tests
* `phpunit --testuite perf-tests` - only runs the performance tests
For information, the performance tests take about one hour to run (processing 2 million rows files is not a quick thing).
## Frequently Asked Questions
#### How can Spout handle such large data sets and still use less than 10MB of memory?
When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them into several small temporary files that allows fast access.
#### How long does it take to generate a file with X rows?
Here are a few numbers regarding the performance of Spout:
| | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
| :------------------------------- | :----------------------: | :--------------------------: | :------------------------------: |
| Read CSV | < 1 second | 4 seconds | 2-3 minutes |
| Write CSV | < 1 second | 2 seconds | 2-3 minutes |
| Read XLSX (using inline strings) | < 1 second | 35-40 seconds | 18-20 minutes |
| Read XLSX (using shared strings) | 1 second | 1-2 minutes | 35-40 minutes |
| Write XLSX | 1 second | 20-25 seconds | 8-10 minutes |
For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing).
## Support ## Support
Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject. Spout is no longer actively supported. You can still ask questions, or discuss about it in the chat room:<br>
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Copyright and License ## Copyright and License
Copyright 2015 Box, Inc. All rights reserved. Copyright 2022 Box, Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

89
UPGRADE-3.0.md Normal file
View File

@ -0,0 +1,89 @@
Upgrading from 2.x to 3.0
=========================
Spout 3.0 introduced several backwards-incompatible changes. The upgrade from Spout 2.x to 3.0 must therefore be done with caution.
This guide is meant to ease this process.
Most notable changes
--------------------
In 2.x, styles were applied per row; it was therefore impossible to apply different styles to cells in the same row.
With the 3.0 version, this is now possible: each cell can have its own style.
Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community.
Reader changes
--------------
Creating a reader should now be done through the Reader `ReaderEntityFactory`, instead of using the `ReaderFactory`.
Also, the `ReaderFactory::create($type)` method was removed and replaced by methods for each reader:
```php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory; // namespace is no longer "Box\Spout\Reader"
...
$reader = ReaderEntityFactory::createXLSXReader(); // replaces ReaderFactory::create(Type::XLSX)
$reader = ReaderEntityFactory::createCSVReader(); // replaces ReaderFactory::create(Type::CSV)
$reader = ReaderEntityFactory::createODSReader(); // replaces ReaderFactory::create(Type::ODS)
```
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:
```php
...
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) { // $row is a "Row" object, not an array
$rowAsArray = $row->toArray(); // this is the 2.x equivalent
// OR
$cellsArray = $row->getCells(); // this can be used to get access to cells' details
...
}
}
```
Writer changes
--------------
Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`, instead of using the `WriterFactory`.
Also, the `WriterFactory::create($type)` method was removed and replaced by methods for each writer:
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory; // namespace is no longer "Box\Spout\Writer"
...
$writer = WriterEntityFactory::createXLSXWriter(); // replaces WriterFactory::create(Type::XLSX)
$writer = WriterEntityFactory::createCSVWriter(); // replaces WriterFactory::create(Type::CSV)
$writer = WriterEntityFactory::createODSWriter(); // replaces WriterFactory::create(Type::ODS)
```
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:
```php
// Adding a row from an array of values (2.x equivalent)
$cellValues = ['foo', 12345];
$row1 = WriterEntityFactory::createRowFromArray($cellValues, $rowStyle);
// Adding a row from an array of Cell
$cell1 = WriterEntityFactory::createCell('foo', $cellStyle1); // this cell has its own style
$cell2 = WriterEntityFactory::createCell(12345, $cellStyle2); // this cell has its own style
$row2 = WriterEntityFactory::createRow([$cell1, $cell2]);
$writer->addRows([$row1, $row2]);
```
Namespace changes for styles
-----------------
The namespaces for styles have changed. Styles are still created by using a `builder` class.
For the builder, please update your import statements to use the following namespaces:
Box\Spout\Writer\Common\Creator\Style\StyleBuilder
Box\Spout\Writer\Common\Creator\Style\BorderBuilder
The `Style` base class and style definitions like `Border`, `BorderPart` and `Color` also have a new namespace.
If your are using these classes directly via an import statement in your code, please use the following namespaces:
Box\Spout\Common\Entity\Style\Border
Box\Spout\Common\Entity\Style\BorderPart
Box\Spout\Common\Entity\Style\Color
Box\Spout\Common\Entity\Style\Style
Handling of empty rows
----------------------
In 2.x, empty rows were not added to the spreadsheet.
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.

View File

@ -1,9 +1,8 @@
{ {
"name": "box/spout", "name": "box/spout",
"description": "PHP Library to read and write CSV and XLSX files, in a fast and scalable way", "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
"type": "library", "type": "library",
"version": "1.0.1", "keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"],
"keywords": ["php","read","write","csv","xlsx","excel","spreadsheet","scale","memory","stream","ooxml"],
"license": "Apache-2.0", "license": "Apache-2.0",
"homepage": "https://www.github.com/box/spout", "homepage": "https://www.github.com/box/spout",
"authors": [ "authors": [
@ -13,17 +12,33 @@
} }
], ],
"require": { "require": {
"php": ">=5.4.0", "php": ">=7.3.0",
"ext-zip": "*", "ext-zip": "*",
"ext-xmlreader" : "*", "ext-xmlreader": "*",
"ext-simplexml": "*" "ext-dom": "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": ">=3.7" "phpunit/phpunit": "^9",
"friendsofphp/php-cs-fixer": "^3",
"phpstan/phpstan": "^1"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
"ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Box\\Spout\\": "src/Spout" "Box\\Spout\\": "src/Spout"
} }
},
"extra": {
"branch-alias": {
"dev-master": "3.1.x-dev"
}
},
"config": {
"platform": {
"php": "7.3"
}
} }
} }

4196
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
docs/.gitignore vendored Executable file
View File

@ -0,0 +1,9 @@
/.idea
*.iml
/tests/resources/generated
/tests/coverage
/vendor
/.sass-cache
/_site

17
docs/README.md Executable file
View File

@ -0,0 +1,17 @@
# Spout Documentation
## Quickstart
1. Install `jekyll`: `sudo gem install jekyll`
2. Run the site locally: `jekyll serve`
## Usage with Docker and Docker Compose
* Install Docker and Docker Compose
* Run ```docker-compose up```
* Point your browser at [http://127.0.0.1:8080/](http://127.0.0.1:8080/)
## Editing the documentation
* All documents relevant for the documentation are located in the ```_pages``` folder.

37
docs/_config.yml Executable file
View File

@ -0,0 +1,37 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely need to edit after that.
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'jekyll serve'. If you change this file, please restart the server process.
# Site settings
title: Spout
email: oss@box.com
description: "An open source PHP library to read and write spreadsheet files (XLSX, ODS and CSV), in a fast and scalable way."
baseurl: "" # the subpath of your site, e.g. /blog
url: "https://opensource.box.com" # the base hostname & protocol for your site
# Build settings
markdown: kramdown
highlighter: rouge
kramdown:
input: GFM
syntax_highlighter: rouge
exclude: ["README.md"]
collections:
pages:
output: true
sections:
output: true
# 3rd parties
#google_analytics:
algolia:
enabled: false
# apiKey:
# indexName:
# Misc
spout_html: <span class="spout">Spout</span>

7
docs/_config_local.yml Normal file
View File

@ -0,0 +1,7 @@
# Github Metadata plugin
repository: box/spout
plugins:
- "jekyll-github-metadata"
github:
url: http://localhost:8080

View File

@ -0,0 +1,9 @@
{% if site.algolia.enabled %}
<script type="text/javascript" src="https://cdn.jsdelivr.net/docsearch.js/1/docsearch.min.js"></script>
<script type="text/javascript"> docsearch({
apiKey: '{{ site.algolia.apiKey }}',
indexName: '{{ site.algolia.indexName }}',
inputSelector: '#algolia-doc-search'
});
</script>
{% endif %}

12
docs/_includes/analytics.html Executable file
View File

@ -0,0 +1,12 @@
{% if site.google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}

9
docs/_includes/banner.html Executable file
View File

@ -0,0 +1,9 @@
<div class="site-banner">
<div class="wrapper vertical-align-middle">
<p class="tag-line">
Read and write spreadsheets
<br />
<strong>quickly</strong> and <strong>at scale</strong>
</p>
</div>
</div>

9
docs/_includes/base.html Executable file
View File

@ -0,0 +1,9 @@
<!-- http://ricostacruz.com/til/relative-paths-in-jekyll.html -->
<!-- _includes/base.html -->
{% assign base = '' %}
{% assign depth = page.url | split: '/' | size | minus: 1 %}
{% if depth == 1 %}{% assign base = '.' %}
{% elsif depth == 2 %}{% assign base = '..' %}
{% elsif depth == 3 %}{% assign base = '../..' %}
{% elsif depth == 4 %}{% assign base = '../../..' %}{% endif %}

25
docs/_includes/footer.html Executable file
View File

@ -0,0 +1,25 @@
<footer class="site-footer">
<div class="wrapper">
<div class="footer-col-wrapper">
<div class="footer-col footer-col-1">
<ul class="contact-list">
<li>Need to contact us?<br><a href="https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/box/spout.svg" alt="Join the chat at https://gitter.im/box/spout" /></a></li>
</ul>
</div>
<div class="footer-col footer-col-2">
<ul class="social-media-list">
<li><a href="{{ site.github.issues_url }}"><span class="icon icon--github">{% include icon-github.svg %}</span><span class="username">GitHub Issues</span></a></li>
</ul>
</div>
<div class="footer-col footer-col-3">
<p>{{ site.title }} - {{ site.description }}</p>
</div>
</div>
</div>
</footer>

17
docs/_includes/head.html Executable file
View File

@ -0,0 +1,17 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title>
<meta name="description" content="{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines | truncate: 160 }}{% else %}{{ site.description }}{% endif %}">
<link rel="stylesheet" href="{{ "/css/main.css" | prepend: site_url }}">
{% if site.algolia.enabled %}
<link rel="stylesheet" href="//cdn.jsdelivr.net/docsearch.js/1/docsearch.min.css" />
{% endif %}
{% if jekyll.environment == 'production' %}
{% include analytics.html %}
{% endif %}
</head>

33
docs/_includes/header.html Executable file
View File

@ -0,0 +1,33 @@
<header class="site-header">
<div class="wrapper">
<a class="site-title" href="{{ site_url }}/">{{ site.title }}</a>
<nav class="site-nav">
<a href="#" class="menu-icon">
<svg viewBox="0 0 18 15">
<path fill="#424242" d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z"/>
<path fill="#424242" d="M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z"/>
<path fill="#424242" d="M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z"/>
</svg>
</a>
<div class="trigger">
<a class="page-link" href="{{ site_url }}/getting-started/">Getting Started</a>
<a class="page-link" href="{{ site_url }}/docs/">Documentation</a>
<a class="page-link" href="{{ site_url }}/guides/">Guides</a>
<a class="page-link" href="{{ site_url }}/faq/">FAQ</a>
<a class="page-link" href="{{ site.github.repository_url }}">
<span class="icon icon--github">{% include icon-github.svg %}</span>
<span class="username">GitHub</span>
</a>
{% if site.algolia.enabled %}
<span class="site-search">
<input type="text" id="algolia-doc-search">
</span>
{% endif %}
</div>
</nav>
</div>
</header>

View File

@ -0,0 +1 @@
<a href="https://github.com/{{ include.username }}"><span class="icon icon--github">{% include icon-github.svg %}</span><span class="username">{{ include.username }}</span></a>

1
docs/_includes/icon-github.svg Executable file
View File

@ -0,0 +1 @@
<svg viewBox="0 0 16 16"><path fill="#828282" d="M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388,0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159,0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481,0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258,0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847,0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209,2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943,0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497,0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713,0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028,5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z"/></svg>

After

Width:  |  Height:  |  Size: 926 B

View File

@ -0,0 +1,11 @@
{% assign sectionClass = "section-even centered" %}
{% assign sectionTitle = "Fast and Scalable" %}
{% assign sectionIcons = "icon-lightning-bolt.png" %}
{% capture sectionContent %}
Reading a small CSV file? No problem!<br>
Reading a huge XLSX file? No extra code needed!<br>
Writing an ODS file with millions of rows? {{ site.spout_html }} can do it in no time!
{% endcapture %}
{% include section.html site_url = include.site_url %}

View File

@ -0,0 +1,14 @@
{% assign sectionClass = "section-odd centered" %}
{% assign sectionTitle = "Supported Spreadsheet Types" %}
{% assign sectionIcons = "icon-xlsx.png~~icon-ods.png~~icon-csv.png" %}
{% assign emS = '<span class="light-em">' %}
{% assign emE = '</span>' %}
{% capture sectionContent %}
{{ site.spout_html }} supports 3 types of spreadsheets: {{emS}}XLSX{{emE}}, {{emS}}ODS{{emE}} and {{emS}}CSV{{emE}}.<br>
{{ site.spout_html }} provides a simple and unified API to read or create these different types of spreadsheets.
Switching from one type to another is ridiculously easy!
{% endcapture %}
{% include section.html site_url=include.site_url %}

View File

@ -0,0 +1,21 @@
{% assign sectionClass = "section-odd last-section" %}
{% capture sectionTitle %}Why use {{ site.spout_html }}?{% endcapture %}
{% assign sectionIcons = "" %}
{% assign emS = '<span class="light-em">' %}
{% assign emE = '</span>' %}
{% capture sectionContent %}
<ul class="feature-list">
<li class="feature-check">{{ site.spout_html }} is capable of processing files of {{emS}}any size{{emE}}.</li>
<li class="feature-check">{{ site.spout_html }} needs {{emS}}only 3MB{{emE}} of memory to process any file.</li>
<li class="feature-check">{{ site.spout_html }}'s streaming mechanism makes it {{emS}}incredibly fast{{emE}}.</li>
<li class="feature-check">{{ site.spout_html }}'s API is {{emS}}developer-friendly{{emE}}.</li>
</ul><br>
<div class="btn-wrapper">
<a href="{{ include.site_url }}/getting-started/" class="page-link"><button class="btn">Get started</button></a>
</div>
{% endcapture %}
{% include section.html site_url=include.site_url %}

16
docs/_includes/section.html Executable file
View File

@ -0,0 +1,16 @@
<div class="section {{ sectionClass }} vertical-align-middle">
<div class="wrapper">
<div class="description mbl">
<h2 class="section-title">{{ sectionTitle }}</h2>
{{ sectionContent }}
</div>
{% if sectionIcons %}
{% assign sectionIconsAsArray = sectionIcons | split:'~~' %}
<div class="icons">
{% for icon in sectionIconsAsArray %}
<img src="{{ include.site_url }}/images/{{ icon }}"/>
{% endfor %}
</div>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,12 @@
{% comment %}
This is a hack to force using HTTPS...
The URL of the docs should be changed in the repo settings instead
{% endcomment %}
{% assign protocol_scheme = page.url | absolute_url | truncate: 5, "" %}
{%- capture site_url -%}
{%- if protocol_scheme == "https" -%}
{{ site.github.url | replace: "http://", "https://" }}
{%- else -%}
{{ site.github.url }}
{%- endif -%}
{%- endcapture -%}

23
docs/_layouts/default.html Executable file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
{% include set-global-site-url.html %}
{% include head.html %}
<body>
{% include header.html %}
{% if page.banner %}
{% include banner.html %}
{% endif %}
{{ content }}
{% include footer.html %}
{% include algolia.html %}
</body>
</html>

40
docs/_layouts/doc.html Executable file
View File

@ -0,0 +1,40 @@
---
layout: default
---
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/anchor-js/4.1.0/anchor.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
anchors.add();
$('.post-title, .doc-content h1, .doc-content h2, .doc-content h3').each(function(index, element) {
var el = $(element);
var title = el.text();
var id = el.attr('id');
var tag = el.prop('tagName');
var titleEl = $('<div>').append(
$('<a>')
.attr('href', '#' + id)
.addClass(tag.toLowerCase())
.text(title));
$('.table-of-content').append(titleEl);
});
});
</script>
<div class="table-of-content" data-spy="affix" data-offset-top="40" data-offset-bottom="300">
</div>
<article class="post">
<div class="wrapper">
<header class="post-header">
<h1 class="post-title">{{ page.title }}</h1>
</header>
<div class="post-content">
<div class="doc-content">
{{ content }}
</div>
</div>
</div>

15
docs/_layouts/page.html Executable file
View File

@ -0,0 +1,15 @@
---
layout: default
---
<article class="post">
<div class="wrapper">
<header class="post-header">
<h1 class="post-title">{{ page.title }}</h1>
</header>
<div class="post-content">
{{ content }}
</div>
</div>
</article>

323
docs/_pages/documentation.md Executable file
View File

@ -0,0 +1,323 @@
---
layout: doc
title: Documentation
permalink: /docs/
---
## Configuration for CSV
It is possible to configure both the CSV reader and writer to adapt them to your requirements:
```php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.csv');
/** All of these methods have to be called before opening the reader. */
$reader->setFieldDelimiter('|');
$reader->setFieldEnclosure('@');
```
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
```php
$reader->setEncoding('UTF-16LE');
```
By default, the writer generates CSV files encoded in UTF-8, with a BOM.
It is however possible to not include the BOM:
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$writer = WriterEntityFactory::createWriterFromFile('/path/to/file.csv');
$writer->setShouldAddBOM(false);
```
## Configuration for XLSX and ODS
### New sheet creation
It is possible to change the behavior of the writers when the maximum number of rows (*1,048,576*) has been written in the current sheet. By default, a new sheet is automatically created so that writing can keep going but that may not always be preferable.
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$writer = WriterEntityFactory::createODSWriter();
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
```
### Using a custom temporary folder
Processing XLSX and ODS files requires temporary files to be created. By default, {{ site.spout_html }} will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer:
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$writer = WriterEntityFactory::createXLSXWriter();
$writer->setTempFolder($customTempFolderPath);
```
### Strings storage (XLSX writer)
XLSX files support different ways to store the string values:
* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored)
* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process
In order to keep the memory usage really low, {{ site.spout_html }} does not de-duplicate strings when using shared strings. It is nevertheless possible to use this mode.
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$writer = WriterEntityFactory::createXLSXWriter();
$writer->setShouldUseInlineStrings(true); // default (and recommended) value
$writer->setShouldUseInlineStrings(false); // will use shared strings
```
> #### Note on Apple Numbers and iOS support
>
> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings!
### Date/Time formatting
When reading a spreadsheet containing dates or times, {{ site.spout_html }} returns the values by default as `DateTime` objects.
It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet.
```php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
$reader = ReaderEntityFactory::createXLSXReader();
$reader->setShouldFormatDates(false); // default value
$reader->setShouldFormatDates(true); // will return formatted dates
```
## Empty rows
By default, when {{ site.spout_html }} reads a spreadsheet it skips empty rows and only return rows containing data.
This behavior can be changed so that {{ site.spout_html }} returns all rows:
```php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.ext');
$reader->setShouldPreserveEmptyRows(true);
```
## Styling
### Available styles
{{ site.spout_html }} supports styling at a row and cell level. It is possible to customize the fonts, backgrounds, alignment as well as borders.
For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones:
| Category | Property | API
|:---------------------|:---------------|:--------------------------------------
| Font | Bold | `StyleBuilder::setFontBold()`
| | Italic | `StyleBuilder::setFontItalic()`
| | Underline | `StyleBuilder::setFontUnderline()`
| | Strikethrough | `StyleBuilder::setFontStrikethrough()`
| | Font name | `StyleBuilder::setFontName('Arial')`
| | Font size | `StyleBuilder::setFontSize(14)`
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)`
| | Wrap text | `StyleBuilder::setShouldWrapText(true)`
| Format _(XLSX only)_ | Number format | `StyleBuilder::setFormat('0.000')`
| | Date format | `StyleBuilder::setFormat('m/d/yy h:mm')`
### Styling rows
It is possible to apply some formatting options to a row. In this case, all cells of the row will have the same style:
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
use Box\Spout\Common\Entity\Style\CellAlignment;
use Box\Spout\Common\Entity\Style\Color;
$writer = WriterEntityFactory::createXLSXWriter();
$writer->openToFile($filePath);
/** Create a style with the StyleBuilder */
$style = (new StyleBuilder())
->setFontBold()
->setFontSize(15)
->setFontColor(Color::BLUE)
->setShouldWrapText()
->setCellAlignment(CellAlignment::RIGHT)
->setBackgroundColor(Color::YELLOW)
->build();
/** Create a row with cells and apply the style to all cells */
$row = WriterEntityFactory::createRowFromArray(['Carl', 'is', 'great'], $style);
/** Add the row to the writer */
$writer->addRow($row);
$writer->close();
```
Adding borders to a row requires a ```Border``` object.
```php
use Box\Spout\Common\Entity\Style\Border;
use Box\Spout\Writer\Common\Creator\Style\BorderBuilder;
use Box\Spout\Common\Entity\Style\Color;
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$border = (new BorderBuilder())
->setBorderBottom(Color::GREEN, Border::WIDTH_THIN, Border::STYLE_DASHED)
->build();
$style = (new StyleBuilder())
->setBorder($border)
->build();
$writer = WriterEntityFactory::createXLSXWriter();
$writer->openToFile($filePath);
$cells = WriterEntityFactory::createCell('Border Bottom Green Thin Dashed');
$row = WriterEntityFactory::createRow($cells);
$row->setStyle($style);
$writer->addRow($row);
$writer->close();
```
### Styling cells
The same styling techniques as described in [Styling rows](#styling-rows) can be applied to individual cells of a row as well.
Cell styles are inherited from the parent row and the default row style respectively.
The styles applied to a specific cell will override any parent styles if present.
Example:
```php
use Box\Spout\Common\Entity\Style\Color;
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$defaultStyle = (new StyleBuilder())
->setFontSize(8)
->build();
$writer = WriterEntityFactory::createXLSXWriter();
$writer->setDefaultRowStyle($defaultStyle)
->openToFile($filePath);
$zebraBlackStyle = (new StyleBuilder())
->setBackgroundColor(Color::BLACK)
->setFontColor(Color::WHITE)
->setFontSize(10)
->build();
$zebraWhiteStyle = (new StyleBuilder())
->setBackgroundColor(Color::WHITE)
->setFontColor(Color::BLACK)
->setFontItalic()
->build();
$cells = [
WriterEntityFactory::createCell('Ze', $zebraBlackStyle),
WriterEntityFactory::createCell('bra', $zebraWhiteStyle)
];
$rowStyle = (new StyleBuilder())
->setFontBold()
->build();
$row = WriterEntityFactory::createRow($cells, $rowStyle);
$writer->addRow($row);
$writer->close();
```
### Default style
{{ site.spout_html }} will use a default style for all created rows. This style can be overridden this way:
```php
$defaultStyle = (new StyleBuilder())
->setFontName('Arial')
->setFontSize(11)
->build();
$writer = WriterEntityFactory::createXLSXWriter();
$writer->setDefaultRowStyle($defaultStyle)
->openToFile($filePath);
```
## Playing with sheets
When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet:
```php
$firstSheet = $writer->getCurrentSheet();
$writer->addRow($rowForSheet1); // writes the row to the first sheet
$newSheet = $writer->addNewSheetAndMakeItCurrent();
$writer->addRow($rowForSheet2); // writes the row to the new sheet
$writer->setCurrentSheet($firstSheet);
$writer->addRow($anotherRowForSheet1); // append the row to the first sheet
```
It is also possible to retrieve all the sheets currently created:
```php
$sheets = $writer->getSheets();
```
It is possible to retrieve some sheet's attributes when reading:
```php
foreach ($reader->getSheetIterator() as $sheet) {
$sheetName = $sheet->getName();
$isSheetVisible = $sheet->isVisible();
$isSheetActive = $sheet->isActive(); // active sheet when spreadsheet last saved
}
```
If you rely on the sheet's name in your application, you can customize it this way:
```php
// Accessing the sheet name when writing
$sheet = $writer->getCurrentSheet();
$sheetName = $sheet->getName();
// Customizing the sheet name when writing
$sheet = $writer->getCurrentSheet();
$sheet->setName('My custom name');
```
> Please note that Excel has some restrictions on the sheet's name:
> * it must not be blank
> * it must not exceed 31 characters
> * it must not contain these characters: \ / ? * : [ or ]
> * it must not start or end with a single quote
> * it must be unique
>
> Handling these restrictions is the developer's responsibility. {{ site.spout_html }} does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in.
## Fluent interface
Because fluent interfaces are great, you can use them with {{ site.spout_html }}:
```php
use Box\Spout\Writer\WriterEntityFactory;
$writer = WriterEntityFactory::createWriterFromFile('path/to/file.ext');
$writer->setTempFolder($customTempFolderPath)
->setShouldUseInlineStrings(true)
->openToFile($filePath)
->addRow($headerRow)
->addRows($dataRows)
->close();
```

30
docs/_pages/faq.md Normal file
View File

@ -0,0 +1,30 @@
---
layout: page
title: Frequently Asked Questions
permalink: /faq/
---
### How can {{ site.spout_html }} handle such large data sets and still use less than 3MB of memory?
When writing data, {{ site.spout_html }} is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access.
### How long does it take to generate a file with X rows?
Here are a few numbers regarding the performance of {{ site.spout_html }}:
| Type | Action | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
|------|-------------------------------|--------------------------|------------------------------|----------------------------------|
| CSV | Read | < 1 second | 4 seconds | 2-3 minutes |
| | Write | < 1 second | 2 seconds | 2-3 minutes |
| XLSX | Read<br>*inline&nbsp;strings* | < 1 second | 35-40 seconds | 18-20 minutes |
| | Read<br>*shared&nbsp;strings* | 1 second | 1-2 minutes | 35-40 minutes |
| | Write | 1 second | 20-25 seconds | 8-10 minutes |
| ODS | Read | 1 second | 1-2 minutes | 5-6 minutes |
| | Write | < 1 second | 35-40 seconds | 5-6 minutes |
### Does {{ site.spout_html }} support charts or formulas?
No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used.
So the larger the file would be, the more memory would be consumed, preventing your code to scale well.

139
docs/_pages/getting-started.md Executable file
View File

@ -0,0 +1,139 @@
---
layout: doc
title: Getting Started
permalink: /getting-started/
---
{% include set-global-site-url.html %}
This guide will help you install {{ site.spout_html }} and teach you how to use it.
## Requirements
* PHP version 7.2 or higher
* PHP extension `ext-zip` enabled
* PHP extension `ext-xmlreader` enabled
## Installation
### Composer (recommended)
{{ site.spout_html }} can be installed directly from [Composer](https://getcomposer.org/).
Run the following command:
```powershell
$ composer require box/spout
```
### Manual installation
If you can't use Composer, no worries! You can still install {{ site.spout_html }} manually.
> Before starting, make sure your system meets the [requirements](#requirements).
1. Download the source code from the [Releases page](https://github.com/box/spout/releases)
2. Extract the downloaded content into your project.
3. Add this code to the top controller (e.g. index.php) or wherever it may be more appropriate:
```php
// don't forget to change the path!
require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php';
```
## Basic usage
### Reader
Regardless of the file type, the interface to read a file is always the same:
```php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.ext');
$reader->open($filePath);
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
// do stuff with the row
$cells = $row->getCells();
...
}
}
$reader->close();
```
If there are multiple sheets in the file, the reader will read all of them sequentially.
Note that {{ site.spout_html }} guesses the reader type based on the file extension. If the extension is not standard (`.csv`, `.ods`, `.xlsx` _- lower/uppercase_), a specific reader can be created directly:
```php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
$reader = ReaderEntityFactory::createXLSXReader();
// $reader = ReaderEntityFactory::createODSReader();
// $reader = ReaderEntityFactory::createCSVReader();
```
### Writer
As with the reader, there is one common interface to write data to a file:
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Common\Entity\Row;
$writer = WriterEntityFactory::createXLSXWriter();
// $writer = WriterEntityFactory::createODSWriter();
// $writer = WriterEntityFactory::createCSVWriter();
$writer->openToFile($filePath); // write data to a file or to a PHP stream
//$writer->openToBrowser($fileName); // stream data directly to the browser
$cells = [
WriterEntityFactory::createCell('Carl'),
WriterEntityFactory::createCell('is'),
WriterEntityFactory::createCell('great!'),
];
/** add a row at a time */
$singleRow = WriterEntityFactory::createRow($cells);
$writer->addRow($singleRow);
/** add multiple rows at a time */
$multipleRows = [
WriterEntityFactory::createRow($cells),
WriterEntityFactory::createRow($cells),
];
$writer->addRows($multipleRows);
/** Shortcut: add a row from an array of values */
$values = ['Carl', 'is', 'great!'];
$rowFromValues = WriterEntityFactory::createRowFromArray($values);
$writer->addRow($rowFromValues);
$writer->close();
```
Similar to the reader, if the file extension of the file to be written is not standard, specific writers can be created this way:
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Common\Entity\Row;
$writer = WriterEntityFactory::createXLSXWriter();
// $writer = WriterEntityFactory::createODSWriter();
// $writer = WriterEntityFactory::createCSVWriter();
```
For XLSX and ODS files, the number of rows per sheet is limited to *1,048,576*. By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
## Advanced usage
You can do a lot more with {{ site.spout_html }}! Check out the [full documentation]({{ site_url }}/docs/) to learn about all the features.

21
docs/_pages/guides.md Normal file
View File

@ -0,0 +1,21 @@
---
layout: page
title: Guides
permalink: /guides/
---
{% include set-global-site-url.html %}
These guides focus on common and more advanced usages of {{ site.spout_html }}.<br>
If you are just starting with {{ site.spout_html }}, check out the [Getting Started page]({{ site_url }}/getting-started/) and the [Documentation]({{ site_url }}/docs/) first.
{% assign pages=site.pages | sort: 'path' %}
<ul>
{% for page in pages %}
{% if page.title and page.category contains 'guide' %}
<li>
<a class="page-link" href="{{ page.url | prepend: site_url }}">{{ page.title }}</a>
</li>
{% endif %}
{% endfor %}
</ul>

BIN
docs/_pages/guides/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,62 @@
---
layout: page
title: "Add data to an existing spreadsheet"
category: guide
permalink: /guides/add-data-to-existing-spreadsheet/
---
A common use case when using spreadsheets is to add data to an existing spreadsheet. For instance, let's assume you built a spreadsheet containing the last orders on your favorite website and want to update it as you make a new order.
We'll start with a file called "orders.xlsx" and add a new row, containing the last order's info, at the end.
In order to avoid memory issues when dealing with large spreadsheets, {{ site.spout_html }} does not hold the whole representation of the spreadsheet in memory. So to alter an existing spreadsheet, we'll have to create a new one that is similar to the existing one and add the new data in the new one.
```php
<?php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$existingFilePath = 'path/to/orders.xlsx';
$newFilePath = 'path/to/new-orders.xlsx';
// we need a reader to read the existing file...
$reader = ReaderEntityFactory::createReaderFromFile($existingFilePath);
$reader->setShouldFormatDates(true); // this is to be able to copy dates
$reader->open($existingFilePath);
// ... and a writer to create the new file
$writer = WriterEntityFactory::createWriterFromFile($newFilePath);
$writer->openToFile($newFilePath);
// let's read the entire spreadsheet...
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
// Add sheets in the new file, as we read new sheets in the existing one
if ($sheetIndex !== 1) {
$writer->addNewSheetAndMakeItCurrent();
}
foreach ($sheet->getRowIterator() as $row) {
// ... and copy each row into the new spreadsheet
$writer->addRow($row);
}
}
// At this point, the new spreadsheet contains the same data as the existing one.
// So let's add the new data:
$writer->addRow(
WriterEntityFactory::createRowFromArray(['2015-12-25', 'Christmas gift', 29, 'USD'])
);
$reader->close();
$writer->close();
```
Optionally, if you rely on the file name or want to keep only one file, simple remove the old file and rename the new one:
```php?start_inline=1
unlink($existingFilePath);
rename($newFilePath, $existingFilePath);
```
That's it! The created file now contains the updated data and is ready to be used.

View File

@ -0,0 +1,89 @@
---
layout: page
title: "Edit an existing spreadsheet"
category: guide
permalink: /guides/edit-existing-spreadsheet/
---
Editing an existing spreadsheet is a pretty common task that {{ site.spout_html }} is totally capable of doing.
With {{ site.spout_html }}, it is not possible to do things like `deleteRow(3)` or `insertRowAfter(5, $newRow)`. This is because {{ site.spout_html }} does not keep an in-memory representation of the entire spreadsheet, to avoid consuming all the memory available with large spreadsheets. This means, {{ site.spout_html }} does not know how to jump to the 3rd row directly and has especially no way of moving backwards (changing row 3 after having changed row 5). So let's see how this can be done, in a scalable way.
For this example, let's assume we have an existing ODS spreadsheet called "my-music.ods" that looks like this:
| Song title | Artist | Album | Year |
| ---------------- | --------------- | --------------- | ---- |
| Yesterday | The Beatles | The White Album | 1968 |
| Yellow Submarine | The Beatles | Unknown | 1968 |
| Space Oddity | David Bowie | David Bowie | 1969 |
| Thriller | Michael Jackson | Thriller | 1982 |
| No Woman No Cry | Bob Marley | Legend | 1984 |
| Buffalo Soldier | Bob Marley | Legend | 1984 |
> Note that the album for "Yellow Submarine" is "Unknown" and that the songs are ordered by year (most recent last).
We'd like to update the missing album for "Yellow Submarine", remove the Bob Marley's songs and add a new song: "Hotel California" from "The Eagles", released in 1976. Here is how this can be done:
```php
<?php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
$existingFilePath = '/path/to/my-music.ods';
$newFilePath = '/path/to/my-new-music.ods';
// we need a reader to read the existing file...
$reader = ReaderEntityFactory::createReaderFromFile($existingFilePath);
$reader->open($existingFilePath);
$reader->setShouldFormatDates(true); // this is to be able to copy dates
// ... and a writer to create the new file
$writer = WriterEntityFactory::createWriterFromFile($newFilePath);
$writer->openToFile($newFilePath);
// let's read the entire spreadsheet
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
// Add sheets in the new file, as you read new sheets in the existing one
if ($sheetIndex !== 1) {
$writer->addNewSheetAndMakeItCurrent();
}
foreach ($sheet->getRowIterator() as $rowIndex => $row) {
$songTitle = $row->getCellAtIndex(0);
$artist = $row->getCellAtIndex(1);
// Change the album name for "Yellow Submarine"
if ($songTitle === 'Yellow Submarine') {
$row->setCellAtIndex(WriterEntityFactory::createCell('The White Album'), 2);
}
// skip Bob Marley's songs
if ($artist === 'Bob Marley') {
continue;
}
// write the edited row to the new file
$writer->addRow($row);
// insert new song at the right position, between the 3rd and 4th rows
if ($rowIndex === 3) {
$writer->addRow(
WriterEntityFactory::createRowFromArray(['Hotel California', 'The Eagles', 'Hotel California', 1976])
);
}
}
}
$reader->close();
$writer->close();
```
Optionally, if you rely on the file name or want to keep only one file, simple remove the old file and rename the new one:
```php?start_inline=1
unlink($existingFilePath);
rename($newFilePath, $existingFilePath);
```
That's it! The created file now contains the updated data and is ready to be used.

View File

@ -0,0 +1,54 @@
---
layout: page
title: "Read data from a specific sheet only"
category: guide
permalink: /guides/read-data-from-specific-sheet/
---
Even though a spreadsheet contains multiple sheets, you may be interested in reading only one of them and skip the other ones. Here is how you can do it with {{ site.spout_html }}:
* If you know the name of the sheet
```php
<?php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
$reader = ReaderEntityFactory::createXLSXReader();
$reader->open($filePath);
foreach ($reader->getSheetIterator() as $sheet) {
// only read data from "summary" sheet
if ($sheet->getName() === 'summary') {
foreach ($sheet->getRowIterator() as $row) {
// do something with the row
}
break; // no need to read more sheets
}
}
$reader->close();
```
* If you know the position of the sheet
```php
<?php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
$reader = ReaderEntityFactory::createXLSXReader();
$reader->open($filePath);
foreach ($reader->getSheetIterator() as $sheet) {
// only read data from 3rd sheet
if ($sheet->getIndex() === 2) { // index is 0-based
foreach ($sheet->getRowIterator() as $row) {
// do something with the row
}
break; // no need to read more sheets
}
}
$reader->close();
```

View File

@ -0,0 +1,110 @@
---
layout: page
title: "[Symfony] Stream content of a large spreadsheet"
category: guide
permalink: /guides/symfony-stream-content-large-spreadsheet/
---
> This tutorial is for the PHP framework [Symfony](http://symfony.com/).
The main benefit of streaming content is that this content can be rendered as soon as it is available. No matter how big the content is, the browser will be able to start rendering it as soon as the first byte is sent.
Reading a static spreadsheet to display its content to a user is a great use case for streaming. The spreadsheet can contain from a few rows to thousands of them and we don't want to wait until the whole file has been read (which can take a long time) before showing something to the user. Let's see how [Symfony's StreamedResponse](http://symfony.com/doc/current/components/http_foundation/introduction.html#streaming-a-response) let us easily stream the content of the spreadsheet.
A regular controller usually builds the content to be displayed and encapsulate it into a `Response` object. Everything happens synchronously. Such a controller may look like this:
```php?start_inline=1
class MyRegularController extends Controller
{
/**
* @Route("/spreadsheet/read")
*/
public function readAction()
{
$filePath = '/path/to/static/file.xlsx';
// The content to be displayed has to be built entirely
// before it can be sent to the browser.
$content = '';
$reader = ReaderEntityFactory::createReaderFromFile($filePath);
$reader->open($filePath);
foreach ($reader->getSheetIterator() as $sheet) {
$content .= '<table>';
foreach ($sheet->getRowIterator() as $row) {
$content .= '<tr>';
$content .= implode(array_map(function($cell) {
return '<td>' . $cell . '</td>';
}, $row->getCells()));
$content .= '</tr>';
}
$content .= '</table><br>';
}
$reader->close();
// The response is sent to the browser
// once the entire file has been read.
$response = new Response($content);
$response->headers->set('Content-Type', 'text/html');
return $response;
}
}
```
Converting a regular controller to return a `StreamedResponse` is super easy! This is what it looks like after conversion:
```php?start_inline=1
class MyStreamController extends Controller
{
// See below how it is used.
const FLUSH_THRESHOLD = 100;
/**
* @Route("/spreadsheet/stream")
*/
public function readAction()
{
$filePath = '/path/to/static/file.xlsx';
// We'll now return a StreamedResponse.
$response = new StreamedResponse();
$response->headers->set('Content-Type', 'text/html');
// Instead of a string, the streamed response will execute
// a callback function to retrieve data chunks.
$response->setCallback(function() use ($filePath) {
// Same code goes inside the callback.
$reader = ReaderEntityFactory::createXLSXReader();
$reader->open($filePath);
$i = 0;
foreach ($reader->getSheetIterator() as $sheet) {
// The main difference with the regular response is
// that the content is now echo'ed, not appended.
echo '<table>';
foreach ($sheet->getRowIterator() as $row) {
echo '<tr>';
echo implode(array_map(function($cell) {
return '<td>' . $cell . '</td>';
}, $row->getCells()));
echo '</tr>';
$i++;
// Flushing the buffer every N rows to stream echo'ed content.
if ($i % self::FLUSH_THRESHOLD === 0) {
flush();
}
}
echo '</table><br>';
}
$reader->close();
});
return $response;
}
}
```

12
docs/_pages/index.md Normal file
View File

@ -0,0 +1,12 @@
---
layout: default
banner: true
title: Spout - Read and write spreadsheets, quickly and at scale
permalink: /
---
{% include set-global-site-url.html %}
{% include section-supported-spreadsheet-types.html site_url=site_url %}
{% include section-fast-and-scalable.html site_url=site_url %}
{% include section-why-use-spout.html site_url=site_url %}

278
docs/_sass/_base.scss Executable file
View File

@ -0,0 +1,278 @@
/**
* Reset some basic elements
*/
body, h1, h2, h3, h4, h5, h6,
p, blockquote, pre, hr,
dl, dd, ol, ul, figure {
margin: 0;
padding: 0;
}
/**
* Basic styling
*/
body {
font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;
color: $text-color;
background-color: $background-color;
-webkit-text-size-adjust: 100%;
-webkit-font-feature-settings: "kern" 1;
-moz-font-feature-settings: "kern" 1;
-o-font-feature-settings: "kern" 1;
font-feature-settings: "kern" 1;
font-kerning: normal;
}
/**
* Set `margin-bottom` to maintain vertical rhythm
*/
h1, h2, h3, h4, h5, h6,
p, blockquote, pre,
ul, ol, dl, figure,
%vertical-rhythm {
margin-bottom: $spacing-unit / 2;
}
h1:not(:nth-child(1)), h2:not(:nth-child(1)), h3:not(:nth-child(1)), h4:not(:nth-child(1)), h5:not(:nth-child(1)), h6:not(:nth-child(1)) {
margin-top: $spacing-unit;
}
/**
* Tables
*/
table {
display: block;
width: 100%;
overflow: auto;
margin-top: 0;
margin-bottom: 16px;
border-spacing: 0;
border-collapse: collapse;
th, td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
&:nth-child(2n) {
background-color: #f6f8fa;
}
}
th {
font-weight: 500;
}
}
/**
* Images
*/
img {
max-width: 100%;
vertical-align: middle;
}
/**
* Figures
*/
figure > img {
display: block;
}
figcaption {
font-size: $small-font-size;
}
/**
* Lists
*/
ul, ol {
margin-left: $spacing-unit;
}
li {
> ul,
> ol {
margin-bottom: 0;
}
}
/**
* Headings
*/
h1, h2, h3, h4, h5, h6 {
font-weight: $base-font-weight;
}
h1 {
font-size: 3em;
}
h2 {
font-size: 2em;
}
/**
* Links
*/
a {
color: $brand-color;
text-decoration: none;
font-weight: $base-font-weight;
&:visited {
color: lighten($brand-color, 15%);
}
&:hover {
color: $text-color;
text-decoration: underline;
}
}
/**
* Blockquotes
*/
blockquote {
color: $grey-color;
border-left: 4px solid lighten($brand-color, 65%);
padding: $spacing-unit / 3 $spacing-unit / 2;
font-style: italic;
font-weight: 200;
> :last-child {
margin-bottom: 0;
}
}
/**
* Code formatting
*/
pre,
code {
font-size: 15px;
// border: 1px solid darken($code-color, 5%);
border-radius: 3px;
// background-color: $code-color;
}
code {
padding: 1px 5px;
}
pre {
padding: 8px 12px;
overflow-x: auto;
> code {
border: 0;
padding-right: 0;
padding-left: 0;
}
}
/**
* Wrapper
*/
.wrapper {
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));
max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));
margin-right: auto;
margin-left: auto;
padding-right: $spacing-unit;
padding-left: $spacing-unit;
@extend %clearfix;
@include media-query($on-laptop) {
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));
max-width: calc(#{$content-width} - (#{$spacing-unit}));
padding-right: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
}
/**
* Clearfix
*/
%clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
/**
* Icons
*/
.icon {
> svg {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
path {
fill: $grey-color;
}
}
}
.pull-left {
float: left;
}
.pull-right {
float: right;
}
.vertical-align-middle {
// position: relative;
// top: 50%;
// transform: translateY(50%);
display: flex;
justify-content: center;
align-items: center;
}
.mrl { margin-right: 20px; }
.mll { margin-left: 20px; }
.mbl { margin-bottom: 20px; }
.text-center { text-align: center; }
.light-em {
font-weight: $base-font-weight + 100;
}

79
docs/_sass/_index.scss Executable file
View File

@ -0,0 +1,79 @@
.spout {
font-variant: small-caps;
}
.section {
padding: $spacing-unit 0;
border-bottom: 1px solid $grey-color-light;
&.last-section {
border-bottom: none;
}
.section-title {
text-align: center;
}
.description {
width: 600px;
}
&.centered .description {
text-align: center;
}
.icons {
text-align: center;
img {
margin: 10px 20px;
}
}
&.section-odd {
background-color: rgba($grey-color, 0.0);
}
&.section-even {
background-color: $grey-color-very-light;
}
.btn-wrapper {
margin: $spacing-unit 0;
text-align: center;
.btn {
border-radius: 5px;
border: 1px solid;
background: $brand-color;
box-shadow: none;
color: white;
font-size: 20px;
padding: 15px 30px;
text-decoration: none;
cursor: pointer;
font-weight: 500;
}
.btn:hover {
background: darken($brand-color, 10%);
}
.page-link:hover {
text-decoration: none;
}
}
}
.feature-list {
list-style: none;
padding: 0;
margin: 0;
}
.feature-check {
background: url("../images/blue-check-mark.png") no-repeat left top;
background-size: 15px 15px;
background-position-y: 4px;
padding-left: 25px;
}

410
docs/_sass/_layout.scss Executable file
View File

@ -0,0 +1,410 @@
/**
* Site header
*/
.site-header {
border-bottom: 1px solid lighten($brand-color, 15%);
min-height: 56px;
// Positioning context for the mobile navigation icon
position: relative;
}
.site-search {
padding-right: 10px;
}
.site-title {
font-size: 26px;
font-weight: 300;
line-height: 56px;
letter-spacing: -1px;
margin-bottom: 0;
float: left;
&,
&:visited {
color: $grey-color-dark;
}
}
.site-nav {
float: right;
line-height: 56px;
.menu-icon {
display: none;
}
.page-link {
color: $grey-color;
line-height: $base-line-height;
// Gaps between nav items, but not on the last one
&:not(:last-child) {
margin-right: 20px;
}
svg {
padding-bottom: 3px;
}
}
@include media-query($on-palm) {
position: absolute;
top: 9px;
right: $spacing-unit / 2;
background-color: $background-color;
border: 1px solid $grey-color-light;
border-radius: 5px;
text-align: right;
.menu-icon {
display: block;
float: right;
width: 36px;
height: 26px;
line-height: 0;
padding-top: 10px;
text-align: center;
> svg {
width: 18px;
height: 15px;
path {
fill: $grey-color-dark;
}
}
}
.trigger {
clear: both;
display: none;
}
&:hover .trigger {
display: block;
padding-bottom: 5px;
}
.page-link {
display: block;
padding: 5px 10px;
&:not(:last-child) {
margin-right: 0;
}
margin-left: 20px;
}
}
}
.site-banner {
width: 100%;
background: $brand-color;
height: 340px;
color: white;
.wrapper {
height: 100%;
background: url('../images/logo.png');
background-size: 30%;
background-repeat: no-repeat;
background-position: 95% center;
justify-content: flex-start;
}
.tag-line {
font-size: 2.8em;
line-height: 1.1em;
width: 60%;
}
@include media-query($on-palm) {
height: 240px;
.tag-line {
font-size: 1.8em;
}
}
}
/**
* Site footer
*/
.site-footer {
background: $grey-color-dark;
color: $grey-color-light;
border-top: 1px solid $grey-color-light;
padding: $spacing-unit 0;
margin-top: $spacing-unit;
a {
color: $grey-color-very-light;
}
.contact-list,
.social-media-list {
list-style: none;
margin-left: 0;
li {
margin-bottom: 2px;
}
}
.footer-col-wrapper {
font-size: 15px;
// color: $grey-color;
margin-left: -$spacing-unit / 2;
@extend %clearfix;
}
.footer-col {
float: left;
margin-bottom: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
.footer-col-1 {
width: -webkit-calc(35% - (#{$spacing-unit} / 2));
width: calc(35% - (#{$spacing-unit} / 2));
}
.footer-col-2 {
width: -webkit-calc(25% - (#{$spacing-unit} / 2));
width: calc(25% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(40% - (#{$spacing-unit} / 2));
width: calc(40% - (#{$spacing-unit} / 2));
}
@include media-query($on-laptop) {
.footer-col-1,
.footer-col-2 {
width: -webkit-calc(50% - (#{$spacing-unit} / 2));
width: calc(50% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
@include media-query($on-palm) {
.footer-col {
float: none;
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
}
/**
* Page content
*/
.page-content {
padding: $spacing-unit 0;
}
.page-heading {
font-size: 20px;
}
.post-list {
margin-left: 0;
list-style: none;
> li {
margin-bottom: $spacing-unit;
}
}
.post-meta {
font-size: $small-font-size;
color: $grey-color;
}
.post-link {
display: block;
font-size: 24px;
}
/**
* Posts
*/
.post-header {
margin-bottom: $spacing-unit;
padding-top: $spacing-unit;
}
.post-title {
font-size: 42px;
letter-spacing: -1px;
line-height: 1;
@include media-query($on-laptop) {
font-size: 36px;
}
}
.post-content {
margin-bottom: $spacing-unit;
h2 {
font-size: 32px;
@include media-query($on-laptop) {
font-size: 28px;
}
}
h3 {
font-size: 26px;
@include media-query($on-laptop) {
font-size: 22px;
}
}
h4 {
font-size: 20px;
@include media-query($on-laptop) {
font-size: 18px;
}
}
}
$table-of-content-width: 250px;
.table-of-content {
position: absolute;
left: 100px;
width: $table-of-content-width;
}
.table-of-content.affix-top {
position: absolute;
top: 70px;
}
.table-of-content.affix-bottom {
position: absolute;
bottom: 300;
}
.table-of-content.affix {
position: fixed;
top: 30px;
}
.table-of-content a.h1 {
font-weight: 600;
}
.table-of-content a.h2 {
font-weight: 400;
font-size: 14px;
position: relative;
left: 10px;
}
.table-of-content a.h3 {
font-size: 12px;
position: relative;
left: 20px;
}
// TODO
@include media-query($on-laptop-big+$table-of-content-width) {
.table-of-content {
left: 20px;
}
}
@include media-query($on-laptop-big) {
.table-of-content {
display: none;
}
}
@include media-query($on-laptop) {
.table-of-content {
display: none;
}
}
/* search */
input#algolia-doc-search {
background: transparent url("/images/search.png") no-repeat 10px center;
background-size: 16px 16px;
position: relative;
vertical-align: top;
margin-left: 10px;
padding: 0 10px;
padding-left: 35px;
height: 30px;
margin-top: 10px;
font-size: 16px;
line-height: 20px;
background-color: #fff;
border-radius: 4px;
color: #333;
outline: none;
width: 100px;
transition: width .2s ease;
box-shadow: none;
border: 1px solid #ddd;
}
input#algolia-doc-search:focus {
width: 200px;
}
/* Bottom border of each suggestion */
.algolia-docsearch-suggestion {
border-bottom-color: $brand-color;
}
/* Main category headers */
.algolia-docsearch-suggestion--category-header {
background-color: lighten($brand-color, 20%);
}
/* Highlighted search terms */
.algolia-docsearch-suggestion--highlight {
color: lighten($brand-color, 10%);
}
/* Highligted search terms in the main category headers */
.algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--highlight {
background-color: $brand-color;
}
/* Currently selected suggestion */
.aa-cursor .algolia-docsearch-suggestion--content {
color: darken($brand-color, 10%);
}
.aa-cursor .algolia-docsearch-suggestion {
background: ligten($brand-color, 30%);
}
/* For bigger screens, when displaying results in two columns */
@media (min-width: 768px) {
/* Bottom border of each suggestion */
.algolia-docsearch-suggestion {
border-bottom-color: lighten($brand-color, 15%);
}
/* Left column, with secondary category header */
.algolia-docsearch-suggestion--subcategory-column {
border-right-color: lighten($brand-color, 25%);
background-color: lighten($brand-color, 45%);
color: darken($brand-color, 35%);
}
}

View File

@ -0,0 +1,109 @@
/**
* Syntax highlighting styles
*/
.highlight {
// markdown editing
$default: #989898;
$bg: #f5f5f5;
$caret: #00bdff;
$black: #000;
$white: #fff;
$invisible: #E0E0E0;
$highlight: #e6e6e6;
$inserted: #DDFFDD;
$output: #7F7F7F;
$promt: #555;
$traceback: #F93232;
$deleted: #fdd;
$selection: #C2E8FF;
$found: #FFE792;
$shadow: #808080;
$comment: #bbbaba;
$invalid: #F9F2CE;
$operator: #626FC9;
$keyword: #7aad36;
$symbol: #E8FFD5;
$type: #6700B9;
$constant: #9870EC;
$var: #4C8FC7;
$attribute: #d42a57;
$function: $keyword;
$built-in: $attribute;
$class: #3A1D72;
$exception: #F93232;
$section: #333333;
$number: $constant;
$literal: #de8325;
$string_re: #699D36;
$tag: $var;
$name-class: #3A77BF;
$entity: #6d98cf;
$punctuation: $black;
color: $default;
background-color: $bg;
//border: 1px solid darken($bg, 5%);
.bp { color: $caret; } // Name.Builtin.Pseudo
.c { color: $comment; } // Comment
.c1 { @extend .c; } // Comment.Single
.cm { @extend .c; } // Comment.Multiline
.cp { color: $shadow } // Comment.Preproc
.cs { @extend .c; } // Comment.Special
.err { color: $exception; background-color: $invalid } // Error
.gd { color: $black; background-color: $deleted; } // Generic.Deleted
.ge { color: $default; background-color: $highlight; } // Generic.Emph
.gh { color: $section;} // Generic.Heading
.gi { color: $black; background-color: $inserted; } // Generic.Inserted
.go { color: $output } // Generic.Output
.gp { color: $promt } // Generic.Prompt
.gr { color: $exception } // Generic.Error
.gs { background-color: $white; } // Generic.Strong
.gt { color: $traceback } // Generic.Traceback
.gu { color: $black; } // Generic.Subheading
.hll { background-color: $found }
.k { color: $keyword } // Keyword
.kc { color: $constant } // Keyword.Constant
.kd { @extend .k; } // Keyword.Declaration
.kn { @extend .k; } // Keyword.Namespace
.kp { @extend .k; } // Keyword.Pseudo
.kr { @extend .k; } // Keyword.Reserved
.kt { color: $type } // Keyword.Type
.m { color: $number; } // Literal.Number
.mf { @extend .m; } // Literal.Number.Float
.mh { @extend .m; } // Literal.Number.Hex
.mi { @extend .m; } // Literal.Number.Integer
.mo { @extend .m; } // Literal.Number.Oct
.il { @extend .m; } // Literal.Number.Integer.Long
.n { color: $default } // Name
.na { color: $attribute } // Name.Attribute
.nb { color: $built-in } // Name.Builtin
.nc { color: $name-class; } // Name.Class
.nd { color: $shadow } // Name.Decorator
.nf { color: $function } // Name.Function
.ni { color: $entity;} // Name.Entity
.nn { color: $class; text-decoration: underline } // Name.Namespace
.no { color: $constant } // Name.Constant
.nt { color: $tag;} // Name.Tag
.nv { @extend .v; } // Name.Variable
.nx { color: $default; }
.o { color: $punctuation } // Operator.Word
.ow { color: $operator } // Operator.Word
.s { color: $literal } // Literal.String
.s1 { @extend .s; } // Literal.String.Single
.s2 { @extend .s; } // Literal.String.Double
.sb { @extend .s; } // Literal.String.Backtick
.sc { @extend .s; } // Literal.String.Char
.sd { @extend .s; } // Literal.String.Doc
.se { @extend .s; } // Literal.String.Escape
.sh { @extend .s; } // Literal.String.Heredoc
.si { @extend .s; } // Literal.String.Interpol
.sr { color: $string_re; } // Literal.String.Regex
.ss { color: $caret; } // Literal.String.Symbol
.sx { @extend .s; } // Literal.String.Other
.v { color: $var }
.vc { color: $class; } // Name.Variable.Class
.vg { @extend .v; } // Name.Variable.Global
.vi { @extend .v; } // Name.Variable.Instance
.w { color: $invisible; } // Text.Whitespace
}

56
docs/css/main.scss Executable file
View File

@ -0,0 +1,56 @@
---
# Only the main Sass file needs front matter (the dashes are enough)
---
@charset "utf-8";
// Our variables
$base-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
$base-font-size: 16px;
$base-font-weight: 300;
$small-font-size: $base-font-size * 0.875;
$base-line-height: 1.5;
$spacing-unit: 30px;
$text-color: #111;
$background-color: #fdfdfd;
$brand-color: #104c81;
$grey-color: #828282;
$grey-color-light: lighten($grey-color, 40%);
$grey-color-very-light: lighten($grey-color, 45%);
$grey-color-dark: darken($grey-color, 25%);
// Width of the content area
$content-width: 800px;
$on-palm: 600px;
$on-laptop: 800px;
$on-laptop-big: 1400px;
// Use media queries like this:
// @include media-query($on-palm) {
// .wrapper {
// padding-right: $spacing-unit / 2;
// padding-left: $spacing-unit / 2;
// }
// }
@mixin media-query($device) {
@media screen and (max-width: $device) {
@content;
}
}
// Import partials from `sass_dir` (defaults to `_sass`)
@import
"base",
"layout",
"syntax-highlighting",
"index"
;

15
docs/docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
version: "3"
services:
site:
command: bash -c "
gem install 'jekyll-github-metadata'
&& jekyll serve --config _config.yml,_config_local.yml"
image: jekyll/jekyll:latest
volumes:
- $PWD:/srv/jekyll
- $PWD/vendor/bundle:/usr/local/bundle
ports:
- 4000:4000
- 35729:35729
- 3000:3000
- 8080:4000

BIN
docs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
docs/images/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
docs/images/icon-csv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
docs/images/icon-ods.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
docs/images/icon-xlsx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

9
phpstan.neon Normal file
View File

@ -0,0 +1,9 @@
parameters:
level: 4
paths:
- src
- tests
excludePaths:
# Exclude these files that are OK
- src/Spout/Reader/Common/Creator/ReaderEntityFactory.php
- src/Spout/Writer/Common/Creator/WriterEntityFactory.php

View File

@ -1,17 +1,36 @@
<phpunit <?xml version="1.0" encoding="UTF-8"?>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.3/phpunit.xsd" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="tests/bootstrap.php" bootstrap="tests/bootstrap.php"
colors="true" colors="true"
convertErrorsToExceptions="false" convertErrorsToExceptions="false"
convertWarningsToExceptions="false" convertWarningsToExceptions="false"
strict="false" defaultTestSuite="unit-tests"
verbose="false"> verbose="false">
<testsuites> <php>
<testsuite name="all-tests"> <ini name="error_reporting" value="-1"/>
<directory>tests/</directory> </php>
</testsuite>
</testsuites> <testsuites>
<testsuite name="unit-tests">
<directory>tests/</directory>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>perf-tests</group>
</exclude>
</groups>
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
<exclude>
<directory>src/Spout/Autoloader</directory>
</exclude>
</coverage>
</phpunit> </phpunit>

View File

@ -0,0 +1,148 @@
<?php
namespace Box\Spout\Autoloader;
/**
* Class Psr4Autoloader
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
*/
class Psr4Autoloader
{
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
*
* @var array
*/
protected $prefixes = [];
/**
* Register loader with SPL autoloader stack.
*
* @return void
*/
public function register()
{
\spl_autoload_register([$this, 'loadClass']);
}
/**
* Adds a base directory for a namespace prefix.
*
* @param string $prefix The namespace prefix.
* @param string $baseDir A base directory for class files in the
* namespace.
* @param bool $prepend If true, prepend the base directory to the stack
* instead of appending it; this causes it to be searched first rather
* than last.
* @return void
*/
public function addNamespace($prefix, $baseDir, $prepend = false)
{
// normalize namespace prefix
$prefix = \trim($prefix, '\\') . '\\';
// normalize the base directory with a trailing separator
$baseDir = \rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
// initialize the namespace prefix array
if (isset($this->prefixes[$prefix]) === false) {
$this->prefixes[$prefix] = [];
}
// retain the base directory for the namespace prefix
if ($prepend) {
\array_unshift($this->prefixes[$prefix], $baseDir);
} else {
\array_push($this->prefixes[$prefix], $baseDir);
}
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name.
* @return mixed The mapped file name on success, or boolean false on
* failure.
*/
public function loadClass($class)
{
// the current namespace prefix
$prefix = $class;
// work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name
while (($pos = \strrpos($prefix, '\\')) !== false) {
// retain the trailing namespace separator in the prefix
$prefix = \substr($class, 0, $pos + 1);
// the rest is the relative class name
$relativeClass = \substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
if ($mappedFile !== false) {
return $mappedFile;
}
// remove the trailing namespace separator for the next iteration
// of strrpos()
$prefix = \rtrim($prefix, '\\');
}
// never found a mapped file
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix.
* @param string $relativeClass The relative class name.
* @return mixed Boolean false if no mapped file can be loaded, or the
* name of the mapped file that was loaded.
*/
protected function loadMappedFile($prefix, $relativeClass)
{
// are there any base directories for this namespace prefix?
if (isset($this->prefixes[$prefix]) === false) {
return false;
}
// look through base directories for this namespace prefix
foreach ($this->prefixes[$prefix] as $baseDir) {
// replace the namespace prefix with the base directory,
// replace namespace separators with directory separators
// in the relative class name, append with .php
$file = $baseDir
. \str_replace('\\', '/', $relativeClass)
. '.php';
// if the mapped file exists, require it
if ($this->requireFile($file)) {
// yes, we're done
return $file;
}
}
// never found it
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file The file to require.
* @return bool True if the file exists, false if not.
*/
protected function requireFile($file)
{
if (\file_exists($file)) {
require $file;
return true;
}
return false;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Box\Spout\Autoloader;
require_once 'Psr4Autoloader.php';
/**
* @var string
* Full path to "src/Spout" which is what we want "Box\Spout" to map to.
*/
$srcBaseDirectory = \dirname(\dirname(__FILE__));
$loader = new Psr4Autoloader();
$loader->register();
$loader->addNamespace('Box\Spout', $srcBaseDirectory);

View File

@ -0,0 +1,49 @@
<?php
namespace Box\Spout\Common\Creator;
use Box\Spout\Common\Helper\EncodingHelper;
use Box\Spout\Common\Helper\FileSystemHelper;
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
use Box\Spout\Common\Helper\StringHelper;
/**
* Class HelperFactory
* Factory to create helpers
*/
class HelperFactory
{
/**
* @return GlobalFunctionsHelper
*/
public function createGlobalFunctionsHelper()
{
return new GlobalFunctionsHelper();
}
/**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
* @return FileSystemHelper
*/
public function createFileSystemHelper($baseFolderPath)
{
return new FileSystemHelper($baseFolderPath);
}
/**
* @param GlobalFunctionsHelper $globalFunctionsHelper
* @return EncodingHelper
*/
public function createEncodingHelper(GlobalFunctionsHelper $globalFunctionsHelper)
{
return new EncodingHelper($globalFunctionsHelper);
}
/**
* @return StringHelper
*/
public function createStringHelper()
{
return new StringHelper();
}
}

View File

@ -0,0 +1,216 @@
<?php
namespace Box\Spout\Common\Entity;
use Box\Spout\Common\Entity\Style\Style;
use Box\Spout\Common\Helper\CellTypeHelper;
/**
* Class Cell
*/
class Cell
{
/**
* Numeric cell type (whole numbers, fractional numbers, dates)
*/
public const TYPE_NUMERIC = 0;
/**
* String (text) cell type
*/
public const TYPE_STRING = 1;
/**
* Formula cell type
* Not used at the moment
*/
public const TYPE_FORMULA = 2;
/**
* Empty cell type
*/
public const TYPE_EMPTY = 3;
/**
* Boolean cell type
*/
public const TYPE_BOOLEAN = 4;
/**
* Date cell type
*/
public const TYPE_DATE = 5;
/**
* Error cell type
*/
public const TYPE_ERROR = 6;
/**
* The value of this cell
* @var mixed|null
*/
protected $value;
/**
* The cell type
* @var int|null
*/
protected $type;
/**
* The cell style
* @var Style
*/
protected $style;
/**
* @param mixed|null $value
* @param Style|null $style
*/
public function __construct($value, Style $style = null)
{
$this->setValue($value);
$this->setStyle($style);
}
/**
* @param mixed|null $value
*/
public function setValue($value)
{
$this->value = $value;
$this->type = $this->detectType($value);
}
/**
* @return mixed|null
*/
public function getValue()
{
return !$this->isError() ? $this->value : null;
}
/**
* @return mixed
*/
public function getValueEvenIfError()
{
return $this->value;
}
/**
* @param Style|null $style
*/
public function setStyle($style)
{
$this->style = $style ?: new Style();
}
/**
* @return Style
*/
public function getStyle()
{
return $this->style;
}
/**
* @return int|null
*/
public function getType()
{
return $this->type;
}
/**
* @param int $type
*/
public function setType($type)
{
$this->type = $type;
}
/**
* Get the current value type
*
* @param mixed|null $value
* @return int
*/
protected function detectType($value)
{
if (CellTypeHelper::isBoolean($value)) {
return self::TYPE_BOOLEAN;
}
if (CellTypeHelper::isEmpty($value)) {
return self::TYPE_EMPTY;
}
if (CellTypeHelper::isNumeric($value)) {
return self::TYPE_NUMERIC;
}
if (CellTypeHelper::isDateTimeOrDateInterval($value)) {
return self::TYPE_DATE;
}
if (CellTypeHelper::isNonEmptyString($value)) {
return self::TYPE_STRING;
}
return self::TYPE_ERROR;
}
/**
* @return bool
*/
public function isBoolean()
{
return $this->type === self::TYPE_BOOLEAN;
}
/**
* @return bool
*/
public function isEmpty()
{
return $this->type === self::TYPE_EMPTY;
}
/**
* @return bool
*/
public function isNumeric()
{
return $this->type === self::TYPE_NUMERIC;
}
/**
* @return bool
*/
public function isString()
{
return $this->type === self::TYPE_STRING;
}
/**
* @return bool
*/
public function isDate()
{
return $this->type === self::TYPE_DATE;
}
/**
* @return bool
*/
public function isError()
{
return $this->type === self::TYPE_ERROR;
}
/**
* @return string
*/
public function __toString()
{
return (string) $this->getValue();
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace Box\Spout\Common\Entity;
use Box\Spout\Common\Entity\Style\Style;
class Row
{
/**
* The cells in this row
* @var Cell[]
*/
protected $cells = [];
/**
* The row style
* @var Style
*/
protected $style;
/**
* Row constructor.
* @param Cell[] $cells
* @param Style|null $style
*/
public function __construct(array $cells, $style)
{
$this
->setCells($cells)
->setStyle($style);
}
/**
* @return Cell[] $cells
*/
public function getCells()
{
return $this->cells;
}
/**
* @param Cell[] $cells
* @return Row
*/
public function setCells(array $cells)
{
$this->cells = [];
foreach ($cells as $cell) {
$this->addCell($cell);
}
return $this;
}
/**
* @param Cell $cell
* @param int $cellIndex
* @return Row
*/
public function setCellAtIndex(Cell $cell, $cellIndex)
{
$this->cells[$cellIndex] = $cell;
return $this;
}
/**
* @param int $cellIndex
* @return Cell|null
*/
public function getCellAtIndex($cellIndex)
{
return $this->cells[$cellIndex] ?? null;
}
/**
* @param Cell $cell
* @return Row
*/
public function addCell(Cell $cell)
{
$this->cells[] = $cell;
return $this;
}
/**
* @return int
*/
public function getNumCells()
{
// When using "setCellAtIndex", it's possible to
// have "$this->cells" contain holes.
if (empty($this->cells)) {
return 0;
}
return \max(\array_keys($this->cells)) + 1;
}
/**
* @return Style
*/
public function getStyle()
{
return $this->style;
}
/**
* @param Style|null $style
* @return Row
*/
public function setStyle($style)
{
$this->style = $style ?: new Style();
return $this;
}
/**
* @return array The row values, as array
*/
public function toArray()
{
return \array_map(function (Cell $cell) {
return $cell->getValue();
}, $this->cells);
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Box\Spout\Common\Entity\Style;
/**
* Class Border
*/
class Border
{
public const LEFT = 'left';
public const RIGHT = 'right';
public const TOP = 'top';
public const BOTTOM = 'bottom';
public const STYLE_NONE = 'none';
public const STYLE_SOLID = 'solid';
public const STYLE_DASHED = 'dashed';
public const STYLE_DOTTED = 'dotted';
public const STYLE_DOUBLE = 'double';
public const WIDTH_THIN = 'thin';
public const WIDTH_MEDIUM = 'medium';
public const WIDTH_THICK = 'thick';
/** @var array A list of BorderPart objects for this border. */
private $parts = [];
/**
* @param array $borderParts
*/
public function __construct(array $borderParts = [])
{
$this->setParts($borderParts);
}
/**
* @param string $name The name of the border part
* @return BorderPart|null
*/
public function getPart($name)
{
return $this->hasPart($name) ? $this->parts[$name] : null;
}
/**
* @param string $name The name of the border part
* @return bool
*/
public function hasPart($name)
{
return isset($this->parts[$name]);
}
/**
* @return array
*/
public function getParts()
{
return $this->parts;
}
/**
* Set BorderParts
* @param array $parts
* @return void
*/
public function setParts($parts)
{
unset($this->parts);
foreach ($parts as $part) {
$this->addPart($part);
}
}
/**
* @param BorderPart $borderPart
* @return Border
*/
public function addPart(BorderPart $borderPart)
{
$this->parts[$borderPart->getName()] = $borderPart;
return $this;
}
}

View File

@ -0,0 +1,184 @@
<?php
namespace Box\Spout\Common\Entity\Style;
use Box\Spout\Writer\Exception\Border\InvalidNameException;
use Box\Spout\Writer\Exception\Border\InvalidStyleException;
use Box\Spout\Writer\Exception\Border\InvalidWidthException;
/**
* Class BorderPart
*/
class BorderPart
{
/**
* @var string The style of this border part.
*/
protected $style;
/**
* @var string The name of this border part.
*/
protected $name;
/**
* @var string The color of this border part.
*/
protected $color;
/**
* @var string The width of this border part.
*/
protected $width;
/**
* @var array Allowed style constants for parts.
*/
protected static $allowedStyles = [
'none',
'solid',
'dashed',
'dotted',
'double',
];
/**
* @var array Allowed names constants for border parts.
*/
protected static $allowedNames = [
'left',
'right',
'top',
'bottom',
];
/**
* @var array Allowed width constants for border parts.
*/
protected static $allowedWidths = [
'thin',
'medium',
'thick',
];
/**
* @param string $name @see BorderPart::$allowedNames
* @param string $color A RGB color code
* @param string $width @see BorderPart::$allowedWidths
* @param string $style @see BorderPart::$allowedStyles
* @throws InvalidNameException
* @throws InvalidStyleException
* @throws InvalidWidthException
*/
public function __construct($name, $color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
{
$this->setName($name);
$this->setColor($color);
$this->setWidth($width);
$this->setStyle($style);
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name The name of the border part @see BorderPart::$allowedNames
* @throws InvalidNameException
* @return void
*/
public function setName($name)
{
if (!\in_array($name, self::$allowedNames)) {
throw new InvalidNameException($name);
}
$this->name = $name;
}
/**
* @return string
*/
public function getStyle()
{
return $this->style;
}
/**
* @param string $style The style of the border part @see BorderPart::$allowedStyles
* @throws InvalidStyleException
* @return void
*/
public function setStyle($style)
{
if (!\in_array($style, self::$allowedStyles)) {
throw new InvalidStyleException($style);
}
$this->style = $style;
}
/**
* @return string
*/
public function getColor()
{
return $this->color;
}
/**
* @param string $color The color of the border part @see Color::rgb()
* @return void
*/
public function setColor($color)
{
$this->color = $color;
}
/**
* @return string
*/
public function getWidth()
{
return $this->width;
}
/**
* @param string $width The width of the border part @see BorderPart::$allowedWidths
* @throws InvalidWidthException
* @return void
*/
public function setWidth($width)
{
if (!\in_array($width, self::$allowedWidths)) {
throw new InvalidWidthException($width);
}
$this->width = $width;
}
/**
* @return array
*/
public static function getAllowedStyles()
{
return self::$allowedStyles;
}
/**
* @return array
*/
public static function getAllowedNames()
{
return self::$allowedNames;
}
/**
* @return array
*/
public static function getAllowedWidths()
{
return self::$allowedWidths;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Box\Spout\Common\Entity\Style;
/**
* Class Alignment
* This class provides constants to work with text alignment.
*/
abstract class CellAlignment
{
public const LEFT = 'left';
public const RIGHT = 'right';
public const CENTER = 'center';
public const JUSTIFY = 'justify';
private static $VALID_ALIGNMENTS = [
self::LEFT => 1,
self::RIGHT => 1,
self::CENTER => 1,
self::JUSTIFY => 1,
];
/**
* @param string $cellAlignment
*
* @return bool Whether the given cell alignment is valid
*/
public static function isValid($cellAlignment)
{
return isset(self::$VALID_ALIGNMENTS[$cellAlignment]);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Box\Spout\Common\Entity\Style;
use Box\Spout\Common\Exception\InvalidColorException;
/**
* Class Color
* This class provides constants and functions to work with colors
*/
abstract class Color
{
/** Standard colors - based on Office Online */
public const BLACK = '000000';
public const WHITE = 'FFFFFF';
public const RED = 'FF0000';
public const DARK_RED = 'C00000';
public const ORANGE = 'FFC000';
public const YELLOW = 'FFFF00';
public const LIGHT_GREEN = '92D040';
public const GREEN = '00B050';
public const LIGHT_BLUE = '00B0E0';
public const BLUE = '0070C0';
public const DARK_BLUE = '002060';
public const PURPLE = '7030A0';
/**
* Returns an RGB color from R, G and B values
*
* @param int $red Red component, 0 - 255
* @param int $green Green component, 0 - 255
* @param int $blue Blue component, 0 - 255
* @return string RGB color
*/
public static function rgb($red, $green, $blue)
{
self::throwIfInvalidColorComponentValue($red);
self::throwIfInvalidColorComponentValue($green);
self::throwIfInvalidColorComponentValue($blue);
return \strtoupper(
self::convertColorComponentToHex($red) .
self::convertColorComponentToHex($green) .
self::convertColorComponentToHex($blue)
);
}
/**
* Throws an exception is the color component value is outside of bounds (0 - 255)
*
* @param int $colorComponent
* @throws \Box\Spout\Common\Exception\InvalidColorException
* @return void
*/
protected static function throwIfInvalidColorComponentValue($colorComponent)
{
if (!\is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent");
}
}
/**
* Converts the color component to its corresponding hexadecimal value
*
* @param int $colorComponent Color component, 0 - 255
* @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d"
*/
protected static function convertColorComponentToHex($colorComponent)
{
return \str_pad(\dechex($colorComponent), 2, '0', STR_PAD_LEFT);
}
/**
* Returns the ARGB color of the given RGB color,
* assuming that alpha value is always 1.
*
* @param string $rgbColor RGB color like "FF08B2"
* @return string ARGB color
*/
public static function toARGB($rgbColor)
{
return 'FF' . $rgbColor;
}
}

View File

@ -0,0 +1,509 @@
<?php
namespace Box\Spout\Common\Entity\Style;
/**
* Class Style
* Represents a style to be applied to a cell
*/
class Style
{
/** Default values */
public const DEFAULT_FONT_SIZE = 11;
public const DEFAULT_FONT_COLOR = Color::BLACK;
public const DEFAULT_FONT_NAME = 'Arial';
/** @var int|null Style ID */
private $id;
/** @var bool Whether the font should be bold */
private $fontBold = false;
/** @var bool Whether the bold property was set */
private $hasSetFontBold = false;
/** @var bool Whether the font should be italic */
private $fontItalic = false;
/** @var bool Whether the italic property was set */
private $hasSetFontItalic = false;
/** @var bool Whether the font should be underlined */
private $fontUnderline = false;
/** @var bool Whether the underline property was set */
private $hasSetFontUnderline = false;
/** @var bool Whether the font should be struck through */
private $fontStrikethrough = false;
/** @var bool Whether the strikethrough property was set */
private $hasSetFontStrikethrough = false;
/** @var int Font size */
private $fontSize = self::DEFAULT_FONT_SIZE;
/** @var bool Whether the font size property was set */
private $hasSetFontSize = false;
/** @var string Font color */
private $fontColor = self::DEFAULT_FONT_COLOR;
/** @var bool Whether the font color property was set */
private $hasSetFontColor = false;
/** @var string Font name */
private $fontName = self::DEFAULT_FONT_NAME;
/** @var bool Whether the font name property was set */
private $hasSetFontName = false;
/** @var bool Whether specific font properties should be applied */
private $shouldApplyFont = false;
/** @var bool Whether specific cell alignment should be applied */
private $shouldApplyCellAlignment = false;
/** @var string Cell alignment */
private $cellAlignment;
/** @var bool Whether the cell alignment property was set */
private $hasSetCellAlignment = false;
/** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */
private $shouldWrapText = false;
/** @var bool Whether the wrap text property was set */
private $hasSetWrapText = false;
/** @var Border|null */
private $border;
/** @var bool Whether border properties should be applied */
private $shouldApplyBorder = false;
/** @var string Background color */
private $backgroundColor;
/** @var bool */
private $hasSetBackgroundColor = false;
/** @var string|null Format */
private $format;
/** @var bool */
private $hasSetFormat = false;
/** @var bool */
private $isRegistered = false;
/** @var bool */
private $isEmpty = true;
/**
* @return int|null
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
* @return Style
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return Border|null
*/
public function getBorder()
{
return $this->border;
}
/**
* @param Border $border
* @return Style
*/
public function setBorder(Border $border)
{
$this->shouldApplyBorder = true;
$this->border = $border;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function shouldApplyBorder()
{
return $this->shouldApplyBorder;
}
/**
* @return bool
*/
public function isFontBold()
{
return $this->fontBold;
}
/**
* @return Style
*/
public function setFontBold()
{
$this->fontBold = true;
$this->hasSetFontBold = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontBold()
{
return $this->hasSetFontBold;
}
/**
* @return bool
*/
public function isFontItalic()
{
return $this->fontItalic;
}
/**
* @return Style
*/
public function setFontItalic()
{
$this->fontItalic = true;
$this->hasSetFontItalic = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontItalic()
{
return $this->hasSetFontItalic;
}
/**
* @return bool
*/
public function isFontUnderline()
{
return $this->fontUnderline;
}
/**
* @return Style
*/
public function setFontUnderline()
{
$this->fontUnderline = true;
$this->hasSetFontUnderline = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontUnderline()
{
return $this->hasSetFontUnderline;
}
/**
* @return bool
*/
public function isFontStrikethrough()
{
return $this->fontStrikethrough;
}
/**
* @return Style
*/
public function setFontStrikethrough()
{
$this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontStrikethrough()
{
return $this->hasSetFontStrikethrough;
}
/**
* @return int
*/
public function getFontSize()
{
return $this->fontSize;
}
/**
* @param int $fontSize Font size, in pixels
* @return Style
*/
public function setFontSize($fontSize)
{
$this->fontSize = $fontSize;
$this->hasSetFontSize = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontSize()
{
return $this->hasSetFontSize;
}
/**
* @return string
*/
public function getFontColor()
{
return $this->fontColor;
}
/**
* Sets the font color.
*
* @param string $fontColor ARGB color (@see Color)
* @return Style
*/
public function setFontColor($fontColor)
{
$this->fontColor = $fontColor;
$this->hasSetFontColor = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontColor()
{
return $this->hasSetFontColor;
}
/**
* @return string
*/
public function getFontName()
{
return $this->fontName;
}
/**
* @param string $fontName Name of the font to use
* @return Style
*/
public function setFontName($fontName)
{
$this->fontName = $fontName;
$this->hasSetFontName = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontName()
{
return $this->hasSetFontName;
}
/**
* @return string
*/
public function getCellAlignment()
{
return $this->cellAlignment;
}
/**
* @param string $cellAlignment The cell alignment
*
* @return Style
*/
public function setCellAlignment($cellAlignment)
{
$this->cellAlignment = $cellAlignment;
$this->hasSetCellAlignment = true;
$this->shouldApplyCellAlignment = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetCellAlignment()
{
return $this->hasSetCellAlignment;
}
/**
* @return bool Whether specific cell alignment should be applied
*/
public function shouldApplyCellAlignment()
{
return $this->shouldApplyCellAlignment;
}
/**
* @return bool
*/
public function shouldWrapText()
{
return $this->shouldWrapText;
}
/**
* @param bool $shouldWrap Should the text be wrapped
* @return Style
*/
public function setShouldWrapText($shouldWrap = true)
{
$this->shouldWrapText = $shouldWrap;
$this->hasSetWrapText = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetWrapText()
{
return $this->hasSetWrapText;
}
/**
* @return bool Whether specific font properties should be applied
*/
public function shouldApplyFont()
{
return $this->shouldApplyFont;
}
/**
* Sets the background color
* @param string $color ARGB color (@see Color)
* @return Style
*/
public function setBackgroundColor($color)
{
$this->hasSetBackgroundColor = true;
$this->backgroundColor = $color;
$this->isEmpty = false;
return $this;
}
/**
* @return string
*/
public function getBackgroundColor()
{
return $this->backgroundColor;
}
/**
* @return bool Whether the background color should be applied
*/
public function shouldApplyBackgroundColor()
{
return $this->hasSetBackgroundColor;
}
/**
* Sets format
* @param string $format
* @return Style
*/
public function setFormat($format)
{
$this->hasSetFormat = true;
$this->format = $format;
$this->isEmpty = false;
return $this;
}
/**
* @return string|null
*/
public function getFormat()
{
return $this->format;
}
/**
* @return bool Whether format should be applied
*/
public function shouldApplyFormat()
{
return $this->hasSetFormat;
}
/**
* @return bool
*/
public function isRegistered() : bool
{
return $this->isRegistered;
}
public function markAsRegistered(?int $id) : void
{
$this->setId($id);
$this->isRegistered = true;
}
public function unmarkAsRegistered() : void
{
$this->setId(0);
$this->isRegistered = false;
}
public function isEmpty() : bool
{
return $this->isEmpty;
}
}

View File

@ -1,140 +0,0 @@
<?php
namespace Box\Spout\Common\Escaper;
/**
* Class XLSX
* Provides functions to escape and unescape data for XLSX files
*
* @package Box\Spout\Common\Escaper
*/
class XLSX implements EscaperInterface
{
/** @var string[] Control characters to be escaped */
protected $controlCharactersEscapingMap;
public function __construct()
{
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
}
/**
* Escapes the given string to make it compatible with XLSX
*
* @param string $string The string to escape
* @return string The escaped string
*/
public function escape($string)
{
$escapedString = $this->escapeControlCharacters($string);
$escapedString = htmlspecialchars($escapedString, ENT_QUOTES, 'utf-8');
return $escapedString;
}
/**
* Unescapes the given string to make it compatible with XLSX
*
* @param string $string The string to unescape
* @return string The unescaped string
*/
public function unescape($string)
{
$unescapedString = htmlspecialchars_decode($string, ENT_QUOTES);
$unescapedString = $this->unescapeControlCharacters($unescapedString);
return $unescapedString;
}
/**
* Builds the map containing control characters to be escaped
* mapped to their escaped values.
* "\t", "\r" and "\n" don't need to be escaped.
*
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @return string[]
*/
protected function getControlCharactersEscapingMap()
{
$controlCharactersEscapingMap = [];
$whitelistedControlCharacters = ["\t", "\r", "\n"];
// control characters values are from 0 to 1F (hex values) in the ASCII table
for ($charValue = 0x0; $charValue <= 0x1F; $charValue++) {
if (!in_array(chr($charValue), $whitelistedControlCharacters)) {
$charHexValue = dechex($charValue);
$escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
$controlCharactersEscapingMap[$escapedChar] = chr($charValue);
}
}
return $controlCharactersEscapingMap;
}
/**
* Converts PHP control characters from the given string to OpenXML escaped control characters
*
* Excel escapes control characters with _xHHHH_ and also escapes any
* literal strings of that type by encoding the leading underscore.
* So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
*
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to escape
* @return string
*/
protected function escapeControlCharacters($string)
{
$escapedString = $this->escapeEscapeCharacter($string);
return str_replace(array_values($this->controlCharactersEscapingMap), array_keys($this->controlCharactersEscapingMap), $escapedString);
}
/**
* Escapes the escape character: "_x0000_" -> "_x005F_x0000_"
*
* @param string $string String to escape
* @return string The escaped string
*/
protected function escapeEscapeCharacter($string)
{
return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
}
/**
* Converts OpenXML escaped control characters from the given string to PHP control characters
*
* Excel escapes control characters with _xHHHH_ and also escapes any
* literal strings of that type by encoding the leading underscore.
* So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
*
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to unescape
* @return string
*/
protected function unescapeControlCharacters($string)
{
$unescapedString = $string;
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
// only unescape characters that don't contain the escaped escape character for now
$unescapedString = preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
}
return $this->unescapeEscapeCharacter($unescapedString);
}
/**
* Unecapes the escape character: "_x005F_x0000_" => "_x0000_"
*
* @param string $string String to unescape
* @return string The unescaped string
*/
protected function unescapeEscapeCharacter($string)
{
return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class BadUsageException
*
* @package Box\Spout\Common\Exception
*/
class BadUsageException extends SpoutException
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class EncodingConversionException
*/
class EncodingConversionException extends SpoutException
{
}

View File

@ -4,8 +4,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class IOException * Class IOException
*
* @package Box\Spout\Common\Exception
*/ */
class IOException extends SpoutException class IOException extends SpoutException
{ {

View File

@ -4,8 +4,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class InvalidArgumentException * Class InvalidArgumentException
*
* @package Box\Spout\Common\Exception
*/ */
class InvalidArgumentException extends SpoutException class InvalidArgumentException extends SpoutException
{ {

View File

@ -0,0 +1,10 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class InvalidColorException
*/
class InvalidColorException extends SpoutException
{
}

View File

@ -5,7 +5,7 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class SpoutException * Class SpoutException
* *
* @package Box\Spout\Common\Exception * @abstract
*/ */
abstract class SpoutException extends \Exception abstract class SpoutException extends \Exception
{ {

View File

@ -4,8 +4,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class UnsupportedTypeException * Class UnsupportedTypeException
*
* @package Box\Spout\Common\Exception
*/ */
class UnsupportedTypeException extends SpoutException class UnsupportedTypeException extends SpoutException
{ {

View File

@ -0,0 +1,68 @@
<?php
namespace Box\Spout\Common\Helper;
/**
* Class CellTypeHelper
* This class provides helper functions to determine the type of the cell value
*/
class CellTypeHelper
{
/**
* @param mixed|null $value
* @return bool Whether the given value is considered "empty"
*/
public static function isEmpty($value)
{
return ($value === null || $value === '');
}
/**
* @param mixed $value
* @return bool Whether the given value is a non empty string
*/
public static function isNonEmptyString($value)
{
return (\gettype($value) === 'string' && $value !== '');
}
/**
* Returns whether the given value is numeric.
* A numeric value is from type "integer" or "double" ("float" is not returned by gettype).
*
* @param mixed $value
* @return bool Whether the given value is numeric
*/
public static function isNumeric($value)
{
$valueType = \gettype($value);
return ($valueType === 'integer' || $valueType === 'double');
}
/**
* Returns whether the given value is boolean.
* "true"/"false" and 0/1 are not booleans.
*
* @param mixed $value
* @return bool Whether the given value is boolean
*/
public static function isBoolean($value)
{
return \gettype($value) === 'boolean';
}
/**
* Returns whether the given value is a DateTime or DateInterval object.
*
* @param mixed $value
* @return bool Whether the given value is a DateTime or DateInterval object
*/
public static function isDateTimeOrDateInterval($value)
{
return (
$value instanceof \DateTimeInterface ||
$value instanceof \DateInterval
);
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace Box\Spout\Common\Helper;
use Box\Spout\Common\Exception\EncodingConversionException;
/**
* Class EncodingHelper
* This class provides helper functions to work with encodings.
*/
class EncodingHelper
{
/** Definition of the encodings that can have a BOM */
public const ENCODING_UTF8 = 'UTF-8';
public const ENCODING_UTF16_LE = 'UTF-16LE';
public const ENCODING_UTF16_BE = 'UTF-16BE';
public const ENCODING_UTF32_LE = 'UTF-32LE';
public const ENCODING_UTF32_BE = 'UTF-32BE';
/** Definition of the BOMs for the different encodings */
public const BOM_UTF8 = "\xEF\xBB\xBF";
public const BOM_UTF16_LE = "\xFF\xFE";
public const BOM_UTF16_BE = "\xFE\xFF";
public const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
public const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */
protected $supportedEncodingsWithBom;
/**
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($globalFunctionsHelper)
{
$this->globalFunctionsHelper = $globalFunctionsHelper;
$this->supportedEncodingsWithBom = [
self::ENCODING_UTF8 => self::BOM_UTF8,
self::ENCODING_UTF16_LE => self::BOM_UTF16_LE,
self::ENCODING_UTF16_BE => self::BOM_UTF16_BE,
self::ENCODING_UTF32_LE => self::BOM_UTF32_LE,
self::ENCODING_UTF32_BE => self::BOM_UTF32_BE,
];
}
/**
* Returns the number of bytes to use as offset in order to skip the BOM.
*
* @param resource $filePointer Pointer to the file to check
* @param string $encoding Encoding of the file to check
* @return int Bytes offset to apply to skip the BOM (0 means no BOM)
*/
public function getBytesOffsetToSkipBOM($filePointer, $encoding)
{
$byteOffsetToSkipBom = 0;
if ($this->hasBOM($filePointer, $encoding)) {
$bomUsed = $this->supportedEncodingsWithBom[$encoding];
// we skip the N first bytes
$byteOffsetToSkipBom = \strlen($bomUsed);
}
return $byteOffsetToSkipBom;
}
/**
* Returns whether the file identified by the given pointer has a BOM.
*
* @param resource $filePointer Pointer to the file to check
* @param string $encoding Encoding of the file to check
* @return bool TRUE if the file has a BOM, FALSE otherwise
*/
protected function hasBOM($filePointer, $encoding)
{
$hasBOM = false;
$this->globalFunctionsHelper->rewind($filePointer);
if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
$potentialBom = $this->supportedEncodingsWithBom[$encoding];
$numBytesInBom = \strlen($potentialBom);
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
}
return $hasBOM;
}
/**
* Attempts to convert a non UTF-8 string into UTF-8.
*
* @param string $string Non UTF-8 string to be converted
* @param string $sourceEncoding The encoding used to encode the source string
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
* @return string The converted, UTF-8 string
*/
public function attemptConversionToUTF8($string, $sourceEncoding)
{
return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8);
}
/**
* Attempts to convert a UTF-8 string into the given encoding.
*
* @param string $string UTF-8 string to be converted
* @param string $targetEncoding The encoding the string should be re-encoded into
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
* @return string The converted string, encoded with the given encoding
*/
public function attemptConversionFromUTF8($string, $targetEncoding)
{
return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding);
}
/**
* Attempts to convert the given string to the given encoding.
* Depending on what is installed on the server, we will try to iconv or mbstring.
*
* @param string $string string to be converted
* @param string $sourceEncoding The encoding used to encode the source string
* @param string $targetEncoding The encoding the string should be re-encoded into
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
* @return string The converted string, encoded with the given encoding
*/
protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
{
// if source and target encodings are the same, it's a no-op
if ($sourceEncoding === $targetEncoding) {
return $string;
}
$convertedString = null;
if ($this->canUseIconv()) {
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
} elseif ($this->canUseMbString()) {
$convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
} else {
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\".");
}
if ($convertedString === false) {
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed.");
}
return $convertedString;
}
/**
* Returns whether "iconv" can be used.
*
* @return bool TRUE if "iconv" is available and can be used, FALSE otherwise
*/
protected function canUseIconv()
{
return $this->globalFunctionsHelper->function_exists('iconv');
}
/**
* Returns whether "mb_string" functions can be used.
* These functions come with the PHP Intl package.
*
* @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise
*/
protected function canUseMbString()
{
return $this->globalFunctionsHelper->function_exists('mb_convert_encoding');
}
}

View File

@ -1,18 +1,18 @@
<?php <?php
namespace Box\Spout\Common\Escaper; namespace Box\Spout\Common\Helper\Escaper;
/** /**
* Class CSV * Class CSV
* Provides functions to escape and unescape data for CSV files * Provides functions to escape and unescape data for CSV files
*
* @package Box\Spout\Common\Escaper
*/ */
class CSV implements EscaperInterface class CSV implements EscaperInterface
{ {
/** /**
* Escapes the given string to make it compatible with CSV * Escapes the given string to make it compatible with CSV
* *
* @codeCoverageIgnore
*
* @param string $string The string to escape * @param string $string The string to escape
* @return string The escaped string * @return string The escaped string
*/ */
@ -24,6 +24,8 @@ class CSV implements EscaperInterface
/** /**
* Unescapes the given string to make it compatible with CSV * Unescapes the given string to make it compatible with CSV
* *
* @codeCoverageIgnore
*
* @param string $string The string to unescape * @param string $string The string to unescape
* @return string The unescaped string * @return string The unescaped string
*/ */

View File

@ -1,11 +1,9 @@
<?php <?php
namespace Box\Spout\Common\Escaper; namespace Box\Spout\Common\Helper\Escaper;
/** /**
* Interface EscaperInterface * Interface EscaperInterface
*
* @package Box\Spout\Common\Escaper
*/ */
interface EscaperInterface interface EscaperInterface
{ {

View File

@ -0,0 +1,59 @@
<?php
namespace Box\Spout\Common\Helper\Escaper;
/**
* Class ODS
* Provides functions to escape and unescape data for ODS files
*/
class ODS implements EscaperInterface
{
/**
* Escapes the given string to make it compatible with XLSX
*
* @param string $string The string to escape
* @return string The escaped string
*/
public function escape($string)
{
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
// single/double quotes (for XML attributes) need to be encoded.
if (\defined('ENT_DISALLOWED')) {
// 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced.
// Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor
// @link https://github.com/box/spout/issues/329
$replacedString = \htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
} else {
// We are on hhvm or any other engine that does not support ENT_DISALLOWED.
$escapedString = \htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
// control characters values are from 0 to 1F (hex values) in the ASCII table
// some characters should not be escaped though: "\t", "\r" and "\n".
$regexPattern = '[\x00-\x08' .
// skipping "\t" (0x9) and "\n" (0xA)
'\x0B-\x0C' .
// skipping "\r" (0xD)
'\x0E-\x1F]';
$replacedString = \preg_replace("/$regexPattern/", '<27>', $escapedString);
}
return $replacedString;
}
/**
* Unescapes the given string to make it compatible with XLSX
*
* @param string $string The string to unescape
* @return string The unescaped string
*/
public function unescape($string)
{
// ==============
// = WARNING =
// ==============
// It is assumed that the given string has already had its XML entities decoded.
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
// Therefore there is no need to call "htmlspecialchars_decode()".
return $string;
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace Box\Spout\Common\Helper\Escaper;
/**
* Class XLSX
* Provides functions to escape and unescape data for XLSX files
*/
class XLSX implements EscaperInterface
{
/** @var bool Whether the escaper has already been initialized */
private $isAlreadyInitialized = false;
/** @var string Regex pattern to detect control characters that need to be escaped */
private $escapableControlCharactersPattern;
/** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
private $controlCharactersEscapingMap;
/** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
private $controlCharactersEscapingReverseMap;
/**
* Initializes the control characters if not already done
*/
protected function initIfNeeded()
{
if (!$this->isAlreadyInitialized) {
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
$this->controlCharactersEscapingReverseMap = \array_flip($this->controlCharactersEscapingMap);
$this->isAlreadyInitialized = true;
}
}
/**
* Escapes the given string to make it compatible with XLSX
*
* @param string $string The string to escape
* @return string The escaped string
*/
public function escape($string)
{
$this->initIfNeeded();
$escapedString = $this->escapeControlCharacters($string);
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
// single/double quotes (for XML attributes) need to be encoded.
$escapedString = \htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8');
return $escapedString;
}
/**
* Unescapes the given string to make it compatible with XLSX
*
* @param string $string The string to unescape
* @return string The unescaped string
*/
public function unescape($string)
{
$this->initIfNeeded();
// ==============
// = WARNING =
// ==============
// It is assumed that the given string has already had its XML entities decoded.
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
// Therefore there is no need to call "htmlspecialchars_decode()".
$unescapedString = $this->unescapeControlCharacters($string);
return $unescapedString;
}
/**
* @return string Regex pattern containing all escapable control characters
*/
protected function getEscapableControlCharactersPattern()
{
// control characters values are from 0 to 1F (hex values) in the ASCII table
// some characters should not be escaped though: "\t", "\r" and "\n".
return '[\x00-\x08' .
// skipping "\t" (0x9) and "\n" (0xA)
'\x0B-\x0C' .
// skipping "\r" (0xD)
'\x0E-\x1F]';
}
/**
* Builds the map containing control characters to be escaped
* mapped to their escaped values.
* "\t", "\r" and "\n" don't need to be escaped.
*
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @return string[]
*/
protected function getControlCharactersEscapingMap()
{
$controlCharactersEscapingMap = [];
// control characters values are from 0 to 1F (hex values) in the ASCII table
for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) {
$character = \chr($charValue);
if (\preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
$charHexValue = \dechex($charValue);
$escapedChar = '_x' . \sprintf('%04s', \strtoupper($charHexValue)) . '_';
$controlCharactersEscapingMap[$escapedChar] = $character;
}
}
return $controlCharactersEscapingMap;
}
/**
* Converts PHP control characters from the given string to OpenXML escaped control characters
*
* Excel escapes control characters with _xHHHH_ and also escapes any
* literal strings of that type by encoding the leading underscore.
* So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
*
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to escape
* @return string
*/
protected function escapeControlCharacters($string)
{
$escapedString = $this->escapeEscapeCharacter($string);
// if no control characters
if (!\preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
return $escapedString;
}
return \preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function ($matches) {
return $this->controlCharactersEscapingReverseMap[$matches[0]];
}, $escapedString);
}
/**
* Escapes the escape character: "_x0000_" -> "_x005F_x0000_"
*
* @param string $string String to escape
* @return string The escaped string
*/
protected function escapeEscapeCharacter($string)
{
return \preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
}
/**
* Converts OpenXML escaped control characters from the given string to PHP control characters
*
* Excel escapes control characters with _xHHHH_ and also escapes any
* literal strings of that type by encoding the leading underscore.
* So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
*
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to unescape
* @return string
*/
protected function unescapeControlCharacters($string)
{
$unescapedString = $string;
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
// only unescape characters that don't contain the escaped escape character for now
$unescapedString = \preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
}
return $this->unescapeEscapeCharacter($unescapedString);
}
/**
* Unecapes the escape character: "_x005F_x0000_" => "_x0000_"
*
* @param string $string String to unescape
* @return string The unescaped string
*/
protected function unescapeEscapeCharacter($string)
{
return \preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
}
}

View File

@ -8,20 +8,18 @@ use Box\Spout\Common\Exception\IOException;
* Class FileSystemHelper * Class FileSystemHelper
* This class provides helper functions to help with the file system operations * This class provides helper functions to help with the file system operations
* like files/folders creation & deletion * like files/folders creation & deletion
*
* @package Box\Spout\Common\Helper
*/ */
class FileSystemHelper class FileSystemHelper implements FileSystemHelperInterface
{ {
/** @var string Path of the base folder where all the I/O can occur */ /** @var string Real path of the base folder where all the I/O can occur */
protected $baseFolderPath; protected $baseFolderRealPath;
/** /**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur * @param string $baseFolderPath The path of the base folder where all the I/O can occur
*/ */
public function __construct($baseFolderPath) public function __construct(string $baseFolderPath)
{ {
$this->baseFolderPath = $baseFolderPath; $this->baseFolderRealPath = \realpath($baseFolderPath);
} }
/** /**
@ -29,18 +27,18 @@ class FileSystemHelper
* *
* @param string $parentFolderPath The parent folder path under which the folder is going to be created * @param string $parentFolderPath The parent folder path under which the folder is going to be created
* @param string $folderName The name of the folder to create * @param string $folderName The name of the folder to create
* @return string Path of the created folder
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
* @return string Path of the created folder
*/ */
public function createFolder($parentFolderPath, $folderName) public function createFolder($parentFolderPath, $folderName)
{ {
$this->throwIfOperationNotInBaseFolder($parentFolderPath); $this->throwIfOperationNotInBaseFolder($parentFolderPath);
$folderPath = $parentFolderPath . DIRECTORY_SEPARATOR . $folderName; $folderPath = $parentFolderPath . '/' . $folderName;
$wasCreationSuccessful = mkdir($folderPath, 0777, true); $wasCreationSuccessful = \mkdir($folderPath, 0777, true);
if (!$wasCreationSuccessful) { if (!$wasCreationSuccessful) {
throw new IOException('Unable to create folder: ' . $folderPath); throw new IOException("Unable to create folder: $folderPath");
} }
return $folderPath; return $folderPath;
@ -53,18 +51,18 @@ class FileSystemHelper
* @param string $parentFolderPath The parent folder path where the file is going to be created * @param string $parentFolderPath The parent folder path where the file is going to be created
* @param string $fileName The name of the file to create * @param string $fileName The name of the file to create
* @param string $fileContents The contents of the file to create * @param string $fileContents The contents of the file to create
* @return string Path of the created file
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
* @return string Path of the created file
*/ */
public function createFileWithContents($parentFolderPath, $fileName, $fileContents) public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
{ {
$this->throwIfOperationNotInBaseFolder($parentFolderPath); $this->throwIfOperationNotInBaseFolder($parentFolderPath);
$filePath = $parentFolderPath . DIRECTORY_SEPARATOR . $fileName; $filePath = $parentFolderPath . '/' . $fileName;
$wasCreationSuccessful = file_put_contents($filePath, $fileContents); $wasCreationSuccessful = \file_put_contents($filePath, $fileContents);
if (!$wasCreationSuccessful) { if ($wasCreationSuccessful === false) {
throw new IOException('Unable to create file: ' . $filePath); throw new IOException("Unable to create file: $filePath");
} }
return $filePath; return $filePath;
@ -74,15 +72,15 @@ class FileSystemHelper
* Delete the file at the given path * Delete the file at the given path
* *
* @param string $filePath Path of the file to delete * @param string $filePath Path of the file to delete
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
* @return void
*/ */
public function deleteFile($filePath) public function deleteFile($filePath)
{ {
$this->throwIfOperationNotInBaseFolder($filePath); $this->throwIfOperationNotInBaseFolder($filePath);
if (file_exists($filePath) && is_file($filePath)) { if (\file_exists($filePath) && \is_file($filePath)) {
unlink($filePath); \unlink($filePath);
} }
} }
@ -90,8 +88,8 @@ class FileSystemHelper
* Delete the folder at the given path as well as all its contents * Delete the folder at the given path as well as all its contents
* *
* @param string $folderPath Path of the folder to delete * @param string $folderPath Path of the folder to delete
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
* @return void
*/ */
public function deleteFolderRecursively($folderPath) public function deleteFolderRecursively($folderPath)
{ {
@ -104,13 +102,13 @@ class FileSystemHelper
foreach ($itemIterator as $item) { foreach ($itemIterator as $item) {
if ($item->isDir()) { if ($item->isDir()) {
rmdir($item->getPathname()); \rmdir($item->getPathname());
} else { } else {
unlink($item->getPathname()); \unlink($item->getPathname());
} }
} }
rmdir($folderPath); \rmdir($folderPath);
} }
/** /**
@ -119,14 +117,19 @@ class FileSystemHelper
* should occur is not inside the base folder. * should occur is not inside the base folder.
* *
* @param string $operationFolderPath The path of the folder where the I/O operation should occur * @param string $operationFolderPath The path of the folder where the I/O operation should occur
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur
* is not inside the base folder or the base folder does not exist
* @return void * @return void
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder
*/ */
protected function throwIfOperationNotInBaseFolder($operationFolderPath) protected function throwIfOperationNotInBaseFolder(string $operationFolderPath)
{ {
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0); $operationFolderRealPath = \realpath($operationFolderPath);
if (!$this->baseFolderRealPath) {
throw new IOException("The base folder path is invalid: {$this->baseFolderRealPath}");
}
$isInBaseFolder = (\strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
if (!$isInBaseFolder) { if (!$isInBaseFolder) {
throw new IOException('Cannot perform I/O operation outside of the base folder: ' . $this->baseFolderPath); throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
} }
} }
} }

View File

@ -0,0 +1,51 @@
<?php
namespace Box\Spout\Common\Helper;
/**
* Class FileSystemHelperInterface
* This interface describes helper functions to help with the file system operations
* like files/folders creation & deletion
*/
interface FileSystemHelperInterface
{
/**
* Creates an empty folder with the given name under the given parent folder.
*
* @param string $parentFolderPath The parent folder path under which the folder is going to be created
* @param string $folderName The name of the folder to create
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
* @return string Path of the created folder
*/
public function createFolder($parentFolderPath, $folderName);
/**
* Creates a file with the given name and content in the given folder.
* The parent folder must exist.
*
* @param string $parentFolderPath The parent folder path where the file is going to be created
* @param string $fileName The name of the file to create
* @param string $fileContents The contents of the file to create
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
* @return string Path of the created file
*/
public function createFileWithContents($parentFolderPath, $fileName, $fileContents);
/**
* Delete the file at the given path
*
* @param string $filePath Path of the file to delete
* @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
* @return void
*/
public function deleteFile($filePath);
/**
* Delete the folder at the given path as well as all its contents
*
* @param string $folderPath Path of the folder to delete
* @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
* @return void
*/
public function deleteFolderRecursively($folderPath);
}

View File

@ -6,7 +6,7 @@ namespace Box\Spout\Common\Helper;
* Class GlobalFunctionsHelper * Class GlobalFunctionsHelper
* This class wraps global functions to facilitate testing * This class wraps global functions to facilitate testing
* *
* @package Box\Spout\Common\Helper * @codeCoverageIgnore
*/ */
class GlobalFunctionsHelper class GlobalFunctionsHelper
{ {
@ -20,7 +20,7 @@ class GlobalFunctionsHelper
*/ */
public function fopen($fileName, $mode) public function fopen($fileName, $mode)
{ {
return fopen($fileName, $mode); return \fopen($fileName, $mode);
} }
/** /**
@ -28,12 +28,12 @@ class GlobalFunctionsHelper
* @see fgets() * @see fgets()
* *
* @param resource $handle * @param resource $handle
* @param int|void $length * @param int|null $length
* @return string * @return string
*/ */
public function fgets($handle, $length = null) public function fgets($handle, $length = null)
{ {
return fgets($handle, $length); return \fgets($handle, $length);
} }
/** /**
@ -46,7 +46,7 @@ class GlobalFunctionsHelper
*/ */
public function fputs($handle, $string) public function fputs($handle, $string)
{ {
return fputs($handle, $string); return \fputs($handle, $string);
} }
/** /**
@ -58,7 +58,7 @@ class GlobalFunctionsHelper
*/ */
public function fflush($handle) public function fflush($handle)
{ {
return fflush($handle); return \fflush($handle);
} }
/** /**
@ -71,7 +71,7 @@ class GlobalFunctionsHelper
*/ */
public function fseek($handle, $offset) public function fseek($handle, $offset)
{ {
return fseek($handle, $offset); return \fseek($handle, $offset);
} }
/** /**
@ -79,14 +79,54 @@ class GlobalFunctionsHelper
* @see fgetcsv() * @see fgetcsv()
* *
* @param resource $handle * @param resource $handle
* @param int|void $length * @param int|null $length
* @param string|void $delimiter * @param string|null $delimiter
* @param string|void $enclosure * @param string|null $enclosure
* @return array * @return array|false
*/ */
public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null) public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null)
{ {
return fgetcsv($handle, $length, $delimiter, $enclosure); // PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
// To fix that, simply disable the escape character.
// @see https://bugs.php.net/bug.php?id=43225
// @see http://tools.ietf.org/html/rfc4180
$escapeCharacter = PHP_VERSION_ID >= 70400 ? '' : "\0";
return \fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter);
}
/**
* Wrapper around global function fputcsv()
* @see fputcsv()
*
* @param resource $handle
* @param array $fields
* @param string|null $delimiter
* @param string|null $enclosure
* @return int|false
*/
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
{
// PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
// To fix that, simply disable the escape character.
// @see https://bugs.php.net/bug.php?id=43225
// @see http://tools.ietf.org/html/rfc4180
$escapeCharacter = PHP_VERSION_ID >= 70400 ? '' : "\0";
return \fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter);
}
/**
* Wrapper around global function fwrite()
* @see fwrite()
*
* @param resource $handle
* @param string $string
* @return int
*/
public function fwrite($handle, $string)
{
return \fwrite($handle, $string);
} }
/** /**
@ -98,7 +138,7 @@ class GlobalFunctionsHelper
*/ */
public function fclose($handle) public function fclose($handle)
{ {
return fclose($handle); return \fclose($handle);
} }
/** /**
@ -110,31 +150,92 @@ class GlobalFunctionsHelper
*/ */
public function rewind($handle) public function rewind($handle)
{ {
return rewind($handle); return \rewind($handle);
} }
/** /**
* Wrapper around global function file_exists() * Wrapper around global function file_exists()
* @see file_exists() * @see file_exists()
* *
* @param string $filename * @param string $fileName
* @return bool * @return bool
*/ */
public function file_exists($fileName) public function file_exists($fileName)
{ {
return file_exists($fileName); return \file_exists($fileName);
}
/**
* Wrapper around global function file_get_contents()
* @see file_get_contents()
*
* @param string $filePath
* @return string
*/
public function file_get_contents($filePath)
{
$realFilePath = $this->convertToUseRealPath($filePath);
return \file_get_contents($realFilePath);
}
/**
* Updates the given file path to use a real path.
* This is to avoid issues on some Windows setup.
*
* @param string $filePath File path
* @return string The file path using a real path
*/
protected function convertToUseRealPath($filePath)
{
$realFilePath = $filePath;
if ($this->isZipStream($filePath)) {
if (\preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
$documentPath = $matches[1];
$documentInsideZipPath = $matches[2];
$realFilePath = 'zip://' . \realpath($documentPath) . '#' . $documentInsideZipPath;
}
} else {
$realFilePath = \realpath($filePath);
}
return $realFilePath;
}
/**
* Returns whether the given path is a zip stream.
*
* @param string $path Path pointing to a document
* @return bool TRUE if path is a zip stream, FALSE otherwise
*/
protected function isZipStream($path)
{
return (\strpos($path, 'zip://') === 0);
}
/**
* Wrapper around global function feof()
* @see feof()
*
* @param resource $handle
* @return bool
*/
public function feof($handle)
{
return \feof($handle);
} }
/** /**
* Wrapper around global function is_readable() * Wrapper around global function is_readable()
* @see is_readable() * @see is_readable()
* *
* @param string $filename * @param string $fileName
* @return bool * @return bool
*/ */
public function is_readable($fileName) public function is_readable($fileName)
{ {
return is_readable($fileName); return \is_readable($fileName);
} }
/** /**
@ -142,11 +243,12 @@ class GlobalFunctionsHelper
* @see basename() * @see basename()
* *
* @param string $path * @param string $path
* @param string $suffix
* @return string * @return string
*/ */
public function basename($path) public function basename($path, $suffix = '')
{ {
return basename($path); return \basename($path, $suffix);
} }
/** /**
@ -158,6 +260,70 @@ class GlobalFunctionsHelper
*/ */
public function header($string) public function header($string)
{ {
header($string); \header($string);
}
/**
* Wrapper around global function ob_end_clean()
* @see ob_end_clean()
*
* @return void
*/
public function ob_end_clean()
{
if (\ob_get_length() > 0) {
\ob_end_clean();
}
}
/**
* Wrapper around global function iconv()
* @see iconv()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
* @return string|bool the converted string or FALSE on failure.
*/
public function iconv($string, $sourceEncoding, $targetEncoding)
{
return \iconv($sourceEncoding, $targetEncoding, $string);
}
/**
* Wrapper around global function mb_convert_encoding()
* @see mb_convert_encoding()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
* @return string|bool the converted string or FALSE on failure.
*/
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
{
return \mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
}
/**
* Wrapper around global function stream_get_wrappers()
* @see stream_get_wrappers()
*
* @return array
*/
public function stream_get_wrappers()
{
return \stream_get_wrappers();
}
/**
* Wrapper around global function function_exists()
* @see function_exists()
*
* @param string $functionName
* @return bool
*/
public function function_exists($functionName)
{
return \function_exists($functionName);
} }
} }

View File

@ -0,0 +1,105 @@
<?php
namespace Box\Spout\Common\Helper;
/**
* Class StringHelper
* This class provides helper functions to work with strings and multibyte strings.
*
* @codeCoverageIgnore
*/
class StringHelper
{
/** @var bool Whether the mbstring extension is loaded */
protected $hasMbstringSupport;
/** @var bool Whether the code is running with PHP7 or older versions */
private $isRunningPhp7OrOlder;
/** @var array Locale info, used for number formatting */
private $localeInfo;
/**
*
*/
public function __construct()
{
$this->hasMbstringSupport = \extension_loaded('mbstring');
$this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0;
$this->localeInfo = \localeconv();
}
/**
* Returns the length of the given string.
* It uses the multi-bytes function is available.
* @see strlen
* @see mb_strlen
*
* @param string $string
* @return int
*/
public function getStringLength($string)
{
return $this->hasMbstringSupport ? \mb_strlen($string) : \strlen($string);
}
/**
* Returns the position of the first occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
* @see strpos
* @see mb_strpos
*
* @param string $char Needle
* @param string $string Haystack
* @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharFirstOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? \mb_strpos($string, $char) : \strpos($string, $char);
return ($position !== false) ? $position : -1;
}
/**
* Returns the position of the last occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
* @see strrpos
* @see mb_strrpos
*
* @param string $char Needle
* @param string $string Haystack
* @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharLastOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? \mb_strrpos($string, $char) : \strrpos($string, $char);
return ($position !== false) ? $position : -1;
}
/**
* Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format.
*
* Formatting of float values is locale dependent in PHP < 8.
* Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34).
* However, float values must be formatted with no thousands separator and a "." as decimal point
* to work properly. This method can be used to convert the value to the correct format before storing it.
*
* @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8.
*
* @param int|float $numericValue
* @return int|float|string
*/
public function formatNumericValue($numericValue)
{
if ($this->isRunningPhp7OrOlder && is_float($numericValue)) {
return str_replace(
[$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
['', '.'],
$numericValue
);
}
return $numericValue;
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Box\Spout\Common\Manager;
/**
* Class OptionsManager
*/
abstract class OptionsManagerAbstract implements OptionsManagerInterface
{
public const PREFIX_OPTION = 'OPTION_';
/** @var string[] List of all supported option names */
private $supportedOptions = [];
/** @var array Associative array [OPTION_NAME => OPTION_VALUE] */
private $options = [];
/**
* OptionsManagerAbstract constructor.
*/
public function __construct()
{
$this->supportedOptions = $this->getSupportedOptions();
$this->setDefaultOptions();
}
/**
* @return array List of supported options
*/
abstract protected function getSupportedOptions();
/**
* Sets the default options.
* To be overriden by child classes
*
* @return void
*/
abstract protected function setDefaultOptions();
/**
* Sets the given option, if this option is supported.
*
* @param string $optionName
* @param mixed $optionValue
* @return void
*/
public function setOption($optionName, $optionValue)
{
if (\in_array($optionName, $this->supportedOptions)) {
$this->options[$optionName] = $optionValue;
}
}
/**
* @param string $optionName
* @return mixed|null The set option or NULL if no option with given name found
*/
public function getOption($optionName)
{
$optionValue = null;
if (isset($this->options[$optionName])) {
$optionValue = $this->options[$optionName];
}
return $optionValue;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Box\Spout\Common\Manager;
/**
* Interface OptionsManagerInterface
*/
interface OptionsManagerInterface
{
/**
* @param string $optionName
* @param mixed $optionValue
* @return void
*/
public function setOption($optionName, $optionValue);
/**
* @param string $optionName
* @return mixed|null The set option or NULL if no option with given name found
*/
public function getOption($optionName);
}

View File

@ -8,6 +8,7 @@ namespace Box\Spout\Common;
*/ */
abstract class Type abstract class Type
{ {
const CSV = "csv"; public const CSV = 'csv';
const XLSX = "xlsx"; public const XLSX = 'xlsx';
public const ODS = 'ods';
} }

View File

@ -1,196 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
use Box\Spout\Reader\Exception\EndOfFileReachedException;
/**
* Class AbstractReader
*
* @package Box\Spout\Reader
* @abstract
*/
abstract class AbstractReader implements ReaderInterface
{
/** @var int Used to keep track of the row number */
protected $currentRowIndex = 0;
/** @var bool Indicates whether the stream is currently open */
protected $isStreamOpened = false;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var array Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/**
* Opens the file at the given file path to make it ready to be read
*
* @param string $filePath Path of the file to be read
* @return void
*/
abstract protected function openReader($filePath);
/**
* Reads and returns next row if available.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
*/
abstract protected function read();
/**
* Closes the reader. To be used after reading the file.
*
* @return AbstractReader
*/
abstract protected function closeReader();
/**
* @param $globalFunctionsHelper
* @return AbstractReader
*/
public function setGlobalFunctionsHelper($globalFunctionsHelper)
{
$this->globalFunctionsHelper = $globalFunctionsHelper;
return $this;
}
/**
* Prepares the reader to read the given file. It also makes sure
* that the file exists and is readable.
*
* @param string $filePath Path of the file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
*/
public function open($filePath)
{
if (!$this->isPhpStream($filePath)) {
// we skip the checks if the provided file path points to a PHP stream
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
throw new IOException('Could not open ' . $filePath . ' for reading! File does not exist.');
} else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
throw new IOException('Could not open ' . $filePath . ' for reading! File is not readable.');
}
}
$this->currentRowIndex = 0;
$this->hasReachedEndOfFile = false;
try {
$this->openReader($filePath);
$this->isStreamOpened = true;
} catch (\Exception $exception) {
throw new IOException('Could not open ' . $filePath . ' for reading! (' . $exception->getMessage() . ')');
}
}
/**
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
*
* @param string $filePath Path of the file to be read
* @return bool Whether the given path maps to a PHP stream
*/
protected function isPhpStream($filePath)
{
return (strpos($filePath, 'php://') === 0);
}
/**
* Returns whether all rows have been read (i.e. if we are at the end of the file).
* To know if the end of file has been reached, it uses a buffer. If the buffer is
* empty (meaning, nothing has been read or previous read line has been consumed), then
* it reads the next line, store it in the buffer for the next time or flip a variable if
* the end of file has been reached.
*
* @return bool Whether all rows have been read (i.e. if we are at the end of the file)
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If the stream was not opened first
*/
public function hasNextRow()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Stream should be opened first.');
}
if ($this->hasReachedEndOfFile) {
return false;
}
// if the buffer contains unprocessed row
if (!$this->isRowDataBufferEmpty()) {
return true;
}
// otherwise, try to read the next line line, and store it in the buffer
$this->rowDataBuffer = $this->read();
// if the buffer is still empty after reading a row, it means end of file was reached
$this->hasReachedEndOfFile = $this->isRowDataBufferEmpty();
return (!$this->hasReachedEndOfFile);
}
/**
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
* actually reading the file.
*
* @return array Array that contains the data for the read row
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
*/
public function nextRow()
{
if (!$this->hasNextRow()) {
throw new EndOfFileReachedException('End of file was reached. Cannot read more rows.');
}
// Get data from buffer (if the buffer was empty, it was filled by the call to hasNextRow())
$rowData = $this->rowDataBuffer;
// empty buffer to mark the row as consumed
$this->emptyRowDataBuffer();
$this->currentRowIndex++;
return $rowData;
}
/**
* Returns whether the buffer where the row data is stored is empty
*
* @return bool
*/
protected function isRowDataBufferEmpty()
{
return ($this->rowDataBuffer === null);
}
/**
* Empty the buffer that stores row data
*
* @return void
*/
protected function emptyRowDataBuffer()
{
$this->rowDataBuffer = null;
}
/**
* Closes the reader, preventing any additional reading
*
* @return void
*/
public function close()
{
if ($this->isStreamOpened) {
$this->closeReader();
$this->isStreamOpened = false;
}
}
}

View File

@ -1,130 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
/**
* Class CSV
* This class provides support to read data from a CSV file.
*
* @package Box\Spout\Reader
*/
class CSV extends AbstractReader
{
const UTF8_BOM = "\xEF\xBB\xBF";
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @var string Defines the character used to delimit fields (one character only) */
protected $fieldDelimiter = ',';
/** @var string Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure = '"';
/**
* Sets the field delimiter for the CSV
*
* @param string $fieldDelimiter Character that delimits fields
* @return CSV
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
return $this;
}
/**
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return CSV
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
return $this;
}
/**
* Opens the file at the given path to make it ready to be read.
* The file must be UTF-8 encoded.
* @TODO add encoding detection/conversion
*
* @param string $filePath Path of the XLSX file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException
*/
protected function openReader($filePath)
{
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException('Could not open file ' . $filePath . ' for reading.');
}
$this->skipUtf8Bom();
}
/**
* This skips the UTF-8 BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*
* @return void
*/
protected function skipUtf8Bom()
{
$this->globalFunctionsHelper->rewind($this->filePointer);
$hasUtf8Bom = ($this->globalFunctionsHelper->fgets($this->filePointer, 4) === self::UTF8_BOM);
if ($hasUtf8Bom) {
// we skip the 2 first bytes (so start from the 3rd byte)
$this->globalFunctionsHelper->fseek($this->filePointer, 3);
} else {
// if no BOM, reset the pointer to read from the beginning
$this->globalFunctionsHelper->fseek($this->filePointer, 0);
}
}
/**
* Reads and returns next row if available.
* Empty rows are skipped.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
*/
protected function read()
{
$lineData = null;
if ($this->filePointer) {
do {
$lineData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, 0, $this->fieldDelimiter, $this->fieldEnclosure);
} while ($lineData && $this->isEmptyLine($lineData));
}
// When reaching the end of the file, return null instead of false
return ($lineData !== false) ? $lineData : null;
}
/**
* @param array $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (count($lineData) === 1 && $lineData[0] === null);
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Box\Spout\Reader\CSV\Creator;
use Box\Spout\Common\Creator\HelperFactory;
use Box\Spout\Common\Entity\Cell;
use Box\Spout\Common\Entity\Row;
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
use Box\Spout\Common\Manager\OptionsManagerInterface;
use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
use Box\Spout\Reader\CSV\RowIterator;
use Box\Spout\Reader\CSV\Sheet;
use Box\Spout\Reader\CSV\SheetIterator;
/**
* Class EntityFactory
* Factory to create entities
*/
class InternalEntityFactory implements InternalEntityFactoryInterface
{
/** @var HelperFactory */
private $helperFactory;
/**
* @param HelperFactory $helperFactory
*/
public function __construct(HelperFactory $helperFactory)
{
$this->helperFactory = $helperFactory;
}
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param OptionsManagerInterface $optionsManager
* @param GlobalFunctionsHelper $globalFunctionsHelper
* @return SheetIterator
*/
public function createSheetIterator($filePointer, $optionsManager, $globalFunctionsHelper)
{
$rowIterator = $this->createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper);
$sheet = $this->createSheet($rowIterator);
return new SheetIterator($sheet);
}
/**
* @param RowIterator $rowIterator
* @return Sheet
*/
private function createSheet($rowIterator)
{
return new Sheet($rowIterator);
}
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param OptionsManagerInterface $optionsManager
* @param GlobalFunctionsHelper $globalFunctionsHelper
* @return RowIterator
*/
private function createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper)
{
$encodingHelper = $this->helperFactory->createEncodingHelper($globalFunctionsHelper);
return new RowIterator($filePointer, $optionsManager, $encodingHelper, $this, $globalFunctionsHelper);
}
/**
* @param Cell[] $cells
* @return Row
*/
public function createRow(array $cells = [])
{
return new Row($cells, null);
}
/**
* @param mixed $cellValue
* @return Cell
*/
public function createCell($cellValue)
{
return new Cell($cellValue);
}
/**
* @param array $cellValues
* @return Row
*/
public function createRowFromArray(array $cellValues = [])
{
$cells = \array_map(function ($cellValue) {
return $this->createCell($cellValue);
}, $cellValues);
return $this->createRow($cells);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Box\Spout\Reader\CSV\Manager;
use Box\Spout\Common\Helper\EncodingHelper;
use Box\Spout\Common\Manager\OptionsManagerAbstract;
use Box\Spout\Reader\Common\Entity\Options;
/**
* Class OptionsManager
* CSV Reader options manager
*/
class OptionsManager extends OptionsManagerAbstract
{
/**
* {@inheritdoc}
*/
protected function getSupportedOptions()
{
return [
Options::SHOULD_FORMAT_DATES,
Options::SHOULD_PRESERVE_EMPTY_ROWS,
Options::FIELD_DELIMITER,
Options::FIELD_ENCLOSURE,
Options::ENCODING,
];
}
/**
* {@inheritdoc}
*/
protected function setDefaultOptions()
{
$this->setOption(Options::SHOULD_FORMAT_DATES, false);
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
$this->setOption(Options::FIELD_DELIMITER, ',');
$this->setOption(Options::FIELD_ENCLOSURE, '"');
$this->setOption(Options::ENCODING, EncodingHelper::ENCODING_UTF8);
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
use Box\Spout\Common\Manager\OptionsManagerInterface;
use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
use Box\Spout\Reader\Common\Entity\Options;
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
use Box\Spout\Reader\ReaderAbstract;
/**
* Class Reader
* This class provides support to read data from a CSV file.
*/
class Reader extends ReaderAbstract
{
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @var SheetIterator To iterator over the CSV unique "sheet" */
protected $sheetIterator;
/** @var string Original value for the "auto_detect_line_endings" INI value */
protected $originalAutoDetectLineEndings;
/** @var bool Whether the code is running with PHP >= 8.1 */
private $isRunningAtLeastPhp81;
/**
* @param OptionsManagerInterface $optionsManager
* @param GlobalFunctionsHelper $globalFunctionsHelper
* @param InternalEntityFactoryInterface $entityFactory
*/
public function __construct(
OptionsManagerInterface $optionsManager,
GlobalFunctionsHelper $globalFunctionsHelper,
InternalEntityFactoryInterface $entityFactory
) {
parent::__construct($optionsManager, $globalFunctionsHelper, $entityFactory);
$this->isRunningAtLeastPhp81 = \version_compare(PHP_VERSION, '8.1.0') >= 0;
}
/**
* Sets the field delimiter for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldDelimiter Character that delimits fields
* @return Reader
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
return $this;
}
/**
* Sets the field enclosure for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldEnclosure Character that enclose fields
* @return Reader
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
return $this;
}
/**
* Sets the encoding of the CSV file to be read.
* Needs to be called before opening the reader.
*
* @param string $encoding Encoding of the CSV file to be read
* @return Reader
*/
public function setEncoding($encoding)
{
$this->optionsManager->setOption(Options::ENCODING, $encoding);
return $this;
}
/**
* Returns whether stream wrappers are supported
*
* @return bool
*/
protected function doesSupportStreamWrapper()
{
return true;
}
/**
* Opens the file at the given path to make it ready to be read.
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
*
* @param string $filePath Path of the CSV file to be read
* @throws \Box\Spout\Common\Exception\IOException
* @return void
*/
protected function openReader($filePath)
{
// "auto_detect_line_endings" is deprecated in PHP 8.1
if (!$this->isRunningAtLeastPhp81) {
$this->originalAutoDetectLineEndings = \ini_get('auto_detect_line_endings');
\ini_set('auto_detect_line_endings', '1');
}
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException("Could not open file $filePath for reading.");
}
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->sheetIterator = $entityFactory->createSheetIterator(
$this->filePointer,
$this->optionsManager,
$this->globalFunctionsHelper
);
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
protected function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if (is_resource($this->filePointer)) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
// "auto_detect_line_endings" is deprecated in PHP 8.1
if (!$this->isRunningAtLeastPhp81) {
\ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
}
}
}

View File

@ -0,0 +1,252 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Common\Entity\Row;
use Box\Spout\Common\Helper\EncodingHelper;
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
use Box\Spout\Common\Manager\OptionsManagerInterface;
use Box\Spout\Reader\Common\Entity\Options;
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
use Box\Spout\Reader\IteratorInterface;
/**
* Class RowIterator
* Iterate over CSV rows.
*/
class RowIterator implements IteratorInterface
{
/**
* Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines).
*/
public const MAX_READ_BYTES_PER_LINE = 0;
/** @var resource|null Pointer to the CSV file to read */
protected $filePointer;
/** @var int Number of read rows */
protected $numReadRows = 0;
/** @var Row|null Buffer used to store the current row, while checking if there are more rows to read */
protected $rowBuffer;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var string Defines the character used to delimit fields (one character only) */
protected $fieldDelimiter;
/** @var string Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure;
/** @var string Encoding of the CSV file to be read */
protected $encoding;
/** @var bool Whether empty rows should be returned or skipped */
protected $shouldPreserveEmptyRows;
/** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */
protected $encodingHelper;
/** @var \Box\Spout\Reader\CSV\Creator\InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param OptionsManagerInterface $optionsManager
* @param EncodingHelper $encodingHelper
* @param InternalEntityFactory $entityFactory
* @param GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct(
$filePointer,
OptionsManagerInterface $optionsManager,
EncodingHelper $encodingHelper,
InternalEntityFactory $entityFactory,
GlobalFunctionsHelper $globalFunctionsHelper
) {
$this->filePointer = $filePointer;
$this->fieldDelimiter = $optionsManager->getOption(Options::FIELD_DELIMITER);
$this->fieldEnclosure = $optionsManager->getOption(Options::FIELD_ENCLOSURE);
$this->encoding = $optionsManager->getOption(Options::ENCODING);
$this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
$this->encodingHelper = $encodingHelper;
$this->entityFactory = $entityFactory;
$this->globalFunctionsHelper = $globalFunctionsHelper;
}
/**
* Rewind the Iterator to the first element
* @see http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind() : void
{
$this->rewindAndSkipBom();
$this->numReadRows = 0;
$this->rowBuffer = null;
$this->next();
}
/**
* This rewinds and skips the BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*
* @return void
*/
protected function rewindAndSkipBom()
{
$byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding);
// sets the cursor after the BOM (0 means no BOM, so rewind it)
$this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom);
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid() : bool
{
return ($this->filePointer && !$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Reads data for the next unprocessed row.
* @see http://php.net/manual/en/iterator.next.php
*
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void
*/
public function next() : void
{
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if (!$this->hasReachedEndOfFile) {
$this->readDataForNextRow();
}
}
/**
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void
*/
protected function readDataForNextRow()
{
do {
$rowData = $this->getNextUTF8EncodedRow();
} while ($this->shouldReadNextRow($rowData));
if ($rowData !== false) {
// array_map will replace NULL values by empty strings
$rowDataBufferAsArray = array_map(function ($value) { return (string) $value; }, $rowData);
$this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
$this->numReadRows++;
} else {
// If we reach this point, it means end of file was reached.
// This happens when the last lines are empty lines.
$this->hasReachedEndOfFile = true;
}
}
/**
* @param array|bool $currentRowData
* @return bool Whether the data for the current row can be returned or if we need to keep reading
*/
protected function shouldReadNextRow($currentRowData)
{
$hasSuccessfullyFetchedRowData = ($currentRowData !== false);
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
$isEmptyLine = $this->isEmptyLine($currentRowData);
return (
(!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) ||
(!$this->shouldPreserveEmptyRows && $isEmptyLine)
);
}
/**
* Returns the next row, converted if necessary to UTF-8.
* As fgetcsv() does not manage correctly encoding for non UTF-8 data,
* we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes)
*
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
*/
protected function getNextUTF8EncodedRow()
{
$encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
if ($encodedRowData === false) {
return false;
}
foreach ($encodedRowData as $cellIndex => $cellValue) {
switch ($this->encoding) {
case EncodingHelper::ENCODING_UTF16_LE:
case EncodingHelper::ENCODING_UTF32_LE:
// remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
$cellValue = \ltrim($cellValue);
break;
case EncodingHelper::ENCODING_UTF16_BE:
case EncodingHelper::ENCODING_UTF32_BE:
// remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
$cellValue = \rtrim($cellValue);
break;
}
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
}
return $encodedRowData;
}
/**
* @param array|bool $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (\is_array($lineData) && \count($lineData) === 1 && $lineData[0] === null);
}
/**
* Return the current element from the buffer
* @see http://php.net/manual/en/iterator.current.php
*
* @return Row|null
*/
public function current() : ?Row
{
return $this->rowBuffer;
}
/**
* Return the key of the current element
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key() : int
{
return $this->numReadRows;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end() : void
{
// do nothing
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\SheetInterface;
/**
* Class Sheet
*/
class Sheet implements SheetInterface
{
/** @var \Box\Spout\Reader\CSV\RowIterator To iterate over the CSV's rows */
protected $rowIterator;
/**
* @param RowIterator $rowIterator Corresponding row iterator
*/
public function __construct(RowIterator $rowIterator)
{
$this->rowIterator = $rowIterator;
}
/**
* @return \Box\Spout\Reader\CSV\RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
/**
* @return int Index of the sheet
*/
public function getIndex()
{
return 0;
}
/**
* @return string Name of the sheet - empty string since CSV does not support that
*/
public function getName()
{
return '';
}
/**
* @return bool Always TRUE as there is only one sheet
*/
public function isActive()
{
return true;
}
/**
* @return bool Always TRUE as the only sheet is always visible
*/
public function isVisible()
{
return true;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\IteratorInterface;
/**
* Class SheetIterator
* Iterate over CSV unique "sheet".
*/
class SheetIterator implements IteratorInterface
{
/** @var Sheet The CSV unique "sheet" */
protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */
protected $hasReadUniqueSheet = false;
/**
* @param Sheet $sheet Corresponding unique sheet
*/
public function __construct($sheet)
{
$this->sheet = $sheet;
}
/**
* Rewind the Iterator to the first element
* @see http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind() : void
{
$this->hasReadUniqueSheet = false;
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid() : bool
{
return (!$this->hasReadUniqueSheet);
}
/**
* Move forward to next element
* @see http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next() : void
{
$this->hasReadUniqueSheet = true;
}
/**
* Return the current element
* @see http://php.net/manual/en/iterator.current.php
*
* @return Sheet
*/
public function current() : Sheet
{
return $this->sheet;
}
/**
* Return the key of the current element
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key() : int
{
return 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end() : void
{
// do nothing
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Box\Spout\Reader\Common\Creator;
use Box\Spout\Common\Entity\Cell;
use Box\Spout\Common\Entity\Row;
/**
* Interface EntityFactoryInterface
*/
interface InternalEntityFactoryInterface
{
/**
* @param Cell[] $cells
* @return Row
*/
public function createRow(array $cells = []);
/**
* @param mixed $cellValue
* @return Cell
*/
public function createCell($cellValue);
}

View File

@ -0,0 +1,71 @@
<?php
namespace Box\Spout\Reader\Common\Creator;
use Box\Spout\Common\Exception\UnsupportedTypeException;
use Box\Spout\Common\Type;
use Box\Spout\Reader\ReaderInterface;
/**
* Class ReaderEntityFactory
* Factory to create external entities
*/
class ReaderEntityFactory
{
/**
* Creates a reader by file extension
*
* @param string $path The path to the spreadsheet file. Supported extensions are .csv, .ods and .xlsx
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
* @return ReaderInterface
*/
public static function createReaderFromFile(string $path)
{
return ReaderFactory::createFromFile($path);
}
/**
* This creates an instance of a CSV reader
*
* @return \Box\Spout\Reader\CSV\Reader
*/
public static function createCSVReader()
{
try {
return ReaderFactory::createFromType(Type::CSV);
} catch (UnsupportedTypeException $e) {
// should never happen
return null;
}
}
/**
* This creates an instance of a XLSX reader
*
* @return \Box\Spout\Reader\XLSX\Reader
*/
public static function createXLSXReader()
{
try {
return ReaderFactory::createFromType(Type::XLSX);
} catch (UnsupportedTypeException $e) {
// should never happen
return null;
}
}
/**
* This creates an instance of a ODS reader
*
* @return \Box\Spout\Reader\ODS\Reader
*/
public static function createODSReader()
{
try {
return ReaderFactory::createFromType(Type::ODS);
} catch (UnsupportedTypeException $e) {
// should never happen
return null;
}
}
}

Some files were not shown because too many files have changed in this diff Show More