Compare commits

...

213 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
321 changed files with 1019703 additions and 7549 deletions

5
.gitattributes vendored
View File

@ -1,11 +1,12 @@
# Ignore all test and documentation for archive
# 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
/composer.lock 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

7
.gitignore vendored
View File

@ -1,5 +1,8 @@
/.idea
*.iml
/tests/resources/generated
/tests/coverage
/vendor
/.idea
*.iml
/.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,57 +0,0 @@
filter:
excluded_paths: [vendor/*, tests/*]
tools:
external_code_coverage:
timeout: 600 # Wait 10 minutes for results
runs: 3 # Merge results for 5.4, 5.5 and 5.6 jobs
php_mess_detector: true
php_code_sniffer:
config:
standard: PSR4
filter:
paths: ['src']
sensiolabs_security_checker: true
php_pdepend: true
php_loc:
enabled: true
filter:
paths: ['src']
php_cpd: false # Must be disabled to use php_sim instead
php_sim:
enabled: true
filter:
paths: ['src']
build_failure_conditions:
- 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9
- 'project.metric_change("scrutinizer.test_coverage", < -0.005)' # Code Coverage decreased by more than 0.5%
- 'project.metric("scrutinizer.test_coverage", < 0.97)' # Code Coverage drops below 97%
checks:
php:
remove_extra_empty_lines: true
remove_php_closing_tag: true
remove_trailing_whitespace: true
fix_use_statements:
remove_unused: true
preserve_multiple: false
preserve_blanklines: true
fix_php_opening_tag: true
fix_linefeed: true
fix_line_ending: true
fix_identation_4spaces: true
fix_doc_comments: true
uppercase_constants: true
use_self_instead_of_fqcn: true
simplify_boolean_return: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
phpunit_assertions: true
parameters_in_camelcaps: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
optional_parameters_at_the_end: true
newline_at_end_of_file: true
encourage_single_quotes: true

View File

@ -1,23 +0,0 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
cache:
directories:
- $HOME/.composer/cache
install:
- composer install --no-interaction --prefer-source
script:
- mkdir -p build/logs
- php vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover
after_script:
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi

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:
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).
@ -68,7 +68,21 @@ This will add your changes on top of what's already in upstream, minimizing merg
Make sure that all tests are passing before submitting a pull request.
### Step 8: Send the pull request
### Step 8: Fix code style
Run the following command to check the code style of your changes:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff
```
This will print a diff of proposed code style changes. To apply these suggestions, run the following command:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist
```
### Step 9: Send the pull request
Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did.

386
README.md
View File

@ -1,390 +1,64 @@
# Spout
[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges)
[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout)
[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![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)
[![License](https://poser.pugx.org/box/spout/license)](https://packagist.org/packages/box/spout)
## 🪦 Archived project 🪦
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.
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB).
Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Installation
### Composer (recommended)
## Documentation
Spout can be installed directly from [Composer](https://getcomposer.org/).
Run the following command:
```
$ composer require box/spout
```
### Manual installation
If you can't use Composer, no worries! You can still install Spout 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 (index.php) or wherever it may be more appropriate:
```php
require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path!
```
Full documentation can be found at [https://opensource.box.com/spout/](https://opensource.box.com/spout/).
## Requirements
* PHP version 5.4.0 or higher
* PHP version 7.2 or higher
* PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled
* PHP extension `php_simplexml` enabled
## Upgrade guide
## Basic usage
### Reader
Regardless of the file type, the interface to read a file is always the same:
```php
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::XLSX); // for XLSX files
//$reader = ReaderFactory::create(Type::CSV); // for CSV files
//$reader = ReaderFactory::create(Type::ODS); // for ODS files
$reader->open($filePath);
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
// do stuff with the row
}
}
$reader->close();
```
If there are multiple sheets in the file, the reader will read all of them sequentially.
### Writer
As with the reader, there is one common interface to write data to a file:
```php
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::XLSX); // for XLSX files
//$writer = WriterFactory::create(Type::CSV); // for CSV files
//$writer = WriterFactory::create(Type::ODS); // for ODS files
$writer->openToFile($filePath); // write data to a file or to a PHP stream
//$writer->openToBrowser($fileName); // stream data directly to the browser
$writer->addRow($singleRow); // add a row at a time
$writer->addRows($multipleRows); // add multiple rows at a time
$writer->close();
```
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
If you are looking for how to perform some common, more advanced tasks with Spout, please take a look at the [Wiki](https://github.com/box/spout/wiki). It contains code snippets, ready to be used.
### Configuring the CSV reader and writer
It is possible to configure 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('@');
$reader->setEndOfLineCharacter("\r");
```
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\WriterFactory;
use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::CSV);
$writer->setShouldAddBOM(false);
```
### Configuring the XLSX and ODS readers and writers
#### Row styling
It is possible to apply some formatting options to a row. Spout supports fonts, background, borders as well as alignment styles.
```php
use Box\Spout\Common\Type;
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Writer\Style\StyleBuilder;
use Box\Spout\Writer\Style\Color;
$style = (new StyleBuilder())
->setFontBold()
->setFontSize(15)
->setFontColor(Color::BLUE)
->setShouldWrapText()
->setBackgroundColor(Color::YELLOW)
->build();
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($filePath);
$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row
$writer->addRow($otherSingleRow); // no style will be applied
$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows
$writer->close();
```
Adding borders to a row requires a ```Border``` object.
```php
use Box\Spout\Common\Type;
use Box\Spout\Writer\Style\Border;
use Box\Spout\Writer\Style\BorderBuilder;
use Box\Spout\Writer\Style\Color;
use Box\Spout\Writer\Style\StyleBuilder;
use Box\Spout\Writer\WriterFactory;
$border = (new BorderBuilder())
->setBorderBottom(Color::GREEN, Border::WIDTH_THIN, Border::STYLE_DASHED)
->build();
$style = (new StyleBuilder())
->setBorder($border)
->build();
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($filePath);
$writer->addRowWithStyle(['Border Bottom Green Thin Dashed'], $style);
$writer->close();
```
Spout 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 = WriterFactory::create(Type::XLSX);
$writer->setDefaultRowStyle($defaultStyle)
->openToFile($filePath);
```
Unfortunately, Spout 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 | Wrap text | `StyleBuilder::setShouldWrapText()`
#### 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::ODS);
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
```
#### Using custom temporary folder
Processing XLSX and ODS 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);
```
#### 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, 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
```
> ##### 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, Spout 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\ReaderFactory;
use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::XLSX);
$reader->setShouldFormatDates(false); // default value
$reader->setShouldFormatDates(true); // will return formatted dates
```
### 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();
```
If you rely on the sheet's name in your application, you can access it and customize it this way:
```php
// Accessing the sheet name when reading
foreach ($reader->getSheetIterator() as $sheet) {
$sheetName = $sheet->getName();
}
// 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. Spout 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 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();
```
Version 3 introduced new functionality but also some breaking changes. If you want to upgrade your Spout codebase from version 2 please consult the [Upgrade guide](UPGRADE-3.0.md).
## Running tests
On the `master` branch, only unit and functional tests are included. The performance tests require very large files and have been excluded.
If you just want to check that everything is working as expected, executing the tests of the `master` branch is enough.
The `master` branch includes unit, functional and performance tests.
If you just want to check that everything is working as expected, executing the unit and functional tests is enough.
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 the whole test suite (unit + functional + performance tests)
* `phpunit --exclude-group perf-tests` - only runs the unit and functional tests
* `phpunit` - runs unit and functional tests
* `phpunit --group perf-tests` - only runs the performance tests
For information, the performance tests take about 30 minutes to run (processing 1 million rows files is not a quick thing).
> Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout)
## Frequently Asked Questions
#### How can Spout handle such large data sets and still use less than 3MB 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 - 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 Spout:
| 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 Spout 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.
For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing).
## Support
Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject.
You can also ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
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 2015 Box, Inc. All rights reserved.
Copyright 2022 Box, Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (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

@ -12,13 +12,15 @@
}
],
"require": {
"php": ">=5.4.0",
"php": ">=7.3.0",
"ext-zip": "*",
"ext-xmlreader" : "*",
"ext-simplexml": "*"
"ext-xmlreader": "*",
"ext-dom": "*"
},
"require-dev": {
"phpunit/phpunit": "^4.8.0"
"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)",
@ -31,8 +33,12 @@
},
"extra": {
"branch-alias": {
"dev-master": "2.6.x-dev"
"dev-master": "3.1.x-dev"
}
},
"config": {
"platform": {
"php": "7.3"
}
}
}

3798
composer.lock generated

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

View File

@ -5,8 +5,6 @@ namespace Box\Spout\Autoloader;
/**
* Class Psr4Autoloader
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
*
* @package Box\Spout\Autoloader
*/
class Psr4Autoloader
{
@ -16,7 +14,7 @@ class Psr4Autoloader
*
* @var array
*/
protected $prefixes = array();
protected $prefixes = [];
/**
* Register loader with SPL autoloader stack.
@ -25,7 +23,7 @@ class Psr4Autoloader
*/
public function register()
{
spl_autoload_register(array($this, 'loadClass'));
\spl_autoload_register([$this, 'loadClass']);
}
/**
@ -42,21 +40,21 @@ class Psr4Autoloader
public function addNamespace($prefix, $baseDir, $prepend = false)
{
// normalize namespace prefix
$prefix = trim($prefix, '\\') . '\\';
$prefix = \trim($prefix, '\\') . '\\';
// normalize the base directory with a trailing separator
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
$baseDir = \rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
// initialize the namespace prefix array
if (isset($this->prefixes[$prefix]) === false) {
$this->prefixes[$prefix] = array();
$this->prefixes[$prefix] = [];
}
// retain the base directory for the namespace prefix
if ($prepend) {
array_unshift($this->prefixes[$prefix], $baseDir);
\array_unshift($this->prefixes[$prefix], $baseDir);
} else {
array_push($this->prefixes[$prefix], $baseDir);
\array_push($this->prefixes[$prefix], $baseDir);
}
}
@ -74,13 +72,12 @@ class Psr4Autoloader
// work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name
while (false !== $pos = strrpos($prefix, '\\')) {
while (($pos = \strrpos($prefix, '\\')) !== false) {
// retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1);
$prefix = \substr($class, 0, $pos + 1);
// the rest is the relative class name
$relativeClass = substr($class, $pos + 1);
$relativeClass = \substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
@ -90,7 +87,7 @@ class Psr4Autoloader
// remove the trailing namespace separator for the next iteration
// of strrpos()
$prefix = rtrim($prefix, '\\');
$prefix = \rtrim($prefix, '\\');
}
// never found a mapped file
@ -114,12 +111,11 @@ class Psr4Autoloader
// 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)
. \str_replace('\\', '/', $relativeClass)
. '.php';
// if the mapped file exists, require it
@ -141,10 +137,12 @@ class Psr4Autoloader
*/
protected function requireFile($file)
{
if (file_exists($file)) {
if (\file_exists($file)) {
require $file;
return true;
}
return false;
}
}

View File

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

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

@ -1,34 +1,32 @@
<?php
namespace Box\Spout\Writer\Style;
namespace Box\Spout\Common\Entity\Style;
/**
* Class Border
*/
class Border
{
const LEFT = 'left';
const RIGHT = 'right';
const TOP = 'top';
const BOTTOM = 'bottom';
public const LEFT = 'left';
public const RIGHT = 'right';
public const TOP = 'top';
public const BOTTOM = 'bottom';
const STYLE_NONE = 'none';
const STYLE_SOLID = 'solid';
const STYLE_DASHED = 'dashed';
const STYLE_DOTTED = 'dotted';
const STYLE_DOUBLE = 'double';
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';
const WIDTH_THIN = 'thin';
const WIDTH_MEDIUM = 'medium';
const WIDTH_THICK = 'thick';
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 = [];
/**
* @var array A list of BorderPart objects for this border.
*/
protected $parts = [];
/**
* @param array|void $borderParts
* @param array $borderParts
*/
public function __construct(array $borderParts = [])
{
@ -36,8 +34,8 @@ class Border
}
/**
* @param $name The name of the border part
* @return null|BorderPart
* @param string $name The name of the border part
* @return BorderPart|null
*/
public function getPart($name)
{
@ -45,7 +43,7 @@ class Border
}
/**
* @param $name The name of the border part
* @param string $name The name of the border part
* @return bool
*/
public function hasPart($name)
@ -64,6 +62,7 @@ class Border
/**
* Set BorderParts
* @param array $parts
* @return void
*/
public function setParts($parts)
{
@ -75,11 +74,12 @@ class Border
/**
* @param BorderPart $borderPart
* @return self
* @return Border
*/
public function addPart(BorderPart $borderPart)
{
$this->parts[$borderPart->getName()] = $borderPart;
return $this;
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace Box\Spout\Writer\Style;
namespace Box\Spout\Common\Entity\Style;
use Box\Spout\Writer\Exception\Border\InvalidNameException;
use Box\Spout\Writer\Exception\Border\InvalidStyleException;
@ -39,7 +39,7 @@ class BorderPart
'solid',
'dashed',
'dotted',
'double'
'double',
];
/**
@ -93,7 +93,7 @@ class BorderPart
*/
public function setName($name)
{
if (!in_array($name, self::$allowedNames)) {
if (!\in_array($name, self::$allowedNames)) {
throw new InvalidNameException($name);
}
$this->name = $name;
@ -114,7 +114,7 @@ class BorderPart
*/
public function setStyle($style)
{
if (!in_array($style, self::$allowedStyles)) {
if (!\in_array($style, self::$allowedStyles)) {
throw new InvalidStyleException($style);
}
$this->style = $style;
@ -152,7 +152,7 @@ class BorderPart
*/
public function setWidth($width)
{
if (!in_array($width, self::$allowedWidths)) {
if (!\in_array($width, self::$allowedWidths)) {
throw new InvalidWidthException($width);
}
$this->width = $width;

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

@ -1,35 +1,32 @@
<?php
namespace Box\Spout\Writer\Style;
namespace Box\Spout\Common\Entity\Style;
use Box\Spout\Writer\Exception\InvalidColorException;
use Box\Spout\Common\Exception\InvalidColorException;
/**
* Class Color
* This class provides constants and functions to work with colors
*
* @package Box\Spout\Writer\Style
*/
class Color
abstract class Color
{
/** Standard colors - based on Office Online */
const BLACK = '000000';
const WHITE = 'FFFFFF';
const RED = 'FF0000';
const DARK_RED = 'C00000';
const ORANGE = 'FFC000';
const YELLOW = 'FFFF00';
const LIGHT_GREEN = '92D040';
const GREEN = '00B050';
const LIGHT_BLUE = '00B0E0';
const BLUE = '0070C0';
const DARK_BLUE = '002060';
const PURPLE = '7030A0';
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
*
* @api
* @param int $red Red component, 0 - 255
* @param int $green Green component, 0 - 255
* @param int $blue Blue component, 0 - 255
@ -41,7 +38,7 @@ class Color
self::throwIfInvalidColorComponentValue($green);
self::throwIfInvalidColorComponentValue($blue);
return strtoupper(
return \strtoupper(
self::convertColorComponentToHex($red) .
self::convertColorComponentToHex($green) .
self::convertColorComponentToHex($blue)
@ -52,12 +49,12 @@ class Color
* 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
* @throws \Box\Spout\Writer\Exception\InvalidColorException
*/
protected static function throwIfInvalidColorComponentValue($colorComponent)
{
if (!is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
if (!\is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent");
}
}
@ -70,7 +67,7 @@ class Color
*/
protected static function convertColorComponentToHex($colorComponent)
{
return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT);
return \str_pad(\dechex($colorComponent), 2, '0', STR_PAD_LEFT);
}
/**

View File

@ -1,82 +1,94 @@
<?php
namespace Box\Spout\Writer\Style;
namespace Box\Spout\Common\Entity\Style;
/**
* Class Style
* Represents a style to be applied to a cell
*
* @package Box\Spout\Writer\Style
*/
class Style
{
/** Default font values */
const DEFAULT_FONT_SIZE = 11;
const DEFAULT_FONT_COLOR = Color::BLACK;
const DEFAULT_FONT_NAME = 'Arial';
/** 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 */
protected $id = null;
private $id;
/** @var bool Whether the font should be bold */
protected $fontBold = false;
private $fontBold = false;
/** @var bool Whether the bold property was set */
protected $hasSetFontBold = false;
private $hasSetFontBold = false;
/** @var bool Whether the font should be italic */
protected $fontItalic = false;
private $fontItalic = false;
/** @var bool Whether the italic property was set */
protected $hasSetFontItalic = false;
private $hasSetFontItalic = false;
/** @var bool Whether the font should be underlined */
protected $fontUnderline = false;
private $fontUnderline = false;
/** @var bool Whether the underline property was set */
protected $hasSetFontUnderline = false;
private $hasSetFontUnderline = false;
/** @var bool Whether the font should be struck through */
protected $fontStrikethrough = false;
private $fontStrikethrough = false;
/** @var bool Whether the strikethrough property was set */
protected $hasSetFontStrikethrough = false;
private $hasSetFontStrikethrough = false;
/** @var int Font size */
protected $fontSize = self::DEFAULT_FONT_SIZE;
private $fontSize = self::DEFAULT_FONT_SIZE;
/** @var bool Whether the font size property was set */
protected $hasSetFontSize = false;
private $hasSetFontSize = false;
/** @var string Font color */
protected $fontColor = self::DEFAULT_FONT_COLOR;
private $fontColor = self::DEFAULT_FONT_COLOR;
/** @var bool Whether the font color property was set */
protected $hasSetFontColor = false;
private $hasSetFontColor = false;
/** @var string Font name */
protected $fontName = self::DEFAULT_FONT_NAME;
private $fontName = self::DEFAULT_FONT_NAME;
/** @var bool Whether the font name property was set */
protected $hasSetFontName = false;
private $hasSetFontName = false;
/** @var bool Whether specific font properties should be applied */
protected $shouldApplyFont = false;
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) */
protected $shouldWrapText = false;
private $shouldWrapText = false;
/** @var bool Whether the wrap text property was set */
protected $hasSetWrapText = false;
private $hasSetWrapText = false;
/**
* @var Border
*/
protected $border = null;
/** @var Border|null */
private $border;
/**
* @var bool Whether border properties should be applied
*/
protected $shouldApplyBorder = false;
/** @var bool Whether border properties should be applied */
private $shouldApplyBorder = false;
/** @var string Background color */
protected $backgroundColor = null;
private $backgroundColor;
/** @var bool */
protected $hasSetBackgroundColor = false;
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
@ -93,11 +105,12 @@ class Style
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return Border
* @return Border|null
*/
public function getBorder()
{
@ -106,16 +119,19 @@ class Style
/**
* @param Border $border
* @return Style
*/
public function setBorder(Border $border)
{
$this->shouldApplyBorder = true;
$this->border = $border;
$this->isEmpty = false;
return $this;
}
/**
* @return boolean
* @return bool
*/
public function shouldApplyBorder()
{
@ -123,7 +139,7 @@ class Style
}
/**
* @return boolean
* @return bool
*/
public function isFontBold()
{
@ -138,11 +154,21 @@ class Style
$this->fontBold = true;
$this->hasSetFontBold = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return boolean
* @return bool
*/
public function hasSetFontBold()
{
return $this->hasSetFontBold;
}
/**
* @return bool
*/
public function isFontItalic()
{
@ -157,11 +183,21 @@ class Style
$this->fontItalic = true;
$this->hasSetFontItalic = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return boolean
* @return bool
*/
public function hasSetFontItalic()
{
return $this->hasSetFontItalic;
}
/**
* @return bool
*/
public function isFontUnderline()
{
@ -176,11 +212,21 @@ class Style
$this->fontUnderline = true;
$this->hasSetFontUnderline = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return boolean
* @return bool
*/
public function hasSetFontUnderline()
{
return $this->hasSetFontUnderline;
}
/**
* @return bool
*/
public function isFontStrikethrough()
{
@ -195,9 +241,19 @@ class Style
$this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontStrikethrough()
{
return $this->hasSetFontStrikethrough;
}
/**
* @return int
*/
@ -215,9 +271,19 @@ class Style
$this->fontSize = $fontSize;
$this->hasSetFontSize = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontSize()
{
return $this->hasSetFontSize;
}
/**
* @return string
*/
@ -237,9 +303,19 @@ class Style
$this->fontColor = $fontColor;
$this->hasSetFontColor = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetFontColor()
{
return $this->hasSetFontColor;
}
/**
* @return string
*/
@ -257,11 +333,60 @@ class Style
$this->fontName = $fontName;
$this->hasSetFontName = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
/**
* @return boolean
* @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()
{
@ -269,15 +394,26 @@ class Style
}
/**
* @param bool $shouldWrap Should the text be wrapped
* @return Style
*/
public function setShouldWrapText()
public function setShouldWrapText($shouldWrap = true)
{
$this->shouldWrapText = 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
*/
@ -295,6 +431,8 @@ class Style
{
$this->hasSetBackgroundColor = true;
$this->backgroundColor = $color;
$this->isEmpty = false;
return $this;
}
@ -307,7 +445,6 @@ class Style
}
/**
*
* @return bool Whether the background color should be applied
*/
public function shouldApplyBackgroundColor()
@ -316,71 +453,57 @@ class Style
}
/**
* Serializes the style for future comparison with other styles.
* The ID is excluded from the comparison, as we only care about
* actual style properties.
*
* @return string The serialized style
* Sets format
* @param string $format
* @return Style
*/
public function serialize()
public function setFormat($format)
{
// In order to be able to properly compare style, set static ID value
$currentId = $this->id;
$this->setId(0);
$this->hasSetFormat = true;
$this->format = $format;
$this->isEmpty = false;
$serializedStyle = serialize($this);
$this->setId($currentId);
return $serializedStyle;
return $this;
}
/**
* Merges the current style with the given style, using the given style as a base. This means that:
* - if current style and base style both have property A set, use current style property's value
* - if current style has property A set but base style does not, use current style property's value
* - if base style has property A set but current style does not, use base style property's value
*
* @NOTE: This function returns a new style.
*
* @param Style $baseStyle
* @return Style New style corresponding to the merge of the 2 styles
* @return string|null
*/
public function mergeWith($baseStyle)
public function getFormat()
{
$mergedStyle = clone $this;
return $this->format;
}
if (!$this->hasSetFontBold && $baseStyle->isFontBold()) {
$mergedStyle->setFontBold();
}
if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) {
$mergedStyle->setFontItalic();
}
if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) {
$mergedStyle->setFontUnderline();
}
if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) {
$mergedStyle->setFontStrikethrough();
}
if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
$mergedStyle->setFontSize($baseStyle->getFontSize());
}
if (!$this->hasSetFontColor && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) {
$mergedStyle->setFontColor($baseStyle->getFontColor());
}
if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
$mergedStyle->setFontName($baseStyle->getFontName());
}
if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) {
$mergedStyle->setShouldWrapText();
}
if (!$this->getBorder() && $baseStyle->shouldApplyBorder()) {
$mergedStyle->setBorder($baseStyle->getBorder());
}
if (!$this->hasSetBackgroundColor && $baseStyle->shouldApplyBackgroundColor()) {
$mergedStyle->setBackgroundColor($baseStyle->getBackgroundColor());
}
/**
* @return bool Whether format should be applied
*/
public function shouldApplyFormat()
{
return $this->hasSetFormat;
}
return $mergedStyle;
/**
* @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,38 +0,0 @@
<?php
namespace Box\Spout\Common\Escaper;
use Box\Spout\Common\Singleton;
/**
* Class ODS
* Provides functions to escape and unescape data for ODS files
*
* @package Box\Spout\Common\Escaper
*/
class ODS implements EscaperInterface
{
use Singleton;
/**
* 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)
{
return htmlspecialchars($string, ENT_QUOTES);
}
/**
* 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)
{
return htmlspecialchars_decode($string, ENT_QUOTES);
}
}

View File

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

View File

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

View File

@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
/**
* Class InvalidArgumentException
*
* @api
* @package Box\Spout\Common\Exception
*/
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,6 @@ namespace Box\Spout\Common\Exception;
/**
* Class SpoutException
*
* @package Box\Spout\Common\Exception
* @abstract
*/
abstract class SpoutException extends \Exception

View File

@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
/**
* Class UnsupportedTypeException
*
* @api
* @package Box\Spout\Common\Exception
*/
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

@ -7,24 +7,22 @@ use Box\Spout\Common\Exception\EncodingConversionException;
/**
* Class EncodingHelper
* This class provides helper functions to work with encodings.
*
* @package Box\Spout\Common\Helper
*/
class EncodingHelper
{
/** Definition of the encodings that can have a BOM */
const ENCODING_UTF8 = 'UTF-8';
const ENCODING_UTF16_LE = 'UTF-16LE';
const ENCODING_UTF16_BE = 'UTF-16BE';
const ENCODING_UTF32_LE = 'UTF-32LE';
const ENCODING_UTF32_BE = 'UTF-32BE';
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 */
const BOM_UTF8 = "\xEF\xBB\xBF";
const BOM_UTF16_LE = "\xFF\xFE";
const BOM_UTF16_BE = "\xFE\xFF";
const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
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;
@ -63,7 +61,7 @@ class EncodingHelper
$bomUsed = $this->supportedEncodingsWithBom[$encoding];
// we skip the N first bytes
$byteOffsetToSkipBom = strlen($bomUsed);
$byteOffsetToSkipBom = \strlen($bomUsed);
}
return $byteOffsetToSkipBom;
@ -82,9 +80,9 @@ class EncodingHelper
$this->globalFunctionsHelper->rewind($filePointer);
if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
$potentialBom = $this->supportedEncodingsWithBom[$encoding];
$numBytesInBom = strlen($potentialBom);
$numBytesInBom = \strlen($potentialBom);
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
}
@ -97,8 +95,8 @@ class EncodingHelper
*
* @param string $string Non UTF-8 string to be converted
* @param string $sourceEncoding The encoding used to encode the source string
* @return string The converted, UTF-8 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)
{
@ -110,8 +108,8 @@ class EncodingHelper
*
* @param string $string UTF-8 string to be converted
* @param string $targetEncoding The encoding the string should be re-encoded into
* @return string The converted string, encoded with the given encoding
* @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)
{
@ -125,8 +123,8 @@ class EncodingHelper
* @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
* @return string The converted string, encoded with the given encoding
* @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)
{
@ -139,7 +137,7 @@ class EncodingHelper
if ($this->canUseIconv()) {
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
} else if ($this->canUseMbString()) {
} 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\".");

View File

@ -1,12 +1,10 @@
<?php
namespace Box\Spout\Common\Escaper;
namespace Box\Spout\Common\Helper\Escaper;
/**
* Class CSV
* Provides functions to escape and unescape data for CSV files
*
* @package Box\Spout\Common\Escaper
*/
class CSV implements EscaperInterface
{

View File

@ -1,11 +1,9 @@
<?php
namespace Box\Spout\Common\Escaper;
namespace Box\Spout\Common\Helper\Escaper;
/**
* Interface EscaperInterface
*
* @package Box\Spout\Common\Escaper
*/
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

@ -1,36 +1,37 @@
<?php
namespace Box\Spout\Common\Escaper;
use Box\Spout\Common\Singleton;
namespace Box\Spout\Common\Helper\Escaper;
/**
* Class XLSX
* Provides functions to escape and unescape data for XLSX files
*
* @package Box\Spout\Common\Escaper
*/
class XLSX implements EscaperInterface
{
use Singleton;
/** @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 */
protected $escapableControlCharactersPattern;
private $escapableControlCharactersPattern;
/** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
protected $controlCharactersEscapingMap;
private $controlCharactersEscapingMap;
/** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
protected $controlCharactersEscapingReverseMap;
private $controlCharactersEscapingReverseMap;
/**
* Initializes the singleton instance
* Initializes the control characters if not already done
*/
protected function init()
protected function initIfNeeded()
{
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
$this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap);
if (!$this->isAlreadyInitialized) {
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
$this->controlCharactersEscapingReverseMap = \array_flip($this->controlCharactersEscapingMap);
$this->isAlreadyInitialized = true;
}
}
/**
@ -41,8 +42,12 @@ class XLSX implements EscaperInterface
*/
public function escape($string)
{
$this->initIfNeeded();
$escapedString = $this->escapeControlCharacters($string);
$escapedString = htmlspecialchars($escapedString, ENT_QUOTES);
// @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;
}
@ -55,8 +60,15 @@ class XLSX implements EscaperInterface
*/
public function unescape($string)
{
$unescapedString = htmlspecialchars_decode($string, ENT_QUOTES);
$unescapedString = $this->unescapeControlCharacters($unescapedString);
$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;
}
@ -81,7 +93,7 @@ class XLSX implements EscaperInterface
* "\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
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @return string[]
*/
@ -91,10 +103,10 @@ class XLSX implements EscaperInterface
// 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)) . '_';
$character = \chr($charValue);
if (\preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
$charHexValue = \dechex($charValue);
$escapedChar = '_x' . \sprintf('%04s', \strtoupper($charHexValue)) . '_';
$controlCharactersEscapingMap[$escapedChar] = $character;
}
}
@ -110,7 +122,7 @@ class XLSX implements EscaperInterface
* 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
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to escape
* @return string
@ -120,11 +132,11 @@ class XLSX implements EscaperInterface
$escapedString = $this->escapeEscapeCharacter($string);
// if no control characters
if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
if (!\preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
return $escapedString;
}
return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function($matches) {
return \preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function ($matches) {
return $this->controlCharactersEscapingReverseMap[$matches[0]];
}, $escapedString);
}
@ -137,7 +149,7 @@ class XLSX implements EscaperInterface
*/
protected function escapeEscapeCharacter($string)
{
return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
return \preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
}
/**
@ -148,7 +160,7 @@ class XLSX implements EscaperInterface
* 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
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
*
* @param string $string String to unescape
* @return string
@ -159,7 +171,7 @@ class XLSX implements EscaperInterface
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);
$unescapedString = \preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
}
return $this->unescapeEscapeCharacter($unescapedString);
@ -173,6 +185,6 @@ class XLSX implements EscaperInterface
*/
protected function unescapeEscapeCharacter($string)
{
return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $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
* This class provides helper functions to help with the file system operations
* 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 */
protected $baseFolderPath;
/** @var string Real path of the base folder where all the I/O can occur */
protected $baseFolderRealPath;
/**
* @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,8 +27,8 @@ class FileSystemHelper
*
* @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
* @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
* @return string Path of the created folder
*/
public function createFolder($parentFolderPath, $folderName)
{
@ -38,7 +36,7 @@ class FileSystemHelper
$folderPath = $parentFolderPath . '/' . $folderName;
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
$wasCreationSuccessful = \mkdir($folderPath, 0777, true);
if (!$wasCreationSuccessful) {
throw new IOException("Unable to create folder: $folderPath");
}
@ -53,8 +51,8 @@ class FileSystemHelper
* @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
* @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
* @return string Path of the created file
*/
public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
{
@ -62,7 +60,7 @@ class FileSystemHelper
$filePath = $parentFolderPath . '/' . $fileName;
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
$wasCreationSuccessful = \file_put_contents($filePath, $fileContents);
if ($wasCreationSuccessful === false) {
throw new IOException("Unable to create file: $filePath");
}
@ -74,15 +72,15 @@ class FileSystemHelper
* Delete the file at the given path
*
* @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
* @return void
*/
public function deleteFile($filePath)
{
$this->throwIfOperationNotInBaseFolder($filePath);
if (file_exists($filePath) && is_file($filePath)) {
unlink($filePath);
if (\file_exists($filePath) && \is_file($filePath)) {
\unlink($filePath);
}
}
@ -90,8 +88,8 @@ class FileSystemHelper
* Delete the folder at the given path as well as all its contents
*
* @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
* @return void
*/
public function deleteFolderRecursively($folderPath)
{
@ -104,13 +102,13 @@ class FileSystemHelper
foreach ($itemIterator as $item) {
if ($item->isDir()) {
rmdir($item->getPathname());
\rmdir($item->getPathname());
} 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.
*
* @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
* @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) {
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

@ -7,8 +7,6 @@ namespace Box\Spout\Common\Helper;
* This class wraps global functions to facilitate testing
*
* @codeCoverageIgnore
*
* @package Box\Spout\Common\Helper
*/
class GlobalFunctionsHelper
{
@ -22,7 +20,7 @@ class GlobalFunctionsHelper
*/
public function fopen($fileName, $mode)
{
return fopen($fileName, $mode);
return \fopen($fileName, $mode);
}
/**
@ -30,12 +28,12 @@ class GlobalFunctionsHelper
* @see fgets()
*
* @param resource $handle
* @param int|void $length
* @param int|null $length
* @return string
*/
public function fgets($handle, $length = null)
{
return fgets($handle, $length);
return \fgets($handle, $length);
}
/**
@ -48,7 +46,7 @@ class GlobalFunctionsHelper
*/
public function fputs($handle, $string)
{
return fputs($handle, $string);
return \fputs($handle, $string);
}
/**
@ -60,7 +58,7 @@ class GlobalFunctionsHelper
*/
public function fflush($handle)
{
return fflush($handle);
return \fflush($handle);
}
/**
@ -73,7 +71,7 @@ class GlobalFunctionsHelper
*/
public function fseek($handle, $offset)
{
return fseek($handle, $offset);
return \fseek($handle, $offset);
}
/**
@ -81,14 +79,20 @@ class GlobalFunctionsHelper
* @see fgetcsv()
*
* @param resource $handle
* @param int|void $length
* @param string|void $delimiter
* @param string|void $enclosure
* @return array
* @param int|null $length
* @param string|null $delimiter
* @param string|null $enclosure
* @return array|false
*/
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);
}
/**
@ -97,13 +101,19 @@ class GlobalFunctionsHelper
*
* @param resource $handle
* @param array $fields
* @param string|void $delimiter
* @param string|void $enclosure
* @return int
* @param string|null $delimiter
* @param string|null $enclosure
* @return int|false
*/
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
{
return fputcsv($handle, $fields, $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 \fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter);
}
/**
@ -116,7 +126,7 @@ class GlobalFunctionsHelper
*/
public function fwrite($handle, $string)
{
return fwrite($handle, $string);
return \fwrite($handle, $string);
}
/**
@ -128,7 +138,7 @@ class GlobalFunctionsHelper
*/
public function fclose($handle)
{
return fclose($handle);
return \fclose($handle);
}
/**
@ -140,7 +150,7 @@ class GlobalFunctionsHelper
*/
public function rewind($handle)
{
return rewind($handle);
return \rewind($handle);
}
/**
@ -152,7 +162,7 @@ class GlobalFunctionsHelper
*/
public function file_exists($fileName)
{
return file_exists($fileName);
return \file_exists($fileName);
}
/**
@ -165,7 +175,8 @@ class GlobalFunctionsHelper
public function file_get_contents($filePath)
{
$realFilePath = $this->convertToUseRealPath($filePath);
return file_get_contents($realFilePath);
return \file_get_contents($realFilePath);
}
/**
@ -180,13 +191,13 @@ class GlobalFunctionsHelper
$realFilePath = $filePath;
if ($this->isZipStream($filePath)) {
if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
if (\preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
$documentPath = $matches[1];
$documentInsideZipPath = $matches[2];
$realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath;
$realFilePath = 'zip://' . \realpath($documentPath) . '#' . $documentInsideZipPath;
}
} else {
$realFilePath = realpath($filePath);
$realFilePath = \realpath($filePath);
}
return $realFilePath;
@ -200,19 +211,19 @@ class GlobalFunctionsHelper
*/
protected function isZipStream($path)
{
return (strpos($path, 'zip://') === 0);
return (\strpos($path, 'zip://') === 0);
}
/**
* Wrapper around global function feof()
* @see feof()
*
* @param resource
* @param resource $handle
* @return bool
*/
public function feof($handle)
{
return feof($handle);
return \feof($handle);
}
/**
@ -224,7 +235,7 @@ class GlobalFunctionsHelper
*/
public function is_readable($fileName)
{
return is_readable($fileName);
return \is_readable($fileName);
}
/**
@ -232,12 +243,12 @@ class GlobalFunctionsHelper
* @see basename()
*
* @param string $path
* @param string|void $suffix
* @param string $suffix
* @return string
*/
public function basename($path, $suffix = null)
public function basename($path, $suffix = '')
{
return basename($path, $suffix);
return \basename($path, $suffix);
}
/**
@ -249,7 +260,7 @@ class GlobalFunctionsHelper
*/
public function header($string)
{
header($string);
\header($string);
}
/**
@ -260,8 +271,8 @@ class GlobalFunctionsHelper
*/
public function ob_end_clean()
{
if (ob_get_length() > 0) {
ob_end_clean();
if (\ob_get_length() > 0) {
\ob_end_clean();
}
}
@ -276,7 +287,7 @@ class GlobalFunctionsHelper
*/
public function iconv($string, $sourceEncoding, $targetEncoding)
{
return iconv($sourceEncoding, $targetEncoding, $string);
return \iconv($sourceEncoding, $targetEncoding, $string);
}
/**
@ -290,7 +301,7 @@ class GlobalFunctionsHelper
*/
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
{
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
return \mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
}
/**
@ -301,7 +312,7 @@ class GlobalFunctionsHelper
*/
public function stream_get_wrappers()
{
return stream_get_wrappers();
return \stream_get_wrappers();
}
/**
@ -313,6 +324,6 @@ class GlobalFunctionsHelper
*/
public function function_exists($functionName)
{
return function_exists($functionName);
return \function_exists($functionName);
}
}

View File

@ -7,20 +7,26 @@ namespace Box\Spout\Common\Helper;
* This class provides helper functions to work with strings and multibyte strings.
*
* @codeCoverageIgnore
*
* @package Box\Spout\Common\Helper
*/
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->hasMbstringSupport = \extension_loaded('mbstring');
$this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0;
$this->localeInfo = \localeconv();
}
/**
@ -34,7 +40,7 @@ class StringHelper
*/
public function getStringLength($string)
{
return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string);
return $this->hasMbstringSupport ? \mb_strlen($string) : \strlen($string);
}
/**
@ -49,7 +55,8 @@ class StringHelper
*/
public function getCharFirstOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char);
$position = $this->hasMbstringSupport ? \mb_strpos($string, $char) : \strpos($string, $char);
return ($position !== false) ? $position : -1;
}
@ -65,7 +72,34 @@ class StringHelper
*/
public function getCharLastOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char);
$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

@ -1,41 +0,0 @@
<?php
namespace Box\Spout\Common;
/**
* Class Singleton
* Defines a class as a singleton.
*
* @package Box\Spout\Common
*/
trait Singleton
{
protected static $instance;
/**
* @return static
*/
final public static function getInstance()
{
return isset(static::$instance)
? static::$instance
: static::$instance = new static;
}
/**
* Singleton constructor.
*/
final private function __construct()
{
$this->init();
}
/**
* Initializes the singleton
* @return void
*/
protected function init() {}
final private function __wakeup() {}
final private function __clone() {}
}

View File

@ -5,12 +5,10 @@ namespace Box\Spout\Common;
/**
* Class Type
* This class references the supported types
*
* @api
*/
abstract class Type
{
const CSV = 'csv';
const XLSX = 'xlsx';
const ODS = 'ods';
public const CSV = 'csv';
public const XLSX = 'xlsx';
public const ODS = 'ods';
}

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

@ -2,17 +2,19 @@
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\AbstractReader;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\EncodingHelper;
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.
*
* @package Box\Spout\Reader\CSV
*/
class Reader extends AbstractReader
class Reader extends ReaderAbstract
{
/** @var resource Pointer to the file to be written */
protected $filePointer;
@ -20,20 +22,25 @@ class Reader extends AbstractReader
/** @var SheetIterator To iterator over the CSV unique "sheet" */
protected $sheetIterator;
/** @var string Defines the character used to delimit fields (one character only) */
protected $fieldDelimiter = ',';
/** @var string Original value for the "auto_detect_line_endings" INI value */
protected $originalAutoDetectLineEndings;
/** @var string Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure = '"';
/** @var bool Whether the code is running with PHP >= 8.1 */
private $isRunningAtLeastPhp81;
/** @var string Encoding of the CSV file to be read */
protected $encoding = EncodingHelper::ENCODING_UTF8;
/** @var string Defines the End of line */
protected $endOfLineCharacter = "\n";
/** @var string */
protected $autoDetectLineEndings;
/**
* @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.
@ -44,7 +51,8 @@ class Reader extends AbstractReader
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
$this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
return $this;
}
@ -57,7 +65,8 @@ class Reader extends AbstractReader
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
$this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
return $this;
}
@ -70,20 +79,8 @@ class Reader extends AbstractReader
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
$this->optionsManager->setOption(Options::ENCODING, $encoding);
/**
* Sets the EOL for the CSV.
* Needs to be called before opening the reader.
*
* @param string $endOfLineCharacter used to properly get lines from the CSV file.
* @return Reader
*/
public function setEndOfLineCharacter($endOfLineCharacter)
{
$this->endOfLineCharacter = $endOfLineCharacter;
return $this;
}
@ -102,25 +99,28 @@ class Reader extends AbstractReader
* 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
* @return void
* @throws \Box\Spout\Common\Exception\IOException
* @return void
*/
protected function openReader($filePath)
{
$this->autoDetectLineEndings = ini_get('auto_detect_line_endings');
ini_set('auto_detect_line_endings', '1');
// "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.");
}
$this->sheetIterator = new SheetIterator(
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->sheetIterator = $entityFactory->createSheetIterator(
$this->filePointer,
$this->fieldDelimiter,
$this->fieldEnclosure,
$this->encoding,
$this->endOfLineCharacter,
$this->optionsManager,
$this->globalFunctionsHelper
);
}
@ -130,12 +130,11 @@ class Reader extends AbstractReader
*
* @return SheetIterator To iterate over sheets
*/
public function getConcreteSheetIterator()
protected function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
@ -143,10 +142,13 @@ class Reader extends AbstractReader
*/
protected function closeReader()
{
if ($this->filePointer) {
if (is_resource($this->filePointer)) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
ini_set('auto_detect_line_endings', $this->autoDetectLineEndings);
// "auto_detect_line_endings" is deprecated in PHP 8.1
if (!$this->isRunningAtLeastPhp81) {
\ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
}
}
}

View File

@ -2,31 +2,33 @@
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\IteratorInterface;
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.
*
* @package Box\Spout\Reader\CSV
*/
class RowIterator implements IteratorInterface
{
/**
* If no value is given to fgetcsv(), it defaults to 8192 (which may be too low).
* Alignement with other functions like fgets() is discussed here: https://bugs.php.net/bug.php?id=48421
* Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines).
*/
const MAX_READ_BYTES_PER_LINE = 32768;
public const MAX_READ_BYTES_PER_LINE = 0;
/** @var resource Pointer to the CSV file to read */
/** @var resource|null Pointer to the CSV file to read */
protected $filePointer;
/** @var int Number of read rows */
protected $numReadRows = 0;
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @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;
@ -40,50 +42,54 @@ class RowIterator implements IteratorInterface
/** @var string Encoding of the CSV file to be read */
protected $encoding;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @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 string End of line delimiter, encoded using the same encoding as the CSV */
protected $encodedEOLDelimiter;
/** @var \Box\Spout\Reader\CSV\Creator\InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var string End of line delimiter, given by the user as input. */
protected $inputEOLDelimiter;
/** @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 string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param string $encoding Encoding of the CSV file to be read
* @param string $endOfLineDelimiter End of line delimiter
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
* @param OptionsManagerInterface $optionsManager
* @param EncodingHelper $encodingHelper
* @param InternalEntityFactory $entityFactory
* @param GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineDelimiter, $globalFunctionsHelper)
{
public function __construct(
$filePointer,
OptionsManagerInterface $optionsManager,
EncodingHelper $encodingHelper,
InternalEntityFactory $entityFactory,
GlobalFunctionsHelper $globalFunctionsHelper
) {
$this->filePointer = $filePointer;
$this->fieldDelimiter = $fieldDelimiter;
$this->fieldEnclosure = $fieldEnclosure;
$this->encoding = $encoding;
$this->inputEOLDelimiter = $endOfLineDelimiter;
$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;
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
* @see http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
public function rewind() : void
{
$this->rewindAndSkipBom();
$this->numReadRows = 0;
$this->rowDataBuffer = null;
$this->rowBuffer = null;
$this->next();
}
@ -104,72 +110,96 @@ class RowIterator implements IteratorInterface
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
* @see http://php.net/manual/en/iterator.valid.php
*
* @return boolean
* @return bool
*/
public function valid()
public function valid() : bool
{
return ($this->filePointer && !$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Empty rows are skipped.
* @link http://php.net/manual/en/iterator.next.php
* Move forward to next element. Reads data for the next unprocessed row.
* @see http://php.net/manual/en/iterator.next.php
*
* @return void
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void
*/
public function next()
public function next() : void
{
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if ($this->hasReachedEndOfFile) {
return;
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();
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
} while (($rowData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($rowData));
} while ($this->shouldReadNextRow($rowData));
if ($rowData !== false) {
$this->rowDataBuffer = $rowData;
// array_map will replace NULL values by empty strings
$rowDataBufferAsArray = array_map(function ($value) { return (string) $value; }, $rowData);
$this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
$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 = $hasNowReachedEndOfFile;
$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)
*
* @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
* @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 (false === $encodedRowData) {
if ($encodedRowData === false) {
return false;
}
foreach ($encodedRowData as $cellIndex => $cellValue) {
switch($this->encoding) {
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);
$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);
$cellValue = \rtrim($cellValue);
break;
}
@ -180,47 +210,32 @@ class RowIterator implements IteratorInterface
}
/**
* Returns the end of line delimiter, encoded using the same encoding as the CSV.
* The return value is cached.
*
* @return string
*/
protected function getEncodedEOLDelimiter()
{
if (!isset($this->encodedEOLDelimiter)) {
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding);
}
return $this->encodedEOLDelimiter;
}
/**
* @param array $lineData Array containing the cells value for the line
* @param array|bool $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null);
return (\is_array($lineData) && \count($lineData) === 1 && $lineData[0] === null);
}
/**
* Return the current element from the buffer
* @link http://php.net/manual/en/iterator.current.php
* @see http://php.net/manual/en/iterator.current.php
*
* @return array|null
* @return Row|null
*/
public function current()
public function current() : ?Row
{
return $this->rowDataBuffer;
return $this->rowBuffer;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
public function key() : int
{
return $this->numReadRows;
}
@ -230,7 +245,7 @@ class RowIterator implements IteratorInterface
*
* @return void
*/
public function end()
public function end() : void
{
// do nothing
}

View File

@ -6,8 +6,6 @@ use Box\Spout\Reader\SheetInterface;
/**
* Class Sheet
*
* @package Box\Spout\Reader\CSV
*/
class Sheet implements SheetInterface
{
@ -15,23 +13,50 @@ class Sheet implements SheetInterface
protected $rowIterator;
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param string $encoding Encoding of the CSV file to be read
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
* @param RowIterator $rowIterator Corresponding row iterator
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
public function __construct(RowIterator $rowIterator)
{
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
$this->rowIterator = $rowIterator;
}
/**
* @api
* @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

@ -7,80 +7,74 @@ use Box\Spout\Reader\IteratorInterface;
/**
* Class SheetIterator
* Iterate over CSV unique "sheet".
*
* @package Box\Spout\Reader\CSV
*/
class SheetIterator implements IteratorInterface
{
/** @var \Box\Spout\Reader\CSV\Sheet The CSV unique "sheet" */
/** @var Sheet The CSV unique "sheet" */
protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */
protected $hasReadUniqueSheet = false;
/**
* @param resource $filePointer
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param string $encoding Encoding of the CSV file to be read
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
* @param Sheet $sheet Corresponding unique sheet
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
public function __construct($sheet)
{
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
$this->sheet = $sheet;
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
* @see http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
public function rewind() : void
{
$this->hasReadUniqueSheet = false;
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
* @see http://php.net/manual/en/iterator.valid.php
*
* @return boolean
* @return bool
*/
public function valid()
public function valid() : bool
{
return (!$this->hasReadUniqueSheet);
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
* @see http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
public function next() : void
{
$this->hasReadUniqueSheet = true;
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
* @see http://php.net/manual/en/iterator.current.php
*
* @return \Box\Spout\Reader\CSV\Sheet
* @return Sheet
*/
public function current()
public function current() : Sheet
{
return $this->sheet;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
public function key() : int
{
return 1;
}
@ -90,7 +84,7 @@ class SheetIterator implements IteratorInterface
*
* @return void
*/
public function end()
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;
}
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Box\Spout\Reader\Common\Creator;
use Box\Spout\Common\Creator\HelperFactory;
use Box\Spout\Common\Exception\UnsupportedTypeException;
use Box\Spout\Common\Type;
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory as CSVInternalEntityFactory;
use Box\Spout\Reader\CSV\Manager\OptionsManager as CSVOptionsManager;
use Box\Spout\Reader\CSV\Reader as CSVReader;
use Box\Spout\Reader\ODS\Creator\HelperFactory as ODSHelperFactory;
use Box\Spout\Reader\ODS\Creator\InternalEntityFactory as ODSInternalEntityFactory;
use Box\Spout\Reader\ODS\Creator\ManagerFactory as ODSManagerFactory;
use Box\Spout\Reader\ODS\Manager\OptionsManager as ODSOptionsManager;
use Box\Spout\Reader\ODS\Reader as ODSReader;
use Box\Spout\Reader\ReaderInterface;
use Box\Spout\Reader\XLSX\Creator\HelperFactory as XLSXHelperFactory;
use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory as XLSXInternalEntityFactory;
use Box\Spout\Reader\XLSX\Creator\ManagerFactory as XLSXManagerFactory;
use Box\Spout\Reader\XLSX\Manager\OptionsManager as XLSXOptionsManager;
use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\XLSX\Reader as XLSXReader;
/**
* Class ReaderFactory
* This factory is used to create readers, based on the type of the file to be read.
* It supports CSV, XLSX and ODS formats.
*/
class ReaderFactory
{
/**
* 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 createFromFile(string $path)
{
$extension = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
return self::createFromType($extension);
}
/**
* This creates an instance of the appropriate reader, given the type of the file to be read
*
* @param string $readerType Type of the reader to instantiate
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
* @return ReaderInterface
*/
public static function createFromType($readerType)
{
switch ($readerType) {
case Type::CSV: return self::createCSVReader();
case Type::XLSX: return self::createXLSXReader();
case Type::ODS: return self::createODSReader();
default:
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
}
}
/**
* @return CSVReader
*/
private static function createCSVReader()
{
$optionsManager = new CSVOptionsManager();
$helperFactory = new HelperFactory();
$entityFactory = new CSVInternalEntityFactory($helperFactory);
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
return new CSVReader($optionsManager, $globalFunctionsHelper, $entityFactory);
}
/**
* @return XLSXReader
*/
private static function createXLSXReader()
{
$optionsManager = new XLSXOptionsManager();
$helperFactory = new XLSXHelperFactory();
$managerFactory = new XLSXManagerFactory($helperFactory, new CachingStrategyFactory());
$entityFactory = new XLSXInternalEntityFactory($managerFactory, $helperFactory);
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
return new XLSXReader($optionsManager, $globalFunctionsHelper, $entityFactory, $managerFactory);
}
/**
* @return ODSReader
*/
private static function createODSReader()
{
$optionsManager = new ODSOptionsManager();
$helperFactory = new ODSHelperFactory();
$managerFactory = new ODSManagerFactory();
$entityFactory = new ODSInternalEntityFactory($helperFactory, $managerFactory);
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
return new ODSReader($optionsManager, $globalFunctionsHelper, $entityFactory);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Box\Spout\Reader\Common\Entity;
/**
* Class Options
* Readers' options holder
*/
abstract class Options
{
// Common options
public const SHOULD_FORMAT_DATES = 'shouldFormatDates';
public const SHOULD_PRESERVE_EMPTY_ROWS = 'shouldPreserveEmptyRows';
// CSV specific options
public const FIELD_DELIMITER = 'fieldDelimiter';
public const FIELD_ENCLOSURE = 'fieldEnclosure';
public const ENCODING = 'encoding';
// XLSX specific options
public const TEMP_FOLDER = 'tempFolder';
public const SHOULD_USE_1904_DATES = 'shouldUse1904Dates';
}

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