First commit to gh-pages branch
This commit is contained in:
parent
4e6db6a8a1
commit
7777955577
@ -1,15 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[{.travis.yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
11
.gitattributes
vendored
11
.gitattributes
vendored
@ -1,11 +0,0 @@
|
||||
# Ignore all test and documentation for archive
|
||||
/tests export-ignore
|
||||
/.editorconfig export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.scrutinizer.yml export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/composer.lock export-ignore
|
||||
/CONTRIBUTING.md export-ignore
|
||||
/logo.png export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +0,0 @@
|
||||
/tests/resources/generated
|
||||
/tests/coverage
|
||||
/vendor
|
||||
/.idea
|
||||
*.iml
|
||||
@ -1,57 +0,0 @@
|
||||
filter:
|
||||
excluded_paths: [vendor/*, tests/*]
|
||||
|
||||
tools:
|
||||
external_code_coverage:
|
||||
timeout: 600 # Wait 10 minutes for results
|
||||
runs: 3 # Merge results for 5.4, 5.5 and 5.6 jobs
|
||||
php_mess_detector: true
|
||||
php_code_sniffer:
|
||||
config:
|
||||
standard: PSR4
|
||||
filter:
|
||||
paths: ['src']
|
||||
sensiolabs_security_checker: true
|
||||
php_pdepend: true
|
||||
php_loc:
|
||||
enabled: true
|
||||
filter:
|
||||
paths: ['src']
|
||||
php_cpd: false # Must be disabled to use php_sim instead
|
||||
php_sim:
|
||||
enabled: true
|
||||
filter:
|
||||
paths: ['src']
|
||||
|
||||
build_failure_conditions:
|
||||
- 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9
|
||||
- 'project.metric_change("scrutinizer.test_coverage", < -0.005)' # Code Coverage decreased by more than 0.5%
|
||||
- 'project.metric("scrutinizer.test_coverage", < 0.97)' # Code Coverage drops below 97%
|
||||
|
||||
checks:
|
||||
php:
|
||||
remove_extra_empty_lines: true
|
||||
remove_php_closing_tag: true
|
||||
remove_trailing_whitespace: true
|
||||
fix_use_statements:
|
||||
remove_unused: true
|
||||
preserve_multiple: false
|
||||
preserve_blanklines: true
|
||||
fix_php_opening_tag: true
|
||||
fix_linefeed: true
|
||||
fix_line_ending: true
|
||||
fix_identation_4spaces: true
|
||||
fix_doc_comments: true
|
||||
uppercase_constants: true
|
||||
use_self_instead_of_fqcn: true
|
||||
simplify_boolean_return: true
|
||||
return_doc_comments: true
|
||||
return_doc_comment_if_not_inferrable: true
|
||||
phpunit_assertions: true
|
||||
parameters_in_camelcaps: true
|
||||
parameter_doc_comments: true
|
||||
param_doc_comment_if_not_inferrable: true
|
||||
optional_parameters_at_the_end: true
|
||||
newline_at_end_of_file: true
|
||||
encourage_single_quotes: true
|
||||
|
||||
23
.travis.yml
23
.travis.yml
@ -1,23 +0,0 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- hhvm
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
install:
|
||||
- composer install --no-interaction --prefer-source
|
||||
|
||||
script:
|
||||
- mkdir -p build/logs
|
||||
- php vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover
|
||||
|
||||
after_script:
|
||||
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi
|
||||
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi
|
||||
@ -1,75 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
All contributions are welcome to this project.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at:
|
||||
|
||||
http://opensource.box.com/cla
|
||||
|
||||
To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement).
|
||||
|
||||
## How to contribute
|
||||
|
||||
* **File an issue** - if you found a bug, want to request an enhancement, or want to implement something (bug fix or feature).
|
||||
* **Send a pull request** - if you want to contribute code. Please be sure to file an issue first.
|
||||
|
||||
## Pull request best practices
|
||||
|
||||
We want to accept your pull requests. Please follow these steps:
|
||||
|
||||
### Step 1: File an issue
|
||||
|
||||
Before writing any code, please file an issue stating the problem you want to solve or the feature you want to implement. This allows us to give you feedback before you spend any time writing code. There may be a known limitation that can't be addressed, or a bug that has already been fixed in a different way. The issue allows us to communicate and figure out if it's worth your time to write a bunch of code for the project.
|
||||
|
||||
### Step 2: Fork this repository in GitHub
|
||||
|
||||
This will create your own copy of our repository.
|
||||
|
||||
### Step 3: Add the upstream source
|
||||
|
||||
The upstream source is the project under the Box organization on GitHub. To add an upstream source for this project, type:
|
||||
|
||||
```
|
||||
git remote add upstream git@github.com:box/spout.git
|
||||
```
|
||||
|
||||
This will come in useful later.
|
||||
|
||||
### Step 4: Create a feature branch
|
||||
|
||||
Create a branch with a descriptive name, such as `add-search`.
|
||||
|
||||
### Step 5: Push your feature branch to your fork
|
||||
|
||||
As you develop code, continue to push code to your remote feature branch. Please make sure to include the issue number you're addressing in your commit message, such as:
|
||||
|
||||
```
|
||||
git commit -m "Adding search (fixes #123)"
|
||||
```
|
||||
|
||||
This helps us out by allowing us to track which issue your commit relates to.
|
||||
|
||||
Keep a separate feature branch for each issue you want to address.
|
||||
|
||||
### Step 6: Rebase
|
||||
|
||||
Before sending a pull request, rebase against upstream, such as:
|
||||
|
||||
```
|
||||
git fetch upstream
|
||||
git rebase upstream/master
|
||||
```
|
||||
|
||||
This will add your changes on top of what's already in upstream, minimizing merge issues.
|
||||
|
||||
### Step 7: Run the tests
|
||||
|
||||
Make sure that all tests are passing before submitting a pull request.
|
||||
|
||||
### Step 8: Send the pull request
|
||||
|
||||
Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did.
|
||||
|
||||
Keep in mind that we like to see one issue addressed per pull request, as this helps keep our git history clean and we can more easily track down issues.
|
||||
166
LICENSE
166
LICENSE
@ -1,166 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
407
README.md
407
README.md
@ -1,407 +0,0 @@
|
||||
# Spout
|
||||
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
[](http://opensource.box.com/badges)
|
||||
[](https://travis-ci.org/box/spout)
|
||||
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
||||
[](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.
|
||||
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: [](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
## Installation
|
||||
|
||||
### Composer (recommended)
|
||||
|
||||
Spout can be installed directly from [Composer](https://getcomposer.org/).
|
||||
|
||||
Run the following command:
|
||||
```
|
||||
$ composer require box/spout
|
||||
```
|
||||
|
||||
### Manual installation
|
||||
|
||||
If you can't use Composer, no worries! You can still install Spout manually.
|
||||
|
||||
> Before starting, make sure your system meets the [requirements](#requirements).
|
||||
|
||||
1. Download the source code from the [Releases page](https://github.com/box/spout/releases)
|
||||
2. Extract the downloaded content into your project.
|
||||
3. Add this code to the top controller (index.php) or wherever it may be more appropriate:
|
||||
```php
|
||||
require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path!
|
||||
```
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* PHP version 5.4.0 or higher
|
||||
* PHP extension `php_zip` enabled
|
||||
* PHP extension `php_xmlreader` enabled
|
||||
|
||||
|
||||
## Basic usage
|
||||
|
||||
### Reader
|
||||
|
||||
Regardless of the file type, the interface to read a file is always the same:
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\ReaderFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$reader = ReaderFactory::create(Type::XLSX); // for XLSX files
|
||||
//$reader = ReaderFactory::create(Type::CSV); // for CSV files
|
||||
//$reader = ReaderFactory::create(Type::ODS); // for ODS files
|
||||
|
||||
$reader->open($filePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
// do stuff with the row
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
```
|
||||
|
||||
If there are multiple sheets in the file, the reader will read all of them sequentially.
|
||||
|
||||
### Writer
|
||||
|
||||
As with the reader, there is one common interface to write data to a file:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX); // for XLSX files
|
||||
//$writer = WriterFactory::create(Type::CSV); // for CSV files
|
||||
//$writer = WriterFactory::create(Type::ODS); // for ODS files
|
||||
|
||||
$writer->openToFile($filePath); // write data to a file or to a PHP stream
|
||||
//$writer->openToBrowser($fileName); // stream data directly to the browser
|
||||
|
||||
$writer->addRow($singleRow); // add a row at a time
|
||||
$writer->addRows($multipleRows); // add multiple rows at a time
|
||||
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
For XLSX and ODS files, the number of rows per sheet is limited to 1,048,576. By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
|
||||
|
||||
|
||||
## Advanced usage
|
||||
|
||||
If you are looking for how to perform some common, more advanced tasks with Spout, please take a look at the [Wiki](https://github.com/box/spout/wiki). It contains code snippets, ready to be used.
|
||||
|
||||
### Configuring the CSV reader and writer
|
||||
|
||||
It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
|
||||
```php
|
||||
use Box\Spout\Reader\ReaderFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->setFieldDelimiter('|');
|
||||
$reader->setFieldEnclosure('@');
|
||||
$reader->setEndOfLineCharacter("\r");
|
||||
```
|
||||
|
||||
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
|
||||
```php
|
||||
$reader->setEncoding('UTF-16LE');
|
||||
```
|
||||
|
||||
By default, the writer generates CSV files encoded in UTF-8, with a BOM.
|
||||
It is however possible to not include the BOM:
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::CSV);
|
||||
$writer->setShouldAddBOM(false);
|
||||
```
|
||||
|
||||
|
||||
### Configuring the XLSX and ODS readers and writers
|
||||
|
||||
#### Row styling
|
||||
|
||||
It is possible to apply some formatting options to a row. Spout supports fonts, background, borders as well as alignment styles.
|
||||
|
||||
```php
|
||||
use Box\Spout\Common\Type;
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Writer\Style\StyleBuilder;
|
||||
use Box\Spout\Writer\Style\Color;
|
||||
|
||||
$style = (new StyleBuilder())
|
||||
->setFontBold()
|
||||
->setFontSize(15)
|
||||
->setFontColor(Color::BLUE)
|
||||
->setShouldWrapText()
|
||||
->setBackgroundColor(Color::YELLOW)
|
||||
->build();
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->openToFile($filePath);
|
||||
|
||||
$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row
|
||||
$writer->addRow($otherSingleRow); // no style will be applied
|
||||
$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows
|
||||
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
Adding borders to a row requires a ```Border``` object.
|
||||
|
||||
```php
|
||||
use Box\Spout\Common\Type;
|
||||
use Box\Spout\Writer\Style\Border;
|
||||
use Box\Spout\Writer\Style\BorderBuilder;
|
||||
use Box\Spout\Writer\Style\Color;
|
||||
use Box\Spout\Writer\Style\StyleBuilder;
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
|
||||
$border = (new BorderBuilder())
|
||||
->setBorderBottom(Color::GREEN, Border::WIDTH_THIN, Border::STYLE_DASHED)
|
||||
->build();
|
||||
|
||||
$style = (new StyleBuilder())
|
||||
->setBorder($border)
|
||||
->build();
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->openToFile($filePath);
|
||||
|
||||
$writer->addRowWithStyle(['Border Bottom Green Thin Dashed'], $style);
|
||||
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
Spout will use a default style for all created rows. This style can be overridden this way:
|
||||
|
||||
```php
|
||||
$defaultStyle = (new StyleBuilder())
|
||||
->setFontName('Arial')
|
||||
->setFontSize(11)
|
||||
->build();
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setDefaultRowStyle($defaultStyle)
|
||||
->openToFile($filePath);
|
||||
```
|
||||
|
||||
Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
|
||||
|
||||
| Category | Property | API
|
||||
|-----------|---------------|---------------------------------------
|
||||
| Font | Bold | `StyleBuilder::setFontBold()`
|
||||
| | Italic | `StyleBuilder::setFontItalic()`
|
||||
| | Underline | `StyleBuilder::setFontUnderline()`
|
||||
| | Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
||||
| | Font name | `StyleBuilder::setFontName('Arial')`
|
||||
| | Font size | `StyleBuilder::setFontSize(14)`
|
||||
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
|
||||
| Alignment | Wrap text | `StyleBuilder::setShouldWrapText(true|false)`
|
||||
|
||||
#### New sheet creation
|
||||
|
||||
It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::ODS);
|
||||
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
|
||||
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
|
||||
```
|
||||
|
||||
#### Using custom temporary folder
|
||||
|
||||
Processing XLSX and ODS files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer:
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setTempFolder($customTempFolderPath);
|
||||
```
|
||||
|
||||
#### Strings storage (XLSX writer)
|
||||
|
||||
XLSX files support different ways to store the string values:
|
||||
* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored)
|
||||
* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process
|
||||
|
||||
In order to keep the memory usage really low, Spout does not optimize strings when using shared strings. It is nevertheless possible to use this mode.
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setShouldUseInlineStrings(true); // default (and recommended) value
|
||||
$writer->setShouldUseInlineStrings(false); // will use shared strings
|
||||
```
|
||||
|
||||
> ##### Note on Apple Numbers and iOS support
|
||||
>
|
||||
> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings!
|
||||
|
||||
|
||||
#### Date/Time formatting
|
||||
|
||||
When reading a spreadsheet containing dates or times, Spout returns the values by default as DateTime objects.
|
||||
It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet.
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\ReaderFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->setShouldFormatDates(false); // default value
|
||||
$reader->setShouldFormatDates(true); // will return formatted dates
|
||||
```
|
||||
|
||||
### Playing with sheets
|
||||
|
||||
When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet:
|
||||
```php
|
||||
$firstSheet = $writer->getCurrentSheet();
|
||||
$writer->addRow($rowForSheet1); // writes the row to the first sheet
|
||||
|
||||
$newSheet = $writer->addNewSheetAndMakeItCurrent();
|
||||
$writer->addRow($rowForSheet2); // writes the row to the new sheet
|
||||
|
||||
$writer->setCurrentSheet($firstSheet);
|
||||
$writer->addRow($anotherRowForSheet1); // append the row to the first sheet
|
||||
```
|
||||
|
||||
It is also possible to retrieve all the sheets currently created:
|
||||
```php
|
||||
$sheets = $writer->getSheets();
|
||||
```
|
||||
|
||||
If you rely on the sheet's name in your application, you can access it and customize it this way:
|
||||
```php
|
||||
// Accessing the sheet name when reading
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
$sheetName = $sheet->getName();
|
||||
}
|
||||
|
||||
// Accessing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheetName = $sheet->getName();
|
||||
|
||||
// Customizing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheet->setName('My custom name');
|
||||
```
|
||||
|
||||
> Please note that Excel has some restrictions on the sheet's name:
|
||||
> * it must not be blank
|
||||
> * it must not exceed 31 characters
|
||||
> * it must not contain these characters: \ / ? * : [ or ]
|
||||
> * it must not start or end with a single quote
|
||||
> * it must be unique
|
||||
>
|
||||
> Handling these restrictions is the developer's responsibility. Spout does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in.
|
||||
|
||||
Finally, it is possible to know which sheet was active when the spreadsheet was last saved. This can be useful if you are only interested in processing the one sheet that was last focused.
|
||||
```php
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
// only process data for the active sheet
|
||||
if ($sheet->isActive()) {
|
||||
// do something...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
On the `master` branch, only unit and functional tests are included. The performance tests require very large files and have been excluded.
|
||||
If you just want to check that everything is working as expected, executing the tests of the `master` branch is enough.
|
||||
|
||||
If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output:
|
||||
|
||||
* `phpunit` - runs the whole test suite (unit + functional + performance tests)
|
||||
* `phpunit --exclude-group perf-tests` - only runs the unit and functional tests
|
||||
* `phpunit --group perf-tests` - only runs the performance tests
|
||||
|
||||
For information, the performance tests take about 30 minutes to run (processing 1 million rows files is not a quick thing).
|
||||
|
||||
> Performance tests status: [](https://travis-ci.org/box/spout)
|
||||
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
#### How can Spout handle such large data sets and still use less than 3MB of memory?
|
||||
|
||||
When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
|
||||
|
||||
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access.
|
||||
|
||||
#### How long does it take to generate a file with X rows?
|
||||
|
||||
Here are a few numbers regarding the performance of Spout:
|
||||
|
||||
| Type | Action | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
|
||||
|------|-------------------------------|--------------------------|------------------------------|----------------------------------|
|
||||
| CSV | Read | < 1 second | 4 seconds | 2-3 minutes |
|
||||
| | Write | < 1 second | 2 seconds | 2-3 minutes |
|
||||
| XLSX | Read<br>*inline strings* | < 1 second | 35-40 seconds | 18-20 minutes |
|
||||
| | Read<br>*shared 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
|
||||
|
||||
Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject.
|
||||
|
||||
You can also ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
|
||||
[](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
## Copyright and License
|
||||
|
||||
Copyright 2017 Box, Inc. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "box/spout",
|
||||
"description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
|
||||
"type": "library",
|
||||
"keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"],
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://www.github.com/box/spout",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Adrien Loison",
|
||||
"email": "adrien@box.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"ext-zip": "*",
|
||||
"ext-xmlreader" : "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
|
||||
"ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Box\\Spout\\": "src/Spout"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.8.x-dev"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
980
composer.lock
generated
980
composer.lock
generated
@ -1,980 +0,0 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "8957b9da742e28d7250c02fca8f9a5a7",
|
||||
"content-hash": "973b8a4a1d8c520dd99fcd32cb5e022f",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "doctrine/instantiator",
|
||||
"version": "1.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/instantiator.git",
|
||||
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
|
||||
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3,<8.0-DEV"
|
||||
},
|
||||
"require-dev": {
|
||||
"athletic/athletic": "~0.1.8",
|
||||
"ext-pdo": "*",
|
||||
"ext-phar": "*",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"squizlabs/php_codesniffer": "~2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marco Pivetta",
|
||||
"email": "ocramius@gmail.com",
|
||||
"homepage": "http://ocramius.github.com/"
|
||||
}
|
||||
],
|
||||
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
|
||||
"homepage": "https://github.com/doctrine/instantiator",
|
||||
"keywords": [
|
||||
"constructor",
|
||||
"instantiate"
|
||||
],
|
||||
"time": "2015-06-14 21:17:01"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
"version": "2.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
|
||||
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
|
||||
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"suggest": {
|
||||
"dflydev/markdown": "~1.0",
|
||||
"erusev/parsedown": "~1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"phpDocumentor": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike van Riel",
|
||||
"email": "mike.vanriel@naenius.com"
|
||||
}
|
||||
],
|
||||
"time": "2015-02-03 12:10:50"
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
"version": "v1.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpspec/prophecy.git",
|
||||
"reference": "3c91bdf81797d725b14cb62906f9a4ce44235972"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972",
|
||||
"reference": "3c91bdf81797d725b14cb62906f9a4ce44235972",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/instantiator": "^1.0.2",
|
||||
"php": "^5.3|^7.0",
|
||||
"phpdocumentor/reflection-docblock": "~2.0",
|
||||
"sebastian/comparator": "~1.1",
|
||||
"sebastian/recursion-context": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/phpspec": "~2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Prophecy\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Kudryashov",
|
||||
"email": "ever.zet@gmail.com",
|
||||
"homepage": "http://everzet.com"
|
||||
},
|
||||
{
|
||||
"name": "Marcello Duarte",
|
||||
"email": "marcello.duarte@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Highly opinionated mocking framework for PHP 5.3+",
|
||||
"homepage": "https://github.com/phpspec/prophecy",
|
||||
"keywords": [
|
||||
"Double",
|
||||
"Dummy",
|
||||
"fake",
|
||||
"mock",
|
||||
"spy",
|
||||
"stub"
|
||||
],
|
||||
"time": "2016-02-15 07:46:21"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "2.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
|
||||
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3",
|
||||
"phpunit/php-file-iterator": "~1.3",
|
||||
"phpunit/php-text-template": "~1.2",
|
||||
"phpunit/php-token-stream": "~1.3",
|
||||
"sebastian/environment": "^1.3.2",
|
||||
"sebastian/version": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-xdebug": ">=2.1.4",
|
||||
"phpunit/phpunit": "~4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-dom": "*",
|
||||
"ext-xdebug": ">=2.2.1",
|
||||
"ext-xmlwriter": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sb@sebastian-bergmann.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
|
||||
"homepage": "https://github.com/sebastianbergmann/php-code-coverage",
|
||||
"keywords": [
|
||||
"coverage",
|
||||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2015-10-06 15:47:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
"version": "1.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
|
||||
"reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
|
||||
"reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sb@sebastian-bergmann.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "FilterIterator implementation that filters files based on a list of suffixes.",
|
||||
"homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
|
||||
"keywords": [
|
||||
"filesystem",
|
||||
"iterator"
|
||||
],
|
||||
"time": "2015-06-21 13:08:43"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-text-template",
|
||||
"version": "1.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-text-template.git",
|
||||
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
|
||||
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Simple template engine.",
|
||||
"homepage": "https://github.com/sebastianbergmann/php-text-template/",
|
||||
"keywords": [
|
||||
"template"
|
||||
],
|
||||
"time": "2015-06-21 13:50:34"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-timer",
|
||||
"version": "1.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-timer.git",
|
||||
"reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260",
|
||||
"reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4|~5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sb@sebastian-bergmann.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Utility class for timing",
|
||||
"homepage": "https://github.com/sebastianbergmann/php-timer/",
|
||||
"keywords": [
|
||||
"timer"
|
||||
],
|
||||
"time": "2016-05-12 18:03:57"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-token-stream",
|
||||
"version": "1.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
|
||||
"reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
|
||||
"reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-tokenizer": "*",
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
}
|
||||
],
|
||||
"description": "Wrapper around PHP's tokenizer extension.",
|
||||
"homepage": "https://github.com/sebastianbergmann/php-token-stream/",
|
||||
"keywords": [
|
||||
"tokenizer"
|
||||
],
|
||||
"time": "2015-09-15 10:49:45"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "4.8.26",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "fc1d8cd5b5de11625979125c5639347896ac2c74"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74",
|
||||
"reference": "fc1d8cd5b5de11625979125c5639347896ac2c74",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-reflection": "*",
|
||||
"ext-spl": "*",
|
||||
"php": ">=5.3.3",
|
||||
"phpspec/prophecy": "^1.3.1",
|
||||
"phpunit/php-code-coverage": "~2.1",
|
||||
"phpunit/php-file-iterator": "~1.4",
|
||||
"phpunit/php-text-template": "~1.2",
|
||||
"phpunit/php-timer": "^1.0.6",
|
||||
"phpunit/phpunit-mock-objects": "~2.3",
|
||||
"sebastian/comparator": "~1.1",
|
||||
"sebastian/diff": "~1.2",
|
||||
"sebastian/environment": "~1.3",
|
||||
"sebastian/exporter": "~1.2",
|
||||
"sebastian/global-state": "~1.0",
|
||||
"sebastian/version": "~1.0",
|
||||
"symfony/yaml": "~2.1|~3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"phpunit/php-invoker": "~1.1"
|
||||
},
|
||||
"bin": [
|
||||
"phpunit"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.8.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "The PHP Unit Testing framework.",
|
||||
"homepage": "https://phpunit.de/",
|
||||
"keywords": [
|
||||
"phpunit",
|
||||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2016-05-17 03:09:28"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit-mock-objects",
|
||||
"version": "2.3.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
|
||||
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
|
||||
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/instantiator": "^1.0.2",
|
||||
"php": ">=5.3.3",
|
||||
"phpunit/php-text-template": "~1.2",
|
||||
"sebastian/exporter": "~1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-soap": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sb@sebastian-bergmann.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Mock Object library for PHPUnit",
|
||||
"homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
|
||||
"keywords": [
|
||||
"mock",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2015-10-02 06:51:40"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "937efb279bd37a375bcadf584dec0726f84dbf22"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22",
|
||||
"reference": "937efb279bd37a375bcadf584dec0726f84dbf22",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3",
|
||||
"sebastian/diff": "~1.2",
|
||||
"sebastian/exporter": "~1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jeff Welch",
|
||||
"email": "whatthejeff@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Volker Dusch",
|
||||
"email": "github@wallbash.com"
|
||||
},
|
||||
{
|
||||
"name": "Bernhard Schussek",
|
||||
"email": "bschussek@2bepublished.at"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
}
|
||||
],
|
||||
"description": "Provides the functionality to compare PHP values for equality",
|
||||
"homepage": "http://www.github.com/sebastianbergmann/comparator",
|
||||
"keywords": [
|
||||
"comparator",
|
||||
"compare",
|
||||
"equality"
|
||||
],
|
||||
"time": "2015-07-26 15:48:44"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/diff",
|
||||
"version": "1.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/diff.git",
|
||||
"reference": "13edfd8706462032c2f52b4b862974dd46b71c9e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e",
|
||||
"reference": "13edfd8706462032c2f52b4b862974dd46b71c9e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.8"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kore Nordmann",
|
||||
"email": "mail@kore-nordmann.de"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
}
|
||||
],
|
||||
"description": "Diff implementation",
|
||||
"homepage": "https://github.com/sebastianbergmann/diff",
|
||||
"keywords": [
|
||||
"diff"
|
||||
],
|
||||
"time": "2015-12-08 07:14:41"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/environment",
|
||||
"version": "1.3.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/environment.git",
|
||||
"reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716",
|
||||
"reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
}
|
||||
],
|
||||
"description": "Provides functionality to handle HHVM/PHP environments",
|
||||
"homepage": "http://www.github.com/sebastianbergmann/environment",
|
||||
"keywords": [
|
||||
"Xdebug",
|
||||
"environment",
|
||||
"hhvm"
|
||||
],
|
||||
"time": "2016-05-17 03:18:57"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/exporter",
|
||||
"version": "1.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/exporter.git",
|
||||
"reference": "7ae5513327cb536431847bcc0c10edba2701064e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e",
|
||||
"reference": "7ae5513327cb536431847bcc0c10edba2701064e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3",
|
||||
"sebastian/recursion-context": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jeff Welch",
|
||||
"email": "whatthejeff@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Volker Dusch",
|
||||
"email": "github@wallbash.com"
|
||||
},
|
||||
{
|
||||
"name": "Bernhard Schussek",
|
||||
"email": "bschussek@2bepublished.at"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
},
|
||||
{
|
||||
"name": "Adam Harvey",
|
||||
"email": "aharvey@php.net"
|
||||
}
|
||||
],
|
||||
"description": "Provides the functionality to export PHP variables for visualization",
|
||||
"homepage": "http://www.github.com/sebastianbergmann/exporter",
|
||||
"keywords": [
|
||||
"export",
|
||||
"exporter"
|
||||
],
|
||||
"time": "2015-06-21 07:55:53"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/global-state",
|
||||
"version": "1.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/global-state.git",
|
||||
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
|
||||
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-uopz": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
}
|
||||
],
|
||||
"description": "Snapshotting of global state",
|
||||
"homepage": "http://www.github.com/sebastianbergmann/global-state",
|
||||
"keywords": [
|
||||
"global state"
|
||||
],
|
||||
"time": "2015-10-12 03:26:01"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/recursion-context",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/recursion-context.git",
|
||||
"reference": "913401df809e99e4f47b27cdd781f4a258d58791"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791",
|
||||
"reference": "913401df809e99e4f47b27cdd781f4a258d58791",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jeff Welch",
|
||||
"email": "whatthejeff@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de"
|
||||
},
|
||||
{
|
||||
"name": "Adam Harvey",
|
||||
"email": "aharvey@php.net"
|
||||
}
|
||||
],
|
||||
"description": "Provides functionality to recursively process PHP variables",
|
||||
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
|
||||
"time": "2015-11-11 19:50:13"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/version",
|
||||
"version": "1.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/version.git",
|
||||
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
|
||||
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
|
||||
"homepage": "https://github.com/sebastianbergmann/version",
|
||||
"time": "2015-06-21 13:59:46"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v2.8.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/e4fbcc65f90909c999ac3b4dfa699ee6563a9940",
|
||||
"reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.8-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Yaml\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Yaml Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2016-03-29 19:00:15"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=5.4.0",
|
||||
"ext-zip": "*",
|
||||
"ext-xmlreader": "*",
|
||||
"ext-simplexml": "*"
|
||||
},
|
||||
"platform-dev": []
|
||||
}
|
||||
25
phpunit.xml
25
phpunit.xml
@ -1,25 +0,0 @@
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.3/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="false"
|
||||
convertWarningsToExceptions="false"
|
||||
verbose="false">
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="all-tests">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">src/</directory>
|
||||
<exclude>
|
||||
<directory>src/Spout/Autoloader</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
</phpunit>
|
||||
@ -1,150 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Autoloader;
|
||||
|
||||
/**
|
||||
* Class Psr4Autoloader
|
||||
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
|
||||
*
|
||||
* @package Box\Spout\Autoloader
|
||||
*/
|
||||
class Psr4Autoloader
|
||||
{
|
||||
/**
|
||||
* An associative array where the key is a namespace prefix and the value
|
||||
* is an array of base directories for classes in that namespace.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $prefixes = array();
|
||||
|
||||
/**
|
||||
* Register loader with SPL autoloader stack.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a base directory for a namespace prefix.
|
||||
*
|
||||
* @param string $prefix The namespace prefix.
|
||||
* @param string $baseDir A base directory for class files in the
|
||||
* namespace.
|
||||
* @param bool $prepend If true, prepend the base directory to the stack
|
||||
* instead of appending it; this causes it to be searched first rather
|
||||
* than last.
|
||||
* @return void
|
||||
*/
|
||||
public function addNamespace($prefix, $baseDir, $prepend = false)
|
||||
{
|
||||
// normalize namespace prefix
|
||||
$prefix = trim($prefix, '\\') . '\\';
|
||||
|
||||
// normalize the base directory with a trailing separator
|
||||
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
|
||||
|
||||
// initialize the namespace prefix array
|
||||
if (isset($this->prefixes[$prefix]) === false) {
|
||||
$this->prefixes[$prefix] = array();
|
||||
}
|
||||
|
||||
// retain the base directory for the namespace prefix
|
||||
if ($prepend) {
|
||||
array_unshift($this->prefixes[$prefix], $baseDir);
|
||||
} else {
|
||||
array_push($this->prefixes[$prefix], $baseDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the class file for a given class name.
|
||||
*
|
||||
* @param string $class The fully-qualified class name.
|
||||
* @return mixed The mapped file name on success, or boolean false on
|
||||
* failure.
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
// the current namespace prefix
|
||||
$prefix = $class;
|
||||
|
||||
// work backwards through the namespace names of the fully-qualified
|
||||
// class name to find a mapped file name
|
||||
while (false !== $pos = strrpos($prefix, '\\')) {
|
||||
|
||||
// retain the trailing namespace separator in the prefix
|
||||
$prefix = substr($class, 0, $pos + 1);
|
||||
|
||||
// the rest is the relative class name
|
||||
$relativeClass = substr($class, $pos + 1);
|
||||
|
||||
// try to load a mapped file for the prefix and relative class
|
||||
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
|
||||
if ($mappedFile !== false) {
|
||||
return $mappedFile;
|
||||
}
|
||||
|
||||
// remove the trailing namespace separator for the next iteration
|
||||
// of strrpos()
|
||||
$prefix = rtrim($prefix, '\\');
|
||||
}
|
||||
|
||||
// never found a mapped file
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the mapped file for a namespace prefix and relative class.
|
||||
*
|
||||
* @param string $prefix The namespace prefix.
|
||||
* @param string $relativeClass The relative class name.
|
||||
* @return mixed Boolean false if no mapped file can be loaded, or the
|
||||
* name of the mapped file that was loaded.
|
||||
*/
|
||||
protected function loadMappedFile($prefix, $relativeClass)
|
||||
{
|
||||
// are there any base directories for this namespace prefix?
|
||||
if (isset($this->prefixes[$prefix]) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// look through base directories for this namespace prefix
|
||||
foreach ($this->prefixes[$prefix] as $baseDir) {
|
||||
|
||||
// replace the namespace prefix with the base directory,
|
||||
// replace namespace separators with directory separators
|
||||
// in the relative class name, append with .php
|
||||
$file = $baseDir
|
||||
. str_replace('\\', '/', $relativeClass)
|
||||
. '.php';
|
||||
|
||||
// if the mapped file exists, require it
|
||||
if ($this->requireFile($file)) {
|
||||
// yes, we're done
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// never found it
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a file exists, require it from the file system.
|
||||
*
|
||||
* @param string $file The file to require.
|
||||
* @return bool True if the file exists, false if not.
|
||||
*/
|
||||
protected function requireFile($file)
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Autoloader;
|
||||
|
||||
require_once 'Psr4Autoloader.php';
|
||||
|
||||
/**
|
||||
* @var string $srcBaseDirectory
|
||||
* Full path to "src/Spout" which is what we want "Box\Spout" to map to.
|
||||
*/
|
||||
$srcBaseDirectory = dirname(dirname(__FILE__));
|
||||
|
||||
$loader = new Psr4Autoloader();
|
||||
$loader->register();
|
||||
$loader->addNamespace('Box\Spout', $srcBaseDirectory);
|
||||
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Escaper;
|
||||
|
||||
/**
|
||||
* Class CSV
|
||||
* Provides functions to escape and unescape data for CSV files
|
||||
*
|
||||
* @package Box\Spout\Common\Escaper
|
||||
*/
|
||||
class CSV implements EscaperInterface
|
||||
{
|
||||
/**
|
||||
* Escapes the given string to make it compatible with CSV
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $string The string to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
public function escape($string)
|
||||
{
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes the given string to make it compatible with CSV
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $string The string to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
public function unescape($string)
|
||||
{
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Escaper;
|
||||
|
||||
/**
|
||||
* Interface EscaperInterface
|
||||
*
|
||||
* @package Box\Spout\Common\Escaper
|
||||
*/
|
||||
interface EscaperInterface
|
||||
{
|
||||
/**
|
||||
* Escapes the given string to make it compatible with PHP
|
||||
*
|
||||
* @param string $string The string to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
public function escape($string);
|
||||
|
||||
/**
|
||||
* Unescapes the given string to make it compatible with PHP
|
||||
*
|
||||
* @param string $string The string to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
public function unescape($string);
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Escaper;
|
||||
|
||||
use Box\Spout\Common\Singleton;
|
||||
|
||||
/**
|
||||
* Class ODS
|
||||
* Provides functions to escape and unescape data for ODS files
|
||||
*
|
||||
* @package Box\Spout\Common\Escaper
|
||||
*/
|
||||
class ODS implements EscaperInterface
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* Escapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
public function escape($string)
|
||||
{
|
||||
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_NOQUOTES | ENT_DISALLOWED);
|
||||
} else {
|
||||
// We are on hhvm or any other engine that does not support ENT_DISALLOWED.
|
||||
//
|
||||
// @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded.
|
||||
// Single and double quotes can be left as is.
|
||||
$escapedString = htmlspecialchars($string, ENT_NOQUOTES);
|
||||
|
||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||
// some characters should not be escaped though: "\t", "\r" and "\n".
|
||||
$regexPattern = '[\x00-\x08' .
|
||||
// skipping "\t" (0x9) and "\n" (0xA)
|
||||
'\x0B-\x0C' .
|
||||
// skipping "\r" (0xD)
|
||||
'\x0E-\x1F]';
|
||||
$replacedString = preg_replace("/$regexPattern/", '<27>', $escapedString);
|
||||
}
|
||||
|
||||
return $replacedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
public function unescape($string)
|
||||
{
|
||||
// ==============
|
||||
// = WARNING =
|
||||
// ==============
|
||||
// It is assumed that the given string has already had its XML entities decoded.
|
||||
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
|
||||
// Therefore there is no need to call "htmlspecialchars_decode()".
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Escaper;
|
||||
|
||||
use Box\Spout\Common\Singleton;
|
||||
|
||||
/**
|
||||
* Class XLSX
|
||||
* Provides functions to escape and unescape data for XLSX files
|
||||
*
|
||||
* @package Box\Spout\Common\Escaper
|
||||
*/
|
||||
class XLSX implements EscaperInterface
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/** @var string Regex pattern to detect control characters that need to be escaped */
|
||||
protected $escapableControlCharactersPattern;
|
||||
|
||||
/** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
|
||||
protected $controlCharactersEscapingMap;
|
||||
|
||||
/** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
|
||||
protected $controlCharactersEscapingReverseMap;
|
||||
|
||||
/**
|
||||
* Initializes the singleton instance
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
|
||||
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
|
||||
$this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
public function escape($string)
|
||||
{
|
||||
$escapedString = $this->escapeControlCharacters($string);
|
||||
// @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded.
|
||||
// Single and double quotes can be left as is.
|
||||
$escapedString = htmlspecialchars($escapedString, ENT_NOQUOTES);
|
||||
|
||||
return $escapedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
public function unescape($string)
|
||||
{
|
||||
// ==============
|
||||
// = WARNING =
|
||||
// ==============
|
||||
// It is assumed that the given string has already had its XML entities decoded.
|
||||
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
|
||||
// Therefore there is no need to call "htmlspecialchars_decode()".
|
||||
$unescapedString = $this->unescapeControlCharacters($string);
|
||||
|
||||
return $unescapedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Regex pattern containing all escapable control characters
|
||||
*/
|
||||
protected function getEscapableControlCharactersPattern()
|
||||
{
|
||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||
// some characters should not be escaped though: "\t", "\r" and "\n".
|
||||
return '[\x00-\x08' .
|
||||
// skipping "\t" (0x9) and "\n" (0xA)
|
||||
'\x0B-\x0C' .
|
||||
// skipping "\r" (0xD)
|
||||
'\x0E-\x1F]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the map containing control characters to be escaped
|
||||
* mapped to their escaped values.
|
||||
* "\t", "\r" and "\n" don't need to be escaped.
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getControlCharactersEscapingMap()
|
||||
{
|
||||
$controlCharactersEscapingMap = [];
|
||||
|
||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||
for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) {
|
||||
$character = chr($charValue);
|
||||
if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
|
||||
$charHexValue = dechex($charValue);
|
||||
$escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
|
||||
$controlCharactersEscapingMap[$escapedChar] = $character;
|
||||
}
|
||||
}
|
||||
|
||||
return $controlCharactersEscapingMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PHP control characters from the given string to OpenXML escaped control characters
|
||||
*
|
||||
* Excel escapes control characters with _xHHHH_ and also escapes any
|
||||
* literal strings of that type by encoding the leading underscore.
|
||||
* So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @param string $string String to escape
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeControlCharacters($string)
|
||||
{
|
||||
$escapedString = $this->escapeEscapeCharacter($string);
|
||||
|
||||
// if no control characters
|
||||
if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
|
||||
return $escapedString;
|
||||
}
|
||||
|
||||
return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function($matches) {
|
||||
return $this->controlCharactersEscapingReverseMap[$matches[0]];
|
||||
}, $escapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the escape character: "_x0000_" -> "_x005F_x0000_"
|
||||
*
|
||||
* @param string $string String to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
protected function escapeEscapeCharacter($string)
|
||||
{
|
||||
return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts OpenXML escaped control characters from the given string to PHP control characters
|
||||
*
|
||||
* Excel escapes control characters with _xHHHH_ and also escapes any
|
||||
* literal strings of that type by encoding the leading underscore.
|
||||
* So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @param string $string String to unescape
|
||||
* @return string
|
||||
*/
|
||||
protected function unescapeControlCharacters($string)
|
||||
{
|
||||
$unescapedString = $string;
|
||||
|
||||
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
|
||||
// only unescape characters that don't contain the escaped escape character for now
|
||||
$unescapedString = preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
|
||||
}
|
||||
|
||||
return $this->unescapeEscapeCharacter($unescapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unecapes the escape character: "_x005F_x0000_" => "_x0000_"
|
||||
*
|
||||
* @param string $string String to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
protected function unescapeEscapeCharacter($string)
|
||||
{
|
||||
return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class EncodingConversionException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class EncodingConversionException extends SpoutException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class IOException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class IOException extends SpoutException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class InvalidArgumentException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class InvalidArgumentException extends SpoutException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class SpoutException
|
||||
*
|
||||
* @package Box\Spout\Common\Exception
|
||||
* @abstract
|
||||
*/
|
||||
abstract class SpoutException extends \Exception
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class UnsupportedTypeException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class UnsupportedTypeException extends SpoutException
|
||||
{
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper;
|
||||
|
||||
use Box\Spout\Common\Exception\EncodingConversionException;
|
||||
|
||||
/**
|
||||
* Class EncodingHelper
|
||||
* This class provides helper functions to work with encodings.
|
||||
*
|
||||
* @package Box\Spout\Common\Helper
|
||||
*/
|
||||
class EncodingHelper
|
||||
{
|
||||
/** Definition of the encodings that can have a BOM */
|
||||
const ENCODING_UTF8 = 'UTF-8';
|
||||
const ENCODING_UTF16_LE = 'UTF-16LE';
|
||||
const ENCODING_UTF16_BE = 'UTF-16BE';
|
||||
const ENCODING_UTF32_LE = 'UTF-32LE';
|
||||
const ENCODING_UTF32_BE = 'UTF-32BE';
|
||||
|
||||
/** Definition of the BOMs for the different encodings */
|
||||
const BOM_UTF8 = "\xEF\xBB\xBF";
|
||||
const BOM_UTF16_LE = "\xFF\xFE";
|
||||
const BOM_UTF16_BE = "\xFE\xFF";
|
||||
const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
|
||||
const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */
|
||||
protected $supportedEncodingsWithBom;
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($globalFunctionsHelper)
|
||||
{
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
|
||||
$this->supportedEncodingsWithBom = [
|
||||
self::ENCODING_UTF8 => self::BOM_UTF8,
|
||||
self::ENCODING_UTF16_LE => self::BOM_UTF16_LE,
|
||||
self::ENCODING_UTF16_BE => self::BOM_UTF16_BE,
|
||||
self::ENCODING_UTF32_LE => self::BOM_UTF32_LE,
|
||||
self::ENCODING_UTF32_BE => self::BOM_UTF32_BE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes to use as offset in order to skip the BOM.
|
||||
*
|
||||
* @param resource $filePointer Pointer to the file to check
|
||||
* @param string $encoding Encoding of the file to check
|
||||
* @return int Bytes offset to apply to skip the BOM (0 means no BOM)
|
||||
*/
|
||||
public function getBytesOffsetToSkipBOM($filePointer, $encoding)
|
||||
{
|
||||
$byteOffsetToSkipBom = 0;
|
||||
|
||||
if ($this->hasBOM($filePointer, $encoding)) {
|
||||
$bomUsed = $this->supportedEncodingsWithBom[$encoding];
|
||||
|
||||
// we skip the N first bytes
|
||||
$byteOffsetToSkipBom = strlen($bomUsed);
|
||||
}
|
||||
|
||||
return $byteOffsetToSkipBom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the file identified by the given pointer has a BOM.
|
||||
*
|
||||
* @param resource $filePointer Pointer to the file to check
|
||||
* @param string $encoding Encoding of the file to check
|
||||
* @return bool TRUE if the file has a BOM, FALSE otherwise
|
||||
*/
|
||||
protected function hasBOM($filePointer, $encoding)
|
||||
{
|
||||
$hasBOM = false;
|
||||
|
||||
$this->globalFunctionsHelper->rewind($filePointer);
|
||||
|
||||
if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
|
||||
$potentialBom = $this->supportedEncodingsWithBom[$encoding];
|
||||
$numBytesInBom = strlen($potentialBom);
|
||||
|
||||
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
|
||||
}
|
||||
|
||||
return $hasBOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to convert a non UTF-8 string into UTF-8.
|
||||
*
|
||||
* @param string $string Non UTF-8 string to be converted
|
||||
* @param string $sourceEncoding The encoding used to encode the source string
|
||||
* @return string The converted, UTF-8 string
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
|
||||
*/
|
||||
public function attemptConversionToUTF8($string, $sourceEncoding)
|
||||
{
|
||||
return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to convert a UTF-8 string into the given encoding.
|
||||
*
|
||||
* @param string $string UTF-8 string to be converted
|
||||
* @param string $targetEncoding The encoding the string should be re-encoded into
|
||||
* @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
|
||||
*/
|
||||
public function attemptConversionFromUTF8($string, $targetEncoding)
|
||||
{
|
||||
return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to convert the given string to the given encoding.
|
||||
* Depending on what is installed on the server, we will try to iconv or mbstring.
|
||||
*
|
||||
* @param string $string string to be converted
|
||||
* @param string $sourceEncoding The encoding used to encode the source string
|
||||
* @param string $targetEncoding The encoding the string should be re-encoded into
|
||||
* @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
|
||||
*/
|
||||
protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
|
||||
{
|
||||
// if source and target encodings are the same, it's a no-op
|
||||
if ($sourceEncoding === $targetEncoding) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
$convertedString = null;
|
||||
|
||||
if ($this->canUseIconv()) {
|
||||
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
|
||||
} else if ($this->canUseMbString()) {
|
||||
$convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
|
||||
} else {
|
||||
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\".");
|
||||
}
|
||||
|
||||
if ($convertedString === false) {
|
||||
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed.");
|
||||
}
|
||||
|
||||
return $convertedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether "iconv" can be used.
|
||||
*
|
||||
* @return bool TRUE if "iconv" is available and can be used, FALSE otherwise
|
||||
*/
|
||||
protected function canUseIconv()
|
||||
{
|
||||
return $this->globalFunctionsHelper->function_exists('iconv');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether "mb_string" functions can be used.
|
||||
* These functions come with the PHP Intl package.
|
||||
*
|
||||
* @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise
|
||||
*/
|
||||
protected function canUseMbString()
|
||||
{
|
||||
return $this->globalFunctionsHelper->function_exists('mb_convert_encoding');
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
|
||||
/**
|
||||
* Class FileSystemHelper
|
||||
* This class provides helper functions to help with the file system operations
|
||||
* like files/folders creation & deletion
|
||||
*
|
||||
* @package Box\Spout\Common\Helper
|
||||
*/
|
||||
class FileSystemHelper
|
||||
{
|
||||
/** @var string Real path of the base folder where all the I/O can occur */
|
||||
protected $baseFolderRealPath;
|
||||
|
||||
/**
|
||||
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
|
||||
*/
|
||||
public function __construct($baseFolderPath)
|
||||
{
|
||||
$this->baseFolderRealPath = realpath($baseFolderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
*/
|
||||
public function createFolder($parentFolderPath, $folderName)
|
||||
{
|
||||
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
||||
|
||||
$folderPath = $parentFolderPath . '/' . $folderName;
|
||||
|
||||
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
|
||||
if (!$wasCreationSuccessful) {
|
||||
throw new IOException("Unable to create folder: $folderPath");
|
||||
}
|
||||
|
||||
return $folderPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
*/
|
||||
public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
|
||||
{
|
||||
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
||||
|
||||
$filePath = $parentFolderPath . '/' . $fileName;
|
||||
|
||||
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
|
||||
if ($wasCreationSuccessful === false) {
|
||||
throw new IOException("Unable to create file: $filePath");
|
||||
}
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the file at the given path
|
||||
*
|
||||
* @param string $filePath Path of the file to delete
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
|
||||
*/
|
||||
public function deleteFile($filePath)
|
||||
{
|
||||
$this->throwIfOperationNotInBaseFolder($filePath);
|
||||
|
||||
if (file_exists($filePath) && is_file($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the folder at the given path as well as all its contents
|
||||
*
|
||||
* @param string $folderPath Path of the folder to delete
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
|
||||
*/
|
||||
public function deleteFolderRecursively($folderPath)
|
||||
{
|
||||
$this->throwIfOperationNotInBaseFolder($folderPath);
|
||||
|
||||
$itemIterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($itemIterator as $item) {
|
||||
if ($item->isDir()) {
|
||||
rmdir($item->getPathname());
|
||||
} else {
|
||||
unlink($item->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* All I/O operations must occur inside the base folder, for security reasons.
|
||||
* This function will throw an exception if the folder where the I/O operation
|
||||
* should occur is not inside the base folder.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
protected function throwIfOperationNotInBaseFolder($operationFolderPath)
|
||||
{
|
||||
$operationFolderRealPath = realpath($operationFolderPath);
|
||||
$isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
|
||||
if (!$isInBaseFolder) {
|
||||
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,318 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class GlobalFunctionsHelper
|
||||
* This class wraps global functions to facilitate testing
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @package Box\Spout\Common\Helper
|
||||
*/
|
||||
class GlobalFunctionsHelper
|
||||
{
|
||||
/**
|
||||
* Wrapper around global function fopen()
|
||||
* @see fopen()
|
||||
*
|
||||
* @param string $fileName
|
||||
* @param string $mode
|
||||
* @return resource|bool
|
||||
*/
|
||||
public function fopen($fileName, $mode)
|
||||
{
|
||||
return fopen($fileName, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fgets()
|
||||
* @see fgets()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int|void $length
|
||||
* @return string
|
||||
*/
|
||||
public function fgets($handle, $length = null)
|
||||
{
|
||||
return fgets($handle, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fputs()
|
||||
* @see fputs()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param string $string
|
||||
* @return int
|
||||
*/
|
||||
public function fputs($handle, $string)
|
||||
{
|
||||
return fputs($handle, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fflush()
|
||||
* @see fflush()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return bool
|
||||
*/
|
||||
public function fflush($handle)
|
||||
{
|
||||
return fflush($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fseek()
|
||||
* @see fseek()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $offset
|
||||
* @return int
|
||||
*/
|
||||
public function fseek($handle, $offset)
|
||||
{
|
||||
return fseek($handle, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fgetcsv()
|
||||
* @see fgetcsv()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int|void $length
|
||||
* @param string|void $delimiter
|
||||
* @param string|void $enclosure
|
||||
* @return array
|
||||
*/
|
||||
public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null)
|
||||
{
|
||||
return fgetcsv($handle, $length, $delimiter, $enclosure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fputcsv()
|
||||
* @see fputcsv()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param array $fields
|
||||
* @param string|void $delimiter
|
||||
* @param string|void $enclosure
|
||||
* @return int
|
||||
*/
|
||||
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
|
||||
{
|
||||
return fputcsv($handle, $fields, $delimiter, $enclosure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fwrite()
|
||||
* @see fwrite()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param string $string
|
||||
* @return int
|
||||
*/
|
||||
public function fwrite($handle, $string)
|
||||
{
|
||||
return fwrite($handle, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fclose()
|
||||
* @see fclose()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return bool
|
||||
*/
|
||||
public function fclose($handle)
|
||||
{
|
||||
return fclose($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function rewind()
|
||||
* @see rewind()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return bool
|
||||
*/
|
||||
public function rewind($handle)
|
||||
{
|
||||
return rewind($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function file_exists()
|
||||
* @see file_exists()
|
||||
*
|
||||
* @param string $fileName
|
||||
* @return bool
|
||||
*/
|
||||
public function file_exists($fileName)
|
||||
{
|
||||
return file_exists($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function file_get_contents()
|
||||
* @see file_get_contents()
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
public function file_get_contents($filePath)
|
||||
{
|
||||
$realFilePath = $this->convertToUseRealPath($filePath);
|
||||
return file_get_contents($realFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given file path to use a real path.
|
||||
* This is to avoid issues on some Windows setup.
|
||||
*
|
||||
* @param string $filePath File path
|
||||
* @return string The file path using a real path
|
||||
*/
|
||||
protected function convertToUseRealPath($filePath)
|
||||
{
|
||||
$realFilePath = $filePath;
|
||||
|
||||
if ($this->isZipStream($filePath)) {
|
||||
if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
|
||||
$documentPath = $matches[1];
|
||||
$documentInsideZipPath = $matches[2];
|
||||
$realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath;
|
||||
}
|
||||
} else {
|
||||
$realFilePath = realpath($filePath);
|
||||
}
|
||||
|
||||
return $realFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given path is a zip stream.
|
||||
*
|
||||
* @param string $path Path pointing to a document
|
||||
* @return bool TRUE if path is a zip stream, FALSE otherwise
|
||||
*/
|
||||
protected function isZipStream($path)
|
||||
{
|
||||
return (strpos($path, 'zip://') === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function feof()
|
||||
* @see feof()
|
||||
*
|
||||
* @param resource
|
||||
* @return bool
|
||||
*/
|
||||
public function feof($handle)
|
||||
{
|
||||
return feof($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function is_readable()
|
||||
* @see is_readable()
|
||||
*
|
||||
* @param string $fileName
|
||||
* @return bool
|
||||
*/
|
||||
public function is_readable($fileName)
|
||||
{
|
||||
return is_readable($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function basename()
|
||||
* @see basename()
|
||||
*
|
||||
* @param string $path
|
||||
* @param string|void $suffix
|
||||
* @return string
|
||||
*/
|
||||
public function basename($path, $suffix = null)
|
||||
{
|
||||
return basename($path, $suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function header()
|
||||
* @see header()
|
||||
*
|
||||
* @param string $string
|
||||
* @return void
|
||||
*/
|
||||
public function header($string)
|
||||
{
|
||||
header($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function ob_end_clean()
|
||||
* @see ob_end_clean()
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ob_end_clean()
|
||||
{
|
||||
if (ob_get_length() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function iconv()
|
||||
* @see iconv()
|
||||
*
|
||||
* @param string $string The string to be converted
|
||||
* @param string $sourceEncoding The encoding of the source string
|
||||
* @param string $targetEncoding The encoding the source string should be converted to
|
||||
* @return string|bool the converted string or FALSE on failure.
|
||||
*/
|
||||
public function iconv($string, $sourceEncoding, $targetEncoding)
|
||||
{
|
||||
return iconv($sourceEncoding, $targetEncoding, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function mb_convert_encoding()
|
||||
* @see mb_convert_encoding()
|
||||
*
|
||||
* @param string $string The string to be converted
|
||||
* @param string $sourceEncoding The encoding of the source string
|
||||
* @param string $targetEncoding The encoding the source string should be converted to
|
||||
* @return string|bool the converted string or FALSE on failure.
|
||||
*/
|
||||
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
|
||||
{
|
||||
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function stream_get_wrappers()
|
||||
* @see stream_get_wrappers()
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function stream_get_wrappers()
|
||||
{
|
||||
return stream_get_wrappers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function function_exists()
|
||||
* @see function_exists()
|
||||
*
|
||||
* @param string $functionName
|
||||
* @return bool
|
||||
*/
|
||||
public function function_exists($functionName)
|
||||
{
|
||||
return function_exists($functionName);
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class StringHelper
|
||||
* This class provides helper functions to work with strings and multibyte strings.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @package Box\Spout\Common\Helper
|
||||
*/
|
||||
class StringHelper
|
||||
{
|
||||
/** @var bool Whether the mbstring extension is loaded */
|
||||
protected $hasMbstringSupport;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->hasMbstringSupport = extension_loaded('mbstring');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the given string.
|
||||
* It uses the multi-bytes function is available.
|
||||
* @see strlen
|
||||
* @see mb_strlen
|
||||
*
|
||||
* @param string $string
|
||||
* @return int
|
||||
*/
|
||||
public function getStringLength($string)
|
||||
{
|
||||
return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the first occurrence of the given character/substring within the given string.
|
||||
* It uses the multi-bytes function is available.
|
||||
* @see strpos
|
||||
* @see mb_strpos
|
||||
*
|
||||
* @param string $char Needle
|
||||
* @param string $string Haystack
|
||||
* @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found
|
||||
*/
|
||||
public function getCharFirstOccurrencePosition($char, $string)
|
||||
{
|
||||
$position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char);
|
||||
return ($position !== false) ? $position : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the last occurrence of the given character/substring within the given string.
|
||||
* It uses the multi-bytes function is available.
|
||||
* @see strrpos
|
||||
* @see mb_strrpos
|
||||
*
|
||||
* @param string $char Needle
|
||||
* @param string $string Haystack
|
||||
* @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found
|
||||
*/
|
||||
public function getCharLastOccurrencePosition($char, $string)
|
||||
{
|
||||
$position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char);
|
||||
return ($position !== false) ? $position : -1;
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common;
|
||||
|
||||
/**
|
||||
* Class Singleton
|
||||
* Defines a class as a singleton.
|
||||
*
|
||||
* @package Box\Spout\Common
|
||||
*/
|
||||
trait Singleton
|
||||
{
|
||||
protected static $instance;
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
final public static function getInstance()
|
||||
{
|
||||
return isset(static::$instance)
|
||||
? static::$instance
|
||||
: static::$instance = new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton constructor.
|
||||
*/
|
||||
final private function __construct()
|
||||
{
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the singleton
|
||||
* @return void
|
||||
*/
|
||||
protected function init() {}
|
||||
|
||||
final private function __wakeup() {}
|
||||
final private function __clone() {}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common;
|
||||
|
||||
/**
|
||||
* Class Type
|
||||
* This class references the supported types
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
abstract class Type
|
||||
{
|
||||
const CSV = 'csv';
|
||||
const XLSX = 'xlsx';
|
||||
const ODS = 'ods';
|
||||
}
|
||||
@ -1,238 +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;
|
||||
|
||||
/** @var \Box\Spout\Reader\Common\ReaderOptions Reader's customized options */
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return \Box\Spout\Reader\Common\ReaderOptions
|
||||
*/
|
||||
abstract protected function getOptions();
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function doesSupportStreamWrapper();
|
||||
|
||||
/**
|
||||
* 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 protected function getConcreteSheetIterator();
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
*
|
||||
* @return AbstractReader
|
||||
*/
|
||||
abstract protected function closeReader();
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @return AbstractReader
|
||||
*/
|
||||
public function setGlobalFunctionsHelper($globalFunctionsHelper)
|
||||
{
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether date/time values should be returned as PHP objects or be formatted as strings.
|
||||
*
|
||||
* @api
|
||||
* @param bool $shouldFormatDates
|
||||
* @return AbstractReader
|
||||
*/
|
||||
public function setShouldFormatDates($shouldFormatDates)
|
||||
{
|
||||
$this->getOptions()->setShouldFormatDates($shouldFormatDates);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether empty rows should be returned or skipped.
|
||||
*
|
||||
* @api
|
||||
* @param bool $shouldPreserveEmptyRows
|
||||
* @return AbstractReader
|
||||
*/
|
||||
public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
|
||||
{
|
||||
$this->getOptions()->setShouldPreserveEmptyRows($shouldPreserveEmptyRows);
|
||||
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->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) {
|
||||
throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file.");
|
||||
}
|
||||
|
||||
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 {
|
||||
$fileRealPath = $this->getFileRealPath($filePath);
|
||||
$this->openReader($fileRealPath);
|
||||
$this->isStreamOpened = true;
|
||||
} catch (\Exception $exception) {
|
||||
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the real path of the given path.
|
||||
* If the given path is a valid stream wrapper, returns the path unchanged.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
protected function getFileRealPath($filePath)
|
||||
{
|
||||
if ($this->isSupportedStreamWrapper($filePath)) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
// Need to use realpath to fix "Can't open file" on some Windows setup
|
||||
return realpath($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used.
|
||||
* For example, php://temp => php, s3://path/to/file => s3...
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return string|null The stream wrapper scheme or NULL if not a stream wrapper
|
||||
*/
|
||||
protected function getStreamWrapperScheme($filePath)
|
||||
{
|
||||
$streamScheme = null;
|
||||
if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
|
||||
$streamScheme = $matches[1];
|
||||
}
|
||||
return $streamScheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is an unsupported stream wrapper
|
||||
* (like local path, php://temp, mystream://foo/bar...).
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path is an unsupported stream wrapper
|
||||
*/
|
||||
protected function isStreamWrapper($filePath)
|
||||
{
|
||||
return ($this->getStreamWrapperScheme($filePath) !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is an supported stream wrapper
|
||||
* (like php://temp, mystream://foo/bar...).
|
||||
* If the given path is a local path, returns true.
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path is an supported stream wrapper
|
||||
*/
|
||||
protected function isSupportedStreamWrapper($filePath)
|
||||
{
|
||||
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||
return ($streamScheme !== null) ?
|
||||
in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) :
|
||||
true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||
return ($streamScheme === 'php');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Reader\AbstractReader;
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
|
||||
/**
|
||||
* Class Reader
|
||||
* This class provides support to read data from a CSV file.
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class Reader extends AbstractReader
|
||||
{
|
||||
/** @var resource Pointer to the file to be written */
|
||||
protected $filePointer;
|
||||
|
||||
/** @var SheetIterator To iterator over the CSV unique "sheet" */
|
||||
protected $sheetIterator;
|
||||
|
||||
/** @var string Original value for the "auto_detect_line_endings" INI value */
|
||||
protected $originalAutoDetectLineEndings;
|
||||
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
if (!isset($this->options)) {
|
||||
$this->options = new ReaderOptions();
|
||||
}
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field delimiter for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @return Reader
|
||||
*/
|
||||
public function setFieldDelimiter($fieldDelimiter)
|
||||
{
|
||||
$this->getOptions()->setFieldDelimiter($fieldDelimiter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field enclosure for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @return Reader
|
||||
*/
|
||||
public function setFieldEnclosure($fieldEnclosure)
|
||||
{
|
||||
$this->getOptions()->setFieldEnclosure($fieldEnclosure);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encoding of the CSV file to be read.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @return Reader
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
$this->getOptions()->setEncoding($encoding);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the EOL for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $endOfLineCharacter used to properly get lines from the CSV file.
|
||||
* @return Reader
|
||||
*/
|
||||
public function setEndOfLineCharacter($endOfLineCharacter)
|
||||
{
|
||||
$this->getOptions()->setEndOfLineCharacter($endOfLineCharacter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given path to make it ready to be read.
|
||||
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
|
||||
*
|
||||
* @param string $filePath Path of the CSV file to be read
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException
|
||||
*/
|
||||
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');
|
||||
if (!$this->filePointer) {
|
||||
throw new IOException("Could not open file $filePath for reading.");
|
||||
}
|
||||
|
||||
$this->sheetIterator = new SheetIterator(
|
||||
$this->filePointer,
|
||||
$this->getOptions(),
|
||||
$this->globalFunctionsHelper
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator to iterate over sheets.
|
||||
*
|
||||
* @return SheetIterator To iterate over sheets
|
||||
*/
|
||||
protected function getConcreteSheetIterator()
|
||||
{
|
||||
return $this->sheetIterator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function closeReader()
|
||||
{
|
||||
if ($this->filePointer) {
|
||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||
}
|
||||
|
||||
ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* This class is used to customize the reader's behavior
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
|
||||
{
|
||||
/** @var string Defines the character used to delimit fields (one character only) */
|
||||
protected $fieldDelimiter = ',';
|
||||
|
||||
/** @var string Defines the character used to enclose fields (one character only) */
|
||||
protected $fieldEnclosure = '"';
|
||||
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
||||
|
||||
/** @var string Defines the End of line */
|
||||
protected $endOfLineCharacter = "\n";
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFieldDelimiter()
|
||||
{
|
||||
return $this->fieldDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field delimiter for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setFieldDelimiter($fieldDelimiter)
|
||||
{
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFieldEnclosure()
|
||||
{
|
||||
return $this->fieldEnclosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field enclosure for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setFieldEnclosure($fieldEnclosure)
|
||||
{
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encoding of the CSV file to be read.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
$this->encoding = $encoding;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string EOL for the CSV
|
||||
*/
|
||||
public function getEndOfLineCharacter()
|
||||
{
|
||||
return $this->endOfLineCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the EOL for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $endOfLineCharacter used to properly get lines from the CSV file.
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setEndOfLineCharacter($endOfLineCharacter)
|
||||
{
|
||||
$this->endOfLineCharacter = $endOfLineCharacter;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -1,260 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
|
||||
/**
|
||||
* Class RowIterator
|
||||
* Iterate over CSV rows.
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
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 */
|
||||
protected $filePointer;
|
||||
|
||||
/** @var int Number of read rows */
|
||||
protected $numReadRows = 0;
|
||||
|
||||
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
|
||||
protected $rowDataBuffer = null;
|
||||
|
||||
/** @var bool Indicates whether all rows have been read */
|
||||
protected $hasReachedEndOfFile = false;
|
||||
|
||||
/** @var string Defines the character used to delimit fields (one character only) */
|
||||
protected $fieldDelimiter;
|
||||
|
||||
/** @var string Defines the character used to enclose fields (one character only) */
|
||||
protected $fieldEnclosure;
|
||||
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding;
|
||||
|
||||
/** @var string End of line delimiter, given by the user as input. */
|
||||
protected $inputEOLDelimiter;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */
|
||||
protected $encodingHelper;
|
||||
|
||||
/** @var string End of line delimiter, encoded using the same encoding as the CSV */
|
||||
protected $encodedEOLDelimiter;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param \Box\Spout\Reader\CSV\ReaderOptions $options
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $options, $globalFunctionsHelper)
|
||||
{
|
||||
$this->filePointer = $filePointer;
|
||||
$this->fieldDelimiter = $options->getFieldDelimiter();
|
||||
$this->fieldEnclosure = $options->getFieldEnclosure();
|
||||
$this->encoding = $options->getEncoding();
|
||||
$this->inputEOLDelimiter = $options->getEndOfLineCharacter();
|
||||
$this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
|
||||
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->rewindAndSkipBom();
|
||||
|
||||
$this->numReadRows = 0;
|
||||
$this->rowDataBuffer = null;
|
||||
|
||||
$this->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* This rewinds and skips the BOM if inserted at the beginning of the file
|
||||
* by moving the file pointer after it, so that it is not read.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function rewindAndSkipBom()
|
||||
{
|
||||
$byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding);
|
||||
|
||||
// sets the cursor after the BOM (0 means no BOM, so rewind it)
|
||||
$this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return ($this->filePointer && !$this->hasReachedEndOfFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element. Reads data for the next unprocessed row.
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
|
||||
if (!$this->hasReachedEndOfFile) {
|
||||
$this->readDataForNextRow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
*/
|
||||
protected function readDataForNextRow()
|
||||
{
|
||||
do {
|
||||
$rowData = $this->getNextUTF8EncodedRow();
|
||||
} while ($this->shouldReadNextRow($rowData));
|
||||
|
||||
if ($rowData !== false) {
|
||||
// str_replace will replace NULL values by empty strings
|
||||
$this->rowDataBuffer = str_replace(null, null, $rowData);
|
||||
$this->numReadRows++;
|
||||
} else {
|
||||
// If we reach this point, it means end of file was reached.
|
||||
// This happens when the last lines are empty lines.
|
||||
$this->hasReachedEndOfFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|bool $currentRowData
|
||||
* @return bool Whether the data for the current row can be returned or if we need to keep reading
|
||||
*/
|
||||
protected function shouldReadNextRow($currentRowData)
|
||||
{
|
||||
$hasSuccessfullyFetchedRowData = ($currentRowData !== false);
|
||||
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
$isEmptyLine = $this->isEmptyLine($currentRowData);
|
||||
|
||||
return (
|
||||
(!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) ||
|
||||
(!$this->shouldPreserveEmptyRows && $isEmptyLine)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next row, converted if necessary to UTF-8.
|
||||
* As fgetcsv() does not manage correctly encoding for non UTF-8 data,
|
||||
* we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes)
|
||||
*
|
||||
* @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
*/
|
||||
protected function getNextUTF8EncodedRow()
|
||||
{
|
||||
$encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
if ($encodedRowData === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($encodedRowData as $cellIndex => $cellValue) {
|
||||
switch($this->encoding) {
|
||||
case EncodingHelper::ENCODING_UTF16_LE:
|
||||
case EncodingHelper::ENCODING_UTF32_LE:
|
||||
// remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
|
||||
$cellValue = ltrim($cellValue);
|
||||
break;
|
||||
|
||||
case EncodingHelper::ENCODING_UTF16_BE:
|
||||
case EncodingHelper::ENCODING_UTF32_BE:
|
||||
// remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
|
||||
$cellValue = rtrim($cellValue);
|
||||
break;
|
||||
}
|
||||
|
||||
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
|
||||
}
|
||||
|
||||
return $encodedRowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the end of line delimiter, encoded using the same encoding as the CSV.
|
||||
* The return value is cached.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getEncodedEOLDelimiter()
|
||||
{
|
||||
if (!isset($this->encodedEOLDelimiter)) {
|
||||
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding);
|
||||
}
|
||||
|
||||
return $this->encodedEOLDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|bool $lineData Array containing the cells value for the line
|
||||
* @return bool Whether the given line is empty
|
||||
*/
|
||||
protected function isEmptyLine($lineData)
|
||||
{
|
||||
return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element from the buffer
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->rowDataBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->numReadRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up what was created to iterate over the object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Reader\SheetInterface;
|
||||
|
||||
/**
|
||||
* Class Sheet
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class Sheet implements SheetInterface
|
||||
{
|
||||
/** @var \Box\Spout\Reader\CSV\RowIterator To iterate over the CSV's rows */
|
||||
protected $rowIterator;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param \Box\Spout\Reader\CSV\ReaderOptions $options
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $options, $globalFunctionsHelper)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($filePointer, $options, $globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return \Box\Spout\Reader\CSV\RowIterator
|
||||
*/
|
||||
public function getRowIterator()
|
||||
{
|
||||
return $this->rowIterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return int Index of the sheet
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return string Name of the sheet - empty string since CSV does not support that
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return bool Always TRUE as there is only one sheet
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
|
||||
/**
|
||||
* Class SheetIterator
|
||||
* Iterate over CSV unique "sheet".
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class SheetIterator implements IteratorInterface
|
||||
{
|
||||
/** @var \Box\Spout\Reader\CSV\Sheet The CSV unique "sheet" */
|
||||
protected $sheet;
|
||||
|
||||
/** @var bool Whether the unique "sheet" has already been read */
|
||||
protected $hasReadUniqueSheet = false;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer
|
||||
* @param \Box\Spout\Reader\CSV\ReaderOptions $options
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $options, $globalFunctionsHelper)
|
||||
{
|
||||
$this->sheet = new Sheet($filePointer, $options, $globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->hasReadUniqueSheet = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return (!$this->hasReadUniqueSheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->hasReadUniqueSheet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return \Box\Spout\Reader\CSV\Sheet
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->sheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up what was created to iterate over the object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Common;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* Readers' common options
|
||||
*
|
||||
* @package Box\Spout\Reader\Common
|
||||
*/
|
||||
class ReaderOptions
|
||||
{
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
protected $shouldFormatDates = false;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows = false;
|
||||
|
||||
/**
|
||||
* @return bool Whether date/time values should be returned as PHP objects or be formatted as strings.
|
||||
*/
|
||||
public function shouldFormatDates()
|
||||
{
|
||||
return $this->shouldFormatDates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether date/time values should be returned as PHP objects or be formatted as strings.
|
||||
*
|
||||
* @param bool $shouldFormatDates
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setShouldFormatDates($shouldFormatDates)
|
||||
{
|
||||
$this->shouldFormatDates = $shouldFormatDates;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether empty rows should be returned or skipped.
|
||||
*/
|
||||
public function shouldPreserveEmptyRows()
|
||||
{
|
||||
return $this->shouldPreserveEmptyRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether empty rows should be returned or skipped.
|
||||
*
|
||||
* @param bool $shouldPreserveEmptyRows
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
|
||||
{
|
||||
$this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -1,152 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Common;
|
||||
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
|
||||
/**
|
||||
* Class XMLProcessor
|
||||
* Helps process XML files
|
||||
*
|
||||
* @package Box\Spout\Reader\Common
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\XMLProcessingException
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class IteratorNotRewindableException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class IteratorNotRewindableException extends ReaderException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class NoSheetsFoundException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class NoSheetsFoundException extends ReaderException
|
||||
{
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
use Box\Spout\Common\Exception\SpoutException;
|
||||
|
||||
/**
|
||||
* Class ReaderException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
* @abstract
|
||||
*/
|
||||
abstract class ReaderException extends SpoutException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class ReaderNotOpenedException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class ReaderNotOpenedException extends ReaderException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class SharedStringNotFoundException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class SharedStringNotFoundException extends ReaderException
|
||||
{
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class XMLProcessingException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class XMLProcessingException extends ReaderException
|
||||
{
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
/**
|
||||
* Interface IteratorInterface
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
interface IteratorInterface extends \Iterator
|
||||
{
|
||||
/**
|
||||
* Cleans up what was created to iterate over the object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end();
|
||||
}
|
||||
@ -1,231 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS\Helper;
|
||||
|
||||
/**
|
||||
* Class CellValueFormatter
|
||||
* This class provides helper functions to format cell values
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS\Helper
|
||||
*/
|
||||
class CellValueFormatter
|
||||
{
|
||||
/** Definition of all possible cell types */
|
||||
const CELL_TYPE_STRING = 'string';
|
||||
const CELL_TYPE_FLOAT = 'float';
|
||||
const CELL_TYPE_BOOLEAN = 'boolean';
|
||||
const CELL_TYPE_DATE = 'date';
|
||||
const CELL_TYPE_TIME = 'time';
|
||||
const CELL_TYPE_CURRENCY = 'currency';
|
||||
const CELL_TYPE_PERCENTAGE = 'percentage';
|
||||
const CELL_TYPE_VOID = 'void';
|
||||
|
||||
/** Definition of XML nodes names used to parse data */
|
||||
const XML_NODE_P = 'p';
|
||||
const XML_NODE_S = 'text:s';
|
||||
const XML_NODE_A = 'text:a';
|
||||
const XML_NODE_SPAN = 'text:span';
|
||||
|
||||
/** Definition of XML attributes used to parse data */
|
||||
const XML_ATTRIBUTE_TYPE = 'office:value-type';
|
||||
const XML_ATTRIBUTE_VALUE = 'office:value';
|
||||
const XML_ATTRIBUTE_BOOLEAN_VALUE = 'office:boolean-value';
|
||||
const XML_ATTRIBUTE_DATE_VALUE = 'office:date-value';
|
||||
const XML_ATTRIBUTE_TIME_VALUE = 'office:time-value';
|
||||
const XML_ATTRIBUTE_CURRENCY = 'office:currency';
|
||||
const XML_ATTRIBUTE_C = 'text:c';
|
||||
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
protected $shouldFormatDates;
|
||||
|
||||
/** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */
|
||||
protected $escaper;
|
||||
|
||||
/**
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
*/
|
||||
public function __construct($shouldFormatDates)
|
||||
{
|
||||
$this->shouldFormatDates = $shouldFormatDates;
|
||||
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
$this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
|
||||
* @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error
|
||||
*/
|
||||
public function extractAndFormatNodeValue($node)
|
||||
{
|
||||
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE);
|
||||
|
||||
switch ($cellType) {
|
||||
case self::CELL_TYPE_STRING:
|
||||
return $this->formatStringCellValue($node);
|
||||
case self::CELL_TYPE_FLOAT:
|
||||
return $this->formatFloatCellValue($node);
|
||||
case self::CELL_TYPE_BOOLEAN:
|
||||
return $this->formatBooleanCellValue($node);
|
||||
case self::CELL_TYPE_DATE:
|
||||
return $this->formatDateCellValue($node);
|
||||
case self::CELL_TYPE_TIME:
|
||||
return $this->formatTimeCellValue($node);
|
||||
case self::CELL_TYPE_CURRENCY:
|
||||
return $this->formatCurrencyCellValue($node);
|
||||
case self::CELL_TYPE_PERCENTAGE:
|
||||
return $this->formatPercentageCellValue($node);
|
||||
case self::CELL_TYPE_VOID:
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell String value.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string The value associated with the cell
|
||||
*/
|
||||
protected function formatStringCellValue($node)
|
||||
{
|
||||
$pNodeValues = [];
|
||||
$pNodes = $node->getElementsByTagName(self::XML_NODE_P);
|
||||
|
||||
foreach ($pNodes as $pNode) {
|
||||
$currentPValue = '';
|
||||
|
||||
foreach ($pNode->childNodes as $childNode) {
|
||||
if ($childNode instanceof \DOMText) {
|
||||
$currentPValue .= $childNode->nodeValue;
|
||||
} else if ($childNode->nodeName === self::XML_NODE_S) {
|
||||
$spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C);
|
||||
$numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1;
|
||||
$currentPValue .= str_repeat(' ', $numSpaces);
|
||||
} else if ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) {
|
||||
$currentPValue .= $childNode->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
$pNodeValues[] = $currentPValue;
|
||||
}
|
||||
|
||||
$escapedCellValue = implode("\n", $pNodeValues);
|
||||
$cellValue = $this->escaper->unescape($escapedCellValue);
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Numeric value from the given node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return int|float The value associated with the cell
|
||||
*/
|
||||
protected function formatFloatCellValue($node)
|
||||
{
|
||||
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
|
||||
$nodeIntValue = intval($nodeValue);
|
||||
// The "==" is intentionally not a "===" because only the value matters, not the type
|
||||
$cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Boolean value from the given node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return bool The value associated with the cell
|
||||
*/
|
||||
protected function formatBooleanCellValue($node)
|
||||
{
|
||||
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE);
|
||||
// !! is similar to boolval()
|
||||
$cellValue = !!$nodeValue;
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Date value from the given node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
|
||||
*/
|
||||
protected function formatDateCellValue($node)
|
||||
{
|
||||
// The XML node looks like this:
|
||||
// <table:table-cell calcext:value-type="date" office:date-value="2016-05-19T16:39:00" office:value-type="date">
|
||||
// <text:p>05/19/16 04:39 PM</text:p>
|
||||
// </table:table-cell>
|
||||
|
||||
if ($this->shouldFormatDates) {
|
||||
// The date is already formatted in the "p" tag
|
||||
$nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
|
||||
return $nodeWithValueAlreadyFormatted->nodeValue;
|
||||
} else {
|
||||
// otherwise, get it from the "date-value" attribute
|
||||
try {
|
||||
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
|
||||
return new \DateTime($nodeValue);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Time value from the given node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return \DateInterval|string|null The value associated with the cell or NULL if invalid time value
|
||||
*/
|
||||
protected function formatTimeCellValue($node)
|
||||
{
|
||||
// The XML node looks like this:
|
||||
// <table:table-cell calcext:value-type="time" office:time-value="PT13H24M00S" office:value-type="time">
|
||||
// <text:p>01:24:00 PM</text:p>
|
||||
// </table:table-cell>
|
||||
|
||||
if ($this->shouldFormatDates) {
|
||||
// The date is already formatted in the "p" tag
|
||||
$nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
|
||||
return $nodeWithValueAlreadyFormatted->nodeValue;
|
||||
} else {
|
||||
// otherwise, get it from the "time-value" attribute
|
||||
try {
|
||||
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
|
||||
return new \DateInterval($nodeValue);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Currency value from the given node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR")
|
||||
*/
|
||||
protected function formatCurrencyCellValue($node)
|
||||
{
|
||||
$value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
|
||||
$currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY);
|
||||
|
||||
return "$value $currency";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Percentage value from the given node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return int|float The value associated with the cell
|
||||
*/
|
||||
protected function formatPercentageCellValue($node)
|
||||
{
|
||||
// percentages are formatted like floats
|
||||
return $this->formatFloatCellValue($node);
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS\Helper;
|
||||
|
||||
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
|
||||
/**
|
||||
* Class SettingsHelper
|
||||
* This class provides helper functions to extract data from the "settings.xml" file.
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS\Helper
|
||||
*/
|
||||
class SettingsHelper
|
||||
{
|
||||
const SETTINGS_XML_FILE_PATH = 'settings.xml';
|
||||
|
||||
/** Definition of XML nodes name and attribute used to parse settings data */
|
||||
const XML_NODE_CONFIG_ITEM = 'config:config-item';
|
||||
const XML_ATTRIBUTE_CONFIG_NAME = 'config:name';
|
||||
const XML_ATTRIBUTE_VALUE_ACTIVE_TABLE = 'ActiveTable';
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return string|null Name of the sheet that was defined as active or NULL if none found
|
||||
*/
|
||||
public function getActiveSheetName($filePath)
|
||||
{
|
||||
$xmlReader = new XMLReader();
|
||||
if ($xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$activeSheetName = null;
|
||||
|
||||
try {
|
||||
while ($xmlReader->readUntilNodeFound(self::XML_NODE_CONFIG_ITEM)) {
|
||||
if ($xmlReader->getAttribute(self::XML_ATTRIBUTE_CONFIG_NAME) === self::XML_ATTRIBUTE_VALUE_ACTIVE_TABLE) {
|
||||
$activeSheetName = $xmlReader->readString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (XMLProcessingException $exception) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
$xmlReader->close();
|
||||
|
||||
return $activeSheetName;
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\AbstractReader;
|
||||
|
||||
/**
|
||||
* Class Reader
|
||||
* This class provides support to read data from a ODS file
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS
|
||||
*/
|
||||
class Reader extends AbstractReader
|
||||
{
|
||||
/** @var \ZipArchive */
|
||||
protected $zip;
|
||||
|
||||
/** @var SheetIterator To iterator over the ODS sheets */
|
||||
protected $sheetIterator;
|
||||
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
if (!isset($this->options)) {
|
||||
$this->options = new ReaderOptions();
|
||||
}
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
|
||||
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
|
||||
*/
|
||||
protected function openReader($filePath)
|
||||
{
|
||||
$this->zip = new \ZipArchive();
|
||||
|
||||
if ($this->zip->open($filePath) === true) {
|
||||
$this->sheetIterator = new SheetIterator($filePath, $this->getOptions());
|
||||
} else {
|
||||
throw new IOException("Could not open $filePath for reading.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator to iterate over sheets.
|
||||
*
|
||||
* @return SheetIterator To iterate over sheets
|
||||
*/
|
||||
protected function getConcreteSheetIterator()
|
||||
{
|
||||
return $this->sheetIterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function closeReader()
|
||||
{
|
||||
if ($this->zip) {
|
||||
$this->zip->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* This class is used to customize the reader's behavior
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS
|
||||
*/
|
||||
class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
|
||||
{
|
||||
// No extra options
|
||||
}
|
||||
@ -1,352 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\Exception\IteratorNotRewindableException;
|
||||
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
use Box\Spout\Reader\ODS\Helper\CellValueFormatter;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
use Box\Spout\Reader\Common\XMLProcessor;
|
||||
|
||||
/**
|
||||
* Class RowIterator
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS
|
||||
*/
|
||||
class RowIterator implements IteratorInterface
|
||||
{
|
||||
/** Definition of XML nodes names used to parse data */
|
||||
const XML_NODE_TABLE = 'table:table';
|
||||
const XML_NODE_ROW = 'table:table-row';
|
||||
const XML_NODE_CELL = 'table:table-cell';
|
||||
const MAX_COLUMNS_EXCEL = 16384;
|
||||
|
||||
/** Definition of XML attribute used to parse data */
|
||||
const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated';
|
||||
const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
|
||||
|
||||
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
|
||||
protected $xmlReader;
|
||||
|
||||
/** @var \Box\Spout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
|
||||
protected $xmlProcessor;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows;
|
||||
|
||||
/** @var Helper\CellValueFormatter Helper to format cell values */
|
||||
protected $cellValueFormatter;
|
||||
|
||||
/** @var bool Whether the iterator has already been rewound once */
|
||||
protected $hasAlreadyBeenRewound = false;
|
||||
|
||||
/** @var array Contains the data for the currently processed row (key = cell index, value = cell value) */
|
||||
protected $currentlyProcessedRowData = [];
|
||||
|
||||
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
|
||||
protected $rowDataBuffer = null;
|
||||
|
||||
/** @var bool Indicates whether all rows have been read */
|
||||
protected $hasReachedEndOfFile = false;
|
||||
|
||||
/** @var int Last row index processed (one-based) */
|
||||
protected $lastRowIndexProcessed = 0;
|
||||
|
||||
/** @var int Row index to be processed next (one-based) */
|
||||
protected $nextRowIndexToBeProcessed = 1;
|
||||
|
||||
/** @var mixed|null Value of the last processed cell (because when reading cell at column N+1, cell N is processed) */
|
||||
protected $lastProcessedCellValue = null;
|
||||
|
||||
/** @var int Number of times the last processed row should be repeated */
|
||||
protected $numRowsRepeated = 1;
|
||||
|
||||
/** @var int Number of times the last cell value should be copied to the cells on its right */
|
||||
protected $numColumnsRepeated = 1;
|
||||
|
||||
/** @var bool Whether at least one cell has been read for the row currently being processed */
|
||||
protected $hasAlreadyReadOneCellInCurrentRow = false;
|
||||
|
||||
|
||||
/**
|
||||
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||
* @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
|
||||
*/
|
||||
public function __construct($xmlReader, $options)
|
||||
{
|
||||
$this->xmlReader = $xmlReader;
|
||||
$this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
|
||||
$this->cellValueFormatter = new CellValueFormatter($options->shouldFormatDates());
|
||||
|
||||
// Register all callbacks to process different nodes when reading the XML file
|
||||
$this->xmlProcessor = new XMLProcessor($this->xmlReader);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element.
|
||||
* NOTE: It can only be done once, as it is not possible to read an XML file backwards.
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
// Because sheet and row data is located in the file, we can't rewind both the
|
||||
// sheet iterator and the row iterator, as XML file cannot be read backwards.
|
||||
// Therefore, rewinding the row iterator has been disabled.
|
||||
if ($this->hasAlreadyBeenRewound) {
|
||||
throw new IteratorNotRewindableException();
|
||||
}
|
||||
|
||||
$this->hasAlreadyBeenRewound = true;
|
||||
$this->lastRowIndexProcessed = 0;
|
||||
$this->nextRowIndexToBeProcessed = 1;
|
||||
$this->rowDataBuffer = null;
|
||||
$this->hasReachedEndOfFile = false;
|
||||
|
||||
$this->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return (!$this->hasReachedEndOfFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element. Empty rows will be skipped.
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
if ($this->doesNeedDataForNextRowToBeProcessed()) {
|
||||
$this->readDataForNextRow();
|
||||
}
|
||||
|
||||
$this->lastRowIndexProcessed++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we need data for the next row to be processed.
|
||||
* We DO need to read data if:
|
||||
* - we have not read any rows yet
|
||||
* OR
|
||||
* - the next row to be processed immediately follows the last read row
|
||||
*
|
||||
* @return bool Whether we need data for the next row to be processed.
|
||||
*/
|
||||
protected function doesNeedDataForNextRowToBeProcessed()
|
||||
{
|
||||
$hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
|
||||
|
||||
return (
|
||||
!$hasReadAtLeastOneRow ||
|
||||
$this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
|
||||
*/
|
||||
protected function readDataForNextRow()
|
||||
{
|
||||
$this->currentlyProcessedRowData = [];
|
||||
|
||||
try {
|
||||
$this->xmlProcessor->readUntilStopped();
|
||||
} catch (XMLProcessingException $exception) {
|
||||
throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
|
||||
}
|
||||
|
||||
$this->rowDataBuffer = $this->currentlyProcessedRowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowStartingNode($xmlReader)
|
||||
{
|
||||
// Reset data from current row
|
||||
$this->hasAlreadyReadOneCellInCurrentRow = false;
|
||||
$this->lastProcessedCellValue = null;
|
||||
$this->numColumnsRepeated = 1;
|
||||
$this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader);
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processCellStartingNode($xmlReader)
|
||||
{
|
||||
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
|
||||
|
||||
// NOTE: expand() will automatically decode all XML entities of the child nodes
|
||||
$node = $xmlReader->expand();
|
||||
$currentCellValue = $this->getCellValue($node);
|
||||
|
||||
// process cell N only after having read cell N+1 (see below why)
|
||||
if ($this->hasAlreadyReadOneCellInCurrentRow) {
|
||||
for ($i = 0; $i < $this->numColumnsRepeated; $i++) {
|
||||
$this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->hasAlreadyReadOneCellInCurrentRow = true;
|
||||
$this->lastProcessedCellValue = $currentCellValue;
|
||||
$this->numColumnsRepeated = $currentNumColumnsRepeated;
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowEndingNode()
|
||||
{
|
||||
$isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRowData, $this->lastProcessedCellValue);
|
||||
|
||||
// if the fetched row is empty and we don't want to preserve it...
|
||||
if (!$this->shouldPreserveEmptyRows && $isEmptyRow) {
|
||||
// ... skip it
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
// if the row is empty, we don't want to return more than one cell
|
||||
$actualNumColumnsRepeated = (!$isEmptyRow) ? $this->numColumnsRepeated : 1;
|
||||
|
||||
// Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
|
||||
// The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData".
|
||||
// This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
|
||||
// with a number-columns-repeated value equals to the number of (supported columns - used columns).
|
||||
// In Excel, the number of supported columns is 16384, but we don't want to returns rows with
|
||||
// always 16384 cells.
|
||||
if ((count($this->currentlyProcessedRowData) + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
|
||||
for ($i = 0; $i < $actualNumColumnsRepeated; $i++) {
|
||||
$this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are processing row N and the row is repeated M times,
|
||||
// then the next row to be processed will be row (N+M).
|
||||
$this->nextRowIndexToBeProcessed += $this->numRowsRepeated;
|
||||
|
||||
// at this point, we have all the data we need for the row
|
||||
// so that we can populate the buffer
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processTableEndingNode()
|
||||
{
|
||||
// The closing "</table:table>" marks the end of the file
|
||||
$this->hasReachedEndOfFile = true;
|
||||
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
|
||||
* @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing
|
||||
*/
|
||||
protected function getNumRowsRepeatedForCurrentNode($xmlReader)
|
||||
{
|
||||
$numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
|
||||
return ($numRowsRepeated !== null) ? intval($numRowsRepeated) : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
|
||||
* @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
|
||||
*/
|
||||
protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
|
||||
{
|
||||
$numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
|
||||
return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error
|
||||
*/
|
||||
protected function getCellValue($node)
|
||||
{
|
||||
return $this->cellValueFormatter->extractAndFormatNodeValue($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* After finishing processing each cell, a row is considered empty if it contains
|
||||
* no cells or if the value of the last read cell is an empty string.
|
||||
* After finishing processing each cell, the last read cell is not part of the
|
||||
* row data yet (as we still need to apply the "num-columns-repeated" attribute).
|
||||
*
|
||||
* @param array $rowData
|
||||
* @param string|int|float|bool|\DateTime|\DateInterval|null The value of the last read cell
|
||||
* @return bool Whether the row is empty
|
||||
*/
|
||||
protected function isEmptyRow($rowData, $lastReadCellValue)
|
||||
{
|
||||
return (
|
||||
count($rowData) === 0 &&
|
||||
(!isset($lastReadCellValue) || trim($lastReadCellValue) === '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element, from the buffer.
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->rowDataBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->lastRowIndexProcessed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cleans up what was created to iterate over the object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
$this->xmlReader->close();
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS;
|
||||
|
||||
use Box\Spout\Reader\SheetInterface;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
|
||||
/**
|
||||
* Class Sheet
|
||||
* Represents a sheet within a ODS file
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS
|
||||
*/
|
||||
class Sheet implements SheetInterface
|
||||
{
|
||||
/** @var \Box\Spout\Reader\ODS\RowIterator To iterate over sheet's rows */
|
||||
protected $rowIterator;
|
||||
|
||||
/** @var int ID of the sheet */
|
||||
protected $id;
|
||||
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
protected $name;
|
||||
|
||||
/** @var bool Whether the sheet was the active one */
|
||||
protected $isActive;
|
||||
|
||||
/**
|
||||
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param string $sheetName Name of the sheet
|
||||
* @param bool $isSheetActive Whether the sheet was defined as active
|
||||
* @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
|
||||
*/
|
||||
public function __construct($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $options)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($xmlReader, $options);
|
||||
$this->index = $sheetIndex;
|
||||
$this->name = $sheetName;
|
||||
$this->isActive = $isSheetActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return \Box\Spout\Reader\ODS\RowIterator
|
||||
*/
|
||||
public function getRowIterator()
|
||||
{
|
||||
return $this->rowIterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return string Name of the sheet
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return bool Whether the sheet was defined as active
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
}
|
||||
@ -1,168 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
use Box\Spout\Reader\ODS\Helper\SettingsHelper;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
|
||||
/**
|
||||
* Class SheetIterator
|
||||
* Iterate over ODS sheet.
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS
|
||||
*/
|
||||
class SheetIterator implements IteratorInterface
|
||||
{
|
||||
const CONTENT_XML_FILE_PATH = 'content.xml';
|
||||
|
||||
/** Definition of XML nodes name and attribute used to parse sheet data */
|
||||
const XML_NODE_TABLE = 'table:table';
|
||||
const XML_ATTRIBUTE_TABLE_NAME = 'table:name';
|
||||
|
||||
/** @var string $filePath Path of the file to be read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var \Box\Spout\Reader\ODS\ReaderOptions Reader's current options */
|
||||
protected $options;
|
||||
|
||||
/** @var XMLReader The XMLReader object that will help read sheet's XML data */
|
||||
protected $xmlReader;
|
||||
|
||||
/** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */
|
||||
protected $escaper;
|
||||
|
||||
/** @var bool Whether there are still at least a sheet to be read */
|
||||
protected $hasFoundSheet;
|
||||
|
||||
/** @var int The index of the sheet being read (zero-based) */
|
||||
protected $currentSheetIndex;
|
||||
|
||||
/** @var string The name of the sheet that was defined as active */
|
||||
protected $activeSheetName;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
|
||||
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
|
||||
*/
|
||||
public function __construct($filePath, $options)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->options = $options;
|
||||
$this->xmlReader = new XMLReader();
|
||||
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
$this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance();
|
||||
|
||||
$settingsHelper = new SettingsHelper();
|
||||
$this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->xmlReader->close();
|
||||
|
||||
if ($this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH) === false) {
|
||||
$contentXmlFilePath = $this->filePath . '#' . self::CONTENT_XML_FILE_PATH;
|
||||
throw new IOException("Could not open \"{$contentXmlFilePath}\".");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
|
||||
} catch (XMLProcessingException $exception) {
|
||||
throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
|
||||
}
|
||||
|
||||
$this->currentSheetIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->hasFoundSheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
|
||||
|
||||
if ($this->hasFoundSheet) {
|
||||
$this->currentSheetIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return \Box\Spout\Reader\ODS\Sheet
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
|
||||
$sheetName = $this->escaper->unescape($escapedSheetName);
|
||||
$isActiveSheet = $this->isActiveSheet($sheetName, $this->currentSheetIndex, $this->activeSheetName);
|
||||
|
||||
return new Sheet($this->xmlReader, $this->currentSheetIndex, $sheetName, $isActiveSheet, $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current sheet was defined as the active one
|
||||
*
|
||||
* @param string $sheetName Name of the current sheet
|
||||
* @param int $sheetIndex Index of the current sheet
|
||||
* @param string|null Name of the sheet that was defined as active or NULL if none defined
|
||||
* @return bool Whether the current sheet was defined as the active one
|
||||
*/
|
||||
private function isActiveSheet($sheetName, $sheetIndex, $activeSheetName)
|
||||
{
|
||||
// The given sheet is active if its name matches the defined active sheet's name
|
||||
// or if no information about the active sheet was found, it defaults to the first sheet.
|
||||
return (
|
||||
($activeSheetName === null && $sheetIndex === 0) ||
|
||||
($activeSheetName === $sheetName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->currentSheetIndex + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up what was created to iterate over the object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
$this->xmlReader->close();
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
use Box\Spout\Common\Exception\UnsupportedTypeException;
|
||||
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
/**
|
||||
* Class ReaderFactory
|
||||
* This factory is used to create readers, based on the type of the file to be read.
|
||||
* It supports CSV and XLSX formats.
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
class ReaderFactory
|
||||
{
|
||||
/**
|
||||
* This creates an instance of the appropriate reader, given the type of the file to be read
|
||||
*
|
||||
* @api
|
||||
* @param string $readerType Type of the reader to instantiate
|
||||
* @return ReaderInterface
|
||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||
*/
|
||||
public static function create($readerType)
|
||||
{
|
||||
$reader = null;
|
||||
|
||||
switch ($readerType) {
|
||||
case Type::CSV:
|
||||
$reader = new CSV\Reader();
|
||||
break;
|
||||
case Type::XLSX:
|
||||
$reader = new XLSX\Reader();
|
||||
break;
|
||||
case Type::ODS:
|
||||
$reader = new ODS\Reader();
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
|
||||
}
|
||||
|
||||
$reader->setGlobalFunctionsHelper(new GlobalFunctionsHelper());
|
||||
|
||||
return $reader;
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
/**
|
||||
* Interface ReaderInterface
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
interface ReaderInterface
|
||||
{
|
||||
/**
|
||||
* Prepares the reader to read the given file. It also makes sure
|
||||
* that the file exists and is readable.
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException
|
||||
*/
|
||||
public function open($filePath);
|
||||
|
||||
/**
|
||||
* Returns an iterator to iterate over sheets.
|
||||
*
|
||||
* @return \Iterator To iterate over sheets
|
||||
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
|
||||
*/
|
||||
public function getSheetIterator();
|
||||
|
||||
/**
|
||||
* Closes the reader, preventing any additional reading
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close();
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
/**
|
||||
* Interface SheetInterface
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
interface SheetInterface
|
||||
{
|
||||
/**
|
||||
* Returns an iterator to iterate over the sheet's rows.
|
||||
*
|
||||
* @return \Iterator
|
||||
*/
|
||||
public function getRowIterator();
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Wrapper;
|
||||
|
||||
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||
|
||||
/**
|
||||
* Trait XMLInternalErrorsHelper
|
||||
*
|
||||
* @package Box\Spout\Reader\Wrapper
|
||||
*/
|
||||
trait XMLInternalErrorsHelper
|
||||
{
|
||||
/** @var bool Stores whether XML errors were initially stored internally - used to reset */
|
||||
protected $initialUseInternalErrorsValue;
|
||||
|
||||
/**
|
||||
* To avoid displaying lots of warning/error messages on screen,
|
||||
* stores errors internally instead.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function useXMLInternalErrors()
|
||||
{
|
||||
libxml_clear_errors();
|
||||
$this->initialUseInternalErrorsValue = libxml_use_internal_errors(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an XMLProcessingException if an error occured.
|
||||
* It also always resets the "libxml_use_internal_errors" setting back to its initial value.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\XMLProcessingException
|
||||
*/
|
||||
protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured()
|
||||
{
|
||||
if ($this->hasXMLErrorOccured()) {
|
||||
$this->resetXMLInternalErrorsSetting();
|
||||
throw new XMLProcessingException($this->getLastXMLErrorMessage());
|
||||
}
|
||||
|
||||
$this->resetXMLInternalErrorsSetting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the a XML error has occured since the last time errors were cleared.
|
||||
*
|
||||
* @return bool TRUE if an error occured, FALSE otherwise
|
||||
*/
|
||||
private function hasXMLErrorOccured()
|
||||
{
|
||||
return (libxml_get_last_error() !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message for the last XML error that occured.
|
||||
* @see libxml_get_last_error
|
||||
*
|
||||
* @return String|null Last XML error message or null if no error
|
||||
*/
|
||||
private function getLastXMLErrorMessage()
|
||||
{
|
||||
$errorMessage = null;
|
||||
$error = libxml_get_last_error();
|
||||
|
||||
if ($error !== false) {
|
||||
$errorMessage = trim($error->message);
|
||||
}
|
||||
|
||||
return $errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function resetXMLInternalErrorsSetting()
|
||||
{
|
||||
libxml_use_internal_errors($this->initialUseInternalErrorsValue);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,167 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Wrapper;
|
||||
use DOMNode;
|
||||
|
||||
|
||||
/**
|
||||
* Class XMLReader
|
||||
* Wrapper around the built-in XMLReader
|
||||
* @see \XMLReader
|
||||
*
|
||||
* @package Box\Spout\Reader\Wrapper
|
||||
*/
|
||||
class XMLReader extends \XMLReader
|
||||
{
|
||||
use XMLInternalErrorsHelper;
|
||||
|
||||
const ZIP_WRAPPER = 'zip://';
|
||||
|
||||
/**
|
||||
* Opens the XML Reader to read a file located inside a ZIP file.
|
||||
*
|
||||
* @param string $zipFilePath Path to the ZIP file
|
||||
* @param string $fileInsideZipPath Relative or absolute path of the file inside the zip
|
||||
* @return bool TRUE on success or FALSE on failure
|
||||
*/
|
||||
public function openFileInZip($zipFilePath, $fileInsideZipPath)
|
||||
{
|
||||
$wasOpenSuccessful = false;
|
||||
$realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath);
|
||||
|
||||
// We need to check first that the file we are trying to read really exist because:
|
||||
// - PHP emits a warning when trying to open a file that does not exist.
|
||||
// - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779)
|
||||
if ($this->fileExistsWithinZip($realPathURI)) {
|
||||
$wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET);
|
||||
}
|
||||
|
||||
return $wasOpenSuccessful;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the real path for the given path components.
|
||||
* This is useful to avoid issues on some Windows setup.
|
||||
*
|
||||
* @param string $zipFilePath Path to the ZIP file
|
||||
* @param string $fileInsideZipPath Relative or absolute path of the file inside the zip
|
||||
* @return string The real path URI
|
||||
*/
|
||||
public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath)
|
||||
{
|
||||
return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the file at the given location exists
|
||||
*
|
||||
* @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml"
|
||||
* @return bool TRUE if the file exists, FALSE otherwise
|
||||
*/
|
||||
protected function fileExistsWithinZip($zipStreamURI)
|
||||
{
|
||||
$doesFileExists = false;
|
||||
|
||||
$pattern = '/zip:\/\/([^#]+)#(.*)/';
|
||||
if (preg_match($pattern, $zipStreamURI, $matches)) {
|
||||
$zipFilePath = $matches[1];
|
||||
$innerFilePath = $matches[2];
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($zipFilePath) === true) {
|
||||
$doesFileExists = ($zip->locateName($innerFilePath) !== false);
|
||||
$zip->close();
|
||||
}
|
||||
}
|
||||
|
||||
return $doesFileExists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to next node in document
|
||||
* @see \XMLReader::read
|
||||
*
|
||||
* @return bool TRUE on success or FALSE on failure
|
||||
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$this->useXMLInternalErrors();
|
||||
|
||||
$wasReadSuccessful = parent::read();
|
||||
|
||||
$this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
|
||||
|
||||
return $wasReadSuccessful;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read until the element with the given name is found, or the end of the file.
|
||||
*
|
||||
* @param string $nodeName Name of the node to find
|
||||
* @return bool TRUE on success or FALSE on failure
|
||||
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
|
||||
*/
|
||||
public function readUntilNodeFound($nodeName)
|
||||
{
|
||||
do {
|
||||
$wasReadSuccessful = $this->read();
|
||||
$isNotPositionedOnStartingNode = !$this->isPositionedOnStartingNode($nodeName);
|
||||
} while ($wasReadSuccessful && $isNotPositionedOnStartingNode);
|
||||
|
||||
return $wasReadSuccessful;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cursor to next node skipping all subtrees
|
||||
* @see \XMLReader::next
|
||||
*
|
||||
* @param string|void $localName The name of the next node to move to
|
||||
* @return bool TRUE on success or FALSE on failure
|
||||
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
|
||||
*/
|
||||
public function next($localName = null)
|
||||
{
|
||||
$this->useXMLInternalErrors();
|
||||
|
||||
$wasNextSuccessful = parent::next($localName);
|
||||
|
||||
$this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
|
||||
|
||||
return $wasNextSuccessful;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nodeName
|
||||
* @return bool Whether the XML Reader is currently positioned on the starting node with given name
|
||||
*/
|
||||
public function isPositionedOnStartingNode($nodeName)
|
||||
{
|
||||
return $this->isPositionedOnNode($nodeName, XMLReader::ELEMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nodeName
|
||||
* @return bool Whether the XML Reader is currently positioned on the ending node with given name
|
||||
*/
|
||||
public function isPositionedOnEndingNode($nodeName)
|
||||
{
|
||||
return $this->isPositionedOnNode($nodeName, XMLReader::END_ELEMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nodeName
|
||||
* @param int $nodeType
|
||||
* @return bool Whether the XML Reader is currently positioned on the node with given name and type
|
||||
*/
|
||||
private function isPositionedOnNode($nodeName, $nodeType)
|
||||
{
|
||||
// In some cases, the node has a prefix (for instance, "<sheet>" can also be "<x:sheet>").
|
||||
// So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName").
|
||||
// @see https://github.com/box/spout/issues/233
|
||||
$hasPrefix = (strpos($nodeName, ':') !== false);
|
||||
$currentNodeName = ($hasPrefix) ? $this->name : $this->localName;
|
||||
|
||||
return ($this->nodeType === $nodeType && $currentNodeName === $nodeName);
|
||||
}
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper;
|
||||
|
||||
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class CellHelper
|
||||
* This class provides helper functions when working with cells
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper
|
||||
*/
|
||||
class CellHelper
|
||||
{
|
||||
// Using ord() is super slow... Using a pre-computed hash table instead.
|
||||
private static $columnLetterToIndexMapping = [
|
||||
'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6,
|
||||
'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13,
|
||||
'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20,
|
||||
'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25,
|
||||
];
|
||||
|
||||
/**
|
||||
* Fills the missing indexes of an array with a given value.
|
||||
* For instance, $dataArray = []; $a[1] = 1; $a[3] = 3;
|
||||
* Calling fillMissingArrayIndexes($dataArray, 'FILL') will return this array: ['FILL', 1, 'FILL', 3]
|
||||
*
|
||||
* @param array $dataArray The array to fill
|
||||
* @param string|void $fillValue optional
|
||||
* @return array
|
||||
*/
|
||||
public static function fillMissingArrayIndexes($dataArray, $fillValue = '')
|
||||
{
|
||||
if (empty($dataArray)) {
|
||||
return [];
|
||||
}
|
||||
$existingIndexes = array_keys($dataArray);
|
||||
|
||||
$newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue);
|
||||
$dataArray += $newIndexes;
|
||||
|
||||
ksort($dataArray);
|
||||
|
||||
return $dataArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base 10 column index associated to the cell index (base 26).
|
||||
* Excel uses A to Z letters for column indexing, where A is the 1st column,
|
||||
* Z is the 26th and AA is the 27th.
|
||||
* The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26.
|
||||
*
|
||||
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
|
||||
* @return int
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
|
||||
*/
|
||||
public static function getColumnIndexFromCellIndex($cellIndex)
|
||||
{
|
||||
if (!self::isValidCellIndex($cellIndex)) {
|
||||
throw new InvalidArgumentException('Cannot get column index from an invalid cell index.');
|
||||
}
|
||||
|
||||
$columnIndex = 0;
|
||||
|
||||
// Remove row information
|
||||
$columnLetters = preg_replace('/\d/', '', $cellIndex);
|
||||
|
||||
// strlen() is super slow too... Using isset() is way faster and not too unreadable,
|
||||
// since we checked before that there are between 1 and 3 letters.
|
||||
$columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1;
|
||||
|
||||
// Looping over the different letters of the column is slower than this method.
|
||||
// Also, not using the pow() function because it's slooooow...
|
||||
switch ($columnLength) {
|
||||
case 1:
|
||||
$columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]);
|
||||
break;
|
||||
case 2:
|
||||
$firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26;
|
||||
$secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]];
|
||||
$columnIndex = $firstLetterIndex + $secondLetterIndex;
|
||||
break;
|
||||
case 3:
|
||||
$firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676;
|
||||
$secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26;
|
||||
$thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]];
|
||||
$columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
return $columnIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a cell index is valid, in an Excel world.
|
||||
* To be valid, the cell index should start with capital letters and be followed by numbers.
|
||||
* There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'.
|
||||
*
|
||||
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isValidCellIndex($cellIndex)
|
||||
{
|
||||
return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1);
|
||||
}
|
||||
}
|
||||
@ -1,300 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper;
|
||||
|
||||
/**
|
||||
* Class CellValueFormatter
|
||||
* This class provides helper functions to format cell values
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper
|
||||
*/
|
||||
class CellValueFormatter
|
||||
{
|
||||
/** Definition of all possible cell types */
|
||||
const CELL_TYPE_INLINE_STRING = 'inlineStr';
|
||||
const CELL_TYPE_STR = 'str';
|
||||
const CELL_TYPE_SHARED_STRING = 's';
|
||||
const CELL_TYPE_BOOLEAN = 'b';
|
||||
const CELL_TYPE_NUMERIC = 'n';
|
||||
const CELL_TYPE_DATE = 'd';
|
||||
const CELL_TYPE_ERROR = 'e';
|
||||
|
||||
/** Definition of XML nodes names used to parse data */
|
||||
const XML_NODE_VALUE = 'v';
|
||||
const XML_NODE_INLINE_STRING_VALUE = 't';
|
||||
|
||||
/** Definition of XML attributes used to parse data */
|
||||
const XML_ATTRIBUTE_TYPE = 't';
|
||||
const XML_ATTRIBUTE_STYLE_ID = 's';
|
||||
|
||||
/** Constants used for date formatting */
|
||||
const NUM_SECONDS_IN_ONE_DAY = 86400;
|
||||
const NUM_SECONDS_IN_ONE_HOUR = 3600;
|
||||
const NUM_SECONDS_IN_ONE_MINUTE = 60;
|
||||
|
||||
/**
|
||||
* February 29th, 1900 is NOT a leap year but Excel thinks it is...
|
||||
* @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
|
||||
*/
|
||||
const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
|
||||
|
||||
/** @var SharedStringsHelper Helper to work with shared strings */
|
||||
protected $sharedStringsHelper;
|
||||
|
||||
/** @var StyleHelper Helper to work with styles */
|
||||
protected $styleHelper;
|
||||
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
protected $shouldFormatDates;
|
||||
|
||||
/** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
|
||||
protected $escaper;
|
||||
|
||||
/**
|
||||
* @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
|
||||
* @param StyleHelper $styleHelper Helper to work with styles
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
*/
|
||||
public function __construct($sharedStringsHelper, $styleHelper, $shouldFormatDates)
|
||||
{
|
||||
$this->sharedStringsHelper = $sharedStringsHelper;
|
||||
$this->styleHelper = $styleHelper;
|
||||
$this->shouldFormatDates = $shouldFormatDates;
|
||||
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
$this->escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
|
||||
*/
|
||||
public function extractAndFormatNodeValue($node)
|
||||
{
|
||||
// Default cell type is "n"
|
||||
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
|
||||
$cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID));
|
||||
$vNodeValue = $this->getVNodeValue($node);
|
||||
|
||||
if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
|
||||
return $vNodeValue;
|
||||
}
|
||||
|
||||
switch ($cellType) {
|
||||
case self::CELL_TYPE_INLINE_STRING:
|
||||
return $this->formatInlineStringCellValue($node);
|
||||
case self::CELL_TYPE_SHARED_STRING:
|
||||
return $this->formatSharedStringCellValue($vNodeValue);
|
||||
case self::CELL_TYPE_STR:
|
||||
return $this->formatStrCellValue($vNodeValue);
|
||||
case self::CELL_TYPE_BOOLEAN:
|
||||
return $this->formatBooleanCellValue($vNodeValue);
|
||||
case self::CELL_TYPE_NUMERIC:
|
||||
return $this->formatNumericCellValue($vNodeValue, $cellStyleId);
|
||||
case self::CELL_TYPE_DATE:
|
||||
return $this->formatDateCellValue($vNodeValue);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell's string value from a node's nested value node
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string The value associated with the cell
|
||||
*/
|
||||
protected function getVNodeValue($node)
|
||||
{
|
||||
// for cell types having a "v" tag containing the value.
|
||||
// if not, the returned value should be empty string.
|
||||
$vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0);
|
||||
return ($vNode !== null) ? $vNode->nodeValue : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell String value where string is inline.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string The value associated with the cell (null when the cell has an error)
|
||||
*/
|
||||
protected function formatInlineStringCellValue($node)
|
||||
{
|
||||
// inline strings are formatted this way:
|
||||
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
|
||||
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
|
||||
$cellValue = $this->escaper->unescape($tNode->nodeValue);
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell String value from shared-strings file using nodeValue index.
|
||||
*
|
||||
* @param string $nodeValue
|
||||
* @return string The value associated with the cell (null when the cell has an error)
|
||||
*/
|
||||
protected function formatSharedStringCellValue($nodeValue)
|
||||
{
|
||||
// shared strings are formatted this way:
|
||||
// <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
|
||||
$sharedStringIndex = intval($nodeValue);
|
||||
$escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
|
||||
$cellValue = $this->escaper->unescape($escapedCellValue);
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell String value, where string is stored in value node.
|
||||
*
|
||||
* @param string $nodeValue
|
||||
* @return string The value associated with the cell (null when the cell has an error)
|
||||
*/
|
||||
protected function formatStrCellValue($nodeValue)
|
||||
{
|
||||
$escapedCellValue = trim($nodeValue);
|
||||
$cellValue = $this->escaper->unescape($escapedCellValue);
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Numeric value from string of nodeValue.
|
||||
* The value can also represent a timestamp and a DateTime will be returned.
|
||||
*
|
||||
* @param string $nodeValue
|
||||
* @param int $cellStyleId 0 being the default style
|
||||
* @return int|float|\DateTime|null The value associated with the cell
|
||||
*/
|
||||
protected function formatNumericCellValue($nodeValue, $cellStyleId)
|
||||
{
|
||||
// Numeric values can represent numbers as well as timestamps.
|
||||
// We need to look at the style of the cell to determine whether it is one or the other.
|
||||
$shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId);
|
||||
|
||||
if ($shouldFormatAsDate) {
|
||||
return $this->formatExcelTimestampValue(floatval($nodeValue), $cellStyleId);
|
||||
} else {
|
||||
$nodeIntValue = intval($nodeValue);
|
||||
return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cell's PHP Date value, associated to the given timestamp.
|
||||
* NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
|
||||
* NOTE: The timestamp can also represent a time, if it is a value between 0 and 1.
|
||||
*
|
||||
* @param float $nodeValue
|
||||
* @param int $cellStyleId 0 being the default style
|
||||
* @return \DateTime|null The value associated with the cell or NULL if invalid date value
|
||||
*/
|
||||
protected function formatExcelTimestampValue($nodeValue, $cellStyleId)
|
||||
{
|
||||
// Fix for the erroneous leap year in Excel
|
||||
if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
|
||||
--$nodeValue;
|
||||
}
|
||||
|
||||
if ($nodeValue >= 1) {
|
||||
// Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01.
|
||||
return $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId);
|
||||
} else if ($nodeValue >= 0) {
|
||||
// Values between 0 and 1 represent "times".
|
||||
return $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId);
|
||||
} else {
|
||||
// invalid date
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cell's PHP DateTime value, associated to the given timestamp.
|
||||
* Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date).
|
||||
*
|
||||
* @param float $nodeValue
|
||||
* @param int $cellStyleId 0 being the default style
|
||||
* @return \DateTime|string The value associated with the cell
|
||||
*/
|
||||
protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId)
|
||||
{
|
||||
$time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY);
|
||||
$hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR);
|
||||
$minutes = floor($time / self::NUM_SECONDS_IN_ONE_MINUTE) - ($hours * self::NUM_SECONDS_IN_ONE_MINUTE);
|
||||
$seconds = $time - ($hours * self::NUM_SECONDS_IN_ONE_HOUR) - ($minutes * self::NUM_SECONDS_IN_ONE_MINUTE);
|
||||
|
||||
// using the base Excel date (Jan 1st, 1900) - not relevant here
|
||||
$dateObj = new \DateTime('1900-01-01');
|
||||
$dateObj->setTime($hours, $minutes, $seconds);
|
||||
|
||||
if ($this->shouldFormatDates) {
|
||||
$styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
|
||||
$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
|
||||
return $dateObj->format($phpDateFormat);
|
||||
} else {
|
||||
return $dateObj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cell's PHP Date value, associated to the given timestamp.
|
||||
* NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
|
||||
*
|
||||
* @param float $nodeValue
|
||||
* @param int $cellStyleId 0 being the default style
|
||||
* @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
|
||||
*/
|
||||
protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId)
|
||||
{
|
||||
// Do not use any unix timestamps for calculation to prevent
|
||||
// issues with numbers exceeding 2^31.
|
||||
$secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY;
|
||||
$secondsRemainder = round($secondsRemainder, 0);
|
||||
|
||||
try {
|
||||
$dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31');
|
||||
$dateObj->modify('+' . intval($nodeValue) . 'days');
|
||||
$dateObj->modify('+' . $secondsRemainder . 'seconds');
|
||||
|
||||
if ($this->shouldFormatDates) {
|
||||
$styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
|
||||
$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
|
||||
return $dateObj->format($phpDateFormat);
|
||||
} else {
|
||||
return $dateObj;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell Boolean value from a specific node's Value.
|
||||
*
|
||||
* @param string $nodeValue
|
||||
* @return bool The value associated with the cell
|
||||
*/
|
||||
protected function formatBooleanCellValue($nodeValue)
|
||||
{
|
||||
// !! is similar to boolval()
|
||||
$cellValue = !!$nodeValue;
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
|
||||
* @see ECMA-376 Part 1 - §18.17.4
|
||||
*
|
||||
* @param string $nodeValue ISO 8601 Date string
|
||||
* @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
|
||||
*/
|
||||
protected function formatDateCellValue($nodeValue)
|
||||
{
|
||||
// Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
|
||||
try {
|
||||
return ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,124 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper;
|
||||
|
||||
/**
|
||||
* Class DateFormatHelper
|
||||
* This class provides helper functions to format Excel dates
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper
|
||||
*/
|
||||
class DateFormatHelper
|
||||
{
|
||||
const KEY_GENERAL = 'general';
|
||||
const KEY_HOUR_12 = '12h';
|
||||
const KEY_HOUR_24 = '24h';
|
||||
|
||||
/**
|
||||
* This map is used to replace Excel format characters by their PHP equivalent.
|
||||
* Keys should be ordered from longest to smallest.
|
||||
*
|
||||
* @var array Mapping between Excel format characters and PHP format characters
|
||||
*/
|
||||
private static $excelDateFormatToPHPDateFormatMapping = [
|
||||
self::KEY_GENERAL => [
|
||||
// Time
|
||||
'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem
|
||||
':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month)
|
||||
'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month)
|
||||
'ss' => 's', // Seconds, with leading zeros
|
||||
'.s' => '', // Ignore (fractional seconds format does not exist in PHP)
|
||||
|
||||
// Date
|
||||
'e' => 'Y', // Full numeric representation of a year, 4 digits
|
||||
'yyyy' => 'Y', // Full numeric representation of a year, 4 digits
|
||||
'yy' => 'y', // Two digit representation of a year
|
||||
'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...)
|
||||
'mmmm' => 'F', // Full textual representation of a month
|
||||
'mmm' => 'M', // Short textual representation of a month, three letters
|
||||
'mm' => 'm', // Numeric representation of a month, with leading zeros
|
||||
'm' => 'n', // Numeric representation of a month, without leading zeros
|
||||
'dddd' => 'l', // Full textual representation of the day of the week
|
||||
'ddd' => 'D', // Textual representation of a day, three letters
|
||||
'dd' => 'd', // Day of the month, 2 digits with leading zeros
|
||||
'd' => 'j', // Day of the month without leading zeros
|
||||
],
|
||||
self::KEY_HOUR_12 => [
|
||||
'hh' => 'h', // 12-hour format of an hour without leading zeros
|
||||
'h' => 'g', // 12-hour format of an hour without leading zeros
|
||||
],
|
||||
self::KEY_HOUR_24 => [
|
||||
'hh' => 'H', // 24-hour hours with leading zero
|
||||
'h' => 'G', // 24-hour format of an hour without leading zeros
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Converts the given Excel date format to a format understandable by the PHP date function.
|
||||
*
|
||||
* @param string $excelDateFormat Excel date format
|
||||
* @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php)
|
||||
*/
|
||||
public static function toPHPDateFormat($excelDateFormat)
|
||||
{
|
||||
// Remove brackets potentially present at the beginning of the format string
|
||||
// and text portion of the format at the end of it (starting with ";")
|
||||
// See §18.8.31 of ECMA-376 for more detail.
|
||||
$dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat);
|
||||
|
||||
// Double quotes are used to escape characters that must not be interpreted.
|
||||
// For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y"
|
||||
// By exploding the format string using double quote as a delimiter, we can get all parts
|
||||
// that must be transformed (even indexes) and all parts that must not be (odd indexes).
|
||||
$dateFormatParts = explode('"', $dateFormat);
|
||||
|
||||
foreach ($dateFormatParts as $partIndex => $dateFormatPart) {
|
||||
// do not look at odd indexes
|
||||
if ($partIndex % 2 === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure all characters are lowercase, as the mapping table is using lowercase characters
|
||||
$transformedPart = strtolower($dateFormatPart);
|
||||
|
||||
// Remove escapes related to non-format characters
|
||||
$transformedPart = str_replace('\\', '', $transformedPart);
|
||||
|
||||
// Apply general transformation first...
|
||||
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]);
|
||||
|
||||
// ... then apply hour transformation, for 12-hour or 24-hour format
|
||||
if (self::has12HourFormatMarker($dateFormatPart)) {
|
||||
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]);
|
||||
} else {
|
||||
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]);
|
||||
}
|
||||
|
||||
// overwrite the parts array with the new transformed part
|
||||
$dateFormatParts[$partIndex] = $transformedPart;
|
||||
}
|
||||
|
||||
// Merge all transformed parts back together
|
||||
$phpDateFormat = implode('"', $dateFormatParts);
|
||||
|
||||
// Finally, to have the date format compatible with the DateTime::format() function, we need to escape
|
||||
// all characters that are inside double quotes (and double quotes must be removed).
|
||||
// For instance, ["Day " dd] should become [\D\a\y\ dd]
|
||||
$phpDateFormat = preg_replace_callback('/"(.+?)"/', function($matches) {
|
||||
$stringToEscape = $matches[1];
|
||||
$letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY);
|
||||
return '\\' . implode('\\', $letters);
|
||||
}, $phpDateFormat);
|
||||
|
||||
return $phpDateFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $excelDateFormat Date format as defined by Excel
|
||||
* @return bool Whether the given date format has the 12-hour format marker
|
||||
*/
|
||||
private static function has12HourFormatMarker($excelDateFormat)
|
||||
{
|
||||
return (stripos($excelDateFormat, 'am/pm') !== false);
|
||||
}
|
||||
}
|
||||
@ -1,159 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
|
||||
|
||||
/**
|
||||
* Class CachingStrategyFactory
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
|
||||
*/
|
||||
class CachingStrategyFactory
|
||||
{
|
||||
/**
|
||||
* The memory amount needed to store a string was obtained empirically from this data:
|
||||
*
|
||||
* ------------------------------------
|
||||
* | Number of chars⁺ | Memory needed |
|
||||
* ------------------------------------
|
||||
* | 3,000 | 1 MB |
|
||||
* | 15,000 | 2 MB |
|
||||
* | 30,000 | 5 MB |
|
||||
* | 75,000 | 11 MB |
|
||||
* | 150,000 | 21 MB |
|
||||
* | 300,000 | 43 MB |
|
||||
* | 750,000 | 105 MB |
|
||||
* | 1,500,000 | 210 MB |
|
||||
* | 2,250,000 | 315 MB |
|
||||
* | 3,000,000 | 420 MB |
|
||||
* | 4,500,000 | 630 MB |
|
||||
* ------------------------------------
|
||||
*
|
||||
* ⁺ All characters were 1 byte long
|
||||
*
|
||||
* This gives a linear graph where each 1-byte character requires about 150 bytes to be stored.
|
||||
* Given that some characters can take up to 4 bytes, we need 600 bytes per character to be safe.
|
||||
* Also, there is on average about 20 characters per cell (this is entirely empirical data...).
|
||||
*
|
||||
* This means that in order to store one shared string in memory, the memory amount needed is:
|
||||
* => 20 * 600 ≈ 12KB
|
||||
*/
|
||||
const AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB = 12;
|
||||
|
||||
/**
|
||||
* To avoid running out of memory when extracting a huge number of shared strings, they can be saved to temporary files
|
||||
* instead of in memory. Then, when accessing a string, the corresponding file contents will be loaded in memory
|
||||
* and the string will be quickly retrieved.
|
||||
* The performance bottleneck is not when creating these temporary files, but rather when loading their content.
|
||||
* Because the contents of the last loaded file stays in memory until another file needs to be loaded, it works
|
||||
* best when the indexes of the shared strings are sorted in the sheet data.
|
||||
* 10,000 was chosen because it creates small files that are fast to be loaded in memory.
|
||||
*/
|
||||
const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
|
||||
|
||||
/** @var CachingStrategyFactory|null Singleton instance */
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Private constructor for singleton
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of the factory
|
||||
*
|
||||
* @return CachingStrategyFactory
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new CachingStrategyFactory();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the best caching strategy, given the number of unique shared strings
|
||||
* and the amount of memory available.
|
||||
*
|
||||
* @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
|
||||
* @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
|
||||
* @return CachingStrategyInterface The best caching strategy
|
||||
*/
|
||||
public function getBestCachingStrategy($sharedStringsUniqueCount, $tempFolder = null)
|
||||
{
|
||||
if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) {
|
||||
return new InMemoryStrategy($sharedStringsUniqueCount);
|
||||
} else {
|
||||
return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether it is safe to use in-memory caching, given the number of unique shared strings
|
||||
* and the amount of memory available.
|
||||
*
|
||||
* @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
|
||||
* @return bool
|
||||
*/
|
||||
protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)
|
||||
{
|
||||
// if the number of shared strings in unknown, do not use "in memory" strategy
|
||||
if ($sharedStringsUniqueCount === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$memoryAvailable = $this->getMemoryLimitInKB();
|
||||
|
||||
if ($memoryAvailable === -1) {
|
||||
// if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe
|
||||
return ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE);
|
||||
} else {
|
||||
$memoryNeeded = $sharedStringsUniqueCount * self::AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB;
|
||||
return ($memoryAvailable > $memoryNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PHP "memory_limit" in Kilobytes
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function getMemoryLimitInKB()
|
||||
{
|
||||
$memoryLimitFormatted = $this->getMemoryLimitFromIni();
|
||||
$memoryLimitFormatted = strtolower(trim($memoryLimitFormatted));
|
||||
|
||||
// No memory limit
|
||||
if ($memoryLimitFormatted === '-1') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) {
|
||||
$amount = intval($matches[1]);
|
||||
$unit = $matches[2];
|
||||
|
||||
switch ($unit) {
|
||||
case 'b': return ($amount / 1024);
|
||||
case 'k': return $amount;
|
||||
case 'm': return ($amount * 1024);
|
||||
case 'g': return ($amount * 1024 * 1024);
|
||||
case 't': return ($amount * 1024 * 1024 * 1024);
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formatted "memory_limit" value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getMemoryLimitFromIni()
|
||||
{
|
||||
return ini_get('memory_limit');
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
|
||||
|
||||
/**
|
||||
* Interface CachingStrategyInterface
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
|
||||
*/
|
||||
interface CachingStrategyInterface
|
||||
{
|
||||
/**
|
||||
* Adds the given string to the cache.
|
||||
*
|
||||
* @param string $sharedString The string to be added to the cache
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return void
|
||||
*/
|
||||
public function addStringForIndex($sharedString, $sharedStringIndex);
|
||||
|
||||
/**
|
||||
* Closes the cache after the last shared string was added.
|
||||
* This prevents any additional string from being added to the cache.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeCache();
|
||||
|
||||
/**
|
||||
* Returns the string located at the given index from the cache.
|
||||
*
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return string The shared string at the given index
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
|
||||
*/
|
||||
public function getStringAtIndex($sharedStringIndex);
|
||||
|
||||
/**
|
||||
* Destroys the cache, freeing memory and removing any created artifacts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache();
|
||||
}
|
||||
@ -1,193 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
|
||||
|
||||
use Box\Spout\Common\Helper\FileSystemHelper;
|
||||
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
|
||||
use Box\Spout\Reader\Exception\SharedStringNotFoundException;
|
||||
|
||||
/**
|
||||
* Class FileBasedStrategy
|
||||
*
|
||||
* This class implements the file-based caching strategy for shared strings.
|
||||
* Shared strings are stored in small files (with a max number of strings per file).
|
||||
* This strategy is slower than an in-memory strategy but is used to avoid out of memory crashes.
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
|
||||
*/
|
||||
class FileBasedStrategy implements CachingStrategyInterface
|
||||
{
|
||||
/** Value to use to escape the line feed character ("\n") */
|
||||
const ESCAPED_LINE_FEED_CHARACTER = '_x000A_';
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\FileSystemHelper Helper to perform file system operations */
|
||||
protected $fileSystemHelper;
|
||||
|
||||
/** @var string Temporary folder where the temporary files will be created */
|
||||
protected $tempFolder;
|
||||
|
||||
/**
|
||||
* @var int Maximum number of strings that can be stored in one temp file
|
||||
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
|
||||
*/
|
||||
protected $maxNumStringsPerTempFile;
|
||||
|
||||
/** @var resource Pointer to the last temp file a shared string was written to */
|
||||
protected $tempFilePointer;
|
||||
|
||||
/**
|
||||
* @var string Path of the temporary file whose contents is currently stored in memory
|
||||
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
|
||||
*/
|
||||
protected $inMemoryTempFilePath;
|
||||
|
||||
/**
|
||||
* @var array Contents of the temporary file that was last read
|
||||
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
|
||||
*/
|
||||
protected $inMemoryTempFileContents;
|
||||
|
||||
/**
|
||||
* @param string|null $tempFolder Temporary folder where the temporary files to store shared strings will be stored
|
||||
* @param int $maxNumStringsPerTempFile Maximum number of strings that can be stored in one temp file
|
||||
*/
|
||||
public function __construct($tempFolder, $maxNumStringsPerTempFile)
|
||||
{
|
||||
$rootTempFolder = ($tempFolder) ?: sys_get_temp_dir();
|
||||
$this->fileSystemHelper = new FileSystemHelper($rootTempFolder);
|
||||
$this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
|
||||
|
||||
$this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
|
||||
|
||||
$this->globalFunctionsHelper = new GlobalFunctionsHelper();
|
||||
$this->tempFilePointer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given string to the cache.
|
||||
*
|
||||
* @param string $sharedString The string to be added to the cache
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return void
|
||||
*/
|
||||
public function addStringForIndex($sharedString, $sharedStringIndex)
|
||||
{
|
||||
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
|
||||
|
||||
if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
|
||||
if ($this->tempFilePointer) {
|
||||
$this->globalFunctionsHelper->fclose($this->tempFilePointer);
|
||||
}
|
||||
$this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w');
|
||||
}
|
||||
|
||||
// The shared string retrieval logic expects each cell data to be on one line only
|
||||
// Encoding the line feed character allows to preserve this assumption
|
||||
$lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
|
||||
|
||||
$this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path for the temp file that should contain the string for the given index
|
||||
*
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return string The temp file path for the given index
|
||||
*/
|
||||
protected function getSharedStringTempFilePath($sharedStringIndex)
|
||||
{
|
||||
$numTempFile = intval($sharedStringIndex / $this->maxNumStringsPerTempFile);
|
||||
return $this->tempFolder . '/sharedstrings' . $numTempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache after the last shared string was added.
|
||||
* This prevents any additional string from being added to the cache.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeCache()
|
||||
{
|
||||
// close pointer to the last temp file that was written
|
||||
if ($this->tempFilePointer) {
|
||||
$this->globalFunctionsHelper->fclose($this->tempFilePointer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the string located at the given index from the cache.
|
||||
*
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return string The shared string at the given index
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
|
||||
*/
|
||||
public function getStringAtIndex($sharedStringIndex)
|
||||
{
|
||||
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
|
||||
$indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
|
||||
|
||||
if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
|
||||
throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
|
||||
}
|
||||
|
||||
if ($this->inMemoryTempFilePath !== $tempFilePath) {
|
||||
// free memory
|
||||
unset($this->inMemoryTempFileContents);
|
||||
|
||||
$this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath));
|
||||
$this->inMemoryTempFilePath = $tempFilePath;
|
||||
}
|
||||
|
||||
$sharedString = null;
|
||||
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
if (isset($this->inMemoryTempFileContents[$indexInFile])) {
|
||||
$escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
|
||||
$sharedString = $this->unescapeLineFeed($escapedSharedString);
|
||||
}
|
||||
|
||||
if ($sharedString === null) {
|
||||
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
|
||||
}
|
||||
|
||||
return rtrim($sharedString, PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the line feed characters (\n)
|
||||
*
|
||||
* @param string $unescapedString
|
||||
* @return string
|
||||
*/
|
||||
private function escapeLineFeed($unescapedString)
|
||||
{
|
||||
return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes the line feed characters (\n)
|
||||
*
|
||||
* @param string $escapedString
|
||||
* @return string
|
||||
*/
|
||||
private function unescapeLineFeed($escapedString)
|
||||
{
|
||||
return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the cache, freeing memory and removing any created artifacts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache()
|
||||
{
|
||||
if ($this->tempFolder) {
|
||||
$this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
|
||||
|
||||
use Box\Spout\Reader\Exception\SharedStringNotFoundException;
|
||||
|
||||
/**
|
||||
* Class InMemoryStrategy
|
||||
*
|
||||
* This class implements the in-memory caching strategy for shared strings.
|
||||
* This strategy is used when the number of unique strings is low, compared to the memory available.
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
|
||||
*/
|
||||
class InMemoryStrategy implements CachingStrategyInterface
|
||||
{
|
||||
/** @var \SplFixedArray Array used to cache the shared strings */
|
||||
protected $inMemoryCache;
|
||||
|
||||
/** @var bool Whether the cache has been closed */
|
||||
protected $isCacheClosed;
|
||||
|
||||
/**
|
||||
* @param int $sharedStringsUniqueCount Number of unique shared strings
|
||||
*/
|
||||
public function __construct($sharedStringsUniqueCount)
|
||||
{
|
||||
$this->inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount);
|
||||
$this->isCacheClosed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given string to the cache.
|
||||
*
|
||||
* @param string $sharedString The string to be added to the cache
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return void
|
||||
*/
|
||||
public function addStringForIndex($sharedString, $sharedStringIndex)
|
||||
{
|
||||
if (!$this->isCacheClosed) {
|
||||
$this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache after the last shared string was added.
|
||||
* This prevents any additional string from being added to the cache.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeCache()
|
||||
{
|
||||
$this->isCacheClosed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string located at the given index from the cache.
|
||||
*
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return string The shared string at the given index
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
|
||||
*/
|
||||
public function getStringAtIndex($sharedStringIndex)
|
||||
{
|
||||
try {
|
||||
return $this->inMemoryCache->offsetGet($sharedStringIndex);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the cache, freeing memory and removing any created artifacts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache()
|
||||
{
|
||||
unset($this->inMemoryCache);
|
||||
$this->isCacheClosed = false;
|
||||
}
|
||||
}
|
||||
@ -1,245 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory;
|
||||
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyInterface;
|
||||
|
||||
/**
|
||||
* Class SharedStringsHelper
|
||||
* This class provides helper functions for reading sharedStrings XML file
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper
|
||||
*/
|
||||
class SharedStringsHelper
|
||||
{
|
||||
/** Path of sharedStrings XML file inside the XLSX file */
|
||||
const SHARED_STRINGS_XML_FILE_PATH = 'xl/sharedStrings.xml';
|
||||
|
||||
/** Main namespace for the sharedStrings.xml file */
|
||||
const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||
|
||||
/** Definition of XML nodes names used to parse data */
|
||||
const XML_NODE_SST = 'sst';
|
||||
const XML_NODE_SI = 'si';
|
||||
const XML_NODE_R = 'r';
|
||||
const XML_NODE_T = 't';
|
||||
|
||||
/** Definition of XML attributes used to parse data */
|
||||
const XML_ATTRIBUTE_COUNT = 'count';
|
||||
const XML_ATTRIBUTE_UNIQUE_COUNT = 'uniqueCount';
|
||||
const XML_ATTRIBUTE_XML_SPACE = 'xml:space';
|
||||
const XML_ATTRIBUTE_VALUE_PRESERVE = 'preserve';
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var string Temporary folder where the temporary files to store shared strings will be stored */
|
||||
protected $tempFolder;
|
||||
|
||||
/** @var CachingStrategyInterface The best caching strategy for storing shared strings */
|
||||
protected $cachingStrategy;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param string|null|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
|
||||
*/
|
||||
public function __construct($filePath, $tempFolder = null)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->tempFolder = $tempFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the XLSX file contains a shared strings XML file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSharedStrings()
|
||||
{
|
||||
$hasSharedStrings = false;
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($this->filePath) === true) {
|
||||
$hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false);
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
return $hasSharedStrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an in-memory array containing all the shared strings of the sheet.
|
||||
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
|
||||
* It is then accessed by the sheet data, via the string index in the built table.
|
||||
*
|
||||
* More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx
|
||||
*
|
||||
* The XML file can be really big with sheets containing a lot of data. That is why
|
||||
* we need to use a XML reader that provides streaming like the XMLReader library.
|
||||
* Please note that SimpleXML does not provide such a functionality but since it is faster
|
||||
* and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read
|
||||
*/
|
||||
public function extractSharedStrings()
|
||||
{
|
||||
$xmlReader = new XMLReader();
|
||||
$sharedStringIndex = 0;
|
||||
|
||||
$sharedStringsFilePath = $this->getSharedStringsFilePath();
|
||||
if ($xmlReader->open($sharedStringsFilePath) === false) {
|
||||
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
|
||||
}
|
||||
|
||||
try {
|
||||
$sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader);
|
||||
$this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount);
|
||||
|
||||
$xmlReader->readUntilNodeFound(self::XML_NODE_SI);
|
||||
|
||||
while ($xmlReader->name === self::XML_NODE_SI) {
|
||||
$this->processSharedStringsItem($xmlReader, $sharedStringIndex);
|
||||
$sharedStringIndex++;
|
||||
|
||||
// jump to the next '<si>' tag
|
||||
$xmlReader->next(self::XML_NODE_SI);
|
||||
}
|
||||
|
||||
$this->cachingStrategy->closeCache();
|
||||
|
||||
} catch (XMLProcessingException $exception) {
|
||||
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
|
||||
}
|
||||
|
||||
$xmlReader->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The path to the shared strings XML file
|
||||
*/
|
||||
protected function getSharedStringsFilePath()
|
||||
{
|
||||
return 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared strings unique count, as specified in <sst> tag.
|
||||
*
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance
|
||||
* @return int|null Number of unique shared strings in the sharedStrings.xml file
|
||||
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read
|
||||
*/
|
||||
protected function getSharedStringsUniqueCount($xmlReader)
|
||||
{
|
||||
$xmlReader->next(self::XML_NODE_SST);
|
||||
|
||||
// Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE)
|
||||
while ($xmlReader->name === self::XML_NODE_SST && $xmlReader->nodeType !== XMLReader::ELEMENT) {
|
||||
$xmlReader->read();
|
||||
}
|
||||
|
||||
$uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_UNIQUE_COUNT);
|
||||
|
||||
// some software do not add the "uniqueCount" attribute but only use the "count" one
|
||||
// @see https://github.com/box/spout/issues/254
|
||||
if ($uniqueCount === null) {
|
||||
$uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_COUNT);
|
||||
}
|
||||
|
||||
return ($uniqueCount !== null) ? intval($uniqueCount) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the best shared strings caching strategy.
|
||||
*
|
||||
* @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
|
||||
* @return CachingStrategyInterface
|
||||
*/
|
||||
protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount)
|
||||
{
|
||||
return CachingStrategyFactory::getInstance()
|
||||
->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the shared strings item XML node which the given XML reader is positioned on.
|
||||
*
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on a "<si>" node
|
||||
* @param int $sharedStringIndex Index of the processed shared strings item
|
||||
* @return void
|
||||
*/
|
||||
protected function processSharedStringsItem($xmlReader, $sharedStringIndex)
|
||||
{
|
||||
$sharedStringValue = '';
|
||||
|
||||
// NOTE: expand() will automatically decode all XML entities of the child nodes
|
||||
$siNode = $xmlReader->expand();
|
||||
$textNodes = $siNode->getElementsByTagName(self::XML_NODE_T);
|
||||
|
||||
foreach ($textNodes as $textNode) {
|
||||
if ($this->shouldExtractTextNodeValue($textNode)) {
|
||||
$textNodeValue = $textNode->nodeValue;
|
||||
$shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode);
|
||||
|
||||
$sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : trim($textNodeValue);
|
||||
}
|
||||
}
|
||||
|
||||
$this->cachingStrategy->addStringForIndex($sharedStringValue, $sharedStringIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not all text nodes' values must be extracted.
|
||||
* Some text nodes are part of a node describing the pronunciation for instance.
|
||||
* We'll only consider the nodes whose parents are "<si>" or "<r>".
|
||||
*
|
||||
* @param \DOMElement $textNode Text node to check
|
||||
* @return bool Whether the given text node's value must be extracted
|
||||
*/
|
||||
protected function shouldExtractTextNodeValue($textNode)
|
||||
{
|
||||
$parentTagName = $textNode->parentNode->localName;
|
||||
return ($parentTagName === self::XML_NODE_SI || $parentTagName === self::XML_NODE_R);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
|
||||
*
|
||||
* @param \DOMElement $textNode The text node element (<t>) whose whitespace may be preserved
|
||||
* @return bool Whether whitespace should be preserved
|
||||
*/
|
||||
protected function shouldPreserveWhitespace($textNode)
|
||||
{
|
||||
$spaceValue = $textNode->getAttribute(self::XML_ATTRIBUTE_XML_SPACE);
|
||||
return ($spaceValue === self::XML_ATTRIBUTE_VALUE_PRESERVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared string at the given index, using the previously chosen caching strategy.
|
||||
*
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return string The shared string at the given index
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
|
||||
*/
|
||||
public function getStringAtIndex($sharedStringIndex)
|
||||
{
|
||||
return $this->cachingStrategy->getStringAtIndex($sharedStringIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the cache, freeing memory and removing any created artifacts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cleanup()
|
||||
{
|
||||
if ($this->cachingStrategy) {
|
||||
$this->cachingStrategy->clearCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper;
|
||||
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
use Box\Spout\Reader\XLSX\Sheet;
|
||||
|
||||
/**
|
||||
* Class SheetHelper
|
||||
* This class provides helper functions related to XLSX sheets
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper
|
||||
*/
|
||||
class SheetHelper
|
||||
{
|
||||
/** Paths of XML files relative to the XLSX file root */
|
||||
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
|
||||
const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
|
||||
|
||||
/** Definition of XML node names used to parse data */
|
||||
const XML_NODE_WORKBOOK_VIEW = 'workbookView';
|
||||
const XML_NODE_SHEET = 'sheet';
|
||||
const XML_NODE_SHEETS = 'sheets';
|
||||
const XML_NODE_RELATIONSHIP = 'Relationship';
|
||||
|
||||
/** Definition of XML attributes used to parse data */
|
||||
const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab';
|
||||
const XML_ATTRIBUTE_R_ID = 'r:id';
|
||||
const XML_ATTRIBUTE_NAME = 'name';
|
||||
const XML_ATTRIBUTE_ID = 'Id';
|
||||
const XML_ATTRIBUTE_TARGET = 'Target';
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var \Box\Spout\Reader\XLSX\ReaderOptions Reader's current options */
|
||||
protected $options;
|
||||
|
||||
/** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
|
||||
protected $sharedStringsHelper;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->options = $options;
|
||||
$this->sharedStringsHelper = $sharedStringsHelper;
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sheets metadata of the file located at the previously given file path.
|
||||
* The paths to the sheets' data are read from the [Content_Types].xml file.
|
||||
*
|
||||
* @return Sheet[] Sheets within the XLSX file
|
||||
*/
|
||||
public function getSheets()
|
||||
{
|
||||
$sheets = [];
|
||||
$sheetIndex = 0;
|
||||
$activeSheetIndex = 0; // By default, the first sheet is active
|
||||
|
||||
$xmlReader = new XMLReader();
|
||||
if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
|
||||
while ($xmlReader->read()) {
|
||||
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) {
|
||||
// The "workbookView" node is located before "sheet" nodes, ensuring that
|
||||
// the active sheet is known before parsing sheets data.
|
||||
$activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
|
||||
} else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) {
|
||||
$isSheetActive = ($sheetIndex === $activeSheetIndex);
|
||||
$sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive);
|
||||
$sheetIndex++;
|
||||
} else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) {
|
||||
// stop reading once all sheets have been read
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$xmlReader->close();
|
||||
}
|
||||
|
||||
return $sheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
|
||||
* We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
|
||||
* ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
|
||||
*
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
|
||||
* @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
|
||||
* @param bool $isSheetActive Whether this sheet was defined as active
|
||||
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
|
||||
*/
|
||||
protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
|
||||
{
|
||||
$sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
|
||||
$escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
|
||||
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
$escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
|
||||
$sheetName = $escaper->unescape($escapedSheetName);
|
||||
|
||||
$sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
|
||||
|
||||
return new Sheet(
|
||||
$this->filePath, $sheetDataXMLFilePath,
|
||||
$sheetIndexZeroBased, $sheetName, $isSheetActive,
|
||||
$this->options, $this->sharedStringsHelper
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sheetId The sheet ID, as defined in "workbook.xml"
|
||||
* @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
|
||||
*/
|
||||
protected function getSheetDataXMLFilePathForSheetId($sheetId)
|
||||
{
|
||||
$sheetDataXMLFilePath = '';
|
||||
|
||||
// find the file path of the sheet, by looking at the "workbook.xml.res" file
|
||||
$xmlReader = new XMLReader();
|
||||
if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
|
||||
while ($xmlReader->read()) {
|
||||
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
|
||||
$relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
|
||||
|
||||
if ($relationshipSheetId === $sheetId) {
|
||||
// In workbook.xml.rels, it is only "worksheets/sheet1.xml"
|
||||
// In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
|
||||
$sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
|
||||
|
||||
// sometimes, the sheet data file path already contains "/xl/"...
|
||||
if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
|
||||
$sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$xmlReader->close();
|
||||
}
|
||||
|
||||
return $sheetDataXMLFilePath;
|
||||
}
|
||||
}
|
||||
@ -1,330 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX\Helper;
|
||||
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
|
||||
/**
|
||||
* Class StyleHelper
|
||||
* This class provides helper functions related to XLSX styles
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX\Helper
|
||||
*/
|
||||
class StyleHelper
|
||||
{
|
||||
/** Paths of XML files relative to the XLSX file root */
|
||||
const STYLES_XML_FILE_PATH = 'xl/styles.xml';
|
||||
|
||||
/** Nodes used to find relevant information in the styles XML file */
|
||||
const XML_NODE_NUM_FMTS = 'numFmts';
|
||||
const XML_NODE_NUM_FMT = 'numFmt';
|
||||
const XML_NODE_CELL_XFS = 'cellXfs';
|
||||
const XML_NODE_XF = 'xf';
|
||||
|
||||
/** Attributes used to find relevant information in the styles XML file */
|
||||
const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId';
|
||||
const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode';
|
||||
const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat';
|
||||
|
||||
/** By convention, default style ID is 0 */
|
||||
const DEFAULT_STYLE_ID = 0;
|
||||
|
||||
const NUMBER_FORMAT_GENERAL = 'General';
|
||||
|
||||
/**
|
||||
* @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx
|
||||
* @var array Mapping between built-in numFmtId and the associated format - for dates only
|
||||
*/
|
||||
protected static $builtinNumFmtIdToNumFormatMapping = [
|
||||
14 => 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy'
|
||||
15 => 'd-mmm-yy',
|
||||
16 => 'd-mmm',
|
||||
17 => 'mmm-yy',
|
||||
18 => 'h:mm AM/PM',
|
||||
19 => 'h:mm:ss AM/PM',
|
||||
20 => 'h:mm',
|
||||
21 => 'h:mm:ss',
|
||||
22 => 'm/d/yyyy h:mm', // @NOTE: ECMA spec is 'm/d/yy h:mm',
|
||||
45 => 'mm:ss',
|
||||
46 => '[h]:mm:ss',
|
||||
47 => 'mm:ss.0', // @NOTE: ECMA spec is 'mmss.0',
|
||||
];
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var array Array containing the IDs of built-in number formats indicating a date */
|
||||
protected $builtinNumFmtIdIndicatingDates;
|
||||
|
||||
/** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
|
||||
protected $customNumberFormats;
|
||||
|
||||
/** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
|
||||
protected $stylesAttributes;
|
||||
|
||||
/** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */
|
||||
protected $numFmtIdToIsDateFormatCache = [];
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
*/
|
||||
public function __construct($filePath)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the style with the given ID should consider
|
||||
* numeric values as timestamps and format the cell as a date.
|
||||
*
|
||||
* @param int $styleId Zero-based style ID
|
||||
* @return bool Whether the cell with the given cell should display a date instead of a numeric value
|
||||
*/
|
||||
public function shouldFormatNumericValueAsDate($styleId)
|
||||
{
|
||||
$stylesAttributes = $this->getStylesAttributes();
|
||||
|
||||
// Default style (0) does not format numeric values as timestamps. Only custom styles do.
|
||||
// Also if the style ID does not exist in the styles.xml file, format as numeric value.
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$styleAttributes = $stylesAttributes[$styleId];
|
||||
|
||||
return $this->doesStyleIndicateDate($styleAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the styles.xml file and extract the relevant information from the file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function extractRelevantInfo()
|
||||
{
|
||||
$this->customNumberFormats = [];
|
||||
$this->stylesAttributes = [];
|
||||
|
||||
$xmlReader = new XMLReader();
|
||||
|
||||
if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) {
|
||||
while ($xmlReader->read()) {
|
||||
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
|
||||
$this->extractNumberFormats($xmlReader);
|
||||
|
||||
} else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
|
||||
$this->extractStyleAttributes($xmlReader);
|
||||
}
|
||||
}
|
||||
|
||||
$xmlReader->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts number formats from the "numFmt" nodes.
|
||||
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
||||
* to the reuse of formats. So 1 million cells should not use 1 million formats.
|
||||
*
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node
|
||||
* @return void
|
||||
*/
|
||||
protected function extractNumberFormats($xmlReader)
|
||||
{
|
||||
while ($xmlReader->read()) {
|
||||
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) {
|
||||
$numFmtId = intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
|
||||
$formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
|
||||
$this->customNumberFormats[$numFmtId] = $formatCode;
|
||||
} else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) {
|
||||
// Once done reading "numFmts" node's children
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts style attributes from the "xf" nodes, inside the "cellXfs" section.
|
||||
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
||||
* to the reuse of styles. So 1 million cells should not use 1 million styles.
|
||||
*
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node
|
||||
* @return void
|
||||
*/
|
||||
protected function extractStyleAttributes($xmlReader)
|
||||
{
|
||||
while ($xmlReader->read()) {
|
||||
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) {
|
||||
$numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID);
|
||||
$normalizedNumFmtId = ($numFmtId !== null) ? intval($numFmtId) : null;
|
||||
|
||||
$applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT);
|
||||
$normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? !!$applyNumberFormat : null;
|
||||
|
||||
$this->stylesAttributes[] = [
|
||||
self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId,
|
||||
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat,
|
||||
];
|
||||
} else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
|
||||
// Once done reading "cellXfs" node's children
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array The custom number formats
|
||||
*/
|
||||
protected function getCustomNumberFormats()
|
||||
{
|
||||
if (!isset($this->customNumberFormats)) {
|
||||
$this->extractRelevantInfo();
|
||||
}
|
||||
|
||||
return $this->customNumberFormats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array The styles attributes
|
||||
*/
|
||||
protected function getStylesAttributes()
|
||||
{
|
||||
if (!isset($this->stylesAttributes)) {
|
||||
$this->extractRelevantInfo();
|
||||
}
|
||||
|
||||
return $this->stylesAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId")
|
||||
* @return bool Whether the style with the given attributes indicates that the number is a date
|
||||
*/
|
||||
protected function doesStyleIndicateDate($styleAttributes)
|
||||
{
|
||||
$applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
|
||||
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||
|
||||
// A style may apply a date format if it has:
|
||||
// - "applyNumberFormat" attribute not set to "false"
|
||||
// - "numFmtId" attribute set
|
||||
// This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format,
|
||||
// but this is not necessarily a date.
|
||||
if ($applyNumberFormat === false || $numFmtId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->doesNumFmtIdIndicateDate($numFmtId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the number format ID indicates that the number is a date.
|
||||
* The result is cached to avoid recomputing the same thing over and over, as
|
||||
* "numFmtId" attributes can be shared between multiple styles.
|
||||
*
|
||||
* @param int $numFmtId
|
||||
* @return bool Whether the number format ID indicates that the number is a date
|
||||
*/
|
||||
protected function doesNumFmtIdIndicateDate($numFmtId)
|
||||
{
|
||||
if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) {
|
||||
$formatCode = $this->getFormatCodeForNumFmtId($numFmtId);
|
||||
|
||||
$this->numFmtIdToIsDateFormatCache[$numFmtId] = (
|
||||
$this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
|
||||
$this->isFormatCodeCustomDateFormat($formatCode)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->numFmtIdToIsDateFormatCache[$numFmtId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numFmtId
|
||||
* @return string|null The custom number format or NULL if none defined for the given numFmtId
|
||||
*/
|
||||
protected function getFormatCodeForNumFmtId($numFmtId)
|
||||
{
|
||||
$customNumberFormats = $this->getCustomNumberFormats();
|
||||
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
return (isset($customNumberFormats[$numFmtId])) ? $customNumberFormats[$numFmtId] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numFmtId
|
||||
* @return bool Whether the number format ID indicates that the number is a date
|
||||
*/
|
||||
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
|
||||
{
|
||||
return in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $formatCode
|
||||
* @return bool Whether the given format code indicates that the number is a date
|
||||
*/
|
||||
protected function isFormatCodeCustomDateFormat($formatCode)
|
||||
{
|
||||
// if no associated format code or if using the default "General" format
|
||||
if ($formatCode === null || strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isFormatCodeMatchingDateFormatPattern($formatCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $formatCode
|
||||
* @return bool Whether the given format code matches a date format pattern
|
||||
*/
|
||||
protected function isFormatCodeMatchingDateFormatPattern($formatCode)
|
||||
{
|
||||
// Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
|
||||
$pattern = '((?<!\\\)\[.+?(?<!\\\)\])';
|
||||
$formatCode = preg_replace($pattern, '', $formatCode);
|
||||
|
||||
// custom date formats contain specific characters to represent the date:
|
||||
// e - yy - m - d - h - s
|
||||
// and all of their variants (yyyy - mm - dd...)
|
||||
$dateFormatCharacters = ['e', 'yy', 'm', 'd', 'h', 's'];
|
||||
|
||||
$hasFoundDateFormatCharacter = false;
|
||||
foreach ($dateFormatCharacters as $dateFormatCharacter) {
|
||||
// character not preceded by "\" (case insensitive)
|
||||
$pattern = '/(?<!\\\)' . $dateFormatCharacter . '/i';
|
||||
|
||||
if (preg_match($pattern, $formatCode)) {
|
||||
$hasFoundDateFormatCharacter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasFoundDateFormatCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format as defined in "styles.xml" of the given style.
|
||||
* NOTE: It is assumed that the style DOES have a number format associated to it.
|
||||
*
|
||||
* @param int $styleId Zero-based style ID
|
||||
* @return string The number format code associated with the given style
|
||||
*/
|
||||
public function getNumberFormatCode($styleId)
|
||||
{
|
||||
$stylesAttributes = $this->getStylesAttributes();
|
||||
$styleAttributes = $stylesAttributes[$styleId];
|
||||
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||
|
||||
if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) {
|
||||
$numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId];
|
||||
} else {
|
||||
$customNumberFormats = $this->getCustomNumberFormats();
|
||||
$numberFormatCode = $customNumberFormats[$numFmtId];
|
||||
}
|
||||
|
||||
return $numberFormatCode;
|
||||
}
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\AbstractReader;
|
||||
use Box\Spout\Reader\XLSX\Helper\SharedStringsHelper;
|
||||
|
||||
/**
|
||||
* Class Reader
|
||||
* This class provides support to read data from a XLSX file
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX
|
||||
*/
|
||||
class Reader extends AbstractReader
|
||||
{
|
||||
/** @var \ZipArchive */
|
||||
protected $zip;
|
||||
|
||||
/** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
|
||||
protected $sharedStringsHelper;
|
||||
|
||||
/** @var SheetIterator To iterator over the XLSX sheets */
|
||||
protected $sheetIterator;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
if (!isset($this->options)) {
|
||||
$this->options = new ReaderOptions();
|
||||
}
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tempFolder Temporary folder where the temporary files will be created
|
||||
* @return Reader
|
||||
*/
|
||||
public function setTempFolder($tempFolder)
|
||||
{
|
||||
$this->getOptions()->setTempFolder($tempFolder);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read.
|
||||
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
|
||||
* and fetches all the available sheets.
|
||||
*
|
||||
* @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 or its content cannot be read
|
||||
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
|
||||
*/
|
||||
protected function openReader($filePath)
|
||||
{
|
||||
$this->zip = new \ZipArchive();
|
||||
|
||||
if ($this->zip->open($filePath) === true) {
|
||||
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->getOptions()->getTempFolder());
|
||||
|
||||
if ($this->sharedStringsHelper->hasSharedStrings()) {
|
||||
// Extracts all the strings from the sheets for easy access in the future
|
||||
$this->sharedStringsHelper->extractSharedStrings();
|
||||
}
|
||||
|
||||
$this->sheetIterator = new SheetIterator($filePath, $this->getOptions(), $this->sharedStringsHelper, $this->globalFunctionsHelper);
|
||||
} else {
|
||||
throw new IOException("Could not open $filePath for reading.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator to iterate over sheets.
|
||||
*
|
||||
* @return SheetIterator To iterate over sheets
|
||||
*/
|
||||
protected function getConcreteSheetIterator()
|
||||
{
|
||||
return $this->sheetIterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function closeReader()
|
||||
{
|
||||
if ($this->zip) {
|
||||
$this->zip->close();
|
||||
}
|
||||
|
||||
if ($this->sharedStringsHelper) {
|
||||
$this->sharedStringsHelper->cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* This class is used to customize the reader's behavior
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX
|
||||
*/
|
||||
class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
|
||||
{
|
||||
/** @var string|null Temporary folder where the temporary files will be created */
|
||||
protected $tempFolder = null;
|
||||
|
||||
/**
|
||||
* @return string|null Temporary folder where the temporary files will be created
|
||||
*/
|
||||
public function getTempFolder()
|
||||
{
|
||||
return $this->tempFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $tempFolder Temporary folder where the temporary files will be created
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setTempFolder($tempFolder)
|
||||
{
|
||||
$this->tempFolder = $tempFolder;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -1,406 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
use Box\Spout\Reader\XLSX\Helper\CellHelper;
|
||||
use Box\Spout\Reader\XLSX\Helper\CellValueFormatter;
|
||||
use Box\Spout\Reader\XLSX\Helper\StyleHelper;
|
||||
use Box\Spout\Reader\Common\XMLProcessor;
|
||||
|
||||
/**
|
||||
* Class RowIterator
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX
|
||||
*/
|
||||
class RowIterator implements IteratorInterface
|
||||
{
|
||||
/** Definition of XML nodes names used to parse data */
|
||||
const XML_NODE_DIMENSION = 'dimension';
|
||||
const XML_NODE_WORKSHEET = 'worksheet';
|
||||
const XML_NODE_ROW = 'row';
|
||||
const XML_NODE_CELL = 'c';
|
||||
|
||||
/** Definition of XML attributes used to parse data */
|
||||
const XML_ATTRIBUTE_REF = 'ref';
|
||||
const XML_ATTRIBUTE_SPANS = 'spans';
|
||||
const XML_ATTRIBUTE_ROW_INDEX = 'r';
|
||||
const XML_ATTRIBUTE_CELL_INDEX = 'r';
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */
|
||||
protected $sheetDataXMLFilePath;
|
||||
|
||||
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
|
||||
protected $xmlReader;
|
||||
|
||||
/** @var \Box\Spout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
|
||||
protected $xmlProcessor;
|
||||
|
||||
/** @var Helper\CellValueFormatter Helper to format cell values */
|
||||
protected $cellValueFormatter;
|
||||
|
||||
/** @var Helper\StyleHelper $styleHelper Helper to work with styles */
|
||||
protected $styleHelper;
|
||||
|
||||
/**
|
||||
* TODO: This variable can be deleted when row indices get preserved
|
||||
* @var int Number of read rows
|
||||
*/
|
||||
protected $numReadRows = 0;
|
||||
|
||||
/** @var array Contains the data for the currently processed row (key = cell index, value = cell value) */
|
||||
protected $currentlyProcessedRowData = [];
|
||||
|
||||
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
|
||||
protected $rowDataBuffer = null;
|
||||
|
||||
/** @var bool Indicates whether all rows have been read */
|
||||
protected $hasReachedEndOfFile = false;
|
||||
|
||||
/** @var int The number of columns the sheet has (0 meaning undefined) */
|
||||
protected $numColumns = 0;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows;
|
||||
|
||||
/** @var int Last row index processed (one-based) */
|
||||
protected $lastRowIndexProcessed = 0;
|
||||
|
||||
/** @var int Row index to be processed next (one-based) */
|
||||
protected $nextRowIndexToBeProcessed = 0;
|
||||
|
||||
/** @var int Last column index processed (zero-based) */
|
||||
protected $lastColumnIndexProcessed = -1;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
|
||||
*/
|
||||
public function __construct($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
|
||||
|
||||
$this->xmlReader = new XMLReader();
|
||||
|
||||
$this->styleHelper = new StyleHelper($filePath);
|
||||
$this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $options->shouldFormatDates());
|
||||
|
||||
$this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
|
||||
|
||||
// Register all callbacks to process different nodes when reading the XML file
|
||||
$this->xmlProcessor = new XMLProcessor($this->xmlReader);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_DIMENSION, XMLProcessor::NODE_TYPE_START, [$this, 'processDimensionStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_WORKSHEET, XMLProcessor::NODE_TYPE_END, [$this, 'processWorksheetEndingNode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||
* @return string Path of the XML file containing the sheet data,
|
||||
* without the leading slash.
|
||||
*/
|
||||
protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
|
||||
{
|
||||
return ltrim($sheetDataXMLFilePath, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element.
|
||||
* Initializes the XMLReader object that reads the associated sheet data.
|
||||
* The XMLReader is configured to be safe from billion laughs attack.
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->xmlReader->close();
|
||||
|
||||
$sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath;
|
||||
if ($this->xmlReader->open($sheetDataFilePath) === false) {
|
||||
throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
|
||||
}
|
||||
|
||||
$this->numReadRows = 0;
|
||||
$this->lastRowIndexProcessed = 0;
|
||||
$this->nextRowIndexToBeProcessed = 0;
|
||||
$this->rowDataBuffer = null;
|
||||
$this->hasReachedEndOfFile = false;
|
||||
$this->numColumns = 0;
|
||||
|
||||
$this->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return (!$this->hasReachedEndOfFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element. Reads data describing the next unprocessed row.
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->nextRowIndexToBeProcessed++;
|
||||
|
||||
if ($this->doesNeedDataForNextRowToBeProcessed()) {
|
||||
$this->readDataForNextRow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we need data for the next row to be processed.
|
||||
* We don't need to read data if:
|
||||
* we have already read at least one row
|
||||
* AND
|
||||
* we need to preserve empty rows
|
||||
* AND
|
||||
* the last row that was read is not the row that need to be processed
|
||||
* (i.e. if we need to return empty rows)
|
||||
*
|
||||
* @return bool Whether we need data for the next row to be processed.
|
||||
*/
|
||||
protected function doesNeedDataForNextRowToBeProcessed()
|
||||
{
|
||||
$hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
|
||||
|
||||
return (
|
||||
!$hasReadAtLeastOneRow ||
|
||||
!$this->shouldPreserveEmptyRows ||
|
||||
$this->lastRowIndexProcessed < $this->nextRowIndexToBeProcessed
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
|
||||
*/
|
||||
protected function readDataForNextRow()
|
||||
{
|
||||
$this->currentlyProcessedRowData = [];
|
||||
|
||||
try {
|
||||
$this->xmlProcessor->readUntilStopped();
|
||||
} catch (XMLProcessingException $exception) {
|
||||
throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
|
||||
}
|
||||
|
||||
$this->rowDataBuffer = $this->currentlyProcessedRowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<dimension>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processDimensionStartingNode($xmlReader)
|
||||
{
|
||||
// Read dimensions of the sheet
|
||||
$dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
|
||||
if (preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) {
|
||||
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1;
|
||||
}
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowStartingNode($xmlReader)
|
||||
{
|
||||
// Reset index of the last processed column
|
||||
$this->lastColumnIndexProcessed = -1;
|
||||
|
||||
// Mark the last processed row as the one currently being read
|
||||
$this->lastRowIndexProcessed = $this->getRowIndex($xmlReader);
|
||||
|
||||
// Read spans info if present
|
||||
$numberOfColumnsForRow = $this->numColumns;
|
||||
$spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
|
||||
if ($spans) {
|
||||
list(, $numberOfColumnsForRow) = explode(':', $spans);
|
||||
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
|
||||
}
|
||||
|
||||
$this->currentlyProcessedRowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<cell>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processCellStartingNode($xmlReader)
|
||||
{
|
||||
$currentColumnIndex = $this->getColumnIndex($xmlReader);
|
||||
|
||||
// NOTE: expand() will automatically decode all XML entities of the child nodes
|
||||
$node = $xmlReader->expand();
|
||||
$this->currentlyProcessedRowData[$currentColumnIndex] = $this->getCellValue($node);
|
||||
$this->lastColumnIndexProcessed = $currentColumnIndex;
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowEndingNode()
|
||||
{
|
||||
// if the fetched row is empty and we don't want to preserve it..,
|
||||
if (!$this->shouldPreserveEmptyRows && $this->isEmptyRow($this->currentlyProcessedRowData)) {
|
||||
// ... skip it
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
$this->numReadRows++;
|
||||
|
||||
// If needed, we fill the empty cells
|
||||
if ($this->numColumns === 0) {
|
||||
$this->currentlyProcessedRowData = CellHelper::fillMissingArrayIndexes($this->currentlyProcessedRowData);
|
||||
}
|
||||
|
||||
// at this point, we have all the data we need for the row
|
||||
// so that we can populate the buffer
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processWorksheetEndingNode()
|
||||
{
|
||||
// The closing "</worksheet>" marks the end of the file
|
||||
$this->hasReachedEndOfFile = true;
|
||||
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" node
|
||||
* @return int Row index
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
|
||||
*/
|
||||
protected function getRowIndex($xmlReader)
|
||||
{
|
||||
// Get "r" attribute if present (from something like <row r="3"...>
|
||||
$currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX);
|
||||
|
||||
return ($currentRowIndex !== null) ?
|
||||
intval($currentRowIndex) :
|
||||
$this->lastRowIndexProcessed + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<c>" node
|
||||
* @return int Column index
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
|
||||
*/
|
||||
protected function getColumnIndex($xmlReader)
|
||||
{
|
||||
// Get "r" attribute if present (from something like <c r="A1"...>
|
||||
$currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
|
||||
|
||||
return ($currentCellIndex !== null) ?
|
||||
CellHelper::getColumnIndexFromCellIndex($currentCellIndex) :
|
||||
$this->lastColumnIndexProcessed + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
|
||||
*/
|
||||
protected function getCellValue($node)
|
||||
{
|
||||
return $this->cellValueFormatter->extractAndFormatNodeValue($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $rowData
|
||||
* @return bool Whether the given row is empty
|
||||
*/
|
||||
protected function isEmptyRow($rowData)
|
||||
{
|
||||
return (count($rowData) === 1 && $rowData[0] === '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element, either an empty row or from the buffer.
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$rowDataForRowToBeProcessed = $this->rowDataBuffer;
|
||||
|
||||
if ($this->shouldPreserveEmptyRows) {
|
||||
// when we need to preserve empty rows, we will either return
|
||||
// an empty row or the last row read. This depends whether the
|
||||
// index of last row that was read matches the index of the last
|
||||
// row whose value should be returned.
|
||||
if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) {
|
||||
// return empty row if mismatch between last processed row
|
||||
// and the row that needs to be returned
|
||||
$rowDataForRowToBeProcessed = [''];
|
||||
}
|
||||
}
|
||||
|
||||
return $rowDataForRowToBeProcessed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element. Here, the row index.
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
// TODO: This should return $this->nextRowIndexToBeProcessed
|
||||
// but to avoid a breaking change, the return value for
|
||||
// this function has been kept as the number of rows read.
|
||||
return $this->shouldPreserveEmptyRows ?
|
||||
$this->nextRowIndexToBeProcessed :
|
||||
$this->numReadRows;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cleans up what was created to iterate over the object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
$this->xmlReader->close();
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX;
|
||||
|
||||
use Box\Spout\Reader\SheetInterface;
|
||||
|
||||
/**
|
||||
* Class Sheet
|
||||
* Represents a sheet within a XLSX file
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX
|
||||
*/
|
||||
class Sheet implements SheetInterface
|
||||
{
|
||||
/** @var \Box\Spout\Reader\XLSX\RowIterator To iterate over sheet's rows */
|
||||
protected $rowIterator;
|
||||
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
protected $name;
|
||||
|
||||
/** @var bool Whether the sheet was the active one */
|
||||
protected $isActive;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param string $sheetName Name of the sheet
|
||||
* @param bool $isSheetActive Whether the sheet was defined as active
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param Helper\SharedStringsHelper Helper to work with shared strings
|
||||
*/
|
||||
public function __construct($filePath, $sheetDataXMLFilePath, $sheetIndex, $sheetName, $isSheetActive, $options, $sharedStringsHelper)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper);
|
||||
$this->index = $sheetIndex;
|
||||
$this->name = $sheetName;
|
||||
$this->isActive = $isSheetActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return \Box\Spout\Reader\XLSX\RowIterator
|
||||
*/
|
||||
public function getRowIterator()
|
||||
{
|
||||
return $this->rowIterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return string Name of the sheet
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return bool Whether the sheet was defined as active
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
}
|
||||
@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX;
|
||||
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
use Box\Spout\Reader\XLSX\Helper\SheetHelper;
|
||||
use Box\Spout\Reader\Exception\NoSheetsFoundException;
|
||||
|
||||
/**
|
||||
* Class SheetIterator
|
||||
* Iterate over XLSX sheet.
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX
|
||||
*/
|
||||
class SheetIterator implements IteratorInterface
|
||||
{
|
||||
/** @var \Box\Spout\Reader\XLSX\Sheet[] The list of sheet present in the file */
|
||||
protected $sheets;
|
||||
|
||||
/** @var int The index of the sheet being read (zero-based) */
|
||||
protected $currentSheetIndex;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper $sharedStringsHelper
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
|
||||
*/
|
||||
public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper)
|
||||
{
|
||||
// Fetch all available sheets
|
||||
$sheetHelper = new SheetHelper($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper);
|
||||
$this->sheets = $sheetHelper->getSheets();
|
||||
|
||||
if (count($this->sheets) === 0) {
|
||||
throw new NoSheetsFoundException('The file must contain at least one sheet.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->currentSheetIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return ($this->currentSheetIndex < count($this->sheets));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
if (isset($this->sheets[$this->currentSheetIndex])) {
|
||||
$currentSheet = $this->sheets[$this->currentSheetIndex];
|
||||
$currentSheet->getRowIterator()->end();
|
||||
|
||||
$this->currentSheetIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return \Box\Spout\Reader\XLSX\Sheet
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->sheets[$this->currentSheetIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->currentSheetIndex + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up what was created to iterate over the object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
// make sure we are not leaking memory in case the iteration stopped before the end
|
||||
foreach ($this->sheets as $sheet) {
|
||||
$sheet->getRowIterator()->end();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer;
|
||||
|
||||
use Box\Spout\Writer\Exception\WriterNotOpenedException;
|
||||
|
||||
/**
|
||||
* Class AbstractMultiSheetsWriter
|
||||
*
|
||||
* @package Box\Spout\Writer
|
||||
* @abstract
|
||||
*/
|
||||
abstract class AbstractMultiSheetsWriter extends AbstractWriter
|
||||
{
|
||||
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
|
||||
protected $shouldCreateNewSheetsAutomatically = true;
|
||||
|
||||
/**
|
||||
* @return Common\Internal\WorkbookInterface The workbook representing the file to be written
|
||||
*/
|
||||
abstract protected function getWorkbook();
|
||||
|
||||
/**
|
||||
* Sets whether new sheets should be automatically created when the max rows limit per sheet is reached.
|
||||
* This must be set before opening the writer.
|
||||
*
|
||||
* @api
|
||||
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
|
||||
* @return AbstractMultiSheetsWriter
|
||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
||||
*/
|
||||
public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically)
|
||||
{
|
||||
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
|
||||
|
||||
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the workbook's sheets
|
||||
*
|
||||
* @api
|
||||
* @return Common\Sheet[] All the workbook's sheets
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||
*/
|
||||
public function getSheets()
|
||||
{
|
||||
$this->throwIfBookIsNotAvailable();
|
||||
|
||||
$externalSheets = [];
|
||||
$worksheets = $this->getWorkbook()->getWorksheets();
|
||||
|
||||
/** @var Common\Internal\WorksheetInterface $worksheet */
|
||||
foreach ($worksheets as $worksheet) {
|
||||
$externalSheets[] = $worksheet->getExternalSheet();
|
||||
}
|
||||
|
||||
return $externalSheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sheet and make it the current sheet. The data will now be written to this sheet.
|
||||
*
|
||||
* @api
|
||||
* @return Common\Sheet The created sheet
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||
*/
|
||||
public function addNewSheetAndMakeItCurrent()
|
||||
{
|
||||
$this->throwIfBookIsNotAvailable();
|
||||
$worksheet = $this->getWorkbook()->addNewSheetAndMakeItCurrent();
|
||||
|
||||
return $worksheet->getExternalSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current sheet
|
||||
*
|
||||
* @api
|
||||
* @return Common\Sheet The current sheet
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||
*/
|
||||
public function getCurrentSheet()
|
||||
{
|
||||
$this->throwIfBookIsNotAvailable();
|
||||
return $this->getWorkbook()->getCurrentWorksheet()->getExternalSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given sheet as the current one. New data will be written to this sheet.
|
||||
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||
*
|
||||
* @api
|
||||
* @param Common\Sheet $sheet The sheet to set as current
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||
* @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
|
||||
*/
|
||||
public function setCurrentSheet($sheet)
|
||||
{
|
||||
$this->throwIfBookIsNotAvailable();
|
||||
$this->getWorkbook()->setCurrentSheet($sheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the book has been created. Throws an exception if not created yet.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet
|
||||
*/
|
||||
protected function throwIfBookIsNotAvailable()
|
||||
{
|
||||
if (!$this->getWorkbook()) {
|
||||
throw new WriterNotOpenedException('The writer must be opened before performing this action.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,384 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||
use Box\Spout\Common\Exception\SpoutException;
|
||||
use Box\Spout\Common\Helper\FileSystemHelper;
|
||||
use Box\Spout\Writer\Exception\WriterAlreadyOpenedException;
|
||||
use Box\Spout\Writer\Exception\WriterNotOpenedException;
|
||||
use Box\Spout\Writer\Style\StyleBuilder;
|
||||
|
||||
/**
|
||||
* Class AbstractWriter
|
||||
*
|
||||
* @package Box\Spout\Writer
|
||||
* @abstract
|
||||
*/
|
||||
abstract class AbstractWriter implements WriterInterface
|
||||
{
|
||||
/** @var string Path to the output file */
|
||||
protected $outputFilePath;
|
||||
|
||||
/** @var resource Pointer to the file/stream we will write to */
|
||||
protected $filePointer;
|
||||
|
||||
/** @var bool Indicates whether the writer has been opened or not */
|
||||
protected $isWriterOpened = false;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/** @var Style\Style Style to be applied to the next written row(s) */
|
||||
protected $rowStyle;
|
||||
|
||||
/** @var Style\Style Default row style. Each writer can have its own default style */
|
||||
protected $defaultRowStyle;
|
||||
|
||||
/** @var string Content-Type value for the header - to be defined by child class */
|
||||
protected static $headerContentType;
|
||||
|
||||
/**
|
||||
* Opens the streamer and makes it ready to accept data.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
||||
*/
|
||||
abstract protected function openWriter();
|
||||
|
||||
/**
|
||||
* Adds data to the currently openned writer.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be streamed.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param Style\Style $style Style to be applied to the written row
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function addRowToWriter(array $dataRow, $style);
|
||||
|
||||
/**
|
||||
* Closes the streamer, preventing any additional writing.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function closeWriter();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->defaultRowStyle = $this->getDefaultRowStyle();
|
||||
$this->resetRowStyleToDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default styles for all rows added with "addRow".
|
||||
* Overriding the default style instead of using "addRowWithStyle" improves performance by 20%.
|
||||
* @see https://github.com/box/spout/issues/272
|
||||
*
|
||||
* @param Style\Style $defaultStyle
|
||||
* @return AbstractWriter
|
||||
*/
|
||||
public function setDefaultRowStyle($defaultStyle)
|
||||
{
|
||||
$this->defaultRowStyle = $defaultStyle;
|
||||
$this->resetRowStyleToDefault();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @return AbstractWriter
|
||||
*/
|
||||
public function setGlobalFunctionsHelper($globalFunctionsHelper)
|
||||
{
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits the writer and opens it to accept data.
|
||||
* By using this method, the data will be written to a file.
|
||||
*
|
||||
* @api
|
||||
* @param string $outputFilePath Path of the output file that will contain the data
|
||||
* @return AbstractWriter
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
|
||||
*/
|
||||
public function openToFile($outputFilePath)
|
||||
{
|
||||
$this->outputFilePath = $outputFilePath;
|
||||
|
||||
$this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+');
|
||||
$this->throwIfFilePointerIsNotAvailable();
|
||||
|
||||
$this->openWriter();
|
||||
$this->isWriterOpened = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits the writer and opens it to accept data.
|
||||
* By using this method, the data will be outputted directly to the browser.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @api
|
||||
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
|
||||
* @return AbstractWriter
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
||||
*/
|
||||
public function openToBrowser($outputFileName)
|
||||
{
|
||||
$this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName);
|
||||
|
||||
$this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w');
|
||||
$this->throwIfFilePointerIsNotAvailable();
|
||||
|
||||
// Clear any previous output (otherwise the generated file will be corrupted)
|
||||
// @see https://github.com/box/spout/issues/241
|
||||
$this->globalFunctionsHelper->ob_end_clean();
|
||||
|
||||
// Set headers
|
||||
$this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType);
|
||||
$this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"');
|
||||
|
||||
/*
|
||||
* When forcing the download of a file over SSL,IE8 and lower browsers fail
|
||||
* if the Cache-Control and Pragma headers are not set.
|
||||
*
|
||||
* @see http://support.microsoft.com/KB/323308
|
||||
* @see https://github.com/liuggio/ExcelBundle/issues/45
|
||||
*/
|
||||
$this->globalFunctionsHelper->header('Cache-Control: max-age=0');
|
||||
$this->globalFunctionsHelper->header('Pragma: public');
|
||||
|
||||
$this->openWriter();
|
||||
$this->isWriterOpened = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the pointer to the file/stream to write to is available.
|
||||
* Will throw an exception if not available.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the pointer is not available
|
||||
*/
|
||||
protected function throwIfFilePointerIsNotAvailable()
|
||||
{
|
||||
if (!$this->filePointer) {
|
||||
throw new IOException('File pointer has not be opened');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the writer has already been opened, since some actions must be done before it gets opened.
|
||||
* Throws an exception if already opened.
|
||||
*
|
||||
* @param string $message Error message
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be.
|
||||
*/
|
||||
protected function throwIfWriterAlreadyOpened($message)
|
||||
{
|
||||
if ($this->isWriterOpened) {
|
||||
throw new WriterAlreadyOpenedException($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write given data to the output. New data will be appended to end of stream.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be streamed.
|
||||
* If empty, no data is added (i.e. not even as a blank row)
|
||||
* Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
|
||||
* @api
|
||||
* @return AbstractWriter
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||
* @throws \Box\Spout\Common\Exception\SpoutException If anything else goes wrong while writing data
|
||||
*/
|
||||
public function addRow(array $dataRow)
|
||||
{
|
||||
if ($this->isWriterOpened) {
|
||||
// empty $dataRow should not add an empty line
|
||||
if (!empty($dataRow)) {
|
||||
try {
|
||||
$this->addRowToWriter($dataRow, $this->rowStyle);
|
||||
} catch (SpoutException $e) {
|
||||
// if an exception occurs while writing data,
|
||||
// close the writer and remove all files created so far.
|
||||
$this->closeAndAttemptToCleanupAllFiles();
|
||||
|
||||
// re-throw the exception to alert developers of the error
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write given data to the output and apply the given style.
|
||||
* @see addRow
|
||||
*
|
||||
* @api
|
||||
* @param array $dataRow Array of array containing data to be streamed.
|
||||
* @param Style\Style $style Style to be applied to the row.
|
||||
* @return AbstractWriter
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||
*/
|
||||
public function addRowWithStyle(array $dataRow, $style)
|
||||
{
|
||||
if (!$style instanceof Style\Style) {
|
||||
throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
|
||||
}
|
||||
|
||||
$this->setRowStyle($style);
|
||||
$this->addRow($dataRow);
|
||||
$this->resetRowStyleToDefault();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write given data to the output. New data will be appended to end of stream.
|
||||
*
|
||||
* @api
|
||||
* @param array $dataRows Array of array containing data to be streamed.
|
||||
* If a row is empty, it won't be added (i.e. not even as a blank row)
|
||||
* Example: $dataRows = [
|
||||
* ['data11', 12, , '', 'data13'],
|
||||
* ['data21', 'data22', null, false],
|
||||
* ];
|
||||
* @return AbstractWriter
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||
*/
|
||||
public function addRows(array $dataRows)
|
||||
{
|
||||
if (!empty($dataRows)) {
|
||||
$firstRow = reset($dataRows);
|
||||
if (!is_array($firstRow)) {
|
||||
throw new InvalidArgumentException('The input should be an array of arrays');
|
||||
}
|
||||
|
||||
foreach ($dataRows as $dataRow) {
|
||||
$this->addRow($dataRow);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write given data to the output and apply the given style.
|
||||
* @see addRows
|
||||
*
|
||||
* @api
|
||||
* @param array $dataRows Array of array containing data to be streamed.
|
||||
* @param Style\Style $style Style to be applied to the rows.
|
||||
* @return AbstractWriter
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||
*/
|
||||
public function addRowsWithStyle(array $dataRows, $style)
|
||||
{
|
||||
if (!$style instanceof Style\Style) {
|
||||
throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
|
||||
}
|
||||
|
||||
$this->setRowStyle($style);
|
||||
$this->addRows($dataRows);
|
||||
$this->resetRowStyleToDefault();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default style to be applied to rows.
|
||||
* Can be overriden by children to have a custom style.
|
||||
*
|
||||
* @return Style\Style
|
||||
*/
|
||||
protected function getDefaultRowStyle()
|
||||
{
|
||||
return (new StyleBuilder())->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style to be applied to the next written rows
|
||||
* until it is changed or reset.
|
||||
*
|
||||
* @param Style\Style $style
|
||||
* @return void
|
||||
*/
|
||||
private function setRowStyle($style)
|
||||
{
|
||||
// Merge given style with the default one to inherit custom properties
|
||||
$this->rowStyle = $style->mergeWith($this->defaultRowStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the style to be applied to the next written rows.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function resetRowStyleToDefault()
|
||||
{
|
||||
$this->rowStyle = $this->defaultRowStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the writer. This will close the streamer as well, preventing new data
|
||||
* to be written to the file.
|
||||
*
|
||||
* @api
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if (!$this->isWriterOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->closeWriter();
|
||||
|
||||
if (is_resource($this->filePointer)) {
|
||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||
}
|
||||
|
||||
$this->isWriterOpened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the writer and attempts to cleanup all files that were
|
||||
* created during the writing process (temp files & final file).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function closeAndAttemptToCleanupAllFiles()
|
||||
{
|
||||
// close the writer, which should remove all temp files
|
||||
$this->close();
|
||||
|
||||
// remove output file if it was created
|
||||
if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) {
|
||||
$outputFolderPath = dirname($this->outputFilePath);
|
||||
$fileSystemHelper = new FileSystemHelper($outputFolderPath);
|
||||
$fileSystemHelper->deleteFile($this->outputFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\CSV;
|
||||
|
||||
use Box\Spout\Writer\AbstractWriter;
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
|
||||
/**
|
||||
* Class Writer
|
||||
* This class provides support to write data to CSV files
|
||||
*
|
||||
* @package Box\Spout\Writer\CSV
|
||||
*/
|
||||
class Writer extends AbstractWriter
|
||||
{
|
||||
/** Number of rows to write before flushing */
|
||||
const FLUSH_THRESHOLD = 500;
|
||||
|
||||
/** @var string Content-Type value for the header */
|
||||
protected static $headerContentType = 'text/csv; charset=UTF-8';
|
||||
|
||||
/** @var string Defines the character used to delimit fields (one character only) */
|
||||
protected $fieldDelimiter = ',';
|
||||
|
||||
/** @var string Defines the character used to enclose fields (one character only) */
|
||||
protected $fieldEnclosure = '"';
|
||||
|
||||
/** @var int */
|
||||
protected $lastWrittenRowIndex = 0;
|
||||
|
||||
/** @var bool */
|
||||
protected $shouldAddBOM = true;
|
||||
|
||||
/**
|
||||
* Sets the field delimiter for the CSV
|
||||
*
|
||||
* @api
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @return Writer
|
||||
*/
|
||||
public function setFieldDelimiter($fieldDelimiter)
|
||||
{
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field enclosure for the CSV
|
||||
*
|
||||
* @api
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @return Writer
|
||||
*/
|
||||
public function setFieldEnclosure($fieldEnclosure)
|
||||
{
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if a BOM has to be added to the file
|
||||
*
|
||||
* @param bool $shouldAddBOM
|
||||
* @return Writer
|
||||
*/
|
||||
public function setShouldAddBOM($shouldAddBOM)
|
||||
{
|
||||
$this->shouldAddBOM = (bool) $shouldAddBOM;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the CSV streamer and makes it ready to accept data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function openWriter()
|
||||
{
|
||||
if ($this->shouldAddBOM) {
|
||||
// Adds UTF-8 BOM for Unicode compatibility
|
||||
$this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds data to the currently opened writer.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param \Box\Spout\Writer\Style\Style $style Ignored here since CSV does not support styling.
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||
*/
|
||||
protected function addRowToWriter(array $dataRow, $style)
|
||||
{
|
||||
$wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
if ($wasWriteSuccessful === false) {
|
||||
throw new IOException('Unable to write data');
|
||||
}
|
||||
|
||||
$this->lastWrittenRowIndex++;
|
||||
if ($this->lastWrittenRowIndex % self::FLUSH_THRESHOLD === 0) {
|
||||
$this->globalFunctionsHelper->fflush($this->filePointer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the CSV streamer, preventing any additional writing.
|
||||
* If set, sets the headers and redirects output to the browser.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function closeWriter()
|
||||
{
|
||||
$this->lastWrittenRowIndex = 0;
|
||||
}
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class AbstractStyleHelper
|
||||
* This class provides helper functions to manage styles
|
||||
*
|
||||
* @package Box\Spout\Writer\Common\Helper
|
||||
*/
|
||||
abstract class AbstractStyleHelper
|
||||
{
|
||||
/** @var array [SERIALIZED_STYLE] => [STYLE_ID] mapping table, keeping track of the registered styles */
|
||||
protected $serializedStyleToStyleIdMappingTable = [];
|
||||
|
||||
/** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */
|
||||
protected $styleIdToStyleMappingTable = [];
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Writer\Style\Style $defaultStyle
|
||||
*/
|
||||
public function __construct($defaultStyle)
|
||||
{
|
||||
// This ensures that the default style is the first one to be registered
|
||||
$this->registerStyle($defaultStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given style as a used style.
|
||||
* Duplicate styles won't be registered more than once.
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style The style to be registered
|
||||
* @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID.
|
||||
*/
|
||||
public function registerStyle($style)
|
||||
{
|
||||
$serializedStyle = $style->serialize();
|
||||
|
||||
if (!$this->hasStyleAlreadyBeenRegistered($style)) {
|
||||
$nextStyleId = count($this->serializedStyleToStyleIdMappingTable);
|
||||
$style->setId($nextStyleId);
|
||||
|
||||
$this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
|
||||
$this->styleIdToStyleMappingTable[$nextStyleId] = $style;
|
||||
}
|
||||
|
||||
return $this->getStyleFromSerializedStyle($serializedStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given style has already been registered.
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasStyleAlreadyBeenRegistered($style)
|
||||
{
|
||||
$serializedStyle = $style->serialize();
|
||||
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the registered style associated to the given serialization.
|
||||
*
|
||||
* @param string $serializedStyle The serialized style from which the actual style should be fetched from
|
||||
* @return \Box\Spout\Writer\Style\Style
|
||||
*/
|
||||
protected function getStyleFromSerializedStyle($serializedStyle)
|
||||
{
|
||||
$styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle];
|
||||
return $this->styleIdToStyleMappingTable[$styleId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Box\Spout\Writer\Style\Style[] List of registered styles
|
||||
*/
|
||||
protected function getRegisteredStyles()
|
||||
{
|
||||
return array_values($this->styleIdToStyleMappingTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default style
|
||||
*
|
||||
* @return \Box\Spout\Writer\Style\Style Default style
|
||||
*/
|
||||
protected function getDefaultStyle()
|
||||
{
|
||||
// By construction, the default style has ID 0
|
||||
return $this->styleIdToStyleMappingTable[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply additional styles if the given row needs it.
|
||||
* Typically, set "wrap text" if a cell contains a new line.
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style The original style
|
||||
* @param array $dataRow The row the style will be applied to
|
||||
* @return \Box\Spout\Writer\Style\Style The updated style
|
||||
*/
|
||||
public function applyExtraStylesIfNeeded($style, $dataRow)
|
||||
{
|
||||
$updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow);
|
||||
return $updatedStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "wrap text" option if a cell of the given row contains a new line.
|
||||
*
|
||||
* @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines
|
||||
* are ignored even when the "wrap text" option is set. This only occurs with
|
||||
* inline strings (shared strings do work fine).
|
||||
* A workaround would be to encode "\n" as "_x000D_" but it does not work
|
||||
* on the Windows version of Excel...
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style The original style
|
||||
* @param array $dataRow The row the style will be applied to
|
||||
* @return \Box\Spout\Writer\Style\Style The eventually updated style
|
||||
*/
|
||||
protected function applyWrapTextIfCellContainsNewLine($style, $dataRow)
|
||||
{
|
||||
// if the "wrap text" option is already set, no-op
|
||||
if ($style->hasSetWrapText()) {
|
||||
return $style;
|
||||
}
|
||||
|
||||
foreach ($dataRow as $cell) {
|
||||
if (is_string($cell) && strpos($cell, "\n") !== false) {
|
||||
$style->setShouldWrapText();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $style;
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class CellHelper
|
||||
* This class provides helper functions when working with cells
|
||||
*
|
||||
* @package Box\Spout\Writer\Common\Helper
|
||||
*/
|
||||
class CellHelper
|
||||
{
|
||||
/** @var array Cache containing the mapping column index => cell index */
|
||||
private static $columnIndexToCellIndexCache = [];
|
||||
|
||||
/**
|
||||
* Returns the cell index (base 26) associated to the base 10 column index.
|
||||
* Excel uses A to Z letters for column indexing, where A is the 1st column,
|
||||
* Z is the 26th and AA is the 27th.
|
||||
* The mapping is zero based, so that 0 maps to A, B maps to 1, Z to 25 and AA to 26.
|
||||
*
|
||||
* @param int $columnIndex The Excel column index (0, 42, ...)
|
||||
* @return string The associated cell index ('A', 'BC', ...)
|
||||
*/
|
||||
public static function getCellIndexFromColumnIndex($columnIndex)
|
||||
{
|
||||
$originalColumnIndex = $columnIndex;
|
||||
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) {
|
||||
$cellIndex = '';
|
||||
$capitalAAsciiValue = ord('A');
|
||||
|
||||
do {
|
||||
$modulus = $columnIndex % 26;
|
||||
$cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex;
|
||||
|
||||
// substracting 1 because it's zero-based
|
||||
$columnIndex = intval($columnIndex / 26) - 1;
|
||||
|
||||
} while ($columnIndex >= 0);
|
||||
|
||||
self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex;
|
||||
}
|
||||
|
||||
return self::$columnIndexToCellIndexCache[$originalColumnIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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';
|
||||
}
|
||||
}
|
||||
@ -1,217 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class ZipHelper
|
||||
* This class provides helper functions to create zip files
|
||||
*
|
||||
* @package Box\Spout\Writer\Common\Helper
|
||||
*/
|
||||
class ZipHelper
|
||||
{
|
||||
const ZIP_EXTENSION = '.zip';
|
||||
|
||||
/** Controls what to do when trying to add an existing file */
|
||||
const EXISTING_FILES_SKIP = 'skip';
|
||||
const EXISTING_FILES_OVERWRITE = 'overwrite';
|
||||
|
||||
/** @var string Path of the folder where the zip file will be created */
|
||||
protected $tmpFolderPath;
|
||||
|
||||
/** @var \ZipArchive The ZipArchive instance */
|
||||
protected $zip;
|
||||
|
||||
/**
|
||||
* @param string $tmpFolderPath Path of the temp folder where the zip file will be created
|
||||
*/
|
||||
public function __construct($tmpFolderPath)
|
||||
{
|
||||
$this->tmpFolderPath = $tmpFolderPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the already created ZipArchive instance or
|
||||
* creates one if none exists.
|
||||
*
|
||||
* @return \ZipArchive
|
||||
*/
|
||||
protected function createOrGetZip()
|
||||
{
|
||||
if (!isset($this->zip)) {
|
||||
$this->zip = new \ZipArchive();
|
||||
$zipFilePath = $this->getZipFilePath();
|
||||
|
||||
$this->zip->open($zipFilePath, \ZipArchive::CREATE|\ZipArchive::OVERWRITE);
|
||||
}
|
||||
|
||||
return $this->zip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Path where the zip file of the given folder will be created
|
||||
*/
|
||||
public function getZipFilePath()
|
||||
{
|
||||
return $this->tmpFolderPath . self::ZIP_EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given file, located under the given root folder to the archive.
|
||||
* The file will be compressed.
|
||||
*
|
||||
* Example of use:
|
||||
* addFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
|
||||
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
|
||||
*
|
||||
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
|
||||
* @param string $localFilePath Path of the file to be added, under the root folder
|
||||
* @param string|void $existingFileMode Controls what to do when trying to add an existing file
|
||||
* @return void
|
||||
*/
|
||||
public function addFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
|
||||
{
|
||||
$this->addFileToArchiveWithCompressionMethod(
|
||||
$rootFolderPath,
|
||||
$localFilePath,
|
||||
$existingFileMode,
|
||||
\ZipArchive::CM_DEFAULT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given file, located under the given root folder to the archive.
|
||||
* The file will NOT be compressed.
|
||||
*
|
||||
* Example of use:
|
||||
* addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
|
||||
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
|
||||
*
|
||||
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
|
||||
* @param string $localFilePath Path of the file to be added, under the root folder
|
||||
* @param string|void $existingFileMode Controls what to do when trying to add an existing file
|
||||
* @return void
|
||||
*/
|
||||
public function addUncompressedFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
|
||||
{
|
||||
$this->addFileToArchiveWithCompressionMethod(
|
||||
$rootFolderPath,
|
||||
$localFilePath,
|
||||
$existingFileMode,
|
||||
\ZipArchive::CM_STORE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given file, located under the given root folder to the archive.
|
||||
* The file will NOT be compressed.
|
||||
*
|
||||
* Example of use:
|
||||
* addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
|
||||
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
|
||||
*
|
||||
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
|
||||
* @param string $localFilePath Path of the file to be added, under the root folder
|
||||
* @param string $existingFileMode Controls what to do when trying to add an existing file
|
||||
* @param int $compressionMethod The compression method
|
||||
* @return void
|
||||
*/
|
||||
protected function addFileToArchiveWithCompressionMethod($rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod)
|
||||
{
|
||||
$zip = $this->createOrGetZip();
|
||||
|
||||
if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) {
|
||||
$normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath);
|
||||
$zip->addFile($normalizedFullFilePath, $localFilePath);
|
||||
|
||||
if (self::canChooseCompressionMethod()) {
|
||||
$zip->setCompressionName($localFilePath, $compressionMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether it is possible to choose the desired compression method to be used
|
||||
*/
|
||||
public static function canChooseCompressionMethod()
|
||||
{
|
||||
// setCompressionName() is a PHP7+ method...
|
||||
return (method_exists(new \ZipArchive(), 'setCompressionName'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $folderPath Path to the folder to be zipped
|
||||
* @param string|void $existingFileMode Controls what to do when trying to add an existing file
|
||||
* @return void
|
||||
*/
|
||||
public function addFolderToArchive($folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
|
||||
{
|
||||
$zip = $this->createOrGetZip();
|
||||
|
||||
$folderRealPath = $this->getNormalizedRealPath($folderPath) . '/';
|
||||
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
foreach ($itemIterator as $itemInfo) {
|
||||
$itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
|
||||
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
|
||||
|
||||
if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) {
|
||||
$zip->addFile($itemRealPath, $itemLocalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ZipArchive $zip
|
||||
* @param string $itemLocalPath
|
||||
* @param string $existingFileMode
|
||||
* @return bool Whether the file should be added to the archive or skipped
|
||||
*/
|
||||
protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode)
|
||||
{
|
||||
// Skip files if:
|
||||
// - EXISTING_FILES_SKIP mode chosen
|
||||
// - File already exists in the archive
|
||||
return ($existingFileMode === self::EXISTING_FILES_SKIP && $zip->locateName($itemLocalPath) !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns canonicalized absolute pathname, containing only forward slashes.
|
||||
*
|
||||
* @param string $path Path to normalize
|
||||
* @return string Normalized and canonicalized path
|
||||
*/
|
||||
protected function getNormalizedRealPath($path)
|
||||
{
|
||||
$realPath = realpath($path);
|
||||
return str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the archive and copies it into the given stream
|
||||
*
|
||||
* @param resource $streamPointer Pointer to the stream to copy the zip
|
||||
* @return void
|
||||
*/
|
||||
public function closeArchiveAndCopyToStream($streamPointer)
|
||||
{
|
||||
$zip = $this->createOrGetZip();
|
||||
$zip->close();
|
||||
unset($this->zip);
|
||||
|
||||
$this->copyZipToStream($streamPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams the contents of the zip file into the given stream
|
||||
*
|
||||
* @param resource $pointer Pointer to the stream to copy the zip
|
||||
* @return void
|
||||
*/
|
||||
protected function copyZipToStream($pointer)
|
||||
{
|
||||
$zipFilePointer = fopen($this->getZipFilePath(), 'r');
|
||||
stream_copy_to_stream($zipFilePointer, $pointer);
|
||||
fclose($zipFilePointer);
|
||||
}
|
||||
}
|
||||
@ -1,192 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Common\Internal;
|
||||
|
||||
use Box\Spout\Writer\Exception\SheetNotFoundException;
|
||||
|
||||
/**
|
||||
* Class Workbook
|
||||
* Represents a workbook within a spreadsheet file.
|
||||
* It provides the functions to work with worksheets.
|
||||
*
|
||||
* @package Box\Spout\Writer\Common
|
||||
*/
|
||||
abstract class AbstractWorkbook implements WorkbookInterface
|
||||
{
|
||||
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
|
||||
protected $shouldCreateNewSheetsAutomatically;
|
||||
|
||||
/** @var string Timestamp based unique ID identifying the workbook */
|
||||
protected $internalId;
|
||||
|
||||
/** @var WorksheetInterface[] Array containing the workbook's sheets */
|
||||
protected $worksheets = [];
|
||||
|
||||
/** @var WorksheetInterface The worksheet where data will be written to */
|
||||
protected $currentWorksheet;
|
||||
|
||||
/**
|
||||
* @param bool $shouldCreateNewSheetsAutomatically
|
||||
* @param \Box\Spout\Writer\Style\Style $defaultRowStyle
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
|
||||
*/
|
||||
public function __construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle)
|
||||
{
|
||||
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
|
||||
$this->internalId = uniqid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Box\Spout\Writer\Common\Helper\AbstractStyleHelper The specific style helper
|
||||
*/
|
||||
abstract protected function getStyleHelper();
|
||||
|
||||
/**
|
||||
* @return int Maximum number of rows/columns a sheet can contain
|
||||
*/
|
||||
abstract protected function getMaxRowsPerWorksheet();
|
||||
|
||||
/**
|
||||
* Creates a new sheet in the workbook. The current sheet remains unchanged.
|
||||
*
|
||||
* @return WorksheetInterface The created sheet
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
||||
*/
|
||||
abstract public function addNewSheet();
|
||||
|
||||
/**
|
||||
* Creates a new sheet in the workbook and make it the current sheet.
|
||||
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||
*
|
||||
* @return WorksheetInterface The created sheet
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
||||
*/
|
||||
public function addNewSheetAndMakeItCurrent()
|
||||
{
|
||||
$worksheet = $this->addNewSheet();
|
||||
$this->setCurrentWorksheet($worksheet);
|
||||
|
||||
return $worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorksheetInterface[] All the workbook's sheets
|
||||
*/
|
||||
public function getWorksheets()
|
||||
{
|
||||
return $this->worksheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current sheet
|
||||
*
|
||||
* @return WorksheetInterface The current sheet
|
||||
*/
|
||||
public function getCurrentWorksheet()
|
||||
{
|
||||
return $this->currentWorksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given sheet as the current one. New data will be written to this sheet.
|
||||
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||
*
|
||||
* @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
|
||||
*/
|
||||
public function setCurrentSheet($sheet)
|
||||
{
|
||||
$worksheet = $this->getWorksheetFromExternalSheet($sheet);
|
||||
if ($worksheet !== null) {
|
||||
$this->currentWorksheet = $worksheet;
|
||||
} else {
|
||||
throw new SheetNotFoundException('The given sheet does not exist in the workbook.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WorksheetInterface $worksheet
|
||||
* @return void
|
||||
*/
|
||||
protected function setCurrentWorksheet($worksheet)
|
||||
{
|
||||
$this->currentWorksheet = $worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the worksheet associated to the given external sheet.
|
||||
*
|
||||
* @param \Box\Spout\Writer\Common\Sheet $sheet
|
||||
* @return WorksheetInterface|null The worksheet associated to the given external sheet or null if not found.
|
||||
*/
|
||||
protected function getWorksheetFromExternalSheet($sheet)
|
||||
{
|
||||
$worksheetFound = null;
|
||||
|
||||
foreach ($this->worksheets as $worksheet) {
|
||||
if ($worksheet->getExternalSheet() === $sheet) {
|
||||
$worksheetFound = $worksheet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $worksheetFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds data to the current sheet.
|
||||
* If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
|
||||
* with the creation of new worksheets if one worksheet has reached its maximum capicity.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written. Cannot be empty.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing
|
||||
* @throws \Box\Spout\Writer\Exception\WriterException If unable to write data
|
||||
*/
|
||||
public function addRowToCurrentWorksheet($dataRow, $style)
|
||||
{
|
||||
$currentWorksheet = $this->getCurrentWorksheet();
|
||||
$hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows();
|
||||
$styleHelper = $this->getStyleHelper();
|
||||
|
||||
// if we reached the maximum number of rows for the current sheet...
|
||||
if ($hasReachedMaxRows) {
|
||||
// ... continue writing in a new sheet if option set
|
||||
if ($this->shouldCreateNewSheetsAutomatically) {
|
||||
$currentWorksheet = $this->addNewSheetAndMakeItCurrent();
|
||||
|
||||
$updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
|
||||
$registeredStyle = $styleHelper->registerStyle($updatedStyle);
|
||||
$currentWorksheet->addRow($dataRow, $registeredStyle);
|
||||
} else {
|
||||
// otherwise, do nothing as the data won't be read anyways
|
||||
}
|
||||
} else {
|
||||
$updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
|
||||
$registeredStyle = $styleHelper->registerStyle($updatedStyle);
|
||||
$currentWorksheet->addRow($dataRow, $registeredStyle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the current worksheet has reached the maximum number of rows per sheet.
|
||||
*/
|
||||
protected function hasCurrentWorkseetReachedMaxRows()
|
||||
{
|
||||
$currentWorksheet = $this->getCurrentWorksheet();
|
||||
return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the workbook and all its associated sheets.
|
||||
* All the necessary files are written to disk and zipped together to create the ODS file.
|
||||
* All the temporary files are then deleted.
|
||||
*
|
||||
* @param resource $finalFilePointer Pointer to the ODS that will be created
|
||||
* @return void
|
||||
*/
|
||||
abstract public function close($finalFilePointer);
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Common\Internal;
|
||||
|
||||
/**
|
||||
* Interface WorkbookInterface
|
||||
*
|
||||
* @package Box\Spout\Writer\Common\Internal
|
||||
*/
|
||||
interface WorkbookInterface
|
||||
{
|
||||
/**
|
||||
* Creates a new sheet in the workbook. The current sheet remains unchanged.
|
||||
*
|
||||
* @return WorksheetInterface The created sheet
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
||||
*/
|
||||
public function addNewSheet();
|
||||
|
||||
/**
|
||||
* Creates a new sheet in the workbook and make it the current sheet.
|
||||
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||
*
|
||||
* @return WorksheetInterface The created sheet
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
||||
*/
|
||||
public function addNewSheetAndMakeItCurrent();
|
||||
|
||||
/**
|
||||
* @return WorksheetInterface[] All the workbook's sheets
|
||||
*/
|
||||
public function getWorksheets();
|
||||
|
||||
/**
|
||||
* Returns the current sheet
|
||||
*
|
||||
* @return WorksheetInterface The current sheet
|
||||
*/
|
||||
public function getCurrentWorksheet();
|
||||
|
||||
/**
|
||||
* Sets the given sheet as the current one. New data will be written to this sheet.
|
||||
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||
*
|
||||
* @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
|
||||
*/
|
||||
public function setCurrentSheet($sheet);
|
||||
|
||||
/**
|
||||
* Adds data to the current sheet.
|
||||
* If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
|
||||
* with the creation of new worksheets if one worksheet has reached its maximum capicity.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing
|
||||
* @throws \Box\Spout\Writer\Exception\WriterException If unable to write data
|
||||
*/
|
||||
public function addRowToCurrentWorksheet($dataRow, $style);
|
||||
|
||||
/**
|
||||
* Closes the workbook and all its associated sheets.
|
||||
* All the necessary files are written to disk and zipped together to create the ODS file.
|
||||
* All the temporary files are then deleted.
|
||||
*
|
||||
* @param resource $finalFilePointer Pointer to the ODS that will be created
|
||||
* @return void
|
||||
*/
|
||||
public function close($finalFilePointer);
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Common\Internal;
|
||||
|
||||
/**
|
||||
* Interface WorksheetInterface
|
||||
*
|
||||
* @package Box\Spout\Writer\Common\Internal
|
||||
*/
|
||||
interface WorksheetInterface
|
||||
{
|
||||
/**
|
||||
* @return \Box\Spout\Writer\Common\Sheet The "external" sheet
|
||||
*/
|
||||
public function getExternalSheet();
|
||||
|
||||
/**
|
||||
* @return int The index of the last written row
|
||||
*/
|
||||
public function getLastWrittenRowIndex();
|
||||
|
||||
/**
|
||||
* Adds data to the worksheet.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
||||
*/
|
||||
public function addRow($dataRow, $style);
|
||||
|
||||
/**
|
||||
* Closes the worksheet
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close();
|
||||
}
|
||||
@ -1,183 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Common;
|
||||
|
||||
use Box\Spout\Common\Helper\StringHelper;
|
||||
use Box\Spout\Writer\Exception\InvalidSheetNameException;
|
||||
|
||||
/**
|
||||
* Class Sheet
|
||||
* External representation of a worksheet
|
||||
*
|
||||
* @package Box\Spout\Writer\Common
|
||||
*/
|
||||
class Sheet
|
||||
{
|
||||
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
|
||||
|
||||
/** Sheet name should not exceed 31 characters */
|
||||
const MAX_LENGTH_SHEET_NAME = 31;
|
||||
|
||||
/** @var array Invalid characters that cannot be contained in the sheet name */
|
||||
private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', ':', '[', ']'];
|
||||
|
||||
/** @var array Associative array [WORKBOOK_ID] => [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */
|
||||
protected static $SHEETS_NAME_USED = [];
|
||||
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string ID of the sheet's associated workbook. Used to restrict sheet name uniqueness enforcement to a single workbook */
|
||||
protected $associatedWorkbookId;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
protected $name;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\StringHelper */
|
||||
protected $stringHelper;
|
||||
|
||||
/**
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param string $associatedWorkbookId ID of the sheet's associated workbook
|
||||
*/
|
||||
public function __construct($sheetIndex, $associatedWorkbookId)
|
||||
{
|
||||
$this->index = $sheetIndex;
|
||||
$this->associatedWorkbookId = $associatedWorkbookId;
|
||||
if (!isset(self::$SHEETS_NAME_USED[$associatedWorkbookId])) {
|
||||
self::$SHEETS_NAME_USED[$associatedWorkbookId] = [];
|
||||
}
|
||||
|
||||
$this->stringHelper = new StringHelper();
|
||||
$this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return string Name of the sheet
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the sheet. Note that Excel has some restrictions on the name:
|
||||
* - it should not be blank
|
||||
* - it should not exceed 31 characters
|
||||
* - it should not contain these characters: \ / ? * : [ or ]
|
||||
* - it should be unique
|
||||
*
|
||||
* @api
|
||||
* @param string $name Name of the sheet
|
||||
* @return Sheet
|
||||
* @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->throwIfNameIsInvalid($name);
|
||||
|
||||
$this->name = $name;
|
||||
self::$SHEETS_NAME_USED[$this->associatedWorkbookId][$this->index] = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if the given sheet's name is not valid.
|
||||
* @see Sheet::setName for validity rules.
|
||||
*
|
||||
* @param string $name
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
|
||||
*/
|
||||
protected function throwIfNameIsInvalid($name)
|
||||
{
|
||||
if (!is_string($name)) {
|
||||
$actualType = gettype($name);
|
||||
$errorMessage = "The sheet's name is invalid. It must be a string ($actualType given).";
|
||||
throw new InvalidSheetNameException($errorMessage);
|
||||
}
|
||||
|
||||
$failedRequirements = [];
|
||||
$nameLength = $this->stringHelper->getStringLength($name);
|
||||
|
||||
if (!$this->isNameUnique($name)) {
|
||||
$failedRequirements[] = 'It should be unique';
|
||||
} else {
|
||||
if ($nameLength === 0) {
|
||||
$failedRequirements[] = 'It should not be blank';
|
||||
} else {
|
||||
if ($nameLength > self::MAX_LENGTH_SHEET_NAME) {
|
||||
$failedRequirements[] = 'It should not exceed 31 characters';
|
||||
}
|
||||
|
||||
if ($this->doesContainInvalidCharacters($name)) {
|
||||
$failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]';
|
||||
}
|
||||
|
||||
if ($this->doesStartOrEndWithSingleQuote($name)) {
|
||||
$failedRequirements[] = 'It should not start or end with a single quote';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($failedRequirements) !== 0) {
|
||||
$errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - ";
|
||||
$errorMessage .= implode("\n - ", $failedRequirements);
|
||||
throw new InvalidSheetNameException($errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given name contains at least one invalid character.
|
||||
* @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool TRUE if the name contains invalid characters, FALSE otherwise.
|
||||
*/
|
||||
protected function doesContainInvalidCharacters($name)
|
||||
{
|
||||
return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given name starts or ends with a single quote
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise.
|
||||
*/
|
||||
protected function doesStartOrEndWithSingleQuote($name)
|
||||
{
|
||||
$startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0);
|
||||
$endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1));
|
||||
|
||||
return ($startsWithSingleQuote || $endsWithSingleQuote);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given name is unique.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool TRUE if the name is unique, FALSE otherwise.
|
||||
*/
|
||||
protected function isNameUnique($name)
|
||||
{
|
||||
foreach (self::$SHEETS_NAME_USED[$this->associatedWorkbookId] as $sheetIndex => $sheetName) {
|
||||
if ($sheetIndex !== $this->index && $sheetName === $name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception\Border;
|
||||
|
||||
use Box\Spout\Writer\Exception\WriterException;
|
||||
use Box\Spout\Writer\Style\BorderPart;
|
||||
|
||||
class InvalidNameException extends WriterException
|
||||
{
|
||||
public function __construct($name)
|
||||
{
|
||||
$msg = '%s is not a valid name identifier for a border. Valid identifiers are: %s.';
|
||||
|
||||
parent::__construct(sprintf($msg, $name, implode(',', BorderPart::getAllowedNames())));
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception\Border;
|
||||
|
||||
use Box\Spout\Writer\Exception\WriterException;
|
||||
use Box\Spout\Writer\Style\BorderPart;
|
||||
|
||||
class InvalidStyleException extends WriterException
|
||||
{
|
||||
public function __construct($name)
|
||||
{
|
||||
$msg = '%s is not a valid style identifier for a border. Valid identifiers are: %s.';
|
||||
|
||||
parent::__construct(sprintf($msg, $name, implode(',', BorderPart::getAllowedStyles())));
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception\Border;
|
||||
|
||||
use Box\Spout\Writer\Exception\WriterException;
|
||||
use Box\Spout\Writer\Style\BorderPart;
|
||||
|
||||
class InvalidWidthException extends WriterException
|
||||
{
|
||||
public function __construct($name)
|
||||
{
|
||||
$msg = '%s is not a valid width identifier for a border. Valid identifiers are: %s.';
|
||||
|
||||
parent::__construct(sprintf($msg, $name, implode(',', BorderPart::getAllowedWidths())));
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception;
|
||||
|
||||
/**
|
||||
* Class InvalidColorException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Writer\Exception
|
||||
*/
|
||||
class InvalidColorException extends WriterException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception;
|
||||
|
||||
/**
|
||||
* Class InvalidSheetNameException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Writer\Exception
|
||||
*/
|
||||
class InvalidSheetNameException extends WriterException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception;
|
||||
|
||||
/**
|
||||
* Class SheetNotFoundException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Writer\Exception
|
||||
*/
|
||||
class SheetNotFoundException extends WriterException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception;
|
||||
|
||||
/**
|
||||
* Class WriterAlreadyOpenedException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Writer\Exception
|
||||
*/
|
||||
class WriterAlreadyOpenedException extends WriterException
|
||||
{
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception;
|
||||
|
||||
use Box\Spout\Common\Exception\SpoutException;
|
||||
|
||||
/**
|
||||
* Class WriterException
|
||||
*
|
||||
* @package Box\Spout\Writer\Exception
|
||||
* @abstract
|
||||
*/
|
||||
abstract class WriterException extends SpoutException
|
||||
{
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Exception;
|
||||
|
||||
/**
|
||||
* Class WriterNotOpenedException
|
||||
*
|
||||
* @api
|
||||
* @package Box\Spout\Writer\Exception
|
||||
*/
|
||||
class WriterNotOpenedException extends WriterException
|
||||
{
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\ODS\Helper;
|
||||
|
||||
use Box\Spout\Writer\Style\BorderPart;
|
||||
use Box\Spout\Writer\Style\Border;
|
||||
|
||||
/**
|
||||
* Class BorderHelper
|
||||
*
|
||||
* The fo:border, fo:border-top, fo:border-bottom, fo:border-left and fo:border-right attributes
|
||||
* specify border properties
|
||||
* http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1419780_253892949
|
||||
*
|
||||
* Example table-cell-properties
|
||||
*
|
||||
* <style:table-cell-properties
|
||||
* fo:border-bottom="0.74pt solid #ffc000" style:diagonal-bl-tr="none"
|
||||
* style:diagonal-tl-br="none" fo:border-left="none" fo:border-right="none"
|
||||
* style:rotation-align="none" fo:border-top="none"/>
|
||||
*/
|
||||
class BorderHelper
|
||||
{
|
||||
/**
|
||||
* Width mappings
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $widthMap = [
|
||||
Border::WIDTH_THIN => '0.75pt',
|
||||
Border::WIDTH_MEDIUM => '1.75pt',
|
||||
Border::WIDTH_THICK => '2.5pt',
|
||||
];
|
||||
|
||||
/**
|
||||
* Style mapping
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $styleMap = [
|
||||
Border::STYLE_SOLID => 'solid',
|
||||
Border::STYLE_DASHED => 'dashed',
|
||||
Border::STYLE_DOTTED => 'dotted',
|
||||
Border::STYLE_DOUBLE => 'double',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param BorderPart $borderPart
|
||||
* @return string
|
||||
*/
|
||||
public static function serializeBorderPart(BorderPart $borderPart)
|
||||
{
|
||||
$definition = 'fo:border-%s="%s"';
|
||||
|
||||
if ($borderPart->getStyle() === Border::STYLE_NONE) {
|
||||
$borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none');
|
||||
} else {
|
||||
$attributes = [
|
||||
self::$widthMap[$borderPart->getWidth()],
|
||||
self::$styleMap[$borderPart->getStyle()],
|
||||
'#' . $borderPart->getColor(),
|
||||
];
|
||||
$borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes));
|
||||
}
|
||||
|
||||
return $borderPartDefinition;
|
||||
}
|
||||
}
|
||||
@ -1,279 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\ODS\Helper;
|
||||
|
||||
use Box\Spout\Writer\Common\Helper\ZipHelper;
|
||||
use Box\Spout\Writer\ODS\Internal\Worksheet;
|
||||
|
||||
/**
|
||||
* Class FileSystemHelper
|
||||
* This class provides helper functions to help with the file system operations
|
||||
* like files/folders creation & deletion for ODS files
|
||||
*
|
||||
* @package Box\Spout\Writer\ODS\Helper
|
||||
*/
|
||||
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
||||
{
|
||||
const APP_NAME = 'Spout';
|
||||
const MIMETYPE = 'application/vnd.oasis.opendocument.spreadsheet';
|
||||
|
||||
const META_INF_FOLDER_NAME = 'META-INF';
|
||||
const SHEETS_CONTENT_TEMP_FOLDER_NAME = 'worksheets-temp';
|
||||
|
||||
const MANIFEST_XML_FILE_NAME = 'manifest.xml';
|
||||
const CONTENT_XML_FILE_NAME = 'content.xml';
|
||||
const META_XML_FILE_NAME = 'meta.xml';
|
||||
const MIMETYPE_FILE_NAME = 'mimetype';
|
||||
const STYLES_XML_FILE_NAME = 'styles.xml';
|
||||
|
||||
/** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */
|
||||
protected $rootFolder;
|
||||
|
||||
/** @var string Path to the "META-INF" folder inside the root folder */
|
||||
protected $metaInfFolder;
|
||||
|
||||
/** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */
|
||||
protected $sheetsContentTempFolder;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRootFolder()
|
||||
{
|
||||
return $this->rootFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSheetsContentTempFolder()
|
||||
{
|
||||
return $this->sheetsContentTempFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all the folders needed to create a ODS file, as well as the files that won't change.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
|
||||
*/
|
||||
public function createBaseFilesAndFolders()
|
||||
{
|
||||
$this
|
||||
->createRootFolder()
|
||||
->createMetaInfoFolderAndFile()
|
||||
->createSheetsContentTempFolder()
|
||||
->createMetaFile()
|
||||
->createMimetypeFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the folder that will be used as root
|
||||
*
|
||||
* @return FileSystemHelper
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
|
||||
*/
|
||||
protected function createRootFolder()
|
||||
{
|
||||
$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it
|
||||
*
|
||||
* @return FileSystemHelper
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file
|
||||
*/
|
||||
protected function createMetaInfoFolderAndFile()
|
||||
{
|
||||
$this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);
|
||||
|
||||
$this->createManifestFile();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "manifest.xml" file under the "META-INF" folder (under root)
|
||||
*
|
||||
* @return FileSystemHelper
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
|
||||
*/
|
||||
protected function createManifestFile()
|
||||
{
|
||||
$manifestXmlFileContents = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
|
||||
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
|
||||
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
|
||||
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
||||
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
|
||||
</manifest:manifest>
|
||||
EOD;
|
||||
|
||||
$this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the temp folder where specific sheets content will be written to.
|
||||
* This folder is not part of the final ODS file and is only used to be able to jump between sheets.
|
||||
*
|
||||
* @return FileSystemHelper
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
|
||||
*/
|
||||
protected function createSheetsContentTempFolder()
|
||||
{
|
||||
$this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "meta.xml" file under the root folder
|
||||
*
|
||||
* @return FileSystemHelper
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
|
||||
*/
|
||||
protected function createMetaFile()
|
||||
{
|
||||
$appName = self::APP_NAME;
|
||||
$createdDate = (new \DateTime())->format(\DateTime::W3C);
|
||||
|
||||
$metaXmlFileContents = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<office:document-meta office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<office:meta>
|
||||
<dc:creator>$appName</dc:creator>
|
||||
<meta:creation-date>$createdDate</meta:creation-date>
|
||||
<dc:date>$createdDate</dc:date>
|
||||
</office:meta>
|
||||
</office:document-meta>
|
||||
EOD;
|
||||
|
||||
$this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "mimetype" file under the root folder
|
||||
*
|
||||
* @return FileSystemHelper
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
|
||||
*/
|
||||
protected function createMimetypeFile()
|
||||
{
|
||||
$this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "content.xml" file under the root folder
|
||||
*
|
||||
* @param Worksheet[] $worksheets
|
||||
* @param StyleHelper $styleHelper
|
||||
* @return FileSystemHelper
|
||||
*/
|
||||
public function createContentFile($worksheets, $styleHelper)
|
||||
{
|
||||
$contentXmlFileContents = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<office:document-content office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
EOD;
|
||||
|
||||
$contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent();
|
||||
$contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets));
|
||||
|
||||
$contentXmlFileContents .= '<office:body><office:spreadsheet>';
|
||||
|
||||
$this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents);
|
||||
|
||||
// Append sheets content to "content.xml"
|
||||
$contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME;
|
||||
$contentXmlHandle = fopen($contentXmlFilePath, 'a');
|
||||
|
||||
foreach ($worksheets as $worksheet) {
|
||||
// write the "<table:table>" node, with the final sheet's name
|
||||
fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString());
|
||||
|
||||
$worksheetFilePath = $worksheet->getWorksheetFilePath();
|
||||
$this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);
|
||||
|
||||
fwrite($contentXmlHandle, '</table:table>');
|
||||
}
|
||||
|
||||
$contentXmlFileContents = '</office:spreadsheet></office:body></office:document-content>';
|
||||
|
||||
fwrite($contentXmlHandle, $contentXmlFileContents);
|
||||
fclose($contentXmlHandle);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams the content of the file at the given path into the target resource.
|
||||
* Depending on which mode the target resource was created with, it will truncate then copy
|
||||
* or append the content to the target file.
|
||||
*
|
||||
* @param string $sourceFilePath Path of the file whose content will be copied
|
||||
* @param resource $targetResource Target resource that will receive the content
|
||||
* @return void
|
||||
*/
|
||||
protected function copyFileContentsToTarget($sourceFilePath, $targetResource)
|
||||
{
|
||||
$sourceHandle = fopen($sourceFilePath, 'r');
|
||||
stream_copy_to_stream($sourceHandle, $targetResource);
|
||||
fclose($sourceHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the temporary folder where sheets content was stored.
|
||||
*
|
||||
* @return FileSystemHelper
|
||||
*/
|
||||
public function deleteWorksheetTempFolder()
|
||||
{
|
||||
$this->deleteFolderRecursively($this->sheetsContentTempFolder);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates the "styles.xml" file under the root folder
|
||||
*
|
||||
* @param StyleHelper $styleHelper
|
||||
* @param int $numWorksheets Number of created worksheets
|
||||
* @return FileSystemHelper
|
||||
*/
|
||||
public function createStylesFile($styleHelper, $numWorksheets)
|
||||
{
|
||||
$stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets);
|
||||
$this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zips the root folder and streams the contents of the zip into the given stream
|
||||
*
|
||||
* @param resource $streamPointer Pointer to the stream to copy the zip
|
||||
* @return void
|
||||
*/
|
||||
public function zipRootFolderAndCopyToStream($streamPointer)
|
||||
{
|
||||
$zipHelper = new ZipHelper($this->rootFolder);
|
||||
|
||||
// In order to have the file's mime type detected properly, files need to be added
|
||||
// to the zip file in a particular order.
|
||||
// @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
|
||||
$zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME);
|
||||
|
||||
$zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
|
||||
$zipHelper->closeArchiveAndCopyToStream($streamPointer);
|
||||
|
||||
// once the zip is copied, remove it
|
||||
$this->deleteFile($zipHelper->getZipFilePath());
|
||||
}
|
||||
}
|
||||
@ -1,356 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\ODS\Helper;
|
||||
|
||||
use Box\Spout\Writer\Common\Helper\AbstractStyleHelper;
|
||||
use Box\Spout\Writer\Style\BorderPart;
|
||||
|
||||
/**
|
||||
* Class StyleHelper
|
||||
* This class provides helper functions to manage styles
|
||||
*
|
||||
* @package Box\Spout\Writer\ODS\Helper
|
||||
*/
|
||||
class StyleHelper extends AbstractStyleHelper
|
||||
{
|
||||
/** @var string[] [FONT_NAME] => [] Map whose keys contain all the fonts used */
|
||||
protected $usedFontsSet = [];
|
||||
|
||||
/**
|
||||
* Registers the given style as a used style.
|
||||
* Duplicate styles won't be registered more than once.
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style The style to be registered
|
||||
* @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID.
|
||||
*/
|
||||
public function registerStyle($style)
|
||||
{
|
||||
$this->usedFontsSet[$style->getFontName()] = true;
|
||||
return parent::registerStyle($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] List of used fonts name
|
||||
*/
|
||||
protected function getUsedFonts()
|
||||
{
|
||||
return array_keys($this->usedFontsSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the "styles.xml" file, given a list of styles.
|
||||
*
|
||||
* @param int $numWorksheets Number of worksheets created
|
||||
* @return string
|
||||
*/
|
||||
public function getStylesXMLFileContent($numWorksheets)
|
||||
{
|
||||
$content = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<office:document-styles office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
EOD;
|
||||
|
||||
$content .= $this->getFontFaceSectionContent();
|
||||
$content .= $this->getStylesSectionContent();
|
||||
$content .= $this->getAutomaticStylesSectionContent($numWorksheets);
|
||||
$content .= $this->getMasterStylesSectionContent($numWorksheets);
|
||||
|
||||
$content .= <<<EOD
|
||||
</office:document-styles>
|
||||
EOD;
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the "<office:font-face-decls>" section, inside "styles.xml" file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFontFaceSectionContent()
|
||||
{
|
||||
$content = '<office:font-face-decls>';
|
||||
foreach ($this->getUsedFonts() as $fontName) {
|
||||
$content .= '<style:font-face style:name="' . $fontName . '" svg:font-family="' . $fontName . '"/>';
|
||||
}
|
||||
$content .= '</office:font-face-decls>';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the "<office:styles>" section, inside "styles.xml" file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getStylesSectionContent()
|
||||
{
|
||||
$defaultStyle = $this->getDefaultStyle();
|
||||
|
||||
return <<<EOD
|
||||
<office:styles>
|
||||
<number:number-style style:name="N0">
|
||||
<number:number number:min-integer-digits="1"/>
|
||||
</number:number-style>
|
||||
<style:style style:data-style-name="N0" style:family="table-cell" style:name="Default">
|
||||
<style:table-cell-properties fo:background-color="transparent" style:vertical-align="automatic"/>
|
||||
<style:text-properties fo:color="#{$defaultStyle->getFontColor()}"
|
||||
fo:font-size="{$defaultStyle->getFontSize()}pt" style:font-size-asian="{$defaultStyle->getFontSize()}pt" style:font-size-complex="{$defaultStyle->getFontSize()}pt"
|
||||
style:font-name="{$defaultStyle->getFontName()}" style:font-name-asian="{$defaultStyle->getFontName()}" style:font-name-complex="{$defaultStyle->getFontName()}"/>
|
||||
</style:style>
|
||||
</office:styles>
|
||||
EOD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the "<office:automatic-styles>" section, inside "styles.xml" file.
|
||||
*
|
||||
* @param int $numWorksheets Number of worksheets created
|
||||
* @return string
|
||||
*/
|
||||
protected function getAutomaticStylesSectionContent($numWorksheets)
|
||||
{
|
||||
$content = '<office:automatic-styles>';
|
||||
|
||||
for ($i = 1; $i <= $numWorksheets; $i++) {
|
||||
$content .= <<<EOD
|
||||
<style:page-layout style:name="pm$i">
|
||||
<style:page-layout-properties style:first-page-number="continue" style:print="objects charts drawings" style:table-centering="none"/>
|
||||
<style:header-style/>
|
||||
<style:footer-style/>
|
||||
</style:page-layout>
|
||||
EOD;
|
||||
}
|
||||
|
||||
$content .= '</office:automatic-styles>';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the "<office:master-styles>" section, inside "styles.xml" file.
|
||||
*
|
||||
* @param int $numWorksheets Number of worksheets created
|
||||
* @return string
|
||||
*/
|
||||
protected function getMasterStylesSectionContent($numWorksheets)
|
||||
{
|
||||
$content = '<office:master-styles>';
|
||||
|
||||
for ($i = 1; $i <= $numWorksheets; $i++) {
|
||||
$content .= <<<EOD
|
||||
<style:master-page style:name="mp$i" style:page-layout-name="pm$i">
|
||||
<style:header/>
|
||||
<style:header-left style:display="false"/>
|
||||
<style:footer/>
|
||||
<style:footer-left style:display="false"/>
|
||||
</style:master-page>
|
||||
EOD;
|
||||
}
|
||||
|
||||
$content .= '</office:master-styles>';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the contents of the "<office:font-face-decls>" section, inside "content.xml" file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContentXmlFontFaceSectionContent()
|
||||
{
|
||||
$content = '<office:font-face-decls>';
|
||||
foreach ($this->getUsedFonts() as $fontName) {
|
||||
$content .= '<style:font-face style:name="' . $fontName . '" svg:font-family="' . $fontName . '"/>';
|
||||
}
|
||||
$content .= '</office:font-face-decls>';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the "<office:automatic-styles>" section, inside "content.xml" file.
|
||||
*
|
||||
* @param int $numWorksheets Number of worksheets created
|
||||
* @return string
|
||||
*/
|
||||
public function getContentXmlAutomaticStylesSectionContent($numWorksheets)
|
||||
{
|
||||
$content = '<office:automatic-styles>';
|
||||
|
||||
foreach ($this->getRegisteredStyles() as $style) {
|
||||
$content .= $this->getStyleSectionContent($style);
|
||||
}
|
||||
|
||||
$content .= <<<EOD
|
||||
<style:style style:family="table-column" style:name="co1">
|
||||
<style:table-column-properties fo:break-before="auto"/>
|
||||
</style:style>
|
||||
<style:style style:family="table-row" style:name="ro1">
|
||||
<style:table-row-properties fo:break-before="auto" style:row-height="15pt" style:use-optimal-row-height="true"/>
|
||||
</style:style>
|
||||
EOD;
|
||||
|
||||
for ($i = 1; $i <= $numWorksheets; $i++) {
|
||||
$content .= <<<EOD
|
||||
<style:style style:family="table" style:master-page-name="mp$i" style:name="ta$i">
|
||||
<style:table-properties style:writing-mode="lr-tb" table:display="true"/>
|
||||
</style:style>
|
||||
EOD;
|
||||
}
|
||||
|
||||
$content .= '</office:automatic-styles>';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the "<style:style>" section, inside "<office:automatic-styles>" section
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style
|
||||
* @return string
|
||||
*/
|
||||
protected function getStyleSectionContent($style)
|
||||
{
|
||||
$styleIndex = $style->getId() + 1; // 1-based
|
||||
|
||||
$content = '<style:style style:data-style-name="N0" style:family="table-cell" style:name="ce' . $styleIndex . '" style:parent-style-name="Default">';
|
||||
|
||||
$content .= $this->getTextPropertiesSectionContent($style);
|
||||
$content .= $this->getTableCellPropertiesSectionContent($style);
|
||||
|
||||
$content .= '</style:style>';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the "<style:text-properties>" section, inside "<style:style>" section
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style
|
||||
* @return string
|
||||
*/
|
||||
private function getTextPropertiesSectionContent($style)
|
||||
{
|
||||
$content = '';
|
||||
|
||||
if ($style->shouldApplyFont()) {
|
||||
$content .= $this->getFontSectionContent($style);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the "<style:text-properties>" section, inside "<style:style>" section
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style
|
||||
* @return string
|
||||
*/
|
||||
private function getFontSectionContent($style)
|
||||
{
|
||||
$defaultStyle = $this->getDefaultStyle();
|
||||
|
||||
$content = '<style:text-properties';
|
||||
|
||||
$fontColor = $style->getFontColor();
|
||||
if ($fontColor !== $defaultStyle->getFontColor()) {
|
||||
$content .= ' fo:color="#' . $fontColor . '"';
|
||||
}
|
||||
|
||||
$fontName = $style->getFontName();
|
||||
if ($fontName !== $defaultStyle->getFontName()) {
|
||||
$content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"';
|
||||
}
|
||||
|
||||
$fontSize = $style->getFontSize();
|
||||
if ($fontSize !== $defaultStyle->getFontSize()) {
|
||||
$content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"';
|
||||
}
|
||||
|
||||
if ($style->isFontBold()) {
|
||||
$content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"';
|
||||
}
|
||||
if ($style->isFontItalic()) {
|
||||
$content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"';
|
||||
}
|
||||
if ($style->isFontUnderline()) {
|
||||
$content .= ' style:text-underline-style="solid" style:text-underline-type="single"';
|
||||
}
|
||||
if ($style->isFontStrikethrough()) {
|
||||
$content .= ' style:text-line-through-style="solid"';
|
||||
}
|
||||
|
||||
$content .= '/>';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the "<style:table-cell-properties>" section, inside "<style:style>" section
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style
|
||||
* @return string
|
||||
*/
|
||||
private function getTableCellPropertiesSectionContent($style)
|
||||
{
|
||||
$content = '';
|
||||
|
||||
if ($style->shouldWrapText()) {
|
||||
$content .= $this->getWrapTextXMLContent();
|
||||
}
|
||||
|
||||
if ($style->shouldApplyBorder()) {
|
||||
$content .= $this->getBorderXMLContent($style);
|
||||
}
|
||||
|
||||
if ($style->shouldApplyBackgroundColor()) {
|
||||
$content .= $this->getBackgroundColorXMLContent($style);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the wrap text definition for the "<style:table-cell-properties>" section
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getWrapTextXMLContent()
|
||||
{
|
||||
return '<style:table-cell-properties fo:wrap-option="wrap" style:vertical-align="automatic"/>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the borders definition for the "<style:table-cell-properties>" section
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style
|
||||
* @return string
|
||||
*/
|
||||
private function getBorderXMLContent($style)
|
||||
{
|
||||
$borderProperty = '<style:table-cell-properties %s />';
|
||||
|
||||
$borders = array_map(function (BorderPart $borderPart) {
|
||||
return BorderHelper::serializeBorderPart($borderPart);
|
||||
}, $style->getBorder()->getParts());
|
||||
|
||||
return sprintf($borderProperty, implode(' ', $borders));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the background color definition for the "<style:table-cell-properties>" section
|
||||
*
|
||||
* @param \Box\Spout\Writer\Style\Style $style
|
||||
* @return string
|
||||
*/
|
||||
private function getBackgroundColorXMLContent($style)
|
||||
{
|
||||
return sprintf(
|
||||
'<style:table-cell-properties fo:background-color="#%s"/>',
|
||||
$style->getBackgroundColor()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\ODS\Internal;
|
||||
|
||||
use Box\Spout\Writer\Common\Internal\AbstractWorkbook;
|
||||
use Box\Spout\Writer\ODS\Helper\FileSystemHelper;
|
||||
use Box\Spout\Writer\ODS\Helper\StyleHelper;
|
||||
use Box\Spout\Writer\Common\Sheet;
|
||||
|
||||
/**
|
||||
* Class Workbook
|
||||
* Represents a workbook within a ODS file.
|
||||
* It provides the functions to work with worksheets.
|
||||
*
|
||||
* @package Box\Spout\Writer\ODS\Internal
|
||||
*/
|
||||
class Workbook extends AbstractWorkbook
|
||||
{
|
||||
/**
|
||||
* Maximum number of rows a ODS sheet can contain
|
||||
* @see https://ask.libreoffice.org/en/question/8631/upper-limit-to-number-of-rows-in-calc/
|
||||
*/
|
||||
protected static $maxRowsPerWorksheet = 1048576;
|
||||
|
||||
/** @var \Box\Spout\Writer\ODS\Helper\FileSystemHelper Helper to perform file system operations */
|
||||
protected $fileSystemHelper;
|
||||
|
||||
/** @var \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles */
|
||||
protected $styleHelper;
|
||||
|
||||
/**
|
||||
* @param string $tempFolder
|
||||
* @param bool $shouldCreateNewSheetsAutomatically
|
||||
* @param \Box\Spout\Writer\Style\Style $defaultRowStyle
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
|
||||
*/
|
||||
public function __construct($tempFolder, $shouldCreateNewSheetsAutomatically, $defaultRowStyle)
|
||||
{
|
||||
parent::__construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle);
|
||||
|
||||
$this->fileSystemHelper = new FileSystemHelper($tempFolder);
|
||||
$this->fileSystemHelper->createBaseFilesAndFolders();
|
||||
|
||||
$this->styleHelper = new StyleHelper($defaultRowStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles to ODS files
|
||||
*/
|
||||
protected function getStyleHelper()
|
||||
{
|
||||
return $this->styleHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Maximum number of rows/columns a sheet can contain
|
||||
*/
|
||||
protected function getMaxRowsPerWorksheet()
|
||||
{
|
||||
return self::$maxRowsPerWorksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sheet in the workbook. The current sheet remains unchanged.
|
||||
*
|
||||
* @return Worksheet The created sheet
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
||||
*/
|
||||
public function addNewSheet()
|
||||
{
|
||||
$newSheetIndex = count($this->worksheets);
|
||||
$sheet = new Sheet($newSheetIndex, $this->internalId);
|
||||
|
||||
$sheetsContentTempFolder = $this->fileSystemHelper->getSheetsContentTempFolder();
|
||||
$worksheet = new Worksheet($sheet, $sheetsContentTempFolder);
|
||||
$this->worksheets[] = $worksheet;
|
||||
|
||||
return $worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the workbook and all its associated sheets.
|
||||
* All the necessary files are written to disk and zipped together to create the ODS file.
|
||||
* All the temporary files are then deleted.
|
||||
*
|
||||
* @param resource $finalFilePointer Pointer to the ODS that will be created
|
||||
* @return void
|
||||
*/
|
||||
public function close($finalFilePointer)
|
||||
{
|
||||
/** @var Worksheet[] $worksheets */
|
||||
$worksheets = $this->worksheets;
|
||||
$numWorksheets = count($worksheets);
|
||||
|
||||
foreach ($worksheets as $worksheet) {
|
||||
$worksheet->close();
|
||||
}
|
||||
|
||||
// Finish creating all the necessary files before zipping everything together
|
||||
$this->fileSystemHelper
|
||||
->createContentFile($worksheets, $this->styleHelper)
|
||||
->deleteWorksheetTempFolder()
|
||||
->createStylesFile($this->styleHelper, $numWorksheets)
|
||||
->zipRootFolderAndCopyToStream($finalFilePointer);
|
||||
|
||||
$this->cleanupTempFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the root folder created in the temp folder and all its contents.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function cleanupTempFolder()
|
||||
{
|
||||
$xlsxRootFolder = $this->fileSystemHelper->getRootFolder();
|
||||
$this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder);
|
||||
}
|
||||
}
|
||||
@ -1,233 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\ODS\Internal;
|
||||
|
||||
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Common\Helper\StringHelper;
|
||||
use Box\Spout\Writer\Common\Helper\CellHelper;
|
||||
use Box\Spout\Writer\Common\Internal\WorksheetInterface;
|
||||
|
||||
/**
|
||||
* Class Worksheet
|
||||
* Represents a worksheet within a ODS file. The difference with the Sheet object is
|
||||
* that this class provides an interface to write data
|
||||
*
|
||||
* @package Box\Spout\Writer\ODS\Internal
|
||||
*/
|
||||
class Worksheet implements WorksheetInterface
|
||||
{
|
||||
/** @var \Box\Spout\Writer\Common\Sheet The "external" sheet */
|
||||
protected $externalSheet;
|
||||
|
||||
/** @var string Path to the XML file that will contain the sheet data */
|
||||
protected $worksheetFilePath;
|
||||
|
||||
/** @var \Box\Spout\Common\Escaper\ODS Strings escaper */
|
||||
protected $stringsEscaper;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\StringHelper To help with string manipulation */
|
||||
protected $stringHelper;
|
||||
|
||||
/** @var Resource Pointer to the temporary sheet data file (e.g. worksheets-temp/sheet1.xml) */
|
||||
protected $sheetFilePointer;
|
||||
|
||||
/** @var int Maximum number of columns among all the written rows */
|
||||
protected $maxNumColumns = 1;
|
||||
|
||||
/** @var int Index of the last written row */
|
||||
protected $lastWrittenRowIndex = 0;
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet
|
||||
* @param string $worksheetFilesFolder Temporary folder where the files to create the ODS will be stored
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
||||
*/
|
||||
public function __construct($externalSheet, $worksheetFilesFolder)
|
||||
{
|
||||
$this->externalSheet = $externalSheet;
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
$this->stringsEscaper = \Box\Spout\Common\Escaper\ODS::getInstance();
|
||||
$this->worksheetFilePath = $worksheetFilesFolder . '/sheet' . $externalSheet->getIndex() . '.xml';
|
||||
|
||||
$this->stringHelper = new StringHelper();
|
||||
|
||||
$this->startSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the worksheet to accept data
|
||||
* The XML file does not contain the "<table:table>" node as it contains the sheet's name
|
||||
* which may change during the execution of the program. It will be added at the end.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
||||
*/
|
||||
protected function startSheet()
|
||||
{
|
||||
$this->sheetFilePointer = fopen($this->worksheetFilePath, 'w');
|
||||
$this->throwIfSheetFilePointerIsNotAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the book has been created. Throws an exception if not created yet.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
||||
*/
|
||||
protected function throwIfSheetFilePointerIsNotAvailable()
|
||||
{
|
||||
if (!$this->sheetFilePointer) {
|
||||
throw new IOException('Unable to open sheet for writing.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Path to the temporary sheet content XML file
|
||||
*/
|
||||
public function getWorksheetFilePath()
|
||||
{
|
||||
return $this->worksheetFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table XML root node as string.
|
||||
*
|
||||
* @return string <table> node as string
|
||||
*/
|
||||
public function getTableElementStartAsString()
|
||||
{
|
||||
$escapedSheetName = $this->stringsEscaper->escape($this->externalSheet->getName());
|
||||
$tableStyleName = 'ta' . ($this->externalSheet->getIndex() + 1);
|
||||
|
||||
$tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
|
||||
$tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $this->maxNumColumns . '"/>';
|
||||
|
||||
return $tableElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Box\Spout\Writer\Common\Sheet The "external" sheet
|
||||
*/
|
||||
public function getExternalSheet()
|
||||
{
|
||||
return $this->externalSheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int The index of the last written row
|
||||
*/
|
||||
public function getLastWrittenRowIndex()
|
||||
{
|
||||
return $this->lastWrittenRowIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds data to the worksheet.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written. Cannot be empty.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
||||
*/
|
||||
public function addRow($dataRow, $style)
|
||||
{
|
||||
// $dataRow can be an associative array. We need to transform
|
||||
// it into a regular array, as we'll use the numeric indexes.
|
||||
$dataRowWithNumericIndexes = array_values($dataRow);
|
||||
|
||||
$styleIndex = ($style->getId() + 1); // 1-based
|
||||
$cellsCount = count($dataRow);
|
||||
$this->maxNumColumns = max($this->maxNumColumns, $cellsCount);
|
||||
|
||||
$data = '<table:table-row table:style-name="ro1">';
|
||||
|
||||
$currentCellIndex = 0;
|
||||
$nextCellIndex = 1;
|
||||
|
||||
for ($i = 0; $i < $cellsCount; $i++) {
|
||||
$currentCellValue = $dataRowWithNumericIndexes[$currentCellIndex];
|
||||
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
if (!isset($dataRowWithNumericIndexes[$nextCellIndex]) ||
|
||||
$currentCellValue !== $dataRowWithNumericIndexes[$nextCellIndex]) {
|
||||
|
||||
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
|
||||
$data .= $this->getCellXML($currentCellValue, $styleIndex, $numTimesValueRepeated);
|
||||
|
||||
$currentCellIndex = $nextCellIndex;
|
||||
}
|
||||
|
||||
$nextCellIndex++;
|
||||
}
|
||||
|
||||
$data .= '</table:table-row>';
|
||||
|
||||
$wasWriteSuccessful = fwrite($this->sheetFilePointer, $data);
|
||||
if ($wasWriteSuccessful === false) {
|
||||
throw new IOException("Unable to write data in {$this->worksheetFilePath}");
|
||||
}
|
||||
|
||||
// only update the count if the write worked
|
||||
$this->lastWrittenRowIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell XML content, given its value.
|
||||
*
|
||||
* @param mixed $cellValue The value to be written
|
||||
* @param int $styleIndex Index of the used style
|
||||
* @param int $numTimesValueRepeated Number of times the value is consecutively repeated
|
||||
* @return string The cell XML content
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
||||
*/
|
||||
protected function getCellXML($cellValue, $styleIndex, $numTimesValueRepeated)
|
||||
{
|
||||
$data = '<table:table-cell table:style-name="ce' . $styleIndex . '"';
|
||||
|
||||
if ($numTimesValueRepeated !== 1) {
|
||||
$data .= ' table:number-columns-repeated="' . $numTimesValueRepeated . '"';
|
||||
}
|
||||
|
||||
if (CellHelper::isNonEmptyString($cellValue)) {
|
||||
$data .= ' office:value-type="string" calcext:value-type="string">';
|
||||
|
||||
$cellValueLines = explode("\n", $cellValue);
|
||||
foreach ($cellValueLines as $cellValueLine) {
|
||||
$data .= '<text:p>' . $this->stringsEscaper->escape($cellValueLine) . '</text:p>';
|
||||
}
|
||||
|
||||
$data .= '</table:table-cell>';
|
||||
} else if (CellHelper::isBoolean($cellValue)) {
|
||||
$data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cellValue . '">';
|
||||
$data .= '<text:p>' . $cellValue . '</text:p>';
|
||||
$data .= '</table:table-cell>';
|
||||
} else if (CellHelper::isNumeric($cellValue)) {
|
||||
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
|
||||
$data .= '<text:p>' . $cellValue . '</text:p>';
|
||||
$data .= '</table:table-cell>';
|
||||
} else if (empty($cellValue)) {
|
||||
$data .= '/>';
|
||||
} else {
|
||||
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the worksheet
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if (!is_resource($this->sheetFilePointer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fclose($this->sheetFilePointer);
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\ODS;
|
||||
|
||||
use Box\Spout\Writer\AbstractMultiSheetsWriter;
|
||||
use Box\Spout\Writer\Common;
|
||||
use Box\Spout\Writer\ODS\Internal\Workbook;
|
||||
|
||||
/**
|
||||
* Class Writer
|
||||
* This class provides base support to write data to ODS files
|
||||
*
|
||||
* @package Box\Spout\Writer\ODS
|
||||
*/
|
||||
class Writer extends AbstractMultiSheetsWriter
|
||||
{
|
||||
/** @var string Content-Type value for the header */
|
||||
protected static $headerContentType = 'application/vnd.oasis.opendocument.spreadsheet';
|
||||
|
||||
/** @var string Temporary folder where the files to create the ODS will be stored */
|
||||
protected $tempFolder;
|
||||
|
||||
/** @var Internal\Workbook The workbook for the ODS file */
|
||||
protected $book;
|
||||
|
||||
/**
|
||||
* Sets a custom temporary folder for creating intermediate files/folders.
|
||||
* This must be set before opening the writer.
|
||||
*
|
||||
* @api
|
||||
* @param string $tempFolder Temporary folder where the files to create the ODS will be stored
|
||||
* @return Writer
|
||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
||||
*/
|
||||
public function setTempFolder($tempFolder)
|
||||
{
|
||||
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
|
||||
|
||||
$this->tempFolder = $tempFolder;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the write and sets the current sheet pointer to a new sheet.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing
|
||||
*/
|
||||
protected function openWriter()
|
||||
{
|
||||
$tempFolder = ($this->tempFolder) ? : sys_get_temp_dir();
|
||||
$this->book = new Workbook($tempFolder, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle);
|
||||
$this->book->addNewSheetAndMakeItCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Internal\Workbook The workbook representing the file to be written
|
||||
*/
|
||||
protected function getWorkbook()
|
||||
{
|
||||
return $this->book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds data to the currently opened writer.
|
||||
* If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
|
||||
* with the creation of new worksheets if one worksheet has reached its maximum capicity.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||
*/
|
||||
protected function addRowToWriter(array $dataRow, $style)
|
||||
{
|
||||
$this->throwIfBookIsNotAvailable();
|
||||
$this->book->addRowToCurrentWorksheet($dataRow, $style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the writer, preventing any additional writing.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function closeWriter()
|
||||
{
|
||||
if ($this->book) {
|
||||
$this->book->close($this->filePointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\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.
|
||||
*/
|
||||
protected $parts = [];
|
||||
|
||||
/**
|
||||
* @param array|void $borderParts
|
||||
*/
|
||||
public function __construct(array $borderParts = [])
|
||||
{
|
||||
$this->setParts($borderParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name The name of the border part
|
||||
* @return null|BorderPart
|
||||
*/
|
||||
public function getPart($name)
|
||||
{
|
||||
return $this->hasPart($name) ? $this->parts[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name The name of the border part
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPart($name)
|
||||
{
|
||||
return isset($this->parts[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParts()
|
||||
{
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set BorderParts
|
||||
* @param array $parts
|
||||
*/
|
||||
public function setParts($parts)
|
||||
{
|
||||
unset($this->parts);
|
||||
foreach ($parts as $part) {
|
||||
$this->addPart($part);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BorderPart $borderPart
|
||||
* @return self
|
||||
*/
|
||||
public function addPart(BorderPart $borderPart)
|
||||
{
|
||||
$this->parts[$borderPart->getName()] = $borderPart;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Style;
|
||||
|
||||
/**
|
||||
* Class BorderBuilder
|
||||
*/
|
||||
class BorderBuilder
|
||||
{
|
||||
/**
|
||||
* @var Border
|
||||
*/
|
||||
protected $border;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->border = new Border();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|void $color Border A RGB color code
|
||||
* @param string|void $width Border width @see BorderPart::allowedWidths
|
||||
* @param string|void $style Border style @see BorderPart::allowedStyles
|
||||
* @return BorderBuilder
|
||||
*/
|
||||
public function setBorderTop($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
|
||||
{
|
||||
$this->border->addPart(new BorderPart(Border::TOP, $color, $width, $style));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|void $color Border A RGB color code
|
||||
* @param string|void $width Border width @see BorderPart::allowedWidths
|
||||
* @param string|void $style Border style @see BorderPart::allowedStyles
|
||||
* @return BorderBuilder
|
||||
*/
|
||||
public function setBorderRight($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
|
||||
{
|
||||
$this->border->addPart(new BorderPart(Border::RIGHT, $color, $width, $style));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|void $color Border A RGB color code
|
||||
* @param string|void $width Border width @see BorderPart::allowedWidths
|
||||
* @param string|void $style Border style @see BorderPart::allowedStyles
|
||||
* @return BorderBuilder
|
||||
*/
|
||||
public function setBorderBottom($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
|
||||
{
|
||||
$this->border->addPart(new BorderPart(Border::BOTTOM, $color, $width, $style));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|void $color Border A RGB color code
|
||||
* @param string|void $width Border width @see BorderPart::allowedWidths
|
||||
* @param string|void $style Border style @see BorderPart::allowedStyles
|
||||
* @return BorderBuilder
|
||||
*/
|
||||
public function setBorderLeft($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
|
||||
{
|
||||
$this->border->addPart(new BorderPart(Border::LEFT, $color, $width, $style));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Border
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->border;
|
||||
}
|
||||
}
|
||||
@ -1,184 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\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,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Writer\Style;
|
||||
|
||||
use Box\Spout\Writer\Exception\InvalidColorException;
|
||||
|
||||
/**
|
||||
* Class Color
|
||||
* This class provides constants and functions to work with colors
|
||||
*
|
||||
* @package Box\Spout\Writer\Style
|
||||
*/
|
||||
class Color
|
||||
{
|
||||
/** Standard colors - based on Office Online */
|
||||
const BLACK = '000000';
|
||||
const WHITE = 'FFFFFF';
|
||||
const RED = 'FF0000';
|
||||
const DARK_RED = 'C00000';
|
||||
const ORANGE = 'FFC000';
|
||||
const YELLOW = 'FFFF00';
|
||||
const LIGHT_GREEN = '92D040';
|
||||
const GREEN = '00B050';
|
||||
const LIGHT_BLUE = '00B0E0';
|
||||
const BLUE = '0070C0';
|
||||
const DARK_BLUE = '002060';
|
||||
const PURPLE = '7030A0';
|
||||
|
||||
/**
|
||||
* Returns an RGB color from R, G and B values
|
||||
*
|
||||
* @api
|
||||
* @param int $red Red component, 0 - 255
|
||||
* @param int $green Green component, 0 - 255
|
||||
* @param int $blue Blue component, 0 - 255
|
||||
* @return string RGB color
|
||||
*/
|
||||
public static function rgb($red, $green, $blue)
|
||||
{
|
||||
self::throwIfInvalidColorComponentValue($red);
|
||||
self::throwIfInvalidColorComponentValue($green);
|
||||
self::throwIfInvalidColorComponentValue($blue);
|
||||
|
||||
return strtoupper(
|
||||
self::convertColorComponentToHex($red) .
|
||||
self::convertColorComponentToHex($green) .
|
||||
self::convertColorComponentToHex($blue)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception is the color component value is outside of bounds (0 - 255)
|
||||
*
|
||||
* @param int $colorComponent
|
||||
* @return void
|
||||
* @throws \Box\Spout\Writer\Exception\InvalidColorException
|
||||
*/
|
||||
protected static function throwIfInvalidColorComponentValue($colorComponent)
|
||||
{
|
||||
if (!is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
|
||||
throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the color component to its corresponding hexadecimal value
|
||||
*
|
||||
* @param int $colorComponent Color component, 0 - 255
|
||||
* @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d"
|
||||
*/
|
||||
protected static function convertColorComponentToHex($colorComponent)
|
||||
{
|
||||
return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ARGB color of the given RGB color,
|
||||
* assuming that alpha value is always 1.
|
||||
*
|
||||
* @param string $rgbColor RGB color like "FF08B2"
|
||||
* @return string ARGB color
|
||||
*/
|
||||
public static function toARGB($rgbColor)
|
||||
{
|
||||
return 'FF' . $rgbColor;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user