Compare commits

..
247 Commits
Author SHA1 Message Date
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: """ was converted to """ when imported into a SimpleXMLElement and was again converted into " (quote).

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

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

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

* Failing test #267

* Fixed shared border definitions across different styles #267

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

* Minor fix for quick copy+paste

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

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

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# Ignore all tests, documentation and dot files for archive
/docs export-ignore
/tests export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.php_cs.dist export-ignore
/.scrutinizer.yml export-ignore
/.travis.yml export-ignore
/CONTRIBUTING.md export-ignore
/logo.png export-ignore
/phpunit.xml export-ignore
+5 -2
View File
@@ -1,5 +1,8 @@
/.idea
*.iml
/tests/resources/generated /tests/resources/generated
/tests/coverage /tests/coverage
/vendor /vendor
/.idea /composer.lock
*.iml /.php_cs.cache
+58
View File
@@ -0,0 +1,58 @@
<?php
$config = PhpCsFixer\Config::create()
->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' => ['use_yoda_style' => false],
'method_argument_space' => ['ensure_fully_multiline' => true],
'modernize_types_casting' => true,
'no_break_comment' => ['comment_text' => 'do nothing'],
'no_empty_phpdoc' => false,
'no_null_property_initialization' => true,
'no_short_echo_tag' => true,
'no_superfluous_elseif' => true,
'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,
'psr4' => 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],
]);
$config->setFinder(
PhpCsFixer\Finder::create()
->exclude('vendor')
->in(__DIR__)
->name('*.php')
);
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
$config->setCacheFile($cacheDir . '/.php_cs.cache');
return $config;
+7 -4
View File
@@ -4,7 +4,7 @@ filter:
tools: tools:
external_code_coverage: external_code_coverage:
timeout: 600 # Wait 10 minutes for results timeout: 600 # Wait 10 minutes for results
runs: 3 # Merge results for 5.4, 5.5 and 5.6 jobs runs: 1 # Merge results for the 7.1 job
php_mess_detector: true php_mess_detector: true
php_code_sniffer: php_code_sniffer:
config: config:
@@ -15,10 +15,13 @@ tools:
php_pdepend: true php_pdepend: true
php_loc: php_loc:
enabled: true enabled: true
excluded_dirs: [vendor, tests] filter:
php_cpd: paths: ['src']
php_cpd: false # Must be disabled to use php_sim instead
php_sim:
enabled: true enabled: true
excluded_dirs: [vendor, tests] filter:
paths: ['src']
build_failure_conditions: build_failure_conditions:
- 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9 - 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9
+45 -9
View File
@@ -1,22 +1,58 @@
dist: trusty
sudo: false
language: php language: php
php: matrix:
- 5.4 include:
- 5.5 - php: 7.1
- 5.6 env: WITH_CS=true
- 7.0 - php: 7.1
- hhvm env: WITH_PHPUNIT=true WITH_COVERAGE=true
- php: 7.2
env: WITH_PHPUNIT=true
- php: 7.3
env: WITH_PHPUNIT=true
cache: cache:
directories: directories:
- $HOME/.composer/cache - $HOME/.composer/cache
- $HOME/.php-cs-fixer
before_install:
- |
if [[ "$WITH_COVERAGE" != "true" ]]; then
phpenv config-rm xdebug.ini
fi
if [[ "$WITH_CS" != "true" ]]; then
composer remove friendsofphp/php-cs-fixer --dev --no-update
fi
- composer validate
install: install:
- composer install --no-interaction --prefer-source - composer install --no-interaction --prefer-source
script: before_script:
- mkdir -p "$HOME/.php-cs-fixer"
- mkdir -p build/logs - mkdir -p build/logs
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
script:
- |
if [[ "$WITH_CS" == "true" ]]; then
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run
fi
- |
if [[ "$WITH_PHPUNIT" == "true" ]]; then
if [[ "$WITH_COVERAGE" == "true" ]]; then
vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover
else
vendor/bin/phpunit --no-coverage
fi
fi
after_script: after_script:
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover build/logs/clover.xml; fi - |
if [[ "$WITH_COVERAGE" == "true" ]]; then
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover
fi
+16 -293
View File
@@ -2,330 +2,53 @@
[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/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) [![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)](http://travis-ci.org/box/spout) [![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/box/spout/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) [![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)
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. 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 10MB). 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).
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 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)
## Installation
### Composer (recommended) ## Documentation
Spout can be installed directly from [Composer](https://getcomposer.org/). Full documentation can be found at [http://opensource.box.com/spout/](http://opensource.box.com/spout/).
Add "box/spout" as a dependency in your project's composer.json file:
```json
"require": {
"box/spout": "~2.0"
}
```
Then run the install command from Composer:
```
php composer.phar install
```
### 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!
```
## Requirements ## Requirements
* PHP version 5.4.0 or higher * PHP version 7.1 or higher
* PHP extension `php_zip` enabled * PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled * PHP extension `php_xmlreader` enabled
* PHP extension `php_simplexml` enabled
## Upgrade guide
## Basic usage Version 3 introduced new functionality but also some breaking changes. If you want to upgrade your Spout codebase from version 2 please consult the [Upgrade guide](UPGRADE-3.0.md).
### Reader
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::CSV); // for CSV files
//$reader = ReaderFactory::create(Type::XLSX); // for XLSX 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::CSV); // for CSV files
//$writer = WriterFactory::create(Type::XLSX); // for XLSX 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
### 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('@');
```
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');
```
The writer always generate CSV files encoded in UTF-8, with a BOM.
### Configuring the XLSX and ODS writers
#### Row styling
It is possible to apply some formatting options to a row. Spout supports fonts 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()
->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();
```
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::setFontSize(Color::BLUE)`<br>`StyleBuilder::setFontSize(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!
### 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();
```
## Running tests ## Running tests
On the `master` branch, only unit and functional tests are included. The performance tests require very large files and have been excluded. The `master` branch includes unit, functional and performance tests.
If you just want to check that everything is working as expected, executing the tests of the `master` branch is enough. If you just want to check that everything is working as expected, executing the unit and functional tests is enough.
If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output: * `phpunit` - runs unit and functional tests
* `phpunit --group perf-tests` - only runs the performance tests
* `phpunit` - runs the whole test suite (unit + functional + performance tests) For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing).
* `phpunit --testuite no-perf-tests` - only runs the unit and functional tests
* `phpunit --testuite perf-tests` - only runs the performance tests
For information, the performance tests take about one hour to run (processing 2 million rows files is not a quick thing). > 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 10MB of memory?
When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - 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.
## Support ## 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 ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
You can also ask questions, submit new features ideas or discuss about Spout 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) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Copyright and License ## Copyright and License
Copyright 2015 Box, Inc. All rights reserved. Copyright 2017 Box, Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+89
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.0 only supports PHP 7.1 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.
+35
View File
@@ -0,0 +1,35 @@
build: false
clone_depth: 1
environment:
PHP_CHOCO_VERSION: 7.2.0
PHP_CACHE_DIR: C:\tools\php
cache:
- '%PHP_CACHE_DIR% -> appveyor.yml'
init:
- SET PATH=%PHP_CACHE_DIR%;%PATH%
- SET COMPOSER_CACHE_DIR=%PHP_CACHE_DIR%
- SET COMPOSER_NO_INTERACTION=1
- SET PHP=0
- SET ANSICON=121x90 (121x90)
install:
- IF EXIST %PHP_CACHE_DIR% (SET PHP=1)
- IF %PHP%==0 cinst php -y --version %PHP_CHOCO_VERSION% --params "/InstallDir:%PHP_CACHE_DIR%"
- IF %PHP%==0 cinst composer -y --ia "/DEV=%PHP_CACHE_DIR%"
- cd %PHP_CACHE_DIR%
- IF %PHP%==0 copy php.ini-production php.ini
- IF %PHP%==0 echo extension_dir=ext >> php.ini
- IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini
- IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini
- IF %PHP%==0 echo extension=php_openssl.dll >> php.ini
- php -v
- IF %PHP%==0 (composer --version) ELSE (composer self-update)
- cd %APPVEYOR_BUILD_FOLDER%
- composer install --prefer-dist --no-progress
test_script:
- cd %APPVEYOR_BUILD_FOLDER%
- vendor\bin\phpunit --colors=always
+14 -5
View File
@@ -12,14 +12,13 @@
} }
], ],
"require": { "require": {
"php": ">=5.4.0", "php": ">=7.1.0",
"ext-zip": "*", "ext-zip": "*",
"ext-xmlreader" : "*", "ext-xmlreader" : "*"
"ext-simplexml": "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": ">=3.7", "phpunit/phpunit": "^7",
"scrutinizer/ocular": "~1.1" "friendsofphp/php-cs-fixer": "^2"
}, },
"suggest": { "suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
@@ -29,5 +28,15 @@
"psr-4": { "psr-4": {
"Box\\Spout\\": "src/Spout" "Box\\Spout\\": "src/Spout"
} }
},
"extra": {
"branch-alias": {
"dev-master": "3.1.x-dev"
}
},
"config": {
"platform": {
"php": "7.1"
}
} }
} }
Generated
-1647
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -0,0 +1,9 @@
/.idea
*.iml
/tests/resources/generated
/tests/coverage
/vendor
/.sass-cache
/_site
Executable
+17
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
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: "http://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>
+9
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
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
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
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
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="https://github.com/box/spout/issues"><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
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.github.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>
+35
View File
@@ -0,0 +1,35 @@
<header class="site-header">
<div class="wrapper">
<a class="site-title" href="{{ site.github.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.github.url }}/getting-started/">Getting Started</a>
<a class="page-link" href="{{ site.github.url }}/docs/">Documentation</a>
<a class="page-link" href="{{ site.github.url }}/guides/">Guides</a>
<a class="page-link" href="{{ site.github.url }}/faq/">FAQ</a>
<a class="page-link" href="https://github.com/box/spout">
GitHub
<svg width="12" height="12">
<image xlink:href="/images/external-link.svg" src="/images/external-link.png" width="12" height="12"/>
</svg>
</a>
{% if site.algolia.enabled %}
<span class="site-search">
<input type="text" id="algolia-doc-search">
</span>
{% endif %}
</div>
</nav>
</div>
</header>
+1
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
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

@@ -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 %}
@@ -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 %}
+20
View File
@@ -0,0 +1,20 @@
{% 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="{{ site.github.url }}/getting-started/" class="page-link"><button class="btn">Get started</button></a>
</div>
{% endcapture %}
{% include section.html %}
+17
View File
@@ -0,0 +1,17 @@
<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="{{ site.github.url }}/images/{{ icon }}"/>
{% endfor %}
</div>
{% endif %}
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
{% include head.html %}
<body>
{% include header.html %}
{% if page.banner %}
{% include banner.html %}
{% endif %}
{{ content }}
<!--
<div class="page-content">
<div class="wrapper">
</div>
</div>
-->
{% include footer.html %}
{% include algolia.html %}
</body>
</html>
+40
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>
+14
View File
@@ -0,0 +1,14 @@
---
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>
+15
View File
@@ -0,0 +1,15 @@
---
layout: default
---
<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
<header class="post-header">
<h1 class="post-title" itemprop="name headline">{{ page.title }}</h1>
<p class="post-meta"><time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">{{ page.date | date: "%b %-d, %Y" }}</time>{% if page.author %} • <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">{{ page.author }}</span></span>{% endif %}</p>
</header>
<div class="post-content" itemprop="articleBody">
{{ content }}
</div>
</article>
+319
View File
@@ -0,0 +1,319 @@
---
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 | Wrap text | `StyleBuilder::setShouldWrapText(true|false)`
### 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\Color;
$writer = WriterEntityFactory::createXLSXWriter();
$writer->openToFile($filePath);
/** Create a style with the StyleBuilder */
$style = (new StyleBuilder())
->setFontBold()
->setFontSize(15)
->setFontColor(Color::BLUE)
->setShouldWrapText()
->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
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.
+135
View File
@@ -0,0 +1,135 @@
---
layout: doc
title: Getting Started
permalink: /getting-started/
---
This guide will help you install {{ site.spout_html }} and teach you how to use it.
## Requirements
* PHP version 7.1 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;
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.ext');
$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.github.url }}/docs/) to learn about all the features.
+19
View File
@@ -0,0 +1,19 @@
---
layout: page
title: Guides
permalink: /guides/
---
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.github.url }}/getting-started/) and the [Documentation]({{ site.github.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.github.url }}">{{ page.title }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
BIN
View File
Binary file not shown.
@@ -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.
@@ -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.
@@ -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();
```
@@ -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 % FLUSH_THRESHOLD === 0) {
flush();
}
}
echo '</table><br>';
}
$reader->close();
});
return $response;
}
}
```
+10
View File
@@ -0,0 +1,10 @@
---
layout: default
banner: true
title: Spout - Read and write spreadsheets, quickly and at scale
permalink: /
---
{% include section-supported-spreadsheet-types.html %}
{% include section-fast-and-scalable.html %}
{% include section-why-use-spout.html %}
+278
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
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;
}
+406
View File
@@ -0,0 +1,406 @@
/**
* 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;
}
}
@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%);
}
}
+109
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
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"
;
+13
View File
@@ -0,0 +1,13 @@
version: "3"
services:
site:
command: jekyll serve
image: jekyll/jekyll:latest
volumes:
- $PWD:/srv/jekyll
- $PWD/vendor/bundle:/usr/local/bundle
ports:
- 4000:4000
- 35729:35729
- 3000:3000
- 8080:4000
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1850 1850"
id="svg3025"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="external_link_font_awesome.svg">
<metadata
id="metadata3035">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3033" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3031"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg3025" />
<g
transform="matrix(1,0,0,-1,30.372881,1426.9492)"
id="g3027">
<path
d="M 1408,608 V 288 Q 1408,169 1323.5,84.5 1239,0 1120,0 H 288 Q 169,0 84.5,84.5 0,169 0,288 v 832 Q 0,1239 84.5,1323.5 169,1408 288,1408 h 704 q 14,0 23,-9 9,-9 9,-23 v -64 q 0,-14 -9,-23 -9,-9 -23,-9 H 288 q -66,0 -113,-47 -47,-47 -47,-113 V 288 q 0,-66 47,-113 47,-47 113,-47 h 832 q 66,0 113,47 47,47 47,113 v 320 q 0,14 9,23 9,9 23,9 h 64 q 14,0 23,-9 9,-9 9,-23 z m 384,864 V 960 q 0,-26 -19,-45 -19,-19 -45,-19 -26,0 -45,19 L 1507,1091 855,439 q -10,-10 -23,-10 -13,0 -23,10 L 695,553 q -10,10 -10,23 0,13 10,23 l 652,652 -176,176 q -19,19 -19,45 0,26 19,45 19,19 45,19 h 512 q 26,0 45,-19 19,-19 19,-45 z"
id="path3029"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+13 -4
View File
@@ -1,18 +1,27 @@
<phpunit <?xml version="1.0" encoding="UTF-8"?>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.3/phpunit.xsd" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
bootstrap="tests/bootstrap.php" bootstrap="tests/bootstrap.php"
colors="true" colors="true"
convertErrorsToExceptions="false" convertErrorsToExceptions="false"
convertWarningsToExceptions="false" convertWarningsToExceptions="false"
defaultTestSuite="unit-tests"
verbose="false"> verbose="false">
<testsuites> <testsuites>
<testsuite name="all-tests">
<testsuite name="unit-tests">
<directory>tests/</directory> <directory>tests/</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<groups>
<exclude>
<group>perf-tests</group>
</exclude>
</groups>
<filter> <filter>
<whitelist> <whitelist>
<directory suffix=".php">src/</directory> <directory suffix=".php">src/</directory>
+6 -8
View File
@@ -5,8 +5,6 @@ namespace Box\Spout\Autoloader;
/** /**
* Class Psr4Autoloader * Class Psr4Autoloader
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
*
* @package Box\Spout\Autoloader
*/ */
class Psr4Autoloader class Psr4Autoloader
{ {
@@ -16,7 +14,7 @@ class Psr4Autoloader
* *
* @var array * @var array
*/ */
protected $prefixes = array(); protected $prefixes = [];
/** /**
* Register loader with SPL autoloader stack. * Register loader with SPL autoloader stack.
@@ -25,7 +23,7 @@ class Psr4Autoloader
*/ */
public function register() public function register()
{ {
spl_autoload_register(array($this, 'loadClass')); spl_autoload_register([$this, 'loadClass']);
} }
/** /**
@@ -49,7 +47,7 @@ class Psr4Autoloader
// initialize the namespace prefix array // initialize the namespace prefix array
if (isset($this->prefixes[$prefix]) === false) { if (isset($this->prefixes[$prefix]) === false) {
$this->prefixes[$prefix] = array(); $this->prefixes[$prefix] = [];
} }
// retain the base directory for the namespace prefix // retain the base directory for the namespace prefix
@@ -74,8 +72,7 @@ class Psr4Autoloader
// work backwards through the namespace names of the fully-qualified // work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name // 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 // retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1); $prefix = substr($class, 0, $pos + 1);
@@ -114,7 +111,6 @@ class Psr4Autoloader
// look through base directories for this namespace prefix // look through base directories for this namespace prefix
foreach ($this->prefixes[$prefix] as $baseDir) { foreach ($this->prefixes[$prefix] as $baseDir) {
// replace the namespace prefix with the base directory, // replace the namespace prefix with the base directory,
// replace namespace separators with directory separators // replace namespace separators with directory separators
// in the relative class name, append with .php // in the relative class name, append with .php
@@ -143,8 +139,10 @@ class Psr4Autoloader
{ {
if (file_exists($file)) { if (file_exists($file)) {
require $file; require $file;
return true; return true;
} }
return false; return false;
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Box\Spout\Autoloader;
require_once 'Psr4Autoloader.php'; require_once 'Psr4Autoloader.php';
/** /**
* @var string $srcBaseDirectory * @var string
* Full path to "src/Spout" which is what we want "Box\Spout" to map to. * Full path to "src/Spout" which is what we want "Box\Spout" to map to.
*/ */
$srcBaseDirectory = dirname(dirname(__FILE__)); $srcBaseDirectory = dirname(dirname(__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();
}
}
+208
View File
@@ -0,0 +1,208 @@
<?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)
*/
const TYPE_NUMERIC = 0;
/**
* String (text) cell type
*/
const TYPE_STRING = 1;
/**
* Formula cell type
* Not used at the moment
*/
const TYPE_FORMULA = 2;
/**
* Empty cell type
*/
const TYPE_EMPTY = 3;
/**
* Boolean cell type
*/
const TYPE_BOOLEAN = 4;
/**
* Date cell type
*/
const TYPE_DATE = 5;
/**
* Error cell type
*/
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 $value mixed
* @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;
}
/**
* @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();
}
}
+123
View File
@@ -0,0 +1,123 @@
<?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 isset($this->cells[$cellIndex]) ? $this->cells[$cellIndex] : null;
}
/**
* @param Cell $cell
* @return Row
*/
public function addCell(Cell $cell)
{
$this->cells[] = $cell;
return $this;
}
/**
* @return int
*/
public function getNumCells()
{
return count($this->cells);
}
/**
* @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);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace Box\Spout\Common\Entity\Style;
/**
* Class Border
*/
class Border
{
const LEFT = 'left';
const RIGHT = 'right';
const TOP = 'top';
const BOTTOM = 'bottom';
const STYLE_NONE = 'none';
const STYLE_SOLID = 'solid';
const STYLE_DASHED = 'dashed';
const STYLE_DOTTED = 'dotted';
const STYLE_DOUBLE = 'double';
const WIDTH_THIN = 'thin';
const WIDTH_MEDIUM = 'medium';
const WIDTH_THICK = 'thick';
/** @var array A list of BorderPart objects for this border. */
private $parts = [];
/**
* @param array $borderParts
*/
public function __construct(array $borderParts = [])
{
$this->setParts($borderParts);
}
/**
* @param string $name The name of the border part
* @return BorderPart|null
*/
public function getPart($name)
{
return $this->hasPart($name) ? $this->parts[$name] : null;
}
/**
* @param string $name The name of the border part
* @return bool
*/
public function hasPart($name)
{
return isset($this->parts[$name]);
}
/**
* @return array
*/
public function getParts()
{
return $this->parts;
}
/**
* Set BorderParts
* @param array $parts
* @return void
*/
public function setParts($parts)
{
unset($this->parts);
foreach ($parts as $part) {
$this->addPart($part);
}
}
/**
* @param BorderPart $borderPart
* @return Border
*/
public function addPart(BorderPart $borderPart)
{
$this->parts[$borderPart->getName()] = $borderPart;
return $this;
}
}
@@ -0,0 +1,184 @@
<?php
namespace Box\Spout\Common\Entity\Style;
use Box\Spout\Writer\Exception\Border\InvalidNameException;
use Box\Spout\Writer\Exception\Border\InvalidStyleException;
use Box\Spout\Writer\Exception\Border\InvalidWidthException;
/**
* Class BorderPart
*/
class BorderPart
{
/**
* @var string The style of this border part.
*/
protected $style;
/**
* @var string The name of this border part.
*/
protected $name;
/**
* @var string The color of this border part.
*/
protected $color;
/**
* @var string The width of this border part.
*/
protected $width;
/**
* @var array Allowed style constants for parts.
*/
protected static $allowedStyles = [
'none',
'solid',
'dashed',
'dotted',
'double',
];
/**
* @var array Allowed names constants for border parts.
*/
protected static $allowedNames = [
'left',
'right',
'top',
'bottom',
];
/**
* @var array Allowed width constants for border parts.
*/
protected static $allowedWidths = [
'thin',
'medium',
'thick',
];
/**
* @param string $name @see BorderPart::$allowedNames
* @param string $color A RGB color code
* @param string $width @see BorderPart::$allowedWidths
* @param string $style @see BorderPart::$allowedStyles
* @throws InvalidNameException
* @throws InvalidStyleException
* @throws InvalidWidthException
*/
public function __construct($name, $color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
{
$this->setName($name);
$this->setColor($color);
$this->setWidth($width);
$this->setStyle($style);
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name The name of the border part @see BorderPart::$allowedNames
* @throws InvalidNameException
* @return void
*/
public function setName($name)
{
if (!in_array($name, self::$allowedNames)) {
throw new InvalidNameException($name);
}
$this->name = $name;
}
/**
* @return string
*/
public function getStyle()
{
return $this->style;
}
/**
* @param string $style The style of the border part @see BorderPart::$allowedStyles
* @throws InvalidStyleException
* @return void
*/
public function setStyle($style)
{
if (!in_array($style, self::$allowedStyles)) {
throw new InvalidStyleException($style);
}
$this->style = $style;
}
/**
* @return string
*/
public function getColor()
{
return $this->color;
}
/**
* @param string $color The color of the border part @see Color::rgb()
* @return void
*/
public function setColor($color)
{
$this->color = $color;
}
/**
* @return string
*/
public function getWidth()
{
return $this->width;
}
/**
* @param string $width The width of the border part @see BorderPart::$allowedWidths
* @throws InvalidWidthException
* @return void
*/
public function setWidth($width)
{
if (!in_array($width, self::$allowedWidths)) {
throw new InvalidWidthException($width);
}
$this->width = $width;
}
/**
* @return array
*/
public static function getAllowedStyles()
{
return self::$allowedStyles;
}
/**
* @return array
*/
public static function getAllowedNames()
{
return self::$allowedNames;
}
/**
* @return array
*/
public static function getAllowedWidths()
{
return self::$allowedWidths;
}
}
@@ -1,14 +1,12 @@
<?php <?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 * Class Color
* This class provides constants and functions to work with colors * This class provides constants and functions to work with colors
*
* @package Box\Spout\Writer\Style
*/ */
class Color class Color
{ {
@@ -29,7 +27,6 @@ class Color
/** /**
* Returns an RGB color from R, G and B values * Returns an RGB color from R, G and B values
* *
* @api
* @param int $red Red component, 0 - 255 * @param int $red Red component, 0 - 255
* @param int $green Green component, 0 - 255 * @param int $green Green component, 0 - 255
* @param int $blue Blue component, 0 - 255 * @param int $blue Blue component, 0 - 255
@@ -52,8 +49,8 @@ class Color
* Throws an exception is the color component value is outside of bounds (0 - 255) * Throws an exception is the color component value is outside of bounds (0 - 255)
* *
* @param int $colorComponent * @param int $colorComponent
* @throws \Box\Spout\Common\Exception\InvalidColorException
* @return void * @return void
* @throws \Box\Spout\Writer\Exception\InvalidColorException
*/ */
protected static function throwIfInvalidColorComponentValue($colorComponent) protected static function throwIfInvalidColorComponentValue($colorComponent)
{ {
@@ -1,12 +1,10 @@
<?php <?php
namespace Box\Spout\Writer\Style; namespace Box\Spout\Common\Entity\Style;
/** /**
* Class Style * Class Style
* Represents a style to be applied to a cell * Represents a style to be applied to a cell
*
* @package Box\Spout\Writer\Style
*/ */
class Style class Style
{ {
@@ -16,50 +14,62 @@ class Style
const DEFAULT_FONT_NAME = 'Arial'; const DEFAULT_FONT_NAME = 'Arial';
/** @var int|null Style ID */ /** @var int|null Style ID */
protected $id = null; private $id;
/** @var bool Whether the font should be bold */ /** @var bool Whether the font should be bold */
protected $fontBold = false; private $fontBold = false;
/** @var bool Whether the bold property was set */ /** @var bool Whether the bold property was set */
protected $hasSetFontBold = false; private $hasSetFontBold = false;
/** @var bool Whether the font should be italic */ /** @var bool Whether the font should be italic */
protected $fontItalic = false; private $fontItalic = false;
/** @var bool Whether the italic property was set */ /** @var bool Whether the italic property was set */
protected $hasSetFontItalic = false; private $hasSetFontItalic = false;
/** @var bool Whether the font should be underlined */ /** @var bool Whether the font should be underlined */
protected $fontUnderline = false; private $fontUnderline = false;
/** @var bool Whether the underline property was set */ /** @var bool Whether the underline property was set */
protected $hasSetFontUnderline = false; private $hasSetFontUnderline = false;
/** @var bool Whether the font should be struck through */ /** @var bool Whether the font should be struck through */
protected $fontStrikethrough = false; private $fontStrikethrough = false;
/** @var bool Whether the strikethrough property was set */ /** @var bool Whether the strikethrough property was set */
protected $hasSetFontStrikethrough = false; private $hasSetFontStrikethrough = false;
/** @var int Font size */ /** @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 */ /** @var bool Whether the font size property was set */
protected $hasSetFontSize = false; private $hasSetFontSize = false;
/** @var string Font color */ /** @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 */ /** @var bool Whether the font color property was set */
protected $hasSetFontColor = false; private $hasSetFontColor = false;
/** @var string Font name */ /** @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 */ /** @var bool Whether the font name property was set */
protected $hasSetFontName = false; private $hasSetFontName = false;
/** @var bool Whether specific font properties should be applied */ /** @var bool Whether specific font properties should be applied */
protected $shouldApplyFont = false; private $shouldApplyFont = false;
/** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */ /** @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 */ /** @var bool Whether the wrap text property was set */
protected $hasSetWrapText = false; private $hasSetWrapText = false;
/** @var Border */
private $border;
/** @var bool Whether border properties should be applied */
private $shouldApplyBorder = false;
/** @var string Background color */
private $backgroundColor;
/** @var bool */
private $hasSetBackgroundColor = false;
/** /**
* @return int|null * @return int|null
@@ -76,11 +86,40 @@ class Style
public function setId($id) public function setId($id)
{ {
$this->id = $id; $this->id = $id;
return $this; return $this;
} }
/** /**
* @return boolean * @return Border
*/
public function getBorder()
{
return $this->border;
}
/**
* @param Border $border
* @return Style
*/
public function setBorder(Border $border)
{
$this->shouldApplyBorder = true;
$this->border = $border;
return $this;
}
/**
* @return bool
*/
public function shouldApplyBorder()
{
return $this->shouldApplyBorder;
}
/**
* @return bool
*/ */
public function isFontBold() public function isFontBold()
{ {
@@ -95,11 +134,20 @@ class Style
$this->fontBold = true; $this->fontBold = true;
$this->hasSetFontBold = true; $this->hasSetFontBold = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
return $this; return $this;
} }
/** /**
* @return boolean * @return bool
*/
public function hasSetFontBold()
{
return $this->hasSetFontBold;
}
/**
* @return bool
*/ */
public function isFontItalic() public function isFontItalic()
{ {
@@ -114,11 +162,20 @@ class Style
$this->fontItalic = true; $this->fontItalic = true;
$this->hasSetFontItalic = true; $this->hasSetFontItalic = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
return $this; return $this;
} }
/** /**
* @return boolean * @return bool
*/
public function hasSetFontItalic()
{
return $this->hasSetFontItalic;
}
/**
* @return bool
*/ */
public function isFontUnderline() public function isFontUnderline()
{ {
@@ -133,11 +190,20 @@ class Style
$this->fontUnderline = true; $this->fontUnderline = true;
$this->hasSetFontUnderline = true; $this->hasSetFontUnderline = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
return $this; return $this;
} }
/** /**
* @return boolean * @return bool
*/
public function hasSetFontUnderline()
{
return $this->hasSetFontUnderline;
}
/**
* @return bool
*/ */
public function isFontStrikethrough() public function isFontStrikethrough()
{ {
@@ -152,9 +218,18 @@ class Style
$this->fontStrikethrough = true; $this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true; $this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
return $this; return $this;
} }
/**
* @return bool
*/
public function hasSetFontStrikethrough()
{
return $this->hasSetFontStrikethrough;
}
/** /**
* @return int * @return int
*/ */
@@ -172,9 +247,18 @@ class Style
$this->fontSize = $fontSize; $this->fontSize = $fontSize;
$this->hasSetFontSize = true; $this->hasSetFontSize = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
return $this; return $this;
} }
/**
* @return bool
*/
public function hasSetFontSize()
{
return $this->hasSetFontSize;
}
/** /**
* @return string * @return string
*/ */
@@ -194,9 +278,18 @@ class Style
$this->fontColor = $fontColor; $this->fontColor = $fontColor;
$this->hasSetFontColor = true; $this->hasSetFontColor = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
return $this; return $this;
} }
/**
* @return bool
*/
public function hasSetFontColor()
{
return $this->hasSetFontColor;
}
/** /**
* @return string * @return string
*/ */
@@ -214,11 +307,20 @@ class Style
$this->fontName = $fontName; $this->fontName = $fontName;
$this->hasSetFontName = true; $this->hasSetFontName = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
return $this; return $this;
} }
/** /**
* @return boolean * @return bool
*/
public function hasSetFontName()
{
return $this->hasSetFontName;
}
/**
* @return bool
*/ */
public function shouldWrapText() public function shouldWrapText()
{ {
@@ -226,15 +328,25 @@ class Style
} }
/** /**
* @param bool $shouldWrap Should the text be wrapped
* @return Style * @return Style
*/ */
public function setShouldWrapText() public function setShouldWrapText($shouldWrap = true)
{ {
$this->shouldWrapText = true; $this->shouldWrapText = $shouldWrap;
$this->hasSetWrapText = true; $this->hasSetWrapText = true;
return $this; return $this;
} }
/**
* @return bool
*/
public function hasSetWrapText()
{
return $this->hasSetWrapText;
}
/** /**
* @return bool Whether specific font properties should be applied * @return bool Whether specific font properties should be applied
*/ */
@@ -244,65 +356,31 @@ class Style
} }
/** /**
* Serializes the style for future comparison with other styles. * Sets the background color
* The ID is excluded from the comparison, as we only care about * @param string $color ARGB color (@see Color)
* actual style properties. * @return Style
*
* @return string The serialized style
*/ */
public function serialize() public function setBackgroundColor($color)
{ {
// In order to be able to properly compare style, set static ID value $this->hasSetBackgroundColor = true;
$currentId = $this->id; $this->backgroundColor = $color;
$this->setId(0);
$serializedStyle = serialize($this); return $this;
$this->setId($currentId);
return $serializedStyle;
} }
/** /**
* Merges the current style with the given style, using the given style as a base. This means that: * @return string
* - 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
*/ */
public function mergeWith($baseStyle) public function getBackgroundColor()
{ {
$mergedStyle = clone $this; return $this->backgroundColor;
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->hasSetFontSize && $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();
} }
return $mergedStyle; /**
* @return bool Whether the background color should be applied
*/
public function shouldApplyBackgroundColor()
{
return $this->hasSetBackgroundColor;
} }
} }
-34
View File
@@ -1,34 +0,0 @@
<?php
namespace Box\Spout\Common\Escaper;
/**
* Class ODS
* Provides functions to escape and unescape data for ODS files
*
* @package Box\Spout\Common\Escaper
*/
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)
{
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);
}
}
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class EncodingConversionException * Class EncodingConversionException
*
* @api
* @package Box\Spout\Common\Exception
*/ */
class EncodingConversionException extends SpoutException class EncodingConversionException extends SpoutException
{ {
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class IOException * Class IOException
*
* @api
* @package Box\Spout\Common\Exception
*/ */
class IOException extends SpoutException class IOException extends SpoutException
{ {
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class InvalidArgumentException * Class InvalidArgumentException
*
* @api
* @package Box\Spout\Common\Exception
*/ */
class InvalidArgumentException extends SpoutException class InvalidArgumentException extends SpoutException
{ {
@@ -0,0 +1,10 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class InvalidColorException
*/
class InvalidColorException extends SpoutException
{
}
@@ -5,7 +5,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class SpoutException * Class SpoutException
* *
* @package Box\Spout\Common\Exception
* @abstract * @abstract
*/ */
abstract class SpoutException extends \Exception abstract class SpoutException extends \Exception
@@ -4,9 +4,6 @@ namespace Box\Spout\Common\Exception;
/** /**
* Class UnsupportedTypeException * Class UnsupportedTypeException
*
* @api
* @package Box\Spout\Common\Exception
*/ */
class UnsupportedTypeException extends SpoutException class UnsupportedTypeException extends SpoutException
{ {
@@ -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 $value
* @return bool Whether the given value is considered "empty"
*/
public static function isEmpty($value)
{
return ($value === null || $value === '');
}
/**
* @param $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 $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 $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 $value
* @return bool Whether the given value is a DateTime or DateInterval object
*/
public static function isDateTimeOrDateInterval($value)
{
return (
$value instanceof \DateTime ||
$value instanceof \DateInterval
);
}
}
+5 -7
View File
@@ -7,8 +7,6 @@ use Box\Spout\Common\Exception\EncodingConversionException;
/** /**
* Class EncodingHelper * Class EncodingHelper
* This class provides helper functions to work with encodings. * This class provides helper functions to work with encodings.
*
* @package Box\Spout\Common\Helper
*/ */
class EncodingHelper class EncodingHelper
{ {
@@ -59,7 +57,7 @@ class EncodingHelper
{ {
$byteOffsetToSkipBom = 0; $byteOffsetToSkipBom = 0;
if ($this->hasBom($filePointer, $encoding)) { if ($this->hasBOM($filePointer, $encoding)) {
$bomUsed = $this->supportedEncodingsWithBom[$encoding]; $bomUsed = $this->supportedEncodingsWithBom[$encoding];
// we skip the N first bytes // we skip the N first bytes
@@ -97,8 +95,8 @@ class EncodingHelper
* *
* @param string $string Non UTF-8 string to be converted * @param string $string Non UTF-8 string to be converted
* @param string $sourceEncoding The encoding used to encode the source string * @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 * @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) public function attemptConversionToUTF8($string, $sourceEncoding)
{ {
@@ -110,8 +108,8 @@ class EncodingHelper
* *
* @param string $string UTF-8 string to be converted * @param string $string UTF-8 string to be converted
* @param string $targetEncoding The encoding the string should be re-encoded into * @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 * @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) public function attemptConversionFromUTF8($string, $targetEncoding)
{ {
@@ -125,8 +123,8 @@ class EncodingHelper
* @param string $string string to be converted * @param string $string string to be converted
* @param string $sourceEncoding The encoding used to encode the source string * @param string $sourceEncoding The encoding used to encode the source string
* @param string $targetEncoding The encoding the string should be re-encoded into * @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 * @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) protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
{ {
@@ -139,7 +137,7 @@ class EncodingHelper
if ($this->canUseIconv()) { if ($this->canUseIconv()) {
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding); $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
} else if ($this->canUseMbString()) { } elseif ($this->canUseMbString()) {
$convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding); $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
} else { } else {
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\"."); throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\".");
@@ -1,12 +1,10 @@
<?php <?php
namespace Box\Spout\Common\Escaper; namespace Box\Spout\Common\Helper\Escaper;
/** /**
* Class CSV * Class CSV
* Provides functions to escape and unescape data for CSV files * Provides functions to escape and unescape data for CSV files
*
* @package Box\Spout\Common\Escaper
*/ */
class CSV implements EscaperInterface class CSV implements EscaperInterface
{ {
@@ -1,11 +1,9 @@
<?php <?php
namespace Box\Spout\Common\Escaper; namespace Box\Spout\Common\Helper\Escaper;
/** /**
* Interface EscaperInterface * Interface EscaperInterface
*
* @package Box\Spout\Common\Escaper
*/ */
interface EscaperInterface interface EscaperInterface
{ {
+59
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/", '�', $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;
}
}
@@ -1,24 +1,37 @@
<?php <?php
namespace Box\Spout\Common\Escaper; namespace Box\Spout\Common\Helper\Escaper;
/** /**
* Class XLSX * Class XLSX
* Provides functions to escape and unescape data for XLSX files * Provides functions to escape and unescape data for XLSX files
*
* @package Box\Spout\Common\Escaper
*/ */
class XLSX implements EscaperInterface class XLSX implements EscaperInterface
{ {
/** @var string[] Control characters to be escaped */ /** @var bool Whether the escaper has already been initialized */
protected $controlCharactersEscapingMap; private $isAlreadyInitialized = false;
/** @var string Regex pattern to detect control characters that need to be escaped */
private $escapableControlCharactersPattern;
/** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
private $controlCharactersEscapingMap;
/** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
private $controlCharactersEscapingReverseMap;
/** /**
* * Initializes the control characters if not already done
*/ */
public function __construct() protected function initIfNeeded()
{ {
if (!$this->isAlreadyInitialized) {
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap(); $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
$this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap);
$this->isAlreadyInitialized = true;
}
} }
/** /**
@@ -29,8 +42,12 @@ class XLSX implements EscaperInterface
*/ */
public function escape($string) public function escape($string)
{ {
$this->initIfNeeded();
$escapedString = $this->escapeControlCharacters($string); $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; return $escapedString;
} }
@@ -43,33 +60,54 @@ class XLSX implements EscaperInterface
*/ */
public function unescape($string) public function unescape($string)
{ {
$unescapedString = htmlspecialchars_decode($string, ENT_QUOTES); $this->initIfNeeded();
$unescapedString = $this->unescapeControlCharacters($unescapedString);
// ==============
// = WARNING =
// ==============
// It is assumed that the given string has already had its XML entities decoded.
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
// Therefore there is no need to call "htmlspecialchars_decode()".
$unescapedString = $this->unescapeControlCharacters($string);
return $unescapedString; return $unescapedString;
} }
/**
* @return string Regex pattern containing all escapable control characters
*/
protected function getEscapableControlCharactersPattern()
{
// control characters values are from 0 to 1F (hex values) in the ASCII table
// some characters should not be escaped though: "\t", "\r" and "\n".
return '[\x00-\x08' .
// skipping "\t" (0x9) and "\n" (0xA)
'\x0B-\x0C' .
// skipping "\r" (0xD)
'\x0E-\x1F]';
}
/** /**
* Builds the map containing control characters to be escaped * Builds the map containing control characters to be escaped
* mapped to their escaped values. * mapped to their escaped values.
* "\t", "\r" and "\n" don't need to be escaped. * "\t", "\r" and "\n" don't need to be escaped.
* *
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License) * 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[] * @return string[]
*/ */
protected function getControlCharactersEscapingMap() protected function getControlCharactersEscapingMap()
{ {
$controlCharactersEscapingMap = []; $controlCharactersEscapingMap = [];
$whitelistedControlCharacters = ["\t", "\r", "\n"];
// control characters values are from 0 to 1F (hex values) in the ASCII table // control characters values are from 0 to 1F (hex values) in the ASCII table
for ($charValue = 0x0; $charValue <= 0x1F; $charValue++) { for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) {
if (!in_array(chr($charValue), $whitelistedControlCharacters)) { $character = chr($charValue);
if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
$charHexValue = dechex($charValue); $charHexValue = dechex($charValue);
$escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_'; $escapedChar = '_x' . sprintf('%04s', strtoupper($charHexValue)) . '_';
$controlCharactersEscapingMap[$escapedChar] = chr($charValue); $controlCharactersEscapingMap[$escapedChar] = $character;
} }
} }
@@ -84,7 +122,7 @@ class XLSX implements EscaperInterface
* So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_. * So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
* *
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License) * 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 * @param string $string String to escape
* @return string * @return string
@@ -92,7 +130,15 @@ class XLSX implements EscaperInterface
protected function escapeControlCharacters($string) protected function escapeControlCharacters($string)
{ {
$escapedString = $this->escapeEscapeCharacter($string); $escapedString = $this->escapeEscapeCharacter($string);
return str_replace(array_values($this->controlCharactersEscapingMap), array_keys($this->controlCharactersEscapingMap), $escapedString);
// if no control characters
if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
return $escapedString;
}
return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function ($matches) {
return $this->controlCharactersEscapingReverseMap[$matches[0]];
}, $escapedString);
} }
/** /**
@@ -114,7 +160,7 @@ class XLSX implements EscaperInterface
* So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_" * So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
* *
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License) * 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 * @param string $string String to unescape
* @return string * @return string
@@ -122,6 +168,7 @@ class XLSX implements EscaperInterface
protected function unescapeControlCharacters($string) protected function unescapeControlCharacters($string)
{ {
$unescapedString = $string; $unescapedString = $string;
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) { foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
// only unescape characters that don't contain the escaped escape character for now // 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);
+12 -13
View File
@@ -8,20 +8,18 @@ use Box\Spout\Common\Exception\IOException;
* Class FileSystemHelper * Class FileSystemHelper
* This class provides helper functions to help with the file system operations * This class provides helper functions to help with the file system operations
* like files/folders creation & deletion * like files/folders creation & deletion
*
* @package Box\Spout\Common\Helper
*/ */
class FileSystemHelper class FileSystemHelper implements FileSystemHelperInterface
{ {
/** @var string Path of the base folder where all the I/O can occur */ /** @var string Real path of the base folder where all the I/O can occur */
protected $baseFolderPath; protected $baseFolderRealPath;
/** /**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur * @param string $baseFolderPath The path of the base folder where all the I/O can occur
*/ */
public function __construct($baseFolderPath) public function __construct($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 $parentFolderPath The parent folder path under which the folder is going to be created
* @param string $folderName The name of the folder to create * @param string $folderName The name of the folder to create
* @return string Path of the created folder
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
* @return string Path of the created folder
*/ */
public function createFolder($parentFolderPath, $folderName) public function createFolder($parentFolderPath, $folderName)
{ {
@@ -53,8 +51,8 @@ class FileSystemHelper
* @param string $parentFolderPath The parent folder path where the file is going to be created * @param string $parentFolderPath The parent folder path where the file is going to be created
* @param string $fileName The name of the file to create * @param string $fileName The name of the file to create
* @param string $fileContents The contents of the file to create * @param string $fileContents The contents of the file to create
* @return string Path of the created file
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
* @return string Path of the created file
*/ */
public function createFileWithContents($parentFolderPath, $fileName, $fileContents) public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
{ {
@@ -74,8 +72,8 @@ class FileSystemHelper
* Delete the file at the given path * Delete the file at the given path
* *
* @param string $filePath Path of the file to delete * @param string $filePath Path of the file to delete
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
* @return void
*/ */
public function deleteFile($filePath) public function deleteFile($filePath)
{ {
@@ -90,8 +88,8 @@ class FileSystemHelper
* Delete the folder at the given path as well as all its contents * Delete the folder at the given path as well as all its contents
* *
* @param string $folderPath Path of the folder to delete * @param string $folderPath Path of the folder to delete
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
* @return void
*/ */
public function deleteFolderRecursively($folderPath) public function deleteFolderRecursively($folderPath)
{ {
@@ -119,14 +117,15 @@ class FileSystemHelper
* should occur is not inside the base folder. * should occur is not inside the base folder.
* *
* @param string $operationFolderPath The path of the folder where the I/O operation should occur * @param string $operationFolderPath The path of the folder where the I/O operation should occur
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder
* @return void
*/ */
protected function throwIfOperationNotInBaseFolder($operationFolderPath) protected function throwIfOperationNotInBaseFolder($operationFolderPath)
{ {
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0); $operationFolderRealPath = realpath($operationFolderPath);
$isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
if (!$isInBaseFolder) { if (!$isInBaseFolder) {
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderPath}"); throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
} }
} }
} }
@@ -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);
}
@@ -7,8 +7,6 @@ namespace Box\Spout\Common\Helper;
* This class wraps global functions to facilitate testing * This class wraps global functions to facilitate testing
* *
* @codeCoverageIgnore * @codeCoverageIgnore
*
* @package Box\Spout\Common\Helper
*/ */
class GlobalFunctionsHelper class GlobalFunctionsHelper
{ {
@@ -30,7 +28,7 @@ class GlobalFunctionsHelper
* @see fgets() * @see fgets()
* *
* @param resource $handle * @param resource $handle
* @param int|void $length * @param int|null $length
* @return string * @return string
*/ */
public function fgets($handle, $length = null) public function fgets($handle, $length = null)
@@ -81,14 +79,20 @@ class GlobalFunctionsHelper
* @see fgetcsv() * @see fgetcsv()
* *
* @param resource $handle * @param resource $handle
* @param int|void $length * @param int|null $length
* @param string|void $delimiter * @param string|null $delimiter
* @param string|void $enclosure * @param string|null $enclosure
* @return array * @return array
*/ */
public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null) public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null)
{ {
return fgetcsv($handle, $length, $delimiter, $enclosure); // PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
// To fix that, simply disable the escape character.
// @see https://bugs.php.net/bug.php?id=43225
// @see http://tools.ietf.org/html/rfc4180
$escapeCharacter = "\0";
return fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter);
} }
/** /**
@@ -97,13 +101,19 @@ class GlobalFunctionsHelper
* *
* @param resource $handle * @param resource $handle
* @param array $fields * @param array $fields
* @param string|void $delimiter * @param string|null $delimiter
* @param string|void $enclosure * @param string|null $enclosure
* @return int * @return int
*/ */
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null) 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 = "\0";
return fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter);
} }
/** /**
@@ -165,6 +175,7 @@ class GlobalFunctionsHelper
public function file_get_contents($filePath) public function file_get_contents($filePath)
{ {
$realFilePath = $this->convertToUseRealPath($filePath); $realFilePath = $this->convertToUseRealPath($filePath);
return file_get_contents($realFilePath); return file_get_contents($realFilePath);
} }
@@ -207,7 +218,7 @@ class GlobalFunctionsHelper
* Wrapper around global function feof() * Wrapper around global function feof()
* @see feof() * @see feof()
* *
* @param resource * @param resource $handle
* @return bool * @return bool
*/ */
public function feof($handle) public function feof($handle)
@@ -232,7 +243,7 @@ class GlobalFunctionsHelper
* @see basename() * @see basename()
* *
* @param string $path * @param string $path
* @param string|void $suffix * @param string|null $suffix
* @return string * @return string
*/ */
public function basename($path, $suffix = null) public function basename($path, $suffix = null)
@@ -252,6 +263,19 @@ class GlobalFunctionsHelper
header($string); header($string);
} }
/**
* Wrapper around global function ob_end_clean()
* @see ob_end_clean()
*
* @return void
*/
public function ob_end_clean()
{
if (ob_get_length() > 0) {
ob_end_clean();
}
}
/** /**
* Wrapper around global function iconv() * Wrapper around global function iconv()
* @see iconv() * @see iconv()
@@ -281,32 +305,14 @@ class GlobalFunctionsHelper
} }
/** /**
* Wrapper around global function stream_get_line() * Wrapper around global function stream_get_wrappers()
* @see stream_get_line() * @see stream_get_wrappers()
* *
* @param resource $handle
* @param int $length
* @param string|void $ending
* @return string|bool
*/
public function stream_get_line($handle, $length, $ending = null)
{
return stream_get_line($handle, $length, $ending);
}
/**
* Wrapper around global function str_getcsv()
* @see str_getcsv()
*
* @param string $input
* @param string|void $delimiter
* @param string|void $enclosure
* @param string|void $escape
* @return array * @return array
*/ */
public function str_getcsv($input, $delimiter = null, $enclosure = null, $escape = null) public function stream_get_wrappers()
{ {
return str_getcsv($input, $delimiter, $enclosure, $escape); return stream_get_wrappers();
} }
/** /**
+2 -2
View File
@@ -7,8 +7,6 @@ namespace Box\Spout\Common\Helper;
* This class provides helper functions to work with strings and multibyte strings. * This class provides helper functions to work with strings and multibyte strings.
* *
* @codeCoverageIgnore * @codeCoverageIgnore
*
* @package Box\Spout\Common\Helper
*/ */
class StringHelper class StringHelper
{ {
@@ -50,6 +48,7 @@ class StringHelper
public function getCharFirstOccurrencePosition($char, $string) 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; return ($position !== false) ? $position : -1;
} }
@@ -66,6 +65,7 @@ class StringHelper
public function getCharLastOccurrencePosition($char, $string) 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; return ($position !== false) ? $position : -1;
} }
} }
@@ -0,0 +1,68 @@
<?php
namespace Box\Spout\Common\Manager;
/**
* Class OptionsManager
*/
abstract class OptionsManagerAbstract implements OptionsManagerInterface
{
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;
}
}
@@ -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);
}
-2
View File
@@ -5,8 +5,6 @@ namespace Box\Spout\Common;
/** /**
* Class Type * Class Type
* This class references the supported types * This class references the supported types
*
* @api
*/ */
abstract class Type abstract class Type
{ {
-130
View File
@@ -1,130 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
/**
* Class AbstractReader
*
* @package Box\Spout\Reader
* @abstract
*/
abstract class AbstractReader implements ReaderInterface
{
/** @var bool Indicates whether the stream is currently open */
protected $isStreamOpened = false;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/**
* Opens the file at the given file path to make it ready to be read
*
* @param string $filePath Path of the file to be read
* @return void
*/
abstract protected function openReader($filePath);
/**
* Returns an iterator to iterate over sheets.
*
* @return \Iterator To iterate over sheets
*/
abstract public function getConcreteSheetIterator();
/**
* Closes the reader. To be used after reading the file.
*
* @return AbstractReader
*/
abstract protected function closeReader();
/**
* @param $globalFunctionsHelper
* @return AbstractReader
*/
public function setGlobalFunctionsHelper($globalFunctionsHelper)
{
$this->globalFunctionsHelper = $globalFunctionsHelper;
return $this;
}
/**
* Prepares the reader to read the given file. It also makes sure
* that the file exists and is readable.
*
* @api
* @param string $filePath Path of the file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
*/
public function open($filePath)
{
if (!$this->isPhpStream($filePath)) {
// we skip the checks if the provided file path points to a PHP stream
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
throw new IOException("Could not open $filePath for reading! File does not exist.");
} else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
throw new IOException("Could not open $filePath for reading! File is not readable.");
}
}
try {
// Need to use realpath to fix "Can't open file" on some Windows setup
$fileRealPath = realpath($filePath);
$this->openReader($fileRealPath);
$this->isStreamOpened = true;
} catch (\Exception $exception) {
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
}
}
/**
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
*
* @param string $filePath Path of the file to be read
* @return bool Whether the given path maps to a PHP stream
*/
protected function isPhpStream($filePath)
{
return (strpos($filePath, 'php://') === 0);
}
/**
* Returns an iterator to iterate over sheets.
*
* @api
* @return \Iterator To iterate over sheets
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
*/
public function getSheetIterator()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Reader should be opened first.');
}
return $this->getConcreteSheetIterator();
}
/**
* Closes the reader, preventing any additional reading
*
* @api
* @return void
*/
public function close()
{
if ($this->isStreamOpened) {
$this->closeReader();
$sheetIterator = $this->getConcreteSheetIterator();
if ($sheetIterator) {
$sheetIterator->end();
}
$this->isStreamOpened = false;
}
}
}
@@ -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);
}
}
@@ -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);
}
}
+34 -23
View File
@@ -2,17 +2,16 @@
namespace Box\Spout\Reader\CSV; namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\AbstractReader;
use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\EncodingHelper; use Box\Spout\Reader\Common\Entity\Options;
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
use Box\Spout\Reader\ReaderAbstract;
/** /**
* Class Reader * Class Reader
* This class provides support to read data from a CSV file. * 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 */ /** @var resource Pointer to the file to be written */
protected $filePointer; protected $filePointer;
@@ -20,14 +19,8 @@ class Reader extends AbstractReader
/** @var SheetIterator To iterator over the CSV unique "sheet" */ /** @var SheetIterator To iterator over the CSV unique "sheet" */
protected $sheetIterator; protected $sheetIterator;
/** @var string Defines the character used to delimit fields (one character only) */ /** @var string Original value for the "auto_detect_line_endings" INI value */
protected $fieldDelimiter = ','; protected $originalAutoDetectLineEndings;
/** @var string Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure = '"';
/** @var string Encoding of the CSV file to be read */
protected $encoding = EncodingHelper::ENCODING_UTF8;
/** /**
* Sets the field delimiter for the CSV. * Sets the field delimiter for the CSV.
@@ -38,7 +31,8 @@ class Reader extends AbstractReader
*/ */
public function setFieldDelimiter($fieldDelimiter) public function setFieldDelimiter($fieldDelimiter)
{ {
$this->fieldDelimiter = $fieldDelimiter; $this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
return $this; return $this;
} }
@@ -51,7 +45,8 @@ class Reader extends AbstractReader
*/ */
public function setFieldEnclosure($fieldEnclosure) public function setFieldEnclosure($fieldEnclosure)
{ {
$this->fieldEnclosure = $fieldEnclosure; $this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
return $this; return $this;
} }
@@ -64,30 +59,45 @@ class Reader extends AbstractReader
*/ */
public function setEncoding($encoding) public function setEncoding($encoding)
{ {
$this->encoding = $encoding; $this->optionsManager->setOption(Options::ENCODING, $encoding);
return $this; return $this;
} }
/**
* Returns whether stream wrappers are supported
*
* @return bool
*/
protected function doesSupportStreamWrapper()
{
return true;
}
/** /**
* Opens the file at the given path to make it ready to be read. * Opens the file at the given path to make it ready to be read.
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8. * 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 * @param string $filePath Path of the CSV file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException * @throws \Box\Spout\Common\Exception\IOException
* @return void
*/ */
protected function openReader($filePath) protected function openReader($filePath)
{ {
$this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings');
ini_set('auto_detect_line_endings', '1');
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r'); $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) { if (!$this->filePointer) {
throw new IOException("Could not open file $filePath for reading."); 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->filePointer,
$this->fieldDelimiter, $this->optionsManager,
$this->fieldEnclosure,
$this->encoding,
$this->globalFunctionsHelper $this->globalFunctionsHelper
); );
} }
@@ -97,12 +107,11 @@ class Reader extends AbstractReader
* *
* @return SheetIterator To iterate over sheets * @return SheetIterator To iterate over sheets
*/ */
public function getConcreteSheetIterator() protected function getConcreteSheetIterator()
{ {
return $this->sheetIterator; return $this->sheetIterator;
} }
/** /**
* Closes the reader. To be used after reading the file. * Closes the reader. To be used after reading the file.
* *
@@ -113,5 +122,7 @@ class Reader extends AbstractReader
if ($this->filePointer) { if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer); $this->globalFunctionsHelper->fclose($this->filePointer);
} }
ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
} }
} }
+108 -71
View File
@@ -2,25 +2,33 @@
namespace Box\Spout\Reader\CSV; 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\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 * Class RowIterator
* Iterate over CSV rows. * Iterate over CSV rows.
*
* @package Box\Spout\Reader\CSV
*/ */
class RowIterator implements IteratorInterface class RowIterator implements IteratorInterface
{ {
/**
* Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines).
*/
const MAX_READ_BYTES_PER_LINE = 0;
/** @var resource Pointer to the CSV file to read */ /** @var resource Pointer to the CSV file to read */
protected $filePointer; protected $filePointer;
/** @var int Number of read rows */ /** @var int Number of read rows */
protected $numReadRows = 0; protected $numReadRows = 0;
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */ /** @var Row|null Buffer used to store the current row, while checking if there are more rows to read */
protected $rowDataBuffer = null; protected $rowBuffer;
/** @var bool Indicates whether all rows have been read */ /** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false; protected $hasReachedEndOfFile = false;
@@ -34,36 +42,45 @@ class RowIterator implements IteratorInterface
/** @var string Encoding of the CSV file to be read */ /** @var string Encoding of the CSV file to be read */
protected $encoding; protected $encoding;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ /** @var bool Whether empty rows should be returned or skipped */
protected $globalFunctionsHelper; protected $shouldPreserveEmptyRows;
/** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */ /** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */
protected $encodingHelper; protected $encodingHelper;
/** @var string End of line delimiter, encoded using the same encoding as the CSV */ /** @var \Box\Spout\Reader\CSV\Creator\InternalEntityFactory Factory to create entities */
protected $encodedEOLDelimiter; protected $entityFactory;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** /**
* @param resource $filePointer Pointer to the CSV file to read * @param resource $filePointer Pointer to the CSV file to read
* @param string $fieldDelimiter Character that delimits fields * @param OptionsManagerInterface $optionsManager
* @param string $fieldEnclosure Character that enclose fields * @param EncodingHelper $encodingHelper
* @param string $encoding Encoding of the CSV file to be read * @param InternalEntityFactory $entityFactory
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper * @param GlobalFunctionsHelper $globalFunctionsHelper
*/ */
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper) public function __construct(
{ $filePointer,
OptionsManagerInterface $optionsManager,
EncodingHelper $encodingHelper,
InternalEntityFactory $entityFactory,
GlobalFunctionsHelper $globalFunctionsHelper
) {
$this->filePointer = $filePointer; $this->filePointer = $filePointer;
$this->fieldDelimiter = $fieldDelimiter; $this->fieldDelimiter = $optionsManager->getOption(Options::FIELD_DELIMITER);
$this->fieldEnclosure = $fieldEnclosure; $this->fieldEnclosure = $optionsManager->getOption(Options::FIELD_ENCLOSURE);
$this->encoding = $encoding; $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->globalFunctionsHelper = $globalFunctionsHelper;
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
} }
/** /**
* Rewind the Iterator to the first element * 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 * @return void
*/ */
@@ -72,7 +89,7 @@ class RowIterator implements IteratorInterface
$this->rewindAndSkipBom(); $this->rewindAndSkipBom();
$this->numReadRows = 0; $this->numReadRows = 0;
$this->rowDataBuffer = null; $this->rowBuffer = null;
$this->next(); $this->next();
} }
@@ -93,9 +110,9 @@ class RowIterator implements IteratorInterface
/** /**
* Checks if current position is valid * 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()
{ {
@@ -103,77 +120,97 @@ class RowIterator implements IteratorInterface
} }
/** /**
* Move forward to next element. Empty rows are skipped. * Move forward to next element. Reads data for the next unprocessed row.
* @link http://php.net/manual/en/iterator.next.php * @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 * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void
*/ */
public function next() public function next()
{ {
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if ($this->hasReachedEndOfFile) { if (!$this->hasReachedEndOfFile) {
return; $this->readDataForNextRow();
}
} }
/**
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void
*/
protected function readDataForNextRow()
{
do { do {
$lineData = false; $rowData = $this->getNextUTF8EncodedRow();
$utf8EncodedLineData = $this->getNextUTF8EncodedLine(); } while ($this->shouldReadNextRow($rowData));
if ($utf8EncodedLineData !== false) {
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
}
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
} while (($lineData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($lineData));
if ($lineData !== false) { if ($rowData !== false) {
$this->rowDataBuffer = $lineData; // str_replace will replace NULL values by empty strings
$rowDataBufferAsArray = str_replace(null, null, $rowData);
$this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
$this->numReadRows++; $this->numReadRows++;
} else { } else {
// If we reach this point, it means end of file was reached. // If we reach this point, it means end of file was reached.
// This happens when the last lines are empty lines. // This happens when the last lines are empty lines.
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile; $this->hasReachedEndOfFile = true;
} }
} }
/** /**
* Returns the next line, converted if necessary to UTF-8. * @param array|bool $currentRowData
* Neither fgets nor fgetcsv don't work with non UTF-8 data... so we need to do some things manually. * @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 string|false The next line 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 * @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 getNextUTF8EncodedLine() protected function getNextUTF8EncodedRow()
{ {
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding. $encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter(); if ($encodedRowData === false) {
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, 0, $encodedEOLDelimiter); return false;
}
// If the line could have been read, it can be converted to UTF-8 foreach ($encodedRowData as $cellIndex => $cellValue) {
$utf8EncodedLineData = ($encodedLineData !== false) ? switch ($this->encoding) {
$this->encodingHelper->attemptConversionToUTF8($encodedLineData, $this->encoding) : case EncodingHelper::ENCODING_UTF16_LE:
false; case EncodingHelper::ENCODING_UTF32_LE:
// remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
$cellValue = ltrim($cellValue);
break;
return $utf8EncodedLineData; case EncodingHelper::ENCODING_UTF16_BE:
case EncodingHelper::ENCODING_UTF32_BE:
// remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
$cellValue = rtrim($cellValue);
break;
}
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
}
return $encodedRowData;
} }
/** /**
* Returns the end of line delimiter, encoded using the same encoding as the CSV. * @param array|bool $lineData Array containing the cells value for the line
* The return value is cached.
*
* @return string
*/
protected function getEncodedEOLDelimiter()
{
if (!isset($this->encodedEOLDelimiter)) {
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8("\n", $this->encoding);
}
return $this->encodedEOLDelimiter;
}
/**
* @param array $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty * @return bool Whether the given line is empty
*/ */
protected function isEmptyLine($lineData) protected function isEmptyLine($lineData)
@@ -183,18 +220,18 @@ class RowIterator implements IteratorInterface
/** /**
* Return the current element from the buffer * 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()
{ {
return $this->rowDataBuffer; return $this->rowBuffer;
} }
/** /**
* Return the key of the current element * 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 * @return int
*/ */
+37 -12
View File
@@ -6,32 +6,57 @@ use Box\Spout\Reader\SheetInterface;
/** /**
* Class Sheet * Class Sheet
*
* @package Box\Spout\Reader\CSV
*/ */
class Sheet implements SheetInterface class Sheet implements SheetInterface
{ {
/** @var RowIterator To iterate over the CSV's rows */ /** @var \Box\Spout\Reader\CSV\RowIterator To iterate over the CSV's rows */
protected $rowIterator; protected $rowIterator;
/** /**
* @param resource $filePointer Pointer to the CSV file to read * @param RowIterator $rowIterator Corresponding row iterator
* @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
*/ */
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper) public function __construct(RowIterator $rowIterator)
{ {
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper); $this->rowIterator = $rowIterator;
} }
/** /**
* @api * @return \Box\Spout\Reader\CSV\RowIterator
* @return RowIterator
*/ */
public function getRowIterator() public function getRowIterator()
{ {
return $this->rowIterator; 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;
}
} }
+11 -17
View File
@@ -7,32 +7,26 @@ use Box\Spout\Reader\IteratorInterface;
/** /**
* Class SheetIterator * Class SheetIterator
* Iterate over CSV unique "sheet". * Iterate over CSV unique "sheet".
*
* @package Box\Spout\Reader\CSV
*/ */
class SheetIterator implements IteratorInterface class SheetIterator implements IteratorInterface
{ {
/** @var Sheet The CSV unique "sheet" */ /** @var \Box\Spout\Reader\CSV\Sheet The CSV unique "sheet" */
protected $sheet; protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */ /** @var bool Whether the unique "sheet" has already been read */
protected $hasReadUniqueSheet = false; protected $hasReadUniqueSheet = false;
/** /**
* @param resource $filePointer * @param Sheet $sheet Corresponding unique sheet
* @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
*/ */
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper) public function __construct($sheet)
{ {
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper); $this->sheet = $sheet;
} }
/** /**
* Rewind the Iterator to the first element * 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 * @return void
*/ */
@@ -43,9 +37,9 @@ class SheetIterator implements IteratorInterface
/** /**
* Checks if current position is valid * 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()
{ {
@@ -54,7 +48,7 @@ class SheetIterator implements IteratorInterface
/** /**
* Move forward to next element * 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 * @return void
*/ */
@@ -65,9 +59,9 @@ class SheetIterator implements IteratorInterface
/** /**
* Return the current element * Return the current element
* @link http://php.net/manual/en/iterator.current.php * @see http://php.net/manual/en/iterator.current.php
* *
* @return Sheet * @return \Box\Spout\Reader\CSV\Sheet
*/ */
public function current() public function current()
{ {
@@ -76,7 +70,7 @@ class SheetIterator implements IteratorInterface
/** /**
* Return the key of the current element * 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 * @return int
*/ */
@@ -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);
}
@@ -0,0 +1,68 @@
<?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
}
}
/**
* 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
}
}
/**
* 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
}
}
}
@@ -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);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Box\Spout\Reader\Common\Entity;
/**
* Class Options
* Readers' options holder
*/
abstract class Options
{
// Common options
const SHOULD_FORMAT_DATES = 'shouldFormatDates';
const SHOULD_PRESERVE_EMPTY_ROWS = 'shouldPreserveEmptyRows';
// CSV specific options
const FIELD_DELIMITER = 'fieldDelimiter';
const FIELD_ENCLOSURE = 'fieldEnclosure';
const ENCODING = 'encoding';
// XLSX specific options
const TEMP_FOLDER = 'tempFolder';
const SHOULD_USE_1904_DATES = 'shouldUse1904Dates';
}
@@ -0,0 +1,65 @@
<?php
namespace Box\Spout\Reader\Common\Manager;
use Box\Spout\Common\Entity\Row;
use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
/**
* Class RowManager
*/
class RowManager
{
/** @var InternalEntityFactoryInterface Factory to create entities */
private $entityFactory;
/**
* @param InternalEntityFactoryInterface $entityFactory Factory to create entities
*/
public function __construct(InternalEntityFactoryInterface $entityFactory)
{
$this->entityFactory = $entityFactory;
}
/**
* Detect whether a row is considered empty.
* An empty row has all of its cells empty.
*
* @param Row $row
* @return bool
*/
public function isEmpty(Row $row)
{
foreach ($row->getCells() as $cell) {
if (!$cell->isEmpty()) {
return false;
}
}
return true;
}
/**
* Fills the missing indexes of a row with empty cells.
*
* @param Row $row
* @return Row
*/
public function fillMissingIndexesWithEmptyCells(Row $row)
{
if ($row->getNumCells() === 0) {
return $row;
}
$rowCells = $row->getCells();
$maxCellIndex = max(array_keys($rowCells));
for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) {
if (!isset($rowCells[$cellIndex])) {
$row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
}
}
return $row;
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
namespace Box\Spout\Reader\Common;
use Box\Spout\Reader\Wrapper\XMLReader;
/**
* Class XMLProcessor
* Helps process XML files
*/
class XMLProcessor
{
/* Node types */
const NODE_TYPE_START = XMLReader::ELEMENT;
const NODE_TYPE_END = XMLReader::END_ELEMENT;
/* Keys associated to reflection attributes to invoke a callback */
const CALLBACK_REFLECTION_METHOD = 'reflectionMethod';
const CALLBACK_REFLECTION_OBJECT = 'reflectionObject';
/* Values returned by the callbacks to indicate what the processor should do next */
const PROCESSING_CONTINUE = 1;
const PROCESSING_STOP = 2;
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader;
/** @var array Registered callbacks */
private $callbacks = [];
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object
*/
public function __construct($xmlReader)
{
$this->xmlReader = $xmlReader;
}
/**
* @param string $nodeName A callback may be triggered when a node with this name is read
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* @param callable $callback Callback to execute when the read node has the given name and type
* @return XMLProcessor
*/
public function registerCallback($nodeName, $nodeType, $callback)
{
$callbackKey = $this->getCallbackKey($nodeName, $nodeType);
$this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback);
return $this;
}
/**
* @param string $nodeName Name of the node
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* @return string Key used to store the associated callback
*/
private function getCallbackKey($nodeName, $nodeType)
{
return "$nodeName$nodeType";
}
/**
* Because the callback can be a "protected" function, we don't want to use call_user_func() directly
* but instead invoke the callback using Reflection. This allows the invocation of "protected" functions.
* Since some functions can be called a lot, we pre-process the callback to only return the elements that
* will be needed to invoke the callback later.
*
* @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME]
* @return array Associative array containing the elements needed to invoke the callback using Reflection
*/
private function getInvokableCallbackData($callback)
{
$callbackObject = $callback[0];
$callbackMethodName = $callback[1];
$reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName);
$reflectionMethod->setAccessible(true);
return [
self::CALLBACK_REFLECTION_METHOD => $reflectionMethod,
self::CALLBACK_REFLECTION_OBJECT => $callbackObject,
];
}
/**
* Resumes the reading of the XML file where it was left off.
* Stops whenever a callback indicates that reading should stop or at the end of the file.
*
* @throws \Box\Spout\Reader\Exception\XMLProcessingException
* @return void
*/
public function readUntilStopped()
{
while ($this->xmlReader->read()) {
$nodeType = $this->xmlReader->nodeType;
$nodeNamePossiblyWithPrefix = $this->xmlReader->name;
$nodeNameWithoutPrefix = $this->xmlReader->localName;
$callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType);
if ($callbackData !== null) {
$callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]);
if ($callbackResponse === self::PROCESSING_STOP) {
// stop reading
break;
}
}
}
}
/**
* @param string $nodeNamePossiblyWithPrefix Name of the node, possibly prefixed
* @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* @return array|null Callback data to be used for execution when a node of the given name/type is read or NULL if none found
*/
private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType)
{
// With prefixed nodes, we should match if (by order of preference):
// 1. the callback was registered with the prefixed node name (e.g. "x:worksheet")
// 2. the callback was registered with the un-prefixed node name (e.g. "worksheet")
$callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType);
$callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType);
$hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix);
$callbackKeyToUse = $callbackKeyForUnPrefixedName;
if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) {
$callbackKeyToUse = $callbackKeyForPossiblyPrefixedName;
}
// Using isset here because it is way faster than array_key_exists...
return isset($this->callbacks[$callbackKeyToUse]) ? $this->callbacks[$callbackKeyToUse] : null;
}
/**
* @param array $callbackData Associative array containing data to invoke the callback using Reflection
* @param array $args Arguments to pass to the callback
* @return int Callback response
*/
private function invokeCallback($callbackData, $args)
{
$reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
$callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT];
return $reflectionMethod->invokeArgs($callbackObject, $args);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Box\Spout\Reader\Exception;
use Throwable;
/**
* Class InvalidValueException
*/
class InvalidValueException extends ReaderException
{
/** @var mixed */
private $invalidValue;
/**
* @param mixed $invalidValue
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($invalidValue, $message = '', $code = 0, Throwable $previous = null)
{
$this->invalidValue = $invalidValue;
parent::__construct($message, $code, $previous);
}
/**
* @return mixed
*/
public function getInvalidValue()
{
return $this->invalidValue;
}
}
Loaded 100 of 333 files, more files were not shown because too many files have changed in this diff. Show more