Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582da8403d | ||
|
|
8b666fc6cd | ||
|
|
f8aab6eefd | ||
|
|
c4c6dddb20 | ||
|
|
6c97141679 | ||
|
|
9c8cc05364 | ||
|
|
2a9400dfca | ||
|
|
8ef6bdac62 | ||
|
|
46c246e6a4 | ||
|
|
8fd606ae4f | ||
|
|
6767386daf | ||
|
|
3395d3abb3 | ||
|
|
d1c4d563c1 | ||
|
|
01cc8b3da0 | ||
|
|
45980195cd | ||
|
|
a1a1077677 | ||
|
|
16d0290a17 | ||
|
|
f8c39287ad | ||
|
|
118810de22 | ||
|
|
3720c9ea1c | ||
|
|
6124528161 | ||
|
|
818ec2488c | ||
|
|
aa7978146f | ||
|
|
d6e707c5fe | ||
|
|
03ae367fb3 | ||
|
|
ac48dce334 | ||
|
|
e147c580ca | ||
|
|
7615acf398 | ||
|
|
1d38bb00c5 | ||
|
|
79d947a6b3 | ||
|
|
dd3cc5bf47 | ||
|
|
0a5be41c53 | ||
|
|
d2ba6d884c | ||
|
|
e4154dfdc3 | ||
|
|
3f0016f753 | ||
|
|
bc009a3241 | ||
|
|
0f8e7a8f58 | ||
|
|
156fd29a44 | ||
|
|
ef171910b9 | ||
|
|
5949cb2442 | ||
|
|
108a92e259 | ||
|
|
9fb1427944 | ||
|
|
a8f1fba854 | ||
|
|
1812b4f996 | ||
|
|
b4ace972e7 | ||
|
|
c428daff5a | ||
|
|
5908443583 | ||
|
|
050672755d | ||
|
|
e1928eb68a | ||
|
|
9467b5a810 | ||
|
|
2699ffbcae | ||
|
|
3559bc8834 | ||
|
|
506d682e74 | ||
|
|
7efab5576d | ||
|
|
366f121eb0 | ||
|
|
f8d0ac2682 | ||
|
|
82377403ff | ||
|
|
e0c84f77b1 | ||
|
|
b78227c370 | ||
|
|
444308d42c | ||
|
|
353d4e86a5 | ||
|
|
f043f8d4d0 | ||
|
|
c8ddcf5441 | ||
|
|
4a346cef0c | ||
|
|
1c8934790d | ||
|
|
2a1925bc51 | ||
|
|
2183ff6738 | ||
|
|
380ee524a5 | ||
|
|
21263a0730 | ||
|
|
0104714cbd | ||
|
|
611a091290 | ||
|
|
40a86c4b6c | ||
|
|
8a3b895afc | ||
|
|
e3f7ecfa64 | ||
|
|
93d7aafe8b | ||
|
|
dc53b6aa20 | ||
|
|
30aa8970d5 | ||
|
|
2ec12dd16b | ||
|
|
fd84c6f1c8 | ||
|
|
5e1cfbfdbd | ||
|
|
d946f12951 | ||
|
|
03d1917080 | ||
|
|
1ba10ed2b0 | ||
|
|
be3932af18 | ||
|
|
37d87a8a27 | ||
|
|
86a4c3790a | ||
|
|
2345a80784 | ||
|
|
15aab7902a | ||
|
|
c672558a18 | ||
|
|
c52dd7bde8 | ||
|
|
ae3ee357ff | ||
|
|
322c3d0738 | ||
|
|
0adbf439f5 | ||
|
|
ad6b881685 | ||
|
|
d410224e2d | ||
|
|
79982a6107 | ||
|
|
6ae79b63b3 | ||
|
|
277b665dad | ||
|
|
b0c7c6ca84 | ||
|
|
494c506d56 | ||
|
|
334f7087da | ||
|
|
2dcb86aae9 | ||
|
|
3edb056286 | ||
|
|
edc0c22009 | ||
|
|
0319e578cb | ||
|
|
1e2452934c | ||
|
|
3e1793d852 | ||
|
|
c1b1bd0b76 | ||
|
|
cf239905d7 | ||
|
|
7c8f9293cc | ||
|
|
503ba97e9d | ||
|
|
b3df57d2e5 | ||
|
|
c6ebf115fc | ||
|
|
7d922e6776 | ||
|
|
d4cf853270 | ||
|
|
8bac924d48 | ||
|
|
591d86cf07 | ||
|
|
ba4a55292d | ||
|
|
d617a50b85 | ||
|
|
92981fa80b | ||
|
|
766c733466 | ||
|
|
46cae76546 | ||
|
|
03be58b84d | ||
|
|
b21bb86682 | ||
|
|
8a45cec220 | ||
|
|
04d41d7c9f | ||
|
|
f8f194bc8f | ||
|
|
044ad11596 | ||
|
|
935ba1fb5e | ||
|
|
fb0175d633 | ||
|
|
69190cd855 | ||
|
|
13a53fb43f | ||
|
|
a848be52de | ||
|
|
52faef0168 | ||
|
|
3b4dfba38e | ||
|
|
cfd3e0ffa3 | ||
|
|
c6e943041e | ||
|
|
502dd54c81 | ||
|
|
c6cfc4c80f | ||
|
|
d02013c82e | ||
|
|
71d9c18a81 | ||
|
|
e9ec4e745c | ||
|
|
e58284d27b | ||
|
|
27a47f509f | ||
|
|
8aeb883ebc | ||
|
|
05c504e72c | ||
|
|
abbc158877 | ||
|
|
3f3461b002 | ||
|
|
2074781852 | ||
|
|
fd5cfc5440 | ||
|
|
a538813827 | ||
|
|
3e5ef284a5 | ||
|
|
f770f38967 | ||
|
|
0edd9c434f | ||
|
|
3c9bf12a5f | ||
|
|
69c1436625 | ||
|
|
af940069bc | ||
|
|
d6155a4243 |
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[{.travis.yml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/tests/resources/generated
|
/tests/resources/generated
|
||||||
|
/tests/coverage
|
||||||
/vendor
|
/vendor
|
||||||
composer.lock
|
|
||||||
/.idea
|
/.idea
|
||||||
*.iml
|
*.iml
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
excluded_dirs: [vendor, tests]
|
||||||
|
php_cpd:
|
||||||
|
enabled: true
|
||||||
|
excluded_dirs: [vendor, tests]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
+13
-3
@@ -4,9 +4,19 @@ php:
|
|||||||
- 5.4
|
- 5.4
|
||||||
- 5.5
|
- 5.5
|
||||||
- 5.6
|
- 5.6
|
||||||
|
- 7.0
|
||||||
|
- hhvm
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.composer/cache
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- composer self-update
|
- composer install --no-interaction --prefer-source
|
||||||
- composer install --prefer-source
|
|
||||||
|
|
||||||
script: phpunit
|
script:
|
||||||
|
- mkdir -p build/logs
|
||||||
|
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
|
||||||
|
|
||||||
|
after_script:
|
||||||
|
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover build/logs/clover.xml; fi
|
||||||
@@ -1,26 +1,39 @@
|
|||||||
# Spout
|
# Spout
|
||||||
|
|
||||||
Spout is a PHP library to read and write CSV and XLSX files, in a fast and scalable way.
|
[](https://packagist.org/packages/box/spout)
|
||||||
|
[](http://opensource.box.com/badges)
|
||||||
|
[](http://travis-ci.org/box/spout)
|
||||||
|
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
||||||
|
[](https://packagist.org/packages/box/spout)
|
||||||
|
[](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 10MB).
|
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
|
||||||
|
|
||||||
[](http://travis-ci.org/box/spout)
|
Join the community and come discuss about Spout: [](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
[](http://opensource.box.com/badges)
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The Spout library can be installed directly from [Composer](https://getcomposer.org/).
|
### Composer (recommended)
|
||||||
|
|
||||||
Add "box/spout" as a dependency in your project's composer.json file:
|
Spout can be installed directly from [Composer](https://getcomposer.org/).
|
||||||
```json
|
|
||||||
"require": {
|
Run the following command:
|
||||||
"box/spout": "*"
|
```
|
||||||
}
|
$ composer require box/spout
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run the install command from Composer:
|
### Manual installation
|
||||||
```
|
|
||||||
php composer.phar install
|
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!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -36,80 +49,58 @@ php composer.phar install
|
|||||||
|
|
||||||
### Reader
|
### Reader
|
||||||
|
|
||||||
#### How to read a CSV file?
|
Regardless of the file type, the interface to read a file is always the same:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Box\Spout\Reader\ReaderFactory;
|
use Box\Spout\Reader\ReaderFactory;
|
||||||
use Box\Spout\Common\Type;
|
use Box\Spout\Common\Type;
|
||||||
|
|
||||||
$reader = ReaderFactory::create(Type::CSV);
|
$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);
|
$reader->open($filePath);
|
||||||
|
|
||||||
while ($reader->hasNextRow()) {
|
foreach ($reader->getSheetIterator() as $sheet) {
|
||||||
$row = $reader->nextRow();
|
foreach ($sheet->getRowIterator() as $row) {
|
||||||
// do stuff
|
// do stuff with the row
|
||||||
}
|
|
||||||
|
|
||||||
$reader->close();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### How to read a XLSX file?
|
|
||||||
|
|
||||||
```php
|
|
||||||
use Box\Spout\Reader\ReaderFactory;
|
|
||||||
use Box\Spout\Common\Type;
|
|
||||||
|
|
||||||
$reader = ReaderFactory::create(Type::XLSX);
|
|
||||||
$reader->open($filePath);
|
|
||||||
|
|
||||||
while ($reader->hasNextSheet()) {
|
|
||||||
$reader->nextSheet();
|
|
||||||
|
|
||||||
while ($reader->hasNextRow()) {
|
|
||||||
$row = $reader->nextRow();
|
|
||||||
// do stuff
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$reader->close();
|
$reader->close();
|
||||||
```
|
```
|
||||||
|
|
||||||
If there are multiple sheets in the file, the reader will read through all of them sequentially.
|
If there are multiple sheets in the file, the reader will read all of them sequentially.
|
||||||
|
|
||||||
### Writer
|
### Writer
|
||||||
|
|
||||||
### How to create a CSV file?
|
As with the reader, there is one common interface to write data to a file:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Box\Spout\Writer\WriterFactory;
|
use Box\Spout\Writer\WriterFactory;
|
||||||
use Box\Spout\Common\Type;
|
use Box\Spout\Common\Type;
|
||||||
|
|
||||||
$writer = WriterFactory::create(Type::CSV);
|
$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->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->addRow($singleRow); // add a row at a time
|
||||||
$writer->close();
|
|
||||||
```
|
|
||||||
|
|
||||||
### How to create a XLSX file?
|
|
||||||
|
|
||||||
```php
|
|
||||||
use Box\Spout\Writer\WriterFactory;
|
|
||||||
use Box\Spout\Common\Type;
|
|
||||||
|
|
||||||
$writer = WriterFactory::create(Type::XLSX);
|
|
||||||
$writer->openToBrowser($fileName); // stream data directly to the browser
|
|
||||||
$writer->addRows($multipleRows); // add multiple rows at a time
|
$writer->addRows($multipleRows); // add multiple rows at a time
|
||||||
|
|
||||||
$writer->close();
|
$writer->close();
|
||||||
```
|
```
|
||||||
|
|
||||||
For XLSX files, the number of rows per sheet is limited to 1,048,576 (see [Office OpenXML specs](http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx)). By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
|
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
|
## Advanced usage
|
||||||
|
|
||||||
### Configuring the CSV reader and writer
|
### Configuring the CSV reader and writer
|
||||||
|
|
||||||
It is possible to configure the both the CSV reader and writer to specify the field separator as well as the field enclosure:
|
It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
|
||||||
```php
|
```php
|
||||||
use Box\Spout\Reader\ReaderFactory;
|
use Box\Spout\Reader\ReaderFactory;
|
||||||
use Box\Spout\Common\Type;
|
use Box\Spout\Common\Type;
|
||||||
@@ -119,9 +110,81 @@ $reader->setFieldDelimiter('|');
|
|||||||
$reader->setFieldEnclosure('@');
|
$reader->setFieldEnclosure('@');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuring the XLSX writer
|
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');
|
||||||
|
```
|
||||||
|
|
||||||
#### Strings storage
|
The writer always generate CSV files encoded in UTF-8, with a BOM.
|
||||||
|
|
||||||
|
|
||||||
|
### Configuring the XLSX and ODS writers
|
||||||
|
|
||||||
|
#### Row styling
|
||||||
|
|
||||||
|
It is possible to apply some formatting options to a row. Spout supports fonts as well as alignment styles.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Box\Spout\Common\Type;
|
||||||
|
use Box\Spout\Writer\WriterFactory;
|
||||||
|
use Box\Spout\Writer\Style\StyleBuilder;
|
||||||
|
use Box\Spout\Writer\Style\Color;
|
||||||
|
|
||||||
|
$style = (new StyleBuilder())
|
||||||
|
->setFontBold()
|
||||||
|
->setFontSize(15)
|
||||||
|
->setFontColor(Color::BLUE)
|
||||||
|
->setShouldWrapText()
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$writer = WriterFactory::create(Type::XLSX);
|
||||||
|
$writer->openToFile($filePath);
|
||||||
|
|
||||||
|
$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row
|
||||||
|
$writer->addRow($otherSingleRow); // no style will be applied
|
||||||
|
$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows
|
||||||
|
|
||||||
|
$writer->close();
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
|
||||||
|
|
||||||
|
Category | Property | API
|
||||||
|
----------|---------------|---------------------------------------
|
||||||
|
Font | Bold | `StyleBuilder::setFontBold()`
|
||||||
|
| Italic | `StyleBuilder::setFontItalic()`
|
||||||
|
| Underline | `StyleBuilder::setFontUnderline()`
|
||||||
|
| Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
||||||
|
| Font name | `StyleBuilder::setFontName('Arial')`
|
||||||
|
| Font size | `StyleBuilder::setFontSize(14)`
|
||||||
|
| Font color | `StyleBuilder::setFontSize(Color::BLUE)`<br>`StyleBuilder::setFontSize(Color::rgb(0, 128, 255))`
|
||||||
|
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
|
||||||
|
|
||||||
|
|
||||||
|
#### New sheet creation
|
||||||
|
|
||||||
|
It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
|
||||||
|
```php
|
||||||
|
use Box\Spout\Writer\WriterFactory;
|
||||||
|
use Box\Spout\Common\Type;
|
||||||
|
|
||||||
|
$writer = WriterFactory::create(Type::ODS);
|
||||||
|
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
|
||||||
|
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using custom temporary folder
|
||||||
|
|
||||||
|
Processing XLSX and ODS files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer:
|
||||||
|
```php
|
||||||
|
use Box\Spout\Writer\WriterFactory;
|
||||||
|
use Box\Spout\Common\Type;
|
||||||
|
|
||||||
|
$writer = WriterFactory::create(Type::XLSX);
|
||||||
|
$writer->setTempFolder($customTempFolderPath);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Strings storage (XLSX writer)
|
||||||
|
|
||||||
XLSX files support different ways to store the string values:
|
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)
|
* 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)
|
||||||
@@ -137,33 +200,14 @@ $writer->setShouldUseInlineStrings(true); // default (and recommended) value
|
|||||||
$writer->setShouldUseInlineStrings(false); // will use shared strings
|
$writer->setShouldUseInlineStrings(false); // will use shared strings
|
||||||
```
|
```
|
||||||
|
|
||||||
#### New sheet creation
|
> ##### 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!
|
||||||
|
|
||||||
It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
|
|
||||||
```php
|
|
||||||
use Box\Spout\Writer\WriterFactory;
|
|
||||||
use Box\Spout\Common\Type;
|
|
||||||
|
|
||||||
$writer = WriterFactory::create(Type::XLSX);
|
### Playing with sheets
|
||||||
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
|
|
||||||
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using custom temporary folder
|
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:
|
||||||
|
|
||||||
Processing XLSX files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by sys_get_temp_dir()). It is possible to override this by explicitly setting it on the reader or writer:
|
|
||||||
```php
|
|
||||||
use Box\Spout\Writer\WriterFactory;
|
|
||||||
use Box\Spout\Common\Type;
|
|
||||||
|
|
||||||
$writer = WriterFactory::create(Type::XLSX);
|
|
||||||
$writer->setTempFolder($customTempFolderPath);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Playing with XLSX sheets
|
|
||||||
|
|
||||||
When creating a XLSX file, it is possible to control in which sheet the data will be written to.
|
|
||||||
At any point, you can retrieve the current sheet and set the current sheet:
|
|
||||||
```php
|
```php
|
||||||
$firstSheet = $writer->getCurrentSheet();
|
$firstSheet = $writer->getCurrentSheet();
|
||||||
$writer->addRow($rowForSheet1); // writes the row to the first sheet
|
$writer->addRow($rowForSheet1); // writes the row to the first sheet
|
||||||
@@ -180,6 +224,32 @@ It is also possible to retrieve all the sheets currently created:
|
|||||||
$sheets = $writer->getSheets();
|
$sheets = $writer->getSheets();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you rely on the sheet's name in your application, you can access it and customize it this way:
|
||||||
|
```php
|
||||||
|
// Accessing the sheet name when reading
|
||||||
|
foreach ($reader->getSheetIterator() as $sheet) {
|
||||||
|
$sheetName = $sheet->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessing the sheet name when writing
|
||||||
|
$sheet = $writer->getCurrentSheet();
|
||||||
|
$sheetName = $sheet->getName();
|
||||||
|
|
||||||
|
// Customizing the sheet name when writing
|
||||||
|
$sheet = $writer->getCurrentSheet();
|
||||||
|
$sheet->setName('My custom name');
|
||||||
|
```
|
||||||
|
|
||||||
|
> Please note that Excel has some restrictions on the sheet's name:
|
||||||
|
> * it must not be blank
|
||||||
|
> * it must not exceed 31 characters
|
||||||
|
> * it must not contain these characters: \ / ? * : [ or ]
|
||||||
|
> * it must not start or end with a single quote
|
||||||
|
> * it must be unique
|
||||||
|
>
|
||||||
|
> Handling these restrictions is the developer's responsibility. Spout does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in.
|
||||||
|
|
||||||
|
|
||||||
### Fluent interface
|
### Fluent interface
|
||||||
|
|
||||||
Because fluent interfaces are great, you can use them with Spout:
|
Because fluent interfaces are great, you can use them with Spout:
|
||||||
@@ -199,8 +269,8 @@ $writer->setTempFolder($customTempFolderPath)
|
|||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
On the `master` branch, only unit and functional tests are included. The performance requires very large files and have been excluded.
|
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 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:
|
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:
|
||||||
|
|
||||||
@@ -217,25 +287,34 @@ For information, the performance tests take about one hour to run (processing 2
|
|||||||
|
|
||||||
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.
|
When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
|
||||||
|
|
||||||
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them into several small temporary files that allows fast access.
|
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?
|
#### How long does it take to generate a file with X rows?
|
||||||
|
|
||||||
Here are a few numbers regarding the performance of Spout:
|
Here are a few numbers regarding the performance of Spout:
|
||||||
|
|
||||||
| | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
|
| Type | Action | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
|
||||||
| :------------------------------- | :----------------------: | :--------------------------: | :------------------------------: |
|
|------|-------------------------------|--------------------------|------------------------------|----------------------------------|
|
||||||
| Read CSV | < 1 second | 4 seconds | 2-3 minutes |
|
| CSV | Read | < 1 second | 4 seconds | 2-3 minutes |
|
||||||
| Write CSV | < 1 second | 2 seconds | 2-3 minutes |
|
| | Write | < 1 second | 2 seconds | 2-3 minutes |
|
||||||
| Read XLSX (using inline strings) | < 1 second | 35-40 seconds | 18-20 minutes |
|
| XLSX | Read<br>*inline strings* | < 1 second | 35-40 seconds | 18-20 minutes |
|
||||||
| Read XLSX (using shared strings) | 1 second | 1-2 minutes | 35-40 minutes |
|
| | Read<br>*shared strings* | 1 second | 1-2 minutes | 35-40 minutes |
|
||||||
| Write XLSX | 1 second | 20-25 seconds | 8-10 minutes |
|
| | Write | 1 second | 20-25 seconds | 8-10 minutes |
|
||||||
|
| ODS | Read | 1 second | 1-2 minutes | 5-6 minutes |
|
||||||
|
| | Write | < 1 second | 35-40 seconds | 5-6 minutes |
|
||||||
|
|
||||||
|
#### Does Spout support charts or formulas?
|
||||||
|
|
||||||
|
No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used.
|
||||||
|
So the larger the file would be, the more memory would be consumed, preventing your code to scale well.
|
||||||
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject.
|
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 and License
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "box/spout",
|
"name": "box/spout",
|
||||||
"description": "PHP Library to read and write CSV and XLSX files, in a fast and scalable way",
|
"description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"version": "1.0.2",
|
"keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"],
|
||||||
"keywords": ["php","read","write","csv","xlsx","excel","spreadsheet","scale","memory","stream","ooxml"],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"homepage": "https://www.github.com/box/spout",
|
"homepage": "https://www.github.com/box/spout",
|
||||||
"authors": [
|
"authors": [
|
||||||
@@ -19,7 +18,12 @@
|
|||||||
"ext-simplexml": "*"
|
"ext-simplexml": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": ">=3.7"
|
"phpunit/phpunit": ">=3.7",
|
||||||
|
"scrutinizer/ocular": "~1.1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
|
||||||
|
"ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
Generated
+1647
File diff suppressed because it is too large.
Load diff
+9
-1
@@ -5,7 +5,6 @@
|
|||||||
colors="true"
|
colors="true"
|
||||||
convertErrorsToExceptions="false"
|
convertErrorsToExceptions="false"
|
||||||
convertWarningsToExceptions="false"
|
convertWarningsToExceptions="false"
|
||||||
strict="false"
|
|
||||||
verbose="false">
|
verbose="false">
|
||||||
|
|
||||||
<testsuites>
|
<testsuites>
|
||||||
@@ -14,4 +13,13 @@
|
|||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory suffix=".php">src/</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>src/Spout/Autoloader</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?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);
|
||||||
@@ -13,6 +13,8 @@ class CSV implements EscaperInterface
|
|||||||
/**
|
/**
|
||||||
* Escapes the given string to make it compatible with CSV
|
* Escapes the given string to make it compatible with CSV
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*
|
||||||
* @param string $string The string to escape
|
* @param string $string The string to escape
|
||||||
* @return string The escaped string
|
* @return string The escaped string
|
||||||
*/
|
*/
|
||||||
@@ -24,6 +26,8 @@ class CSV implements EscaperInterface
|
|||||||
/**
|
/**
|
||||||
* Unescapes the given string to make it compatible with CSV
|
* Unescapes the given string to make it compatible with CSV
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*
|
||||||
* @param string $string The string to unescape
|
* @param string $string The string to unescape
|
||||||
* @return string The unescaped string
|
* @return string The unescaped string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Common\Escaper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ODS
|
||||||
|
* Provides functions to escape and unescape data for ODS files
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Common\Escaper
|
||||||
|
*/
|
||||||
|
class ODS implements EscaperInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Escapes the given string to make it compatible with XLSX
|
||||||
|
*
|
||||||
|
* @param string $string The string to escape
|
||||||
|
* @return string The escaped string
|
||||||
|
*/
|
||||||
|
public function escape($string)
|
||||||
|
{
|
||||||
|
return htmlspecialchars($string, ENT_QUOTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescapes the given string to make it compatible with XLSX
|
||||||
|
*
|
||||||
|
* @param string $string The string to unescape
|
||||||
|
* @return string The unescaped string
|
||||||
|
*/
|
||||||
|
public function unescape($string)
|
||||||
|
{
|
||||||
|
return htmlspecialchars_decode($string, ENT_QUOTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ class XLSX implements EscaperInterface
|
|||||||
/** @var string[] Control characters to be escaped */
|
/** @var string[] Control characters to be escaped */
|
||||||
protected $controlCharactersEscapingMap;
|
protected $controlCharactersEscapingMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
|
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Common\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class BadUsageException
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Common\Exception
|
|
||||||
*/
|
|
||||||
class BadUsageException extends SpoutException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Common\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class EncodingConversionException
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @package Box\Spout\Common\Exception
|
||||||
|
*/
|
||||||
|
class EncodingConversionException extends SpoutException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class IOException
|
* Class IOException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
*/
|
*/
|
||||||
class IOException extends SpoutException
|
class IOException extends SpoutException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class InvalidArgumentException
|
* Class InvalidArgumentException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
*/
|
*/
|
||||||
class InvalidArgumentException extends SpoutException
|
class InvalidArgumentException extends SpoutException
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
* Class SpoutException
|
* Class SpoutException
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
|
* @abstract
|
||||||
*/
|
*/
|
||||||
abstract class SpoutException extends \Exception
|
abstract class SpoutException extends \Exception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class UnsupportedTypeException
|
* Class UnsupportedTypeException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
*/
|
*/
|
||||||
class UnsupportedTypeException extends SpoutException
|
class UnsupportedTypeException extends SpoutException
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,11 +36,11 @@ class FileSystemHelper
|
|||||||
{
|
{
|
||||||
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
||||||
|
|
||||||
$folderPath = $parentFolderPath . DIRECTORY_SEPARATOR . $folderName;
|
$folderPath = $parentFolderPath . '/' . $folderName;
|
||||||
|
|
||||||
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
|
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
|
||||||
if (!$wasCreationSuccessful) {
|
if (!$wasCreationSuccessful) {
|
||||||
throw new IOException('Unable to create folder: ' . $folderPath);
|
throw new IOException("Unable to create folder: $folderPath");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $folderPath;
|
return $folderPath;
|
||||||
@@ -60,11 +60,11 @@ class FileSystemHelper
|
|||||||
{
|
{
|
||||||
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
||||||
|
|
||||||
$filePath = $parentFolderPath . DIRECTORY_SEPARATOR . $fileName;
|
$filePath = $parentFolderPath . '/' . $fileName;
|
||||||
|
|
||||||
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
|
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
|
||||||
if (!$wasCreationSuccessful) {
|
if ($wasCreationSuccessful === false) {
|
||||||
throw new IOException('Unable to create file: ' . $filePath);
|
throw new IOException("Unable to create file: $filePath");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $filePath;
|
return $filePath;
|
||||||
@@ -126,7 +126,7 @@ class FileSystemHelper
|
|||||||
{
|
{
|
||||||
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0);
|
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0);
|
||||||
if (!$isInBaseFolder) {
|
if (!$isInBaseFolder) {
|
||||||
throw new IOException('Cannot perform I/O operation outside of the base folder: ' . $this->baseFolderPath);
|
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,8 @@ namespace Box\Spout\Common\Helper;
|
|||||||
* Class GlobalFunctionsHelper
|
* Class GlobalFunctionsHelper
|
||||||
* This class wraps global functions to facilitate testing
|
* This class wraps global functions to facilitate testing
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*
|
||||||
* @package Box\Spout\Common\Helper
|
* @package Box\Spout\Common\Helper
|
||||||
*/
|
*/
|
||||||
class GlobalFunctionsHelper
|
class GlobalFunctionsHelper
|
||||||
@@ -89,6 +91,34 @@ class GlobalFunctionsHelper
|
|||||||
return fgetcsv($handle, $length, $delimiter, $enclosure);
|
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()
|
* Wrapper around global function fclose()
|
||||||
* @see fclose()
|
* @see fclose()
|
||||||
@@ -117,7 +147,7 @@ class GlobalFunctionsHelper
|
|||||||
* Wrapper around global function file_exists()
|
* Wrapper around global function file_exists()
|
||||||
* @see file_exists()
|
* @see file_exists()
|
||||||
*
|
*
|
||||||
* @param string $filename
|
* @param string $fileName
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function file_exists($fileName)
|
public function file_exists($fileName)
|
||||||
@@ -125,11 +155,71 @@ class GlobalFunctionsHelper
|
|||||||
return file_exists($fileName);
|
return file_exists($fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around global function file_get_contents()
|
||||||
|
* @see file_get_contents()
|
||||||
|
*
|
||||||
|
* @param string $filePath
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function file_get_contents($filePath)
|
||||||
|
{
|
||||||
|
$realFilePath = $this->convertToUseRealPath($filePath);
|
||||||
|
return file_get_contents($realFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the given file path to use a real path.
|
||||||
|
* This is to avoid issues on some Windows setup.
|
||||||
|
*
|
||||||
|
* @param string $filePath File path
|
||||||
|
* @return string The file path using a real path
|
||||||
|
*/
|
||||||
|
protected function convertToUseRealPath($filePath)
|
||||||
|
{
|
||||||
|
$realFilePath = $filePath;
|
||||||
|
|
||||||
|
if ($this->isZipStream($filePath)) {
|
||||||
|
if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
|
||||||
|
$documentPath = $matches[1];
|
||||||
|
$documentInsideZipPath = $matches[2];
|
||||||
|
$realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$realFilePath = realpath($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $realFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given path is a zip stream.
|
||||||
|
*
|
||||||
|
* @param string $path Path pointing to a document
|
||||||
|
* @return bool TRUE if path is a zip stream, FALSE otherwise
|
||||||
|
*/
|
||||||
|
protected function isZipStream($path)
|
||||||
|
{
|
||||||
|
return (strpos($path, 'zip://') === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around global function feof()
|
||||||
|
* @see feof()
|
||||||
|
*
|
||||||
|
* @param resource
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function feof($handle)
|
||||||
|
{
|
||||||
|
return feof($handle);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around global function is_readable()
|
* Wrapper around global function is_readable()
|
||||||
* @see is_readable()
|
* @see is_readable()
|
||||||
*
|
*
|
||||||
* @param string $filename
|
* @param string $fileName
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function is_readable($fileName)
|
public function is_readable($fileName)
|
||||||
@@ -142,11 +232,12 @@ class GlobalFunctionsHelper
|
|||||||
* @see basename()
|
* @see basename()
|
||||||
*
|
*
|
||||||
* @param string $path
|
* @param string $path
|
||||||
|
* @param string|void $suffix
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function basename($path)
|
public function basename($path, $suffix = null)
|
||||||
{
|
{
|
||||||
return basename($path);
|
return basename($path, $suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,4 +251,73 @@ class GlobalFunctionsHelper
|
|||||||
{
|
{
|
||||||
header($string);
|
header($string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_line()
|
||||||
|
* @see stream_get_line()
|
||||||
|
*
|
||||||
|
* @param resource $handle
|
||||||
|
* @param int $length
|
||||||
|
* @param string|void $ending
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
public function stream_get_line($handle, $length, $ending = null)
|
||||||
|
{
|
||||||
|
return stream_get_line($handle, $length, $ending);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around global function str_getcsv()
|
||||||
|
* @see str_getcsv()
|
||||||
|
*
|
||||||
|
* @param string $input
|
||||||
|
* @param string|void $delimiter
|
||||||
|
* @param string|void $enclosure
|
||||||
|
* @param string|void $escape
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function str_getcsv($input, $delimiter = null, $enclosure = null, $escape = null)
|
||||||
|
{
|
||||||
|
return str_getcsv($input, $delimiter, $enclosure, $escape);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around global function function_exists()
|
||||||
|
* @see function_exists()
|
||||||
|
*
|
||||||
|
* @param string $functionName
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function function_exists($functionName)
|
||||||
|
{
|
||||||
|
return function_exists($functionName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,12 @@ namespace Box\Spout\Common;
|
|||||||
/**
|
/**
|
||||||
* Class Type
|
* Class Type
|
||||||
* This class references the supported types
|
* This class references the supported types
|
||||||
|
*
|
||||||
|
* @api
|
||||||
*/
|
*/
|
||||||
abstract class Type
|
abstract class Type
|
||||||
{
|
{
|
||||||
const CSV = "csv";
|
const CSV = 'csv';
|
||||||
const XLSX = "xlsx";
|
const XLSX = 'xlsx';
|
||||||
|
const ODS = 'ods';
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ namespace Box\Spout\Reader;
|
|||||||
|
|
||||||
use Box\Spout\Common\Exception\IOException;
|
use Box\Spout\Common\Exception\IOException;
|
||||||
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
|
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
|
||||||
use Box\Spout\Reader\Exception\EndOfFileReachedException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AbstractReader
|
* Class AbstractReader
|
||||||
@@ -14,18 +13,9 @@ use Box\Spout\Reader\Exception\EndOfFileReachedException;
|
|||||||
*/
|
*/
|
||||||
abstract class AbstractReader implements ReaderInterface
|
abstract class AbstractReader implements ReaderInterface
|
||||||
{
|
{
|
||||||
/** @var int Used to keep track of the row number */
|
|
||||||
protected $currentRowIndex = 0;
|
|
||||||
|
|
||||||
/** @var bool Indicates whether the stream is currently open */
|
/** @var bool Indicates whether the stream is currently open */
|
||||||
protected $isStreamOpened = false;
|
protected $isStreamOpened = false;
|
||||||
|
|
||||||
/** @var bool Indicates whether all rows have been read */
|
|
||||||
protected $hasReachedEndOfFile = false;
|
|
||||||
|
|
||||||
/** @var array Buffer used to store the row data, while checking if there are more rows to read */
|
|
||||||
protected $rowDataBuffer = null;
|
|
||||||
|
|
||||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||||
protected $globalFunctionsHelper;
|
protected $globalFunctionsHelper;
|
||||||
|
|
||||||
@@ -38,11 +28,11 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
abstract protected function openReader($filePath);
|
abstract protected function openReader($filePath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads and returns next row if available.
|
* Returns an iterator to iterate over sheets.
|
||||||
*
|
*
|
||||||
* @return array|null Array that contains the data for the read row or null at the end of the file
|
* @return \Iterator To iterate over sheets
|
||||||
*/
|
*/
|
||||||
abstract protected function read();
|
abstract public function getConcreteSheetIterator();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the reader. To be used after reading the file.
|
* Closes the reader. To be used after reading the file.
|
||||||
@@ -65,6 +55,7 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
* Prepares the reader to read the given file. It also makes sure
|
* Prepares the reader to read the given file. It also makes sure
|
||||||
* that the file exists and is readable.
|
* that the file exists and is readable.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $filePath Path of the file to be read
|
* @param string $filePath Path of the file to be read
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
||||||
@@ -74,20 +65,19 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
if (!$this->isPhpStream($filePath)) {
|
if (!$this->isPhpStream($filePath)) {
|
||||||
// we skip the checks if the provided file path points to a PHP stream
|
// we skip the checks if the provided file path points to a PHP stream
|
||||||
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
||||||
throw new IOException('Could not open ' . $filePath . ' for reading! File does not exist.');
|
throw new IOException("Could not open $filePath for reading! File does not exist.");
|
||||||
} else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
|
} else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
|
||||||
throw new IOException('Could not open ' . $filePath . ' for reading! File is not readable.');
|
throw new IOException("Could not open $filePath for reading! File is not readable.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->currentRowIndex = 0;
|
|
||||||
$this->hasReachedEndOfFile = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->openReader($filePath);
|
// Need to use realpath to fix "Can't open file" on some Windows setup
|
||||||
|
$fileRealPath = realpath($filePath);
|
||||||
|
$this->openReader($fileRealPath);
|
||||||
$this->isStreamOpened = true;
|
$this->isStreamOpened = true;
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
throw new IOException('Could not open ' . $filePath . ' for reading! (' . $exception->getMessage() . ')');
|
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,93 +93,37 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether all rows have been read (i.e. if we are at the end of the file).
|
* Returns an iterator to iterate over sheets.
|
||||||
* To know if the end of file has been reached, it uses a buffer. If the buffer is
|
|
||||||
* empty (meaning, nothing has been read or previous read line has been consumed), then
|
|
||||||
* it reads the next line, store it in the buffer for the next time or flip a variable if
|
|
||||||
* the end of file has been reached.
|
|
||||||
*
|
*
|
||||||
* @return bool Whether all rows have been read (i.e. if we are at the end of the file)
|
* @api
|
||||||
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If the stream was not opened first
|
* @return \Iterator To iterate over sheets
|
||||||
|
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
|
||||||
*/
|
*/
|
||||||
public function hasNextRow()
|
public function getSheetIterator()
|
||||||
{
|
{
|
||||||
if (!$this->isStreamOpened) {
|
if (!$this->isStreamOpened) {
|
||||||
throw new ReaderNotOpenedException('Stream should be opened first.');
|
throw new ReaderNotOpenedException('Reader should be opened first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasReachedEndOfFile) {
|
return $this->getConcreteSheetIterator();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the buffer contains unprocessed row
|
|
||||||
if (!$this->isRowDataBufferEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, try to read the next line line, and store it in the buffer
|
|
||||||
$this->rowDataBuffer = $this->read();
|
|
||||||
|
|
||||||
// if the buffer is still empty after reading a row, it means end of file was reached
|
|
||||||
$this->hasReachedEndOfFile = $this->isRowDataBufferEmpty();
|
|
||||||
|
|
||||||
return (!$this->hasReachedEndOfFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
|
|
||||||
* actually reading the file.
|
|
||||||
*
|
|
||||||
* @return array Array that contains the data for the read row
|
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
|
|
||||||
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
|
|
||||||
*/
|
|
||||||
public function nextRow()
|
|
||||||
{
|
|
||||||
if (!$this->hasNextRow()) {
|
|
||||||
throw new EndOfFileReachedException('End of file was reached. Cannot read more rows.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get data from buffer (if the buffer was empty, it was filled by the call to hasNextRow())
|
|
||||||
$rowData = $this->rowDataBuffer;
|
|
||||||
|
|
||||||
// empty buffer to mark the row as consumed
|
|
||||||
$this->emptyRowDataBuffer();
|
|
||||||
|
|
||||||
$this->currentRowIndex++;
|
|
||||||
|
|
||||||
return $rowData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the buffer where the row data is stored is empty
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
protected function isRowDataBufferEmpty()
|
|
||||||
{
|
|
||||||
return ($this->rowDataBuffer === null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty the buffer that stores row data
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function emptyRowDataBuffer()
|
|
||||||
{
|
|
||||||
$this->rowDataBuffer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the reader, preventing any additional reading
|
* Closes the reader, preventing any additional reading
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
if ($this->isStreamOpened) {
|
if ($this->isStreamOpened) {
|
||||||
$this->closeReader();
|
$this->closeReader();
|
||||||
|
|
||||||
|
$sheetIterator = $this->getConcreteSheetIterator();
|
||||||
|
if ($sheetIterator) {
|
||||||
|
$sheetIterator->end();
|
||||||
|
}
|
||||||
|
|
||||||
$this->isStreamOpened = false;
|
$this->isStreamOpened = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Reader;
|
|
||||||
|
|
||||||
use Box\Spout\Common\Exception\IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class CSV
|
|
||||||
* This class provides support to read data from a CSV file.
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Reader
|
|
||||||
*/
|
|
||||||
class CSV extends AbstractReader
|
|
||||||
{
|
|
||||||
const UTF8_BOM = "\xEF\xBB\xBF";
|
|
||||||
|
|
||||||
/** @var resource Pointer to the file to be written */
|
|
||||||
protected $filePointer;
|
|
||||||
|
|
||||||
/** @var string Defines the character used to delimit fields (one character only) */
|
|
||||||
protected $fieldDelimiter = ',';
|
|
||||||
|
|
||||||
/** @var string Defines the character used to enclose fields (one character only) */
|
|
||||||
protected $fieldEnclosure = '"';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the field delimiter for the CSV
|
|
||||||
*
|
|
||||||
* @param string $fieldDelimiter Character that delimits fields
|
|
||||||
* @return CSV
|
|
||||||
*/
|
|
||||||
public function setFieldDelimiter($fieldDelimiter)
|
|
||||||
{
|
|
||||||
$this->fieldDelimiter = $fieldDelimiter;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the field enclosure for the CSV
|
|
||||||
*
|
|
||||||
* @param string $fieldEnclosure Character that enclose fields
|
|
||||||
* @return CSV
|
|
||||||
*/
|
|
||||||
public function setFieldEnclosure($fieldEnclosure)
|
|
||||||
{
|
|
||||||
$this->fieldEnclosure = $fieldEnclosure;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the file at the given path to make it ready to be read.
|
|
||||||
* The file must be UTF-8 encoded.
|
|
||||||
* @TODO add encoding detection/conversion
|
|
||||||
*
|
|
||||||
* @param string $filePath Path of the CSV file to be read
|
|
||||||
* @return void
|
|
||||||
* @throws \Box\Spout\Common\Exception\IOException
|
|
||||||
*/
|
|
||||||
protected function openReader($filePath)
|
|
||||||
{
|
|
||||||
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
|
|
||||||
if (!$this->filePointer) {
|
|
||||||
throw new IOException('Could not open file ' . $filePath . ' for reading.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->skipUtf8Bom();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This skips the UTF-8 BOM if inserted at the beginning of the file
|
|
||||||
* by moving the file pointer after it, so that it is not read.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function skipUtf8Bom()
|
|
||||||
{
|
|
||||||
$this->globalFunctionsHelper->rewind($this->filePointer);
|
|
||||||
|
|
||||||
$hasUtf8Bom = ($this->globalFunctionsHelper->fgets($this->filePointer, 4) === self::UTF8_BOM);
|
|
||||||
|
|
||||||
if ($hasUtf8Bom) {
|
|
||||||
// we skip the 2 first bytes (so start from the 3rd byte)
|
|
||||||
$this->globalFunctionsHelper->fseek($this->filePointer, 3);
|
|
||||||
} else {
|
|
||||||
// if no BOM, reset the pointer to read from the beginning
|
|
||||||
$this->globalFunctionsHelper->fseek($this->filePointer, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads and returns next row if available.
|
|
||||||
* Empty rows are skipped.
|
|
||||||
*
|
|
||||||
* @return array|null Array that contains the data for the read row or null at the end of the file
|
|
||||||
*/
|
|
||||||
protected function read()
|
|
||||||
{
|
|
||||||
$lineData = null;
|
|
||||||
|
|
||||||
if ($this->filePointer) {
|
|
||||||
do {
|
|
||||||
$lineData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, 0, $this->fieldDelimiter, $this->fieldEnclosure);
|
|
||||||
} while ($lineData && $this->isEmptyLine($lineData));
|
|
||||||
}
|
|
||||||
|
|
||||||
// When reaching the end of the file, return null instead of false
|
|
||||||
return ($lineData !== false) ? $lineData : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $lineData Array containing the cells value for the line
|
|
||||||
* @return bool Whether the given line is empty
|
|
||||||
*/
|
|
||||||
protected function isEmptyLine($lineData)
|
|
||||||
{
|
|
||||||
return (count($lineData) === 1 && $lineData[0] === null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the reader. To be used after reading the file.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function closeReader()
|
|
||||||
{
|
|
||||||
if ($this->filePointer) {
|
|
||||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\CSV;
|
||||||
|
|
||||||
|
use Box\Spout\Reader\AbstractReader;
|
||||||
|
use Box\Spout\Common\Exception\IOException;
|
||||||
|
use Box\Spout\Common\Helper\EncodingHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->fieldDelimiter = $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->fieldEnclosure = $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->encoding = $encoding;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->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->fieldDelimiter,
|
||||||
|
$this->fieldEnclosure,
|
||||||
|
$this->encoding,
|
||||||
|
$this->globalFunctionsHelper
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator to iterate over sheets.
|
||||||
|
*
|
||||||
|
* @return SheetIterator To iterate over sheets
|
||||||
|
*/
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If no value is given to stream_get_line(), it defaults to 8192 (which may be too low).
|
||||||
|
* Alignement with other functions like fgets() is discussed here: https://bugs.php.net/bug.php?id=48421
|
||||||
|
*/
|
||||||
|
const MAX_READ_BYTES_PER_LINE = 32768;
|
||||||
|
|
||||||
|
/** @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 \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 string $fieldDelimiter Character that delimits fields
|
||||||
|
* @param string $fieldEnclosure Character that enclose fields
|
||||||
|
* @param string $encoding Encoding of the CSV file to be read
|
||||||
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
|
*/
|
||||||
|
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||||
|
{
|
||||||
|
$this->filePointer = $filePointer;
|
||||||
|
$this->fieldDelimiter = $fieldDelimiter;
|
||||||
|
$this->fieldEnclosure = $fieldEnclosure;
|
||||||
|
$this->encoding = $encoding;
|
||||||
|
$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 boolean
|
||||||
|
*/
|
||||||
|
public function valid()
|
||||||
|
{
|
||||||
|
return ($this->filePointer && !$this->hasReachedEndOfFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move forward to next element. Empty rows are skipped.
|
||||||
|
* @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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
$lineData = false;
|
||||||
|
$utf8EncodedLineData = $this->getNextUTF8EncodedLine();
|
||||||
|
if ($utf8EncodedLineData !== false) {
|
||||||
|
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||||
|
}
|
||||||
|
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||||
|
} while (($lineData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($lineData));
|
||||||
|
|
||||||
|
if ($lineData !== false) {
|
||||||
|
$this->rowDataBuffer = $lineData;
|
||||||
|
$this->numReadRows++;
|
||||||
|
} else {
|
||||||
|
// If we reach this point, it means end of file was reached.
|
||||||
|
// This happens when the last lines are empty lines.
|
||||||
|
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next line, converted if necessary to UTF-8.
|
||||||
|
* Neither fgets nor fgetcsv don't work with non UTF-8 data... so we need to do some things manually.
|
||||||
|
*
|
||||||
|
* @return string|false The next line for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
|
||||||
|
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||||
|
*/
|
||||||
|
protected function getNextUTF8EncodedLine()
|
||||||
|
{
|
||||||
|
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
|
||||||
|
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
|
||||||
|
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $encodedEOLDelimiter);
|
||||||
|
|
||||||
|
// If the line could have been read, it can be converted to UTF-8
|
||||||
|
$utf8EncodedLineData = ($encodedLineData !== false) ?
|
||||||
|
$this->encodingHelper->attemptConversionToUTF8($encodedLineData, $this->encoding) :
|
||||||
|
false;
|
||||||
|
|
||||||
|
return $utf8EncodedLineData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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("\n", $this->encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->encodedEOLDelimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $lineData Array containing the cells value for the line
|
||||||
|
* @return bool Whether the given line is empty
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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 string $fieldDelimiter Character that delimits fields
|
||||||
|
* @param string $fieldEnclosure Character that enclose fields
|
||||||
|
* @param string $encoding Encoding of the CSV file to be read
|
||||||
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
|
*/
|
||||||
|
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||||
|
{
|
||||||
|
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return \Box\Spout\Reader\CSV\RowIterator
|
||||||
|
*/
|
||||||
|
public function getRowIterator()
|
||||||
|
{
|
||||||
|
return $this->rowIterator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?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 string $fieldDelimiter Character that delimits fields
|
||||||
|
* @param string $fieldEnclosure Character that enclose fields
|
||||||
|
* @param string $encoding Encoding of the CSV file to be read
|
||||||
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
|
*/
|
||||||
|
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||||
|
{
|
||||||
|
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $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 boolean
|
||||||
|
*/
|
||||||
|
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,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Reader\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class EndOfWorksheetsReachedException
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Reader\Exception
|
|
||||||
*/
|
|
||||||
class EndOfWorksheetsReachedException extends ReaderException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class IteratorNotRewindableException
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @package Box\Spout\Reader\Exception
|
||||||
|
*/
|
||||||
|
class IteratorNotRewindableException extends ReaderException
|
||||||
|
{
|
||||||
|
}
|
||||||
+3
-2
@@ -3,10 +3,11 @@
|
|||||||
namespace Box\Spout\Reader\Exception;
|
namespace Box\Spout\Reader\Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class NoWorksheetsFoundException
|
* Class NoSheetsFoundException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class NoWorksheetsFoundException extends ReaderException
|
class NoSheetsFoundException extends ReaderException
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Reader\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class ReaderNotOpenedException
|
* Class ReaderNotOpenedException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class ReaderNotOpenedException extends ReaderException
|
class ReaderNotOpenedException extends ReaderException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Reader\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class SharedStringNotFoundException
|
* Class SharedStringNotFoundException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class SharedStringNotFoundException extends ReaderException
|
class SharedStringNotFoundException extends ReaderException
|
||||||
|
|||||||
+2
-2
@@ -3,10 +3,10 @@
|
|||||||
namespace Box\Spout\Reader\Exception;
|
namespace Box\Spout\Reader\Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class EndOfFileReachedException
|
* Class XMLProcessingException
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class EndOfFileReachedException extends ReaderException
|
class XMLProcessingException extends ReaderException
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Reader\Helper\XLSX;
|
|
||||||
|
|
||||||
use Box\Spout\Common\Exception\IOException;
|
|
||||||
use Box\Spout\Common\Helper\FileSystemHelper;
|
|
||||||
use Box\Spout\Reader\Exception\SharedStringNotFoundException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class SharedStringsHelper
|
|
||||||
* This class provides helper functions for reading sharedStrings XML file
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Reader\Helper\XLSX
|
|
||||||
*/
|
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To avoid running out of memory when extracting the shared strings, they will 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;
|
|
||||||
|
|
||||||
/** Value to use to escape the line feed character ("\n") */
|
|
||||||
const ESCAPED_LINE_FEED_CHARACTER = '_x000A_';
|
|
||||||
|
|
||||||
/** @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 \Box\Spout\Writer\Helper\XLSX\FileSystemHelper Helper to perform file system operations */
|
|
||||||
protected $fileSystemHelper;
|
|
||||||
|
|
||||||
/** @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 MAX_NUM_STRINGS_PER_TEMP_FILE
|
|
||||||
*/
|
|
||||||
protected $inMemoryTempFilePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string Contents of the temporary file that was last read
|
|
||||||
* @see MAX_NUM_STRINGS_PER_TEMP_FILE
|
|
||||||
*/
|
|
||||||
protected $inMemoryTempFileContents;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $filePath Path of the XLSX file being read
|
|
||||||
* @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
|
|
||||||
*/
|
|
||||||
public function __construct($filePath, $tempFolder = null)
|
|
||||||
{
|
|
||||||
$this->filePath = $filePath;
|
|
||||||
|
|
||||||
$rootTempFolder = ($tempFolder) ?: sys_get_temp_dir();
|
|
||||||
$this->fileSystemHelper = new FileSystemHelper($rootTempFolder);
|
|
||||||
$this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds an in-memory array containing all the shared strings of the worksheet.
|
|
||||||
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
|
|
||||||
* It is then accessed by the worksheet 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 worksheets 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;
|
|
||||||
$this->tempFilePointer = null;
|
|
||||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
|
||||||
|
|
||||||
$sharedStringsFilePath = 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH;
|
|
||||||
if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NONET) === false) {
|
|
||||||
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($xmlReader->read() && $xmlReader->name !== 'si') {
|
|
||||||
// do nothing until a 'si' tag is reached
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($xmlReader->name === 'si') {
|
|
||||||
$node = new \SimpleXMLElement($xmlReader->readOuterXml());
|
|
||||||
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
|
|
||||||
|
|
||||||
// removes nodes that should not be read, like the pronunciation of the Kanji characters
|
|
||||||
$cleanNode = $this->removeSuperfluousTextNodes($node);
|
|
||||||
|
|
||||||
// find all text nodes 't'; there can be multiple if the cell contains formatting
|
|
||||||
$textNodes = $cleanNode->xpath('//ns:t');
|
|
||||||
|
|
||||||
$textValue = '';
|
|
||||||
foreach ($textNodes as $textNode) {
|
|
||||||
if ($this->shouldPreserveWhitespace($textNode)) {
|
|
||||||
$textValue .= $textNode->__toString();
|
|
||||||
} else {
|
|
||||||
$textValue .= trim($textNode->__toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$unescapedTextValue = $escaper->unescape($textValue);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
$lineFeedEncodedTextValue = $this->escapeLineFeed($unescapedTextValue);
|
|
||||||
|
|
||||||
$this->writeSharedStringToTempFile($lineFeedEncodedTextValue, $sharedStringIndex);
|
|
||||||
|
|
||||||
$sharedStringIndex++;
|
|
||||||
|
|
||||||
// jump to the next 'si' tag
|
|
||||||
$xmlReader->next('si');
|
|
||||||
}
|
|
||||||
|
|
||||||
// close pointer to the last temp file that was written
|
|
||||||
if ($this->tempFilePointer) {
|
|
||||||
fclose($this->tempFilePointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
$xmlReader->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes nodes that should not be read, like the pronunciation of the Kanji characters.
|
|
||||||
* By keeping them, their text content would be added to the read string.
|
|
||||||
*
|
|
||||||
* @param \SimpleXMLElement $parentNode Parent node that may contain nodes to remove
|
|
||||||
* @return \SimpleXMLElement Cleaned parent node
|
|
||||||
*/
|
|
||||||
protected function removeSuperfluousTextNodes($parentNode)
|
|
||||||
{
|
|
||||||
$tagsToRemove = [
|
|
||||||
'rPh', // Pronunciation of the text
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($tagsToRemove as $tagToRemove) {
|
|
||||||
$xpath = '//ns:' . $tagToRemove;
|
|
||||||
$nodesToRemove = $parentNode->xpath($xpath);
|
|
||||||
|
|
||||||
foreach ($nodesToRemove as $nodeToRemove) {
|
|
||||||
// This is how to remove a node from the XML
|
|
||||||
unset($nodeToRemove[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
|
|
||||||
*
|
|
||||||
* @param \SimpleXMLElement $textNode The text node element (<t>) whitespace may be preserved
|
|
||||||
* @return bool Whether whitespace should be preserved
|
|
||||||
*/
|
|
||||||
protected function shouldPreserveWhitespace($textNode)
|
|
||||||
{
|
|
||||||
$shouldPreserveWhitespace = false;
|
|
||||||
|
|
||||||
$attributes = $textNode->attributes('xml', true);
|
|
||||||
if ($attributes) {
|
|
||||||
foreach ($attributes as $attributeName => $attributeValue) {
|
|
||||||
if ($attributeName === 'space' && $attributeValue->__toString() === 'preserve') {
|
|
||||||
$shouldPreserveWhitespace = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $shouldPreserveWhitespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the given string to its associated temp file.
|
|
||||||
* A new temporary file is created when the previous one has reached its max capacity.
|
|
||||||
*
|
|
||||||
* @param string $sharedString Shared string to write to the temp file
|
|
||||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function writeSharedStringToTempFile($sharedString, $sharedStringIndex)
|
|
||||||
{
|
|
||||||
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
|
|
||||||
|
|
||||||
if (!file_exists($tempFilePath)) {
|
|
||||||
if ($this->tempFilePointer) {
|
|
||||||
fclose($this->tempFilePointer);
|
|
||||||
}
|
|
||||||
$this->tempFilePointer = fopen($tempFilePath, 'w');
|
|
||||||
}
|
|
||||||
|
|
||||||
fwrite($this->tempFilePointer, $sharedString . 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 / self::MAX_NUM_STRINGS_PER_TEMP_FILE);
|
|
||||||
return $this->tempFolder . DIRECTORY_SEPARATOR . 'sharedstrings' . $numTempFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the shared string at the given index.
|
|
||||||
* Because the strings have been split into different files, it looks for the value in the correct file.
|
|
||||||
*
|
|
||||||
* @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 % self::MAX_NUM_STRINGS_PER_TEMP_FILE;
|
|
||||||
|
|
||||||
if (!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, file_get_contents($tempFilePath));
|
|
||||||
$this->inMemoryTempFilePath = $tempFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sharedString = null;
|
|
||||||
if (array_key_exists($indexInFile, $this->inMemoryTempFileContents)) {
|
|
||||||
$escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
|
|
||||||
$sharedString = $this->unescapeLineFeed($escapedSharedString);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$sharedString) {
|
|
||||||
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
|
|
||||||
}
|
|
||||||
|
|
||||||
return rtrim($sharedString, PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escapes the line feed character (\n)
|
|
||||||
*
|
|
||||||
* @param string $unescapedString
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function escapeLineFeed($unescapedString)
|
|
||||||
{
|
|
||||||
return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescapes the line feed character (\n)
|
|
||||||
*
|
|
||||||
* @param string $escapedString
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function unescapeLineFeed($escapedString)
|
|
||||||
{
|
|
||||||
return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the created temporary folder and all its contents
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function cleanup()
|
|
||||||
{
|
|
||||||
$this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Reader\Helper\XLSX;
|
|
||||||
|
|
||||||
use Box\Spout\Reader\Internal\XLSX\Worksheet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class WorksheetHelper
|
|
||||||
* This class provides helper functions related to XLSX worksheets
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Reader\Helper\XLSX
|
|
||||||
*/
|
|
||||||
class WorksheetHelper
|
|
||||||
{
|
|
||||||
/** Path of Content_Types XML file inside the XLSX file */
|
|
||||||
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
|
||||||
|
|
||||||
/** Main namespace for the [Content_Types].xml file */
|
|
||||||
const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
|
||||||
|
|
||||||
/** Value of the Override attribute used in [Content_Types].xml to define worksheets */
|
|
||||||
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
|
|
||||||
|
|
||||||
/** @var string Path of the XLSX file being read */
|
|
||||||
protected $filePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $filePath Path of the XLSX file being read
|
|
||||||
*/
|
|
||||||
public function __construct($filePath)
|
|
||||||
{
|
|
||||||
$this->filePath = $filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the file paths of the worksheet data XML files within the XLSX file.
|
|
||||||
* The paths are read from the [Content_Types].xml file.
|
|
||||||
*
|
|
||||||
* @return Worksheet[] Worksheets within the XLSX file
|
|
||||||
*/
|
|
||||||
public function getWorksheets()
|
|
||||||
{
|
|
||||||
$worksheets = [];
|
|
||||||
|
|
||||||
$xmlContents = file_get_contents('zip://' . $this->filePath . '#' . self::CONTENT_TYPES_XML_FILE_PATH);
|
|
||||||
|
|
||||||
$contentTypes = new \SimpleXMLElement($xmlContents);
|
|
||||||
$contentTypes->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML);
|
|
||||||
|
|
||||||
// find all nodes defining a worksheet
|
|
||||||
$sheetNodes = $contentTypes->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($sheetNodes); $i++) {
|
|
||||||
$sheetNode = $sheetNodes[$i];
|
|
||||||
$sheetDataXMLFilePath = (string) $sheetNode->attributes()->PartName;
|
|
||||||
$worksheets[] = new Worksheet($i, $sheetDataXMLFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $worksheets;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether another worksheet exists after the current worksheet.
|
|
||||||
* The order is determined by the order of appearance in the [Content_Types].xml file.
|
|
||||||
*
|
|
||||||
* @param Worksheet|null $currentWorksheet The worksheet being currently read or null if reading has not started yet
|
|
||||||
* @param Worksheet[] $allWorksheets A list of all worksheets in the XLSX file. Must contain at least one worksheet
|
|
||||||
* @return bool Whether another worksheet exists after the current sheet
|
|
||||||
*/
|
|
||||||
public function hasNextWorksheet($currentWorksheet, $allWorksheets)
|
|
||||||
{
|
|
||||||
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetNumber() + 1 < count($allWorksheets)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Reader\Internal\XLSX;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Worksheet
|
|
||||||
* Represents a worksheet within a XLSX file
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Reader\Internal\XLSX
|
|
||||||
*/
|
|
||||||
class Worksheet
|
|
||||||
{
|
|
||||||
/** @var int Worksheet number, based on the order of appareance in [Content_Types].xml (zero-based) */
|
|
||||||
protected $worksheetNumber;
|
|
||||||
|
|
||||||
/** @var string Path of the XML file containing the worksheet data */
|
|
||||||
protected $dataXmlFilePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $worksheetNumber Worksheet number, based on the order of appareance in [Content_Types].xml (zero-based)
|
|
||||||
* @param string $dataXmlFilePath Path of the XML file containing the worksheet data
|
|
||||||
*/
|
|
||||||
public function __construct($worksheetNumber, $dataXmlFilePath)
|
|
||||||
{
|
|
||||||
$this->worksheetNumber = $worksheetNumber;
|
|
||||||
$this->dataXmlFilePath = $dataXmlFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string Path of the XML file containing the worksheet data, without the leading slash
|
|
||||||
*/
|
|
||||||
public function getDataXmlFilePath()
|
|
||||||
{
|
|
||||||
return ltrim($this->dataXmlFilePath, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getWorksheetNumber()
|
|
||||||
{
|
|
||||||
return $this->worksheetNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
/** Definition of XML attribute 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 \Box\Spout\Common\Escaper\ODS Used to unescape XML data */
|
||||||
|
protected $escaper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
|
$this->escaper = new \Box\Spout\Common\Escaper\ODS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
|
||||||
|
* @TODO Add other types !!
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
$cellValue = is_int($nodeValue) ? intval($nodeValue) : 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|null The value associated with the cell or NULL if invalid date value
|
||||||
|
*/
|
||||||
|
protected function formatDateCellValue($node)
|
||||||
|
{
|
||||||
|
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|null The value associated with the cell or NULL if invalid time value
|
||||||
|
*/
|
||||||
|
protected function formatTimeCellValue($node)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
} else {
|
||||||
|
throw new IOException("Could not open $filePath for reading.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator to iterate over sheets.
|
||||||
|
*
|
||||||
|
* @return SheetIterator To iterate over sheets
|
||||||
|
*/
|
||||||
|
public 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
/** Definition of XML attribute used to parse data */
|
||||||
|
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 Helper\CellValueFormatter Helper to format cell values */
|
||||||
|
protected $cellValueFormatter;
|
||||||
|
|
||||||
|
/** @var bool Whether the iterator has already been rewound once */
|
||||||
|
protected $hasAlreadyBeenRewound = false;
|
||||||
|
|
||||||
|
/** @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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||||
|
*/
|
||||||
|
public function __construct($xmlReader)
|
||||||
|
{
|
||||||
|
$this->xmlReader = $xmlReader;
|
||||||
|
$this->cellValueFormatter = new CellValueFormatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->numReadRows = 0;
|
||||||
|
$this->rowDataBuffer = null;
|
||||||
|
$this->hasReachedEndOfFile = false;
|
||||||
|
|
||||||
|
$this->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current position is valid
|
||||||
|
* @link http://php.net/manual/en/iterator.valid.php
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
$rowData = [];
|
||||||
|
$cellValue = null;
|
||||||
|
$numColumnsRepeated = 1;
|
||||||
|
$numCellsRead = 0;
|
||||||
|
$hasAlreadyReadOneCell = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while ($this->xmlReader->read()) {
|
||||||
|
if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
|
||||||
|
// Start of a cell description
|
||||||
|
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode();
|
||||||
|
|
||||||
|
$node = $this->xmlReader->expand();
|
||||||
|
$currentCellValue = $this->getCellValue($node);
|
||||||
|
|
||||||
|
// process cell N only after having read cell N+1 (see below why)
|
||||||
|
if ($hasAlreadyReadOneCell) {
|
||||||
|
for ($i = 0; $i < $numColumnsRepeated; $i++) {
|
||||||
|
$rowData[] = $cellValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cellValue = $currentCellValue;
|
||||||
|
$numColumnsRepeated = $currentNumColumnsRepeated;
|
||||||
|
|
||||||
|
$numCellsRead++;
|
||||||
|
$hasAlreadyReadOneCell = true;
|
||||||
|
|
||||||
|
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
|
||||||
|
// End of the row description
|
||||||
|
$isEmptyRow = ($numCellsRead <= 1 && empty($cellValue));
|
||||||
|
if ($isEmptyRow) {
|
||||||
|
// skip empty rows
|
||||||
|
$this->next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add value if the last read cell is not empty or does not need to repeat cell values.
|
||||||
|
// 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 (!empty($cellValue) || $numColumnsRepeated === 1) {
|
||||||
|
for ($i = 0; $i < $numColumnsRepeated; $i++) {
|
||||||
|
$rowData[] = $cellValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->numReadRows++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_TABLE)) {
|
||||||
|
// The closing "</table:table>" marks the end of the file
|
||||||
|
$this->hasReachedEndOfFile = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (XMLProcessingException $exception) {
|
||||||
|
throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rowDataBuffer = $rowData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
|
||||||
|
*/
|
||||||
|
protected function getNumColumnsRepeatedForCurrentNode()
|
||||||
|
{
|
||||||
|
$numColumnsRepeated = $this->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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
{
|
||||||
|
$this->xmlReader->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?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 of creation (zero-based) */
|
||||||
|
protected $index;
|
||||||
|
|
||||||
|
/** @var string Name of the sheet */
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||||
|
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||||
|
* @param string $sheetName Name of the sheet
|
||||||
|
*/
|
||||||
|
public function __construct($xmlReader, $sheetIndex, $sheetName)
|
||||||
|
{
|
||||||
|
$this->rowIterator = new RowIterator($xmlReader);
|
||||||
|
$this->index = $sheetIndex;
|
||||||
|
$this->name = $sheetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return \Box\Spout\Reader\ODS\RowIterator
|
||||||
|
*/
|
||||||
|
public function getRowIterator()
|
||||||
|
{
|
||||||
|
return $this->rowIterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||||
|
*/
|
||||||
|
public function getIndex()
|
||||||
|
{
|
||||||
|
return $this->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return string Name of the sheet
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?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\Wrapper\XMLReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SheetIterator
|
||||||
|
* Iterate over ODS sheet.
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\ODS
|
||||||
|
*/
|
||||||
|
class SheetIterator implements IteratorInterface
|
||||||
|
{
|
||||||
|
/** 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 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $filePath Path of the file to be read
|
||||||
|
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
|
||||||
|
*/
|
||||||
|
public function __construct($filePath)
|
||||||
|
{
|
||||||
|
$this->filePath = $filePath;
|
||||||
|
$this->xmlReader = new XMLReader();
|
||||||
|
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
|
$this->escaper = new \Box\Spout\Common\Escaper\ODS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
$contentXmlFilePath = $this->filePath . '#content.xml';
|
||||||
|
if ($this->xmlReader->open('zip://' . $contentXmlFilePath) === false) {
|
||||||
|
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 boolean
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
return new Sheet($this->xmlReader, $sheetName, $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()
|
||||||
|
{
|
||||||
|
$this->xmlReader->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,9 @@ class ReaderFactory
|
|||||||
/**
|
/**
|
||||||
* This creates an instance of the appropriate reader, given the type of the file to be read
|
* 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
|
* @param string $readerType Type of the reader to instantiate
|
||||||
* @return \Box\Spout\Reader\CSV|\Box\Spout\Reader\XLSX
|
* @return ReaderInterface
|
||||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||||
*/
|
*/
|
||||||
public static function create($readerType)
|
public static function create($readerType)
|
||||||
@@ -28,10 +29,13 @@ class ReaderFactory
|
|||||||
|
|
||||||
switch ($readerType) {
|
switch ($readerType) {
|
||||||
case Type::CSV:
|
case Type::CSV:
|
||||||
$reader = new CSV();
|
$reader = new CSV\Reader();
|
||||||
break;
|
break;
|
||||||
case Type::XLSX:
|
case Type::XLSX:
|
||||||
$reader = new XLSX();
|
$reader = new XLSX\Reader();
|
||||||
|
break;
|
||||||
|
case Type::ODS:
|
||||||
|
$reader = new ODS\Reader();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
|
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
|
||||||
|
|||||||
@@ -20,26 +20,12 @@ interface ReaderInterface
|
|||||||
public function open($filePath);
|
public function open($filePath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether all rows have been read (i.e. if we are at the end of the file).
|
* Returns an iterator to iterate over sheets.
|
||||||
* To know if the end of file has been reached, it uses a buffer. If the buffer is
|
|
||||||
* empty (meaning, nothing has been read or previous read line has been consumed), then
|
|
||||||
* it reads the next line, store it in the buffer for the next time or flip a variable if
|
|
||||||
* the end of file has been reached.
|
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return \Iterator To iterate over sheets
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
|
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
|
||||||
*/
|
*/
|
||||||
public function hasNextRow();
|
public function getSheetIterator();
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
|
|
||||||
* actually reading the file.
|
|
||||||
*
|
|
||||||
* @return array Array that contains the data for the read row
|
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
|
|
||||||
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
|
|
||||||
*/
|
|
||||||
public function nextRow();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the reader, preventing any additional reading
|
* Closes the reader, preventing any additional reading
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\Wrapper;
|
||||||
|
|
||||||
|
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SimpleXMLElement
|
||||||
|
* Wrapper around the built-in SimpleXMLElement. This class does not extend \SimpleXMLElement
|
||||||
|
* because it its constructor is final... Instead, it is used as a passthrough.
|
||||||
|
* @see \SimpleXMLElement
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\Wrapper
|
||||||
|
*/
|
||||||
|
class SimpleXMLElement
|
||||||
|
{
|
||||||
|
use XMLInternalErrorsHelper;
|
||||||
|
|
||||||
|
/** @var \SimpleXMLElement Instance of the wrapped SimpleXMLElement object */
|
||||||
|
protected $simpleXMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SimpleXMLElement object
|
||||||
|
* @see \SimpleXMLElement::__construct
|
||||||
|
*
|
||||||
|
* @param string $xmlData A well-formed XML string
|
||||||
|
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If the XML string is not well-formed
|
||||||
|
*/
|
||||||
|
public function __construct($xmlData)
|
||||||
|
{
|
||||||
|
$this->useXMLInternalErrors();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->simpleXMLElement = new \SimpleXMLElement($xmlData);
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
// if the data is invalid, the constructor will throw an Exception
|
||||||
|
$this->resetXMLInternalErrorsSetting();
|
||||||
|
throw new XMLProcessingException($this->getLastXMLErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resetXMLInternalErrorsSetting();
|
||||||
|
|
||||||
|
return $this->simpleXMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the attribute for the given name.
|
||||||
|
*
|
||||||
|
* @param string $name Attribute name
|
||||||
|
* @param string|null|void $namespace An optional namespace for the retrieved attributes
|
||||||
|
* @return string|null The attribute value or NULL if attribute not found
|
||||||
|
*/
|
||||||
|
public function getAttribute($name, $namespace = null)
|
||||||
|
{
|
||||||
|
$isPrefix = ($namespace !== null);
|
||||||
|
$attributes = $this->simpleXMLElement->attributes($namespace, $isPrefix);
|
||||||
|
$attributeValue = $attributes->{$name};
|
||||||
|
|
||||||
|
return ($attributeValue !== null) ? (string) $attributeValue : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a prefix/ns context for the next XPath query
|
||||||
|
* @see \SimpleXMLElement::registerXPathNamespace
|
||||||
|
*
|
||||||
|
* @param string $prefix The namespace prefix to use in the XPath query for the namespace given in "namespace".
|
||||||
|
* @param string $namespace The namespace to use for the XPath query. This must match a namespace in
|
||||||
|
* use by the XML document or the XPath query using "prefix" will not return any results.
|
||||||
|
* @return bool TRUE on success or FALSE on failure.
|
||||||
|
*/
|
||||||
|
public function registerXPathNamespace($prefix, $namespace)
|
||||||
|
{
|
||||||
|
return $this->simpleXMLElement->registerXPathNamespace($prefix, $namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs XPath query on XML data
|
||||||
|
* @see \SimpleXMLElement::xpath
|
||||||
|
*
|
||||||
|
* @param string $path An XPath path
|
||||||
|
* @return SimpleXMLElement[]|bool an array of SimpleXMLElement objects or FALSE in case of an error.
|
||||||
|
*/
|
||||||
|
public function xpath($path)
|
||||||
|
{
|
||||||
|
$elements = $this->simpleXMLElement->xpath($path);
|
||||||
|
|
||||||
|
if ($elements !== false) {
|
||||||
|
$wrappedElements = [];
|
||||||
|
foreach ($elements as $element) {
|
||||||
|
$wrappedElement = $this->wrapSimpleXMLElement($element);
|
||||||
|
|
||||||
|
if ($wrappedElement !== null) {
|
||||||
|
$wrappedElements[] = $this->wrapSimpleXMLElement($element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$elements = $wrappedElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the given element into an instance of the wrapper
|
||||||
|
*
|
||||||
|
* @param \SimpleXMLElement $element Element to be wrapped
|
||||||
|
* @return SimpleXMLElement|null The wrapped element or NULL if the given element is invalid
|
||||||
|
*/
|
||||||
|
protected function wrapSimpleXMLElement(\SimpleXMLElement $element)
|
||||||
|
{
|
||||||
|
$wrappedElement = null;
|
||||||
|
$elementAsXML = $element->asXML();
|
||||||
|
|
||||||
|
if ($elementAsXML !== false) {
|
||||||
|
$wrappedElement = new SimpleXMLElement($elementAsXML);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wrappedElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all nodes matching the given XPath query.
|
||||||
|
* It does not map to any \SimpleXMLElement function.
|
||||||
|
*
|
||||||
|
* @param string $path An XPath path
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function removeNodesMatchingXPath($path)
|
||||||
|
{
|
||||||
|
$nodesToRemove = $this->simpleXMLElement->xpath($path);
|
||||||
|
|
||||||
|
foreach ($nodesToRemove as $nodeToRemove) {
|
||||||
|
unset($nodeToRemove[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first child matching the given tag name
|
||||||
|
*
|
||||||
|
* @param string $tagName
|
||||||
|
* @return SimpleXMLElement|null The first child matching the tag name or NULL if none found
|
||||||
|
*/
|
||||||
|
public function getFirstChildByTagName($tagName)
|
||||||
|
{
|
||||||
|
$doesElementExist = isset($this->simpleXMLElement->{$tagName});
|
||||||
|
|
||||||
|
/** @var \SimpleXMLElement $realElement */
|
||||||
|
$realElement = $this->simpleXMLElement->{$tagName};
|
||||||
|
|
||||||
|
return $doesElementExist ? $this->wrapSimpleXMLElement($realElement) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the immediate children.
|
||||||
|
*
|
||||||
|
* @return array The children
|
||||||
|
*/
|
||||||
|
public function children()
|
||||||
|
{
|
||||||
|
$children = [];
|
||||||
|
|
||||||
|
foreach ($this->simpleXMLElement->children() as $child) {
|
||||||
|
$children[] = $this->wrapSimpleXMLElement($child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $children;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->simpleXMLElement->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\Wrapper;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class XMLReader
|
||||||
|
* Wrapper around the built-in XMLReader
|
||||||
|
* @see \XMLReader
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\Wrapper
|
||||||
|
*/
|
||||||
|
class XMLReader extends \XMLReader
|
||||||
|
{
|
||||||
|
use XMLInternalErrorsHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the URI containing the XML to parse
|
||||||
|
* @see \XMLReader::open
|
||||||
|
*
|
||||||
|
* @param string $URI URI pointing to the document
|
||||||
|
* @param string|null|void $encoding The document encoding
|
||||||
|
* @param int $options A bitmask of the LIBXML_* constants
|
||||||
|
* @return bool TRUE on success or FALSE on failure
|
||||||
|
*/
|
||||||
|
public function open($URI, $encoding = null, $options = 0)
|
||||||
|
{
|
||||||
|
$wasOpenSuccessful = false;
|
||||||
|
$realPathURI = $this->convertURIToUseRealPath($URI);
|
||||||
|
|
||||||
|
// HHVM does not check if file exists within zip file
|
||||||
|
// @link https://github.com/facebook/hhvm/issues/5779
|
||||||
|
if ($this->isRunningHHVM() && $this->isZipStream($realPathURI)) {
|
||||||
|
if ($this->fileExistsWithinZip($realPathURI)) {
|
||||||
|
$wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wasOpenSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the given URI to use a real path.
|
||||||
|
* This is to avoid issues on some Windows setup.
|
||||||
|
*
|
||||||
|
* @param string $URI URI
|
||||||
|
* @return string The URI using a real path
|
||||||
|
*/
|
||||||
|
protected function convertURIToUseRealPath($URI)
|
||||||
|
{
|
||||||
|
$realPathURI = $URI;
|
||||||
|
|
||||||
|
if ($this->isZipStream($URI)) {
|
||||||
|
if (preg_match('/zip:\/\/(.*)#(.*)/', $URI, $matches)) {
|
||||||
|
$documentPath = $matches[1];
|
||||||
|
$documentInsideZipPath = $matches[2];
|
||||||
|
$realPathURI = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$realPathURI = realpath($URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $realPathURI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given URI is a zip stream.
|
||||||
|
*
|
||||||
|
* @param string $URI URI pointing to a document
|
||||||
|
* @return bool TRUE if URI is a zip stream, FALSE otherwise
|
||||||
|
*/
|
||||||
|
protected function isZipStream($URI)
|
||||||
|
{
|
||||||
|
return (strpos($URI, 'zip://') === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the current environment is HHVM
|
||||||
|
*
|
||||||
|
* @return bool TRUE if running on HHVM, FALSE otherwise
|
||||||
|
*/
|
||||||
|
protected function isRunningHHVM()
|
||||||
|
{
|
||||||
|
return defined('HHVM_VERSION');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
while (($wasReadSuccessful = $this->read()) && ($this->nodeType !== \XMLReader::ELEMENT || $this->name !== $nodeName)) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
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->nodeType === XMLReader::ELEMENT && $this->name === $nodeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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->nodeType === XMLReader::END_ELEMENT && $this->name === $nodeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Reader;
|
|
||||||
|
|
||||||
use Box\Spout\Common\Exception\BadUsageException;
|
|
||||||
use Box\Spout\Common\Exception\IOException;
|
|
||||||
use Box\Spout\Reader\Exception\EndOfWorksheetsReachedException;
|
|
||||||
use Box\Spout\Reader\Exception\NoWorksheetsFoundException;
|
|
||||||
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
|
|
||||||
use Box\Spout\Reader\Helper\XLSX\CellHelper;
|
|
||||||
use Box\Spout\Reader\Helper\XLSX\SharedStringsHelper;
|
|
||||||
use Box\Spout\Reader\Helper\XLSX\WorksheetHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class XLSX
|
|
||||||
* This class provides support to read data from a XLSX file
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Reader
|
|
||||||
*/
|
|
||||||
class XLSX extends AbstractReader
|
|
||||||
{
|
|
||||||
/** @var string Real path of the file to read */
|
|
||||||
protected $filePath;
|
|
||||||
|
|
||||||
/** @var string Temporary folder where the temporary files will be created */
|
|
||||||
protected $tempFolder;
|
|
||||||
|
|
||||||
/** @var \ZipArchive */
|
|
||||||
protected $zip;
|
|
||||||
|
|
||||||
/** @var Helper\XLSX\SharedStringsHelper Helper to work with shared strings */
|
|
||||||
protected $sharedStringsHelper;
|
|
||||||
|
|
||||||
/** @var Helper\XLSX\WorksheetHelper Helper to work with worksheets */
|
|
||||||
protected $worksheetHelper;
|
|
||||||
|
|
||||||
/** @var Internal\XLSX\Worksheet[] The list of worksheets present in the file */
|
|
||||||
protected $worksheets;
|
|
||||||
|
|
||||||
/** @var Internal\XLSX\Worksheet The worksheet being read */
|
|
||||||
protected $currentWorksheet;
|
|
||||||
|
|
||||||
/** @var \XMLReader The XMLReader object that will help read sheets XML data */
|
|
||||||
protected $xmlReader;
|
|
||||||
|
|
||||||
/** @var int The number of columns the worksheet has (0 meaning undefined) */
|
|
||||||
protected $numberOfColumns = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $tempFolder Temporary folder where the temporary files will be created
|
|
||||||
* @return XLSX
|
|
||||||
*/
|
|
||||||
public function setTempFolder($tempFolder)
|
|
||||||
{
|
|
||||||
$this->tempFolder = $tempFolder;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 worksheets.
|
|
||||||
*
|
|
||||||
* @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 Exception\NoWorksheetsFoundException If there are no worksheets in the file
|
|
||||||
*/
|
|
||||||
protected function openReader($filePath)
|
|
||||||
{
|
|
||||||
$this->filePath = $filePath;
|
|
||||||
$this->zip = new \ZipArchive();
|
|
||||||
|
|
||||||
if ($this->zip->open($filePath) === true) {
|
|
||||||
// Extracts all the strings from the worksheets for easy access in the future
|
|
||||||
$this->extractSharedStrings($filePath);
|
|
||||||
|
|
||||||
// Fetch all available worksheets
|
|
||||||
$this->worksheetHelper = new WorksheetHelper($filePath);
|
|
||||||
$this->worksheets = $this->worksheetHelper->getWorksheets($filePath);
|
|
||||||
|
|
||||||
if (count($this->worksheets) === 0) {
|
|
||||||
throw new NoWorksheetsFoundException('The file must contain at least one worksheet.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IOException('Could not open ' . $filePath . ' for reading.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds an in-memory array containing all the shared strings of the worksheets.
|
|
||||||
*
|
|
||||||
* @param string $filePath Path of the XLSX file to be read
|
|
||||||
* @return void
|
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings XML file can't be read
|
|
||||||
*/
|
|
||||||
protected function extractSharedStrings($filePath)
|
|
||||||
{
|
|
||||||
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
|
|
||||||
$this->sharedStringsHelper->extractSharedStrings();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether another worksheet exists after the current worksheet.
|
|
||||||
*
|
|
||||||
* @return bool Whether another worksheet exists after the current worksheet.
|
|
||||||
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
|
|
||||||
*/
|
|
||||||
public function hasNextSheet()
|
|
||||||
{
|
|
||||||
if (!$this->isStreamOpened) {
|
|
||||||
throw new ReaderNotOpenedException('Stream should be opened first.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->worksheetHelper->hasNextWorksheet($this->currentWorksheet, $this->worksheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves the pointer to the current worksheet.
|
|
||||||
* Moving to another worksheet will stop the reading in the current worksheet.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
|
|
||||||
* @throws Exception\EndOfWorksheetsReachedException If there is no more worksheets to read
|
|
||||||
*/
|
|
||||||
public function nextSheet()
|
|
||||||
{
|
|
||||||
if ($this->hasNextSheet()) {
|
|
||||||
if ($this->currentWorksheet === null) {
|
|
||||||
$nextWorksheet = $this->worksheets[0];
|
|
||||||
} else {
|
|
||||||
$currentWorksheetNumber = $this->currentWorksheet->getWorksheetNumber();
|
|
||||||
$nextWorksheet = $this->worksheets[$currentWorksheetNumber + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->initXmlReaderForWorksheetData($nextWorksheet);
|
|
||||||
$this->currentWorksheet = $nextWorksheet;
|
|
||||||
|
|
||||||
// make sure that we are ready to read more rows
|
|
||||||
$this->hasReachedEndOfFile = false;
|
|
||||||
$this->emptyRowDataBuffer();
|
|
||||||
} else {
|
|
||||||
throw new EndOfWorksheetsReachedException('End of worksheets was reached. Cannot read more worksheets.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the XMLReader object that reads worksheet data for the given worksheet.
|
|
||||||
* If another worksheet was being read, it closes the reader before reopening it for the new worksheet.
|
|
||||||
* The XMLReader is configured to be safe from billion laughs attack.
|
|
||||||
*
|
|
||||||
* @param Internal\XLSX\Worksheet $worksheet The worksheet to initialize the XMLReader with
|
|
||||||
* @return void
|
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the worksheet data XML cannot be read
|
|
||||||
*/
|
|
||||||
protected function initXmlReaderForWorksheetData($worksheet)
|
|
||||||
{
|
|
||||||
// if changing worksheet and the XMLReader was initialized for the current worksheet
|
|
||||||
if ($worksheet != $this->currentWorksheet && $this->xmlReader) {
|
|
||||||
$this->xmlReader->close();
|
|
||||||
} else if (!$this->xmlReader) {
|
|
||||||
$this->xmlReader = new \XMLReader();
|
|
||||||
}
|
|
||||||
|
|
||||||
$worksheetDataXMLFilePath = $worksheet->getDataXmlFilePath();
|
|
||||||
|
|
||||||
$worksheetDataFilePath = 'zip://' . $this->filePath . '#' . $worksheetDataXMLFilePath;
|
|
||||||
if ($this->xmlReader->open($worksheetDataFilePath, null, LIBXML_NONET) === false) {
|
|
||||||
throw new IOException('Could not open "' . $worksheetDataXMLFilePath . '".');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads and returns data of the line that comes after the last read line, on the current worksheet.
|
|
||||||
* Empty rows will be skipped.
|
|
||||||
*
|
|
||||||
* @return array|null Array that contains the data for the read line or null at the end of the file
|
|
||||||
* @throws \Box\Spout\Common\Exception\BadUsageException If the pointer to the current worksheet has not been set
|
|
||||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
|
|
||||||
*/
|
|
||||||
protected function read()
|
|
||||||
{
|
|
||||||
if (!$this->currentWorksheet) {
|
|
||||||
throw new BadUsageException('You must call nextSheet() before calling hasNextRow() or nextRow()');
|
|
||||||
}
|
|
||||||
|
|
||||||
$isInsideRowTag = false;
|
|
||||||
$rowData = [];
|
|
||||||
|
|
||||||
while ($this->xmlReader->read()) {
|
|
||||||
if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'dimension') {
|
|
||||||
// Read dimensions of the worksheet
|
|
||||||
$dimensionRef = $this->xmlReader->getAttribute('ref'); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
|
|
||||||
if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
|
|
||||||
$lastCellIndex = $matches[1];
|
|
||||||
$this->numberOfColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
|
|
||||||
}
|
|
||||||
} else if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'row') {
|
|
||||||
// Start of the row description
|
|
||||||
$isInsideRowTag = true;
|
|
||||||
|
|
||||||
// Read spans info if present
|
|
||||||
$numberOfColumnsForRow = $this->numberOfColumns;
|
|
||||||
$spans = $this->xmlReader->getAttribute('spans'); // returns '1:5' for instance
|
|
||||||
if ($spans) {
|
|
||||||
list(, $numberOfColumnsForRow) = explode(':', $spans);
|
|
||||||
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
|
|
||||||
}
|
|
||||||
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
|
|
||||||
} else if ($isInsideRowTag && $this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'c') {
|
|
||||||
// Start of a cell description
|
|
||||||
$currentCellIndex = $this->xmlReader->getAttribute('r');
|
|
||||||
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
|
|
||||||
$node = $this->xmlReader->expand();
|
|
||||||
|
|
||||||
$hasSharedString = ($this->xmlReader->getAttribute('t') === 's');
|
|
||||||
if ($hasSharedString) {
|
|
||||||
$sharedStringIndex = intval($node->nodeValue);
|
|
||||||
$rowData[$currentColumnIndex] = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
|
|
||||||
} else {
|
|
||||||
// for inline strings or numbers, just get the value
|
|
||||||
$rowData[$currentColumnIndex] = trim($node->nodeValue);
|
|
||||||
}
|
|
||||||
} else if ($this->xmlReader->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === 'row') {
|
|
||||||
// End of the row description
|
|
||||||
// If needed, we fill the empty cells
|
|
||||||
$rowData = ($this->numberOfColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no data means "end of file"
|
|
||||||
return ($rowData !== []) ? $rowData : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the reader. To be used after reading the file.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function closeReader()
|
|
||||||
{
|
|
||||||
if ($this->xmlReader) {
|
|
||||||
$this->xmlReader->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->zip) {
|
|
||||||
$this->zip->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->sharedStringsHelper->cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Box\Spout\Reader\Helper\XLSX;
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
use Box\Spout\Common\Exception\InvalidArgumentException;
|
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ use Box\Spout\Common\Exception\InvalidArgumentException;
|
|||||||
* Class CellHelper
|
* Class CellHelper
|
||||||
* This class provides helper functions when working with cells
|
* This class provides helper functions when working with cells
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Reader\Helper\XLSX
|
* @package Box\Spout\Reader\XLSX\Helper
|
||||||
*/
|
*/
|
||||||
class CellHelper
|
class CellHelper
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,7 @@ class CellHelper
|
|||||||
{
|
{
|
||||||
$existingIndexes = array_keys($dataArray);
|
$existingIndexes = array_keys($dataArray);
|
||||||
|
|
||||||
$newIndexes = array_fill_keys(range(min($existingIndexes), max($existingIndexes)), $fillValue);
|
$newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue);
|
||||||
$dataArray += $newIndexes;
|
$dataArray += $newIndexes;
|
||||||
|
|
||||||
ksort($dataArray);
|
ksort($dataArray);
|
||||||
@@ -71,7 +71,7 @@ class CellHelper
|
|||||||
{
|
{
|
||||||
$currentColumnIndex = ord($single_cell_index) - $capitalAAsciiValue;
|
$currentColumnIndex = ord($single_cell_index) - $capitalAAsciiValue;
|
||||||
|
|
||||||
if ($columnLength == 1) {
|
if ($columnLength === 1) {
|
||||||
$columnIndex += $currentColumnIndex;
|
$columnIndex += $currentColumnIndex;
|
||||||
} else {
|
} else {
|
||||||
$columnIndex += pow($step, ($columnLength - 1)) * ($currentColumnIndex + 1);
|
$columnIndex += pow($step, ($columnLength - 1)) * ($currentColumnIndex + 1);
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<?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_DAYS_FROM_JAN_1_1900_TO_JAN_1_1970 = 25569; // actually 25567 but accommodating for an Excel bug with some bisextile years
|
||||||
|
const NUM_SECONDS_IN_ONE_DAY = 86400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 \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
|
||||||
|
*/
|
||||||
|
public function __construct($sharedStringsHelper, $styleHelper)
|
||||||
|
{
|
||||||
|
$this->sharedStringsHelper = $sharedStringsHelper;
|
||||||
|
$this->styleHelper = $styleHelper;
|
||||||
|
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
|
$this->escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
$escapedCellValue = trim($tNode->nodeValue);
|
||||||
|
$cellValue = $this->escaper->unescape($escapedCellValue);
|
||||||
|
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));
|
||||||
|
} else {
|
||||||
|
return is_int($nodeValue) ? intval($nodeValue) : 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.
|
||||||
|
*
|
||||||
|
* @param float $nodeValue
|
||||||
|
* @return \DateTime|null The value associated with the cell or NULL if invalid date value
|
||||||
|
*/
|
||||||
|
protected function formatExcelTimestampValue($nodeValue)
|
||||||
|
{
|
||||||
|
$numDaysSince1970Jan1 = $nodeValue - self::NUM_DAYS_FROM_JAN_1_1900_TO_JAN_1_1970;
|
||||||
|
|
||||||
|
// Fix for the erroneous leap year in Excel
|
||||||
|
if ($nodeValue < self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
|
||||||
|
$numDaysSince1970Jan1++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unixTimestamp = round($numDaysSince1970Jan1 * self::NUM_SECONDS_IN_ONE_DAY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cellValue = (new \DateTime())->setTimestamp($unixTimestamp);
|
||||||
|
return $cellValue;
|
||||||
|
} 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.
|
||||||
|
*
|
||||||
|
* @param string $nodeValue
|
||||||
|
* @return \DateTime|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 {
|
||||||
|
$cellValue = new \DateTime($nodeValue);
|
||||||
|
return $cellValue;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<?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 $sharedStringsUniqueCount Number of unique shared strings
|
||||||
|
* @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 $sharedStringsUniqueCount Number of unique shared strings
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)
|
||||||
|
{
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<?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;
|
||||||
|
if (array_key_exists($indexInFile, $this->inMemoryTempFileContents)) {
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
|
use Box\Spout\Common\Exception\IOException;
|
||||||
|
use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||||
|
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
|
||||||
|
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';
|
||||||
|
|
||||||
|
/** @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|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;
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
|
||||||
|
$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('si');
|
||||||
|
|
||||||
|
while ($xmlReader->name === 'si') {
|
||||||
|
$node = $this->getSimpleXmlElementNodeFromXMLReader($xmlReader);
|
||||||
|
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
|
||||||
|
|
||||||
|
// removes nodes that should not be read, like the pronunciation of the Kanji characters
|
||||||
|
$cleanNode = $this->removeSuperfluousTextNodes($node);
|
||||||
|
|
||||||
|
// find all text nodes 't'; there can be multiple if the cell contains formatting
|
||||||
|
$textNodes = $cleanNode->xpath('//ns:t');
|
||||||
|
|
||||||
|
$textValue = '';
|
||||||
|
foreach ($textNodes as $textNode) {
|
||||||
|
if ($this->shouldPreserveWhitespace($textNode)) {
|
||||||
|
$textValue .= $textNode->__toString();
|
||||||
|
} else {
|
||||||
|
$textValue .= trim($textNode->__toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$unescapedTextValue = $escaper->unescape($textValue);
|
||||||
|
$this->cachingStrategy->addStringForIndex($unescapedTextValue, $sharedStringIndex);
|
||||||
|
|
||||||
|
$sharedStringIndex++;
|
||||||
|
|
||||||
|
// jump to the next 'si' tag
|
||||||
|
$xmlReader->next('si');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (XMLProcessingException $exception) {
|
||||||
|
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cachingStrategy->closeCache();
|
||||||
|
|
||||||
|
$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 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('sst');
|
||||||
|
|
||||||
|
// Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE)
|
||||||
|
while ($xmlReader->name === 'sst' && $xmlReader->nodeType !== XMLReader::ELEMENT) {
|
||||||
|
$xmlReader->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
return intval($xmlReader->getAttribute('uniqueCount'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the best shared strings caching strategy.
|
||||||
|
*
|
||||||
|
* @param int $sharedStringsUniqueCount
|
||||||
|
* @return CachingStrategyInterface
|
||||||
|
*/
|
||||||
|
protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount)
|
||||||
|
{
|
||||||
|
return CachingStrategyFactory::getInstance()
|
||||||
|
->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a SimpleXMLElement node from the current node in the given XMLReader instance.
|
||||||
|
* This is to simplify the parsing of the subtree.
|
||||||
|
*
|
||||||
|
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader
|
||||||
|
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement
|
||||||
|
* @throws \Box\Spout\Common\Exception\IOException If the current node cannot be read
|
||||||
|
*/
|
||||||
|
protected function getSimpleXmlElementNodeFromXMLReader($xmlReader)
|
||||||
|
{
|
||||||
|
$node = null;
|
||||||
|
try {
|
||||||
|
$node = new SimpleXMLElement($xmlReader->readOuterXml());
|
||||||
|
} catch (XMLProcessingException $exception) {
|
||||||
|
throw new IOException("The sharedStrings.xml file contains unreadable data [{$exception->getMessage()}].");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes nodes that should not be read, like the pronunciation of the Kanji characters.
|
||||||
|
* By keeping them, their text content would be added to the read string.
|
||||||
|
*
|
||||||
|
* @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $parentNode Parent node that may contain nodes to remove
|
||||||
|
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement Cleaned parent node
|
||||||
|
*/
|
||||||
|
protected function removeSuperfluousTextNodes($parentNode)
|
||||||
|
{
|
||||||
|
$tagsToRemove = [
|
||||||
|
'rPh', // Pronunciation of the text
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($tagsToRemove as $tagToRemove) {
|
||||||
|
$xpath = '//ns:' . $tagToRemove;
|
||||||
|
$parentNode->removeNodesMatchingXPath($xpath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
|
||||||
|
*
|
||||||
|
* @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $textNode The text node element (<t>) whitespace may be preserved
|
||||||
|
* @return bool Whether whitespace should be preserved
|
||||||
|
*/
|
||||||
|
protected function shouldPreserveWhitespace($textNode)
|
||||||
|
{
|
||||||
|
$spaceValue = $textNode->getAttribute('space', 'xml');
|
||||||
|
return ($spaceValue === '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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
|
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/** Extension for XML files */
|
||||||
|
const XML_EXTENSION = '.xml';
|
||||||
|
|
||||||
|
/** Paths of XML files relative to the XLSX file root */
|
||||||
|
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
||||||
|
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
|
||||||
|
const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
|
||||||
|
|
||||||
|
/** Namespaces for the XML files */
|
||||||
|
const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
||||||
|
const MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
||||||
|
const MAIN_NAMESPACE_FOR_WORKBOOK_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||||
|
|
||||||
|
/** Value of the Override attribute used in [Content_Types].xml to define sheets */
|
||||||
|
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
|
||||||
|
|
||||||
|
/** @var string Path of the XLSX file being read */
|
||||||
|
protected $filePath;
|
||||||
|
|
||||||
|
/** @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;
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml.rels file */
|
||||||
|
protected $workbookXMLRelsAsXMLElement;
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml file */
|
||||||
|
protected $workbookXMLAsXMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $filePath Path of the XLSX file being read
|
||||||
|
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
|
||||||
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
|
*/
|
||||||
|
public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper)
|
||||||
|
{
|
||||||
|
$this->filePath = $filePath;
|
||||||
|
$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 = [];
|
||||||
|
|
||||||
|
$contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||||
|
self::CONTENT_TYPES_XML_FILE_PATH,
|
||||||
|
self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML
|
||||||
|
);
|
||||||
|
|
||||||
|
// find all nodes defining a sheet
|
||||||
|
$sheetNodes = $contentTypesAsXMLElement->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
|
||||||
|
$numSheetNodes = count($sheetNodes);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numSheetNodes; $i++) {
|
||||||
|
$sheetNode = $sheetNodes[$i];
|
||||||
|
$sheetDataXMLFilePath = $sheetNode->getAttribute('PartName');
|
||||||
|
|
||||||
|
$sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath, $i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of a sheet, given the path of its data XML file.
|
||||||
|
* We first look at "xl/_rels/workbook.xml.rels" to find the relationship ID of the sheet.
|
||||||
|
* Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID.
|
||||||
|
* The entry contains the ID and name of the sheet.
|
||||||
|
*
|
||||||
|
* If this piece of data can't be found by parsing the different XML files, the ID will default
|
||||||
|
* to the sheet index, based on order in [Content_Types].xml. Similarly, the sheet's name will
|
||||||
|
* default to the data sheet XML file name ("xl/worksheets/sheet2.xml" => "sheet2").
|
||||||
|
*
|
||||||
|
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||||
|
* @param int $sheetIndexZeroBased Index of the sheet, based on order in [Content_Types].xml (zero-based)
|
||||||
|
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
|
||||||
|
*/
|
||||||
|
protected function getSheetFromXML($sheetDataXMLFilePath, $sheetIndexZeroBased)
|
||||||
|
{
|
||||||
|
$sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
|
||||||
|
* In workbook.xml.rels, it is only "worksheets/sheet1.xml"
|
||||||
|
*/
|
||||||
|
$sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/');
|
||||||
|
|
||||||
|
// find the node associated to the given file path
|
||||||
|
$workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement();
|
||||||
|
$relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]');
|
||||||
|
|
||||||
|
if (count($relationshipNodes) === 1) {
|
||||||
|
$relationshipNode = $relationshipNodes[0];
|
||||||
|
$sheetId = $relationshipNode->getAttribute('Id');
|
||||||
|
|
||||||
|
$workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
|
||||||
|
$sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $sheetId . '"]');
|
||||||
|
|
||||||
|
if (count($sheetNodes) === 1) {
|
||||||
|
$sheetNode = $sheetNodes[0];
|
||||||
|
$escapedSheetName = $sheetNode->getAttribute('name');
|
||||||
|
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
$sheetName = $escaper->unescape($escapedSheetName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default name of the sheet whose data is located
|
||||||
|
* at the given path.
|
||||||
|
*
|
||||||
|
* @param string $sheetDataXMLFilePath Path of the sheet data XML file
|
||||||
|
* @return string The default sheet name
|
||||||
|
*/
|
||||||
|
protected function getDefaultSheetName($sheetDataXMLFilePath)
|
||||||
|
{
|
||||||
|
return $this->globalFunctionsHelper->basename($sheetDataXMLFilePath, self::XML_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a representation of the workbook.xml.rels file, ready to be parsed.
|
||||||
|
* The returned value is cached.
|
||||||
|
*
|
||||||
|
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file
|
||||||
|
*/
|
||||||
|
protected function getWorkbookXMLRelsAsXMLElement()
|
||||||
|
{
|
||||||
|
if (!$this->workbookXMLRelsAsXMLElement) {
|
||||||
|
$this->workbookXMLRelsAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||||
|
self::WORKBOOK_XML_RELS_FILE_PATH,
|
||||||
|
self::MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workbookXMLRelsAsXMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a representation of the workbook.xml file, ready to be parsed.
|
||||||
|
* The returned value is cached.
|
||||||
|
*
|
||||||
|
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file
|
||||||
|
*/
|
||||||
|
protected function getWorkbookXMLAsXMLElement()
|
||||||
|
{
|
||||||
|
if (!$this->workbookXMLAsXMLElement) {
|
||||||
|
$this->workbookXMLAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||||
|
self::WORKBOOK_XML_FILE_PATH,
|
||||||
|
self::MAIN_NAMESPACE_FOR_WORKBOOK_XML
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workbookXMLAsXMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the contents of the given file in an XML parser and register the given XPath namespace.
|
||||||
|
*
|
||||||
|
* @param string $xmlFilePath The path of the XML file inside the XLSX file
|
||||||
|
* @param string $mainNamespace The main XPath namespace to register
|
||||||
|
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement The XML element representing the file
|
||||||
|
*/
|
||||||
|
protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace)
|
||||||
|
{
|
||||||
|
$xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $xmlFilePath);
|
||||||
|
|
||||||
|
$xmlElement = new SimpleXMLElement($xmlContents);
|
||||||
|
$xmlElement->registerXPathNamespace('ns', $mainNamespace);
|
||||||
|
|
||||||
|
return $xmlElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
|
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** @var string Path of the XLSX file being read */
|
||||||
|
protected $filePath;
|
||||||
|
|
||||||
|
/** @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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $filePath Path of the XLSX file being read
|
||||||
|
*/
|
||||||
|
public function __construct($filePath)
|
||||||
|
{
|
||||||
|
$this->filePath = $filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the styles.xml file and extract the relevant information from the file.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function extractRelevantInfo()
|
||||||
|
{
|
||||||
|
$this->customNumberFormats = [];
|
||||||
|
$this->stylesAttributes = [];
|
||||||
|
|
||||||
|
$stylesXmlFilePath = $this->filePath .'#' . self::STYLES_XML_FILE_PATH;
|
||||||
|
$xmlReader = new XMLReader();
|
||||||
|
|
||||||
|
if ($xmlReader->open('zip://' . $stylesXmlFilePath)) {
|
||||||
|
while ($xmlReader->read()) {
|
||||||
|
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
|
||||||
|
$numFmtsNode = new SimpleXMLElement($xmlReader->readOuterXml());
|
||||||
|
$this->extractNumberFormats($numFmtsNode);
|
||||||
|
|
||||||
|
} else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
|
||||||
|
$cellXfsNode = new SimpleXMLElement($xmlReader->readOuterXml());
|
||||||
|
$this->extractStyleAttributes($cellXfsNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 SimpleXMLElement $numFmtsNode The "numFmts" node
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function extractNumberFormats($numFmtsNode)
|
||||||
|
{
|
||||||
|
foreach ($numFmtsNode->children() as $numFmtNode) {
|
||||||
|
$numFmtId = intval($numFmtNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
|
||||||
|
$formatCode = $numFmtNode->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
|
||||||
|
$this->customNumberFormats[$numFmtId] = $formatCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 SimpleXMLElement $cellXfsNode The "cellXfs" node
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function extractStyleAttributes($cellXfsNode)
|
||||||
|
{
|
||||||
|
foreach ($cellXfsNode->children() as $xfNode) {
|
||||||
|
$this->stylesAttributes[] = [
|
||||||
|
self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xfNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)),
|
||||||
|
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xfNode->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
if ($styleId === self::DEFAULT_STYLE_ID || !array_key_exists($styleId, $stylesAttributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$styleAttributes = $stylesAttributes[$styleId];
|
||||||
|
|
||||||
|
$applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
|
||||||
|
if (!$applyNumberFormat) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||||
|
return $this->doesNumFmtIdIndicateDate($numFmtId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $numFmtId
|
||||||
|
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||||
|
*/
|
||||||
|
protected function doesNumFmtIdIndicateDate($numFmtId)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
$this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
|
||||||
|
$this->isNumFmtIdCustomDateFormat($numFmtId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $numFmtId
|
||||||
|
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||||
|
*/
|
||||||
|
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
|
||||||
|
{
|
||||||
|
$builtInDateFormatIds = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47];
|
||||||
|
return in_array($numFmtId, $builtInDateFormatIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $numFmtId
|
||||||
|
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||||
|
*/
|
||||||
|
protected function isNumFmtIdCustomDateFormat($numFmtId)
|
||||||
|
{
|
||||||
|
$customNumberFormats = $this->getCustomNumberFormats();
|
||||||
|
|
||||||
|
if (!array_key_exists($numFmtId, $customNumberFormats)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customNumberFormat = $customNumberFormats[$numFmtId];
|
||||||
|
|
||||||
|
// Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
|
||||||
|
$pattern = '((?<!\\\)\[.+?(?<!\\\)\])';
|
||||||
|
$customNumberFormat = preg_replace($pattern, '', $customNumberFormat);
|
||||||
|
|
||||||
|
// 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 "\"
|
||||||
|
$pattern = '/(?<!\\\)' . $dateFormatCharacter . '/';
|
||||||
|
|
||||||
|
if (preg_match($pattern, $customNumberFormat)) {
|
||||||
|
$hasFoundDateFormatCharacter = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasFoundDateFormatCharacter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?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 string Temporary folder where the temporary files will be created */
|
||||||
|
protected $tempFolder;
|
||||||
|
|
||||||
|
/** @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;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $tempFolder Temporary folder where the temporary files will be created
|
||||||
|
* @return Reader
|
||||||
|
*/
|
||||||
|
public function setTempFolder($tempFolder)
|
||||||
|
{
|
||||||
|
$this->tempFolder = $tempFolder;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->tempFolder);
|
||||||
|
|
||||||
|
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->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
|
||||||
|
*/
|
||||||
|
public 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_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 Helper\CellValueFormatter Helper to format cell values */
|
||||||
|
protected $cellValueFormatter;
|
||||||
|
|
||||||
|
/** @var Helper\StyleHelper $styleHelper Helper to work with styles */
|
||||||
|
protected $styleHelper;
|
||||||
|
|
||||||
|
/** @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 int The number of columns the sheet has (0 meaning undefined) */
|
||||||
|
protected $numColumns = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
|
||||||
|
*/
|
||||||
|
public function __construct($filePath, $sheetDataXMLFilePath, $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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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->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 boolean
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
$rowData = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
while ($this->xmlReader->read()) {
|
||||||
|
if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_DIMENSION)) {
|
||||||
|
// Read dimensions of the sheet
|
||||||
|
$dimensionRef = $this->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)) {
|
||||||
|
$lastCellIndex = $matches[1];
|
||||||
|
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) {
|
||||||
|
// Start of the row description
|
||||||
|
|
||||||
|
// Read spans info if present
|
||||||
|
$numberOfColumnsForRow = $this->numColumns;
|
||||||
|
$spans = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
|
||||||
|
if ($spans) {
|
||||||
|
list(, $numberOfColumnsForRow) = explode(':', $spans);
|
||||||
|
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
|
||||||
|
}
|
||||||
|
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
|
||||||
|
|
||||||
|
} else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
|
||||||
|
// Start of a cell description
|
||||||
|
$currentCellIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
|
||||||
|
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
|
||||||
|
|
||||||
|
$node = $this->xmlReader->expand();
|
||||||
|
$rowData[$currentColumnIndex] = $this->getCellValue($node);
|
||||||
|
|
||||||
|
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
|
||||||
|
// End of the row description
|
||||||
|
// If needed, we fill the empty cells
|
||||||
|
$rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
|
||||||
|
$this->numReadRows++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_WORKSHEET)) {
|
||||||
|
// The closing "</worksheet>" marks the end of the file
|
||||||
|
$this->hasReachedEndOfFile = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (XMLProcessingException $exception) {
|
||||||
|
throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rowDataBuffer = $rowData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
{
|
||||||
|
$this->xmlReader->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?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 of creation (zero-based) */
|
||||||
|
protected $index;
|
||||||
|
|
||||||
|
/** @var string Name of the sheet */
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 Helper\SharedStringsHelper Helper to work with shared strings
|
||||||
|
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||||
|
* @param string $sheetName Name of the sheet
|
||||||
|
*/
|
||||||
|
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetIndex, $sheetName)
|
||||||
|
{
|
||||||
|
$this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper);
|
||||||
|
$this->index = $sheetIndex;
|
||||||
|
$this->name = $sheetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return \Box\Spout\Reader\XLSX\RowIterator
|
||||||
|
*/
|
||||||
|
public function getRowIterator()
|
||||||
|
{
|
||||||
|
return $this->rowIterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||||
|
*/
|
||||||
|
public function getIndex()
|
||||||
|
{
|
||||||
|
return $this->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return string Name of the sheet
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?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\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, $sharedStringsHelper, $globalFunctionsHelper)
|
||||||
|
{
|
||||||
|
// Fetch all available sheets
|
||||||
|
$sheetHelper = new SheetHelper($filePath, $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 boolean
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (array_key_exists($this->currentSheetIndex, $this->sheets)) {
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,7 +4,9 @@ namespace Box\Spout\Writer;
|
|||||||
|
|
||||||
use Box\Spout\Common\Exception\IOException;
|
use Box\Spout\Common\Exception\IOException;
|
||||||
use Box\Spout\Common\Exception\InvalidArgumentException;
|
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||||
|
use Box\Spout\Writer\Exception\WriterAlreadyOpenedException;
|
||||||
use Box\Spout\Writer\Exception\WriterNotOpenedException;
|
use Box\Spout\Writer\Exception\WriterNotOpenedException;
|
||||||
|
use Box\Spout\Writer\Style\StyleBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AbstractWriter
|
* Class AbstractWriter
|
||||||
@@ -26,6 +28,12 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||||
protected $globalFunctionsHelper;
|
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 */
|
/** @var string Content-Type value for the header - to be defined by child class */
|
||||||
protected static $headerContentType;
|
protected static $headerContentType;
|
||||||
|
|
||||||
@@ -39,13 +47,13 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds data to the currently openned writer.
|
* Adds data to the currently openned writer.
|
||||||
* The data must be UTF-8 encoded.
|
|
||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be streamed.
|
* @param array $dataRow Array containing data to be streamed.
|
||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||||
|
* @param Style\Style $style Style to be applied to the written row
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
abstract protected function addRowToWriter(array $dataRow);
|
abstract protected function addRowToWriter(array $dataRow, $style);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the streamer, preventing any additional writing.
|
* Closes the streamer, preventing any additional writing.
|
||||||
@@ -55,7 +63,16 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
abstract protected function closeWriter();
|
abstract protected function closeWriter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $globalFunctionsHelper
|
*
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->defaultRowStyle = $this->getDefaultRowStyle();
|
||||||
|
$this->resetRowStyleToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
*/
|
*/
|
||||||
public function setGlobalFunctionsHelper($globalFunctionsHelper)
|
public function setGlobalFunctionsHelper($globalFunctionsHelper)
|
||||||
@@ -68,8 +85,9 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* Inits the writer and opens it to accept data.
|
* Inits the writer and opens it to accept data.
|
||||||
* By using this method, the data will be written to a file.
|
* 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
|
* @param string $outputFilePath Path of the output file that will contain the data
|
||||||
* @return \Box\Spout\Writer\AbstractWriter
|
* @return AbstractWriter
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
|
||||||
*/
|
*/
|
||||||
public function openToFile($outputFilePath)
|
public function openToFile($outputFilePath)
|
||||||
@@ -89,8 +107,11 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* Inits the writer and opens it to accept data.
|
* Inits the writer and opens it to accept data.
|
||||||
* By using this method, the data will be outputted directly to the browser.
|
* 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
|
* @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 \Box\Spout\Writer\AbstractWriter
|
* @return AbstractWriter
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
||||||
*/
|
*/
|
||||||
public function openToBrowser($outputFileName)
|
public function openToBrowser($outputFileName)
|
||||||
@@ -134,15 +155,29 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Write given data to the output. New data will be appended to end of stream.
|
||||||
* The data must be UTF-8 encoded.
|
|
||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be streamed.
|
* @param array $dataRow Array containing data to be streamed.
|
||||||
* If empty, no data is added (i.e. not even as a blank row)
|
* If empty, no data is added (i.e. not even as a blank row)
|
||||||
* Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
|
* Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
|
||||||
*
|
* @api
|
||||||
* @return \Box\Spout\Writer\AbstractWriter
|
* @return AbstractWriter
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
* @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\IOException If unable to write data
|
||||||
*/
|
*/
|
||||||
@@ -151,7 +186,7 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
if ($this->isWriterOpened) {
|
if ($this->isWriterOpened) {
|
||||||
// empty $dataRow should not add an empty line
|
// empty $dataRow should not add an empty line
|
||||||
if (!empty($dataRow)) {
|
if (!empty($dataRow)) {
|
||||||
$this->addRowToWriter($dataRow);
|
$this->addRowToWriter($dataRow, $this->rowStyle);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
|
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
|
||||||
@@ -161,17 +196,41 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write given data to the output. New data will be appended to end of stream.
|
* Write given data to the output and apply the given style.
|
||||||
* The data must be UTF-8 encoded.
|
* @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.
|
* @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)
|
* If a row is empty, it won't be added (i.e. not even as a blank row)
|
||||||
* Example: $dataRows = [
|
* Example: $dataRows = [
|
||||||
* ['data11', 12, , '', 'data13'],
|
* ['data11', 12, , '', 'data13'],
|
||||||
* ['data21', 'data22', null, false],
|
* ['data21', 'data22', null, false],
|
||||||
* ];
|
* ];
|
||||||
*
|
* @return AbstractWriter
|
||||||
* @return \Box\Spout\Writer\AbstractWriter
|
|
||||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
* @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\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\IOException If unable to write data
|
||||||
@@ -191,10 +250,70 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
return $this;
|
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
|
* Closes the writer. This will close the streamer as well, preventing new data
|
||||||
* to be written to the file.
|
* to be written to the file.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Box\Spout\Writer;
|
namespace Box\Spout\Writer\CSV;
|
||||||
|
|
||||||
|
use Box\Spout\Writer\AbstractWriter;
|
||||||
use Box\Spout\Common\Exception\IOException;
|
use Box\Spout\Common\Exception\IOException;
|
||||||
|
use Box\Spout\Common\Helper\EncodingHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class CSV
|
* Class Writer
|
||||||
* This class provides support to write data to CSV files
|
* This class provides support to write data to CSV files
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Writer
|
* @package Box\Spout\Writer\CSV
|
||||||
*/
|
*/
|
||||||
class CSV extends AbstractWriter
|
class Writer extends AbstractWriter
|
||||||
{
|
{
|
||||||
/** Number of rows to write before flushing */
|
/** Number of rows to write before flushing */
|
||||||
const FLUSH_THRESHOLD = 500;
|
const FLUSH_THRESHOLD = 500;
|
||||||
const UTF8_BOM = "\xEF\xBB\xBF";
|
|
||||||
|
|
||||||
/** @var string Content-Type value for the header */
|
/** @var string Content-Type value for the header */
|
||||||
protected static $headerContentType = 'text/csv; charset=UTF-8';
|
protected static $headerContentType = 'text/csv; charset=UTF-8';
|
||||||
@@ -31,8 +32,9 @@ class CSV extends AbstractWriter
|
|||||||
/**
|
/**
|
||||||
* Sets the field delimiter for the CSV
|
* Sets the field delimiter for the CSV
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $fieldDelimiter Character that delimits fields
|
* @param string $fieldDelimiter Character that delimits fields
|
||||||
* @return CSV
|
* @return Writer
|
||||||
*/
|
*/
|
||||||
public function setFieldDelimiter($fieldDelimiter)
|
public function setFieldDelimiter($fieldDelimiter)
|
||||||
{
|
{
|
||||||
@@ -43,8 +45,9 @@ class CSV extends AbstractWriter
|
|||||||
/**
|
/**
|
||||||
* Sets the field enclosure for the CSV
|
* Sets the field enclosure for the CSV
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $fieldEnclosure Character that enclose fields
|
* @param string $fieldEnclosure Character that enclose fields
|
||||||
* @return CSV
|
* @return Writer
|
||||||
*/
|
*/
|
||||||
public function setFieldEnclosure($fieldEnclosure)
|
public function setFieldEnclosure($fieldEnclosure)
|
||||||
{
|
{
|
||||||
@@ -60,7 +63,7 @@ class CSV extends AbstractWriter
|
|||||||
protected function openWriter()
|
protected function openWriter()
|
||||||
{
|
{
|
||||||
// Adds UTF-8 BOM for Unicode compatibility
|
// Adds UTF-8 BOM for Unicode compatibility
|
||||||
$this->globalFunctionsHelper->fputs($this->filePointer, self::UTF8_BOM);
|
$this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,12 +71,13 @@ class CSV extends AbstractWriter
|
|||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be written.
|
* @param array $dataRow Array containing data to be written.
|
||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||||
|
* @param \Box\Spout\Writer\Style\Style $style Ignored here since CSV does not support styling.
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||||
*/
|
*/
|
||||||
protected function addRowToWriter(array $dataRow)
|
protected function addRowToWriter(array $dataRow, $style)
|
||||||
{
|
{
|
||||||
$wasWriteSuccessful = fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
|
$wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||||
if ($wasWriteSuccessful === false) {
|
if ($wasWriteSuccessful === false) {
|
||||||
throw new IOException('Unable to write data');
|
throw new IOException('Unable to write data');
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<?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();
|
||||||
|
return array_key_exists($serializedStyle, $this->serializedStyleToStyleIdMappingTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->shouldWrapText()) {
|
||||||
|
return $style;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($dataRow as $cell) {
|
||||||
|
if (is_string($cell) && strpos($cell, "\n") !== false) {
|
||||||
|
$style->setShouldWrapText();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $style;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
$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);
|
||||||
|
|
||||||
|
return $cellIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zips the root folder and streams the contents of the zip into the given stream
|
||||||
|
*
|
||||||
|
* @param string $folderPath Path to the folder to be zipped
|
||||||
|
* @param resource $streamPointer Pointer to the stream to copy the zip
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function zipFolderAndCopyToStream($folderPath, $streamPointer)
|
||||||
|
{
|
||||||
|
$zipFilePath = $this->getZipFilePath($folderPath);
|
||||||
|
$this->zipFolder($folderPath, $zipFilePath);
|
||||||
|
$this->copyZipToStream($zipFilePath, $streamPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $folderPathToZip Path to the folder to be zipped
|
||||||
|
* @return string Path where the zip file of the given folder will be created
|
||||||
|
*/
|
||||||
|
public function getZipFilePath($folderPathToZip)
|
||||||
|
{
|
||||||
|
return $folderPathToZip . self::ZIP_EXTENSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zips the given folder
|
||||||
|
*
|
||||||
|
* @param string $folderPath Path of the folder to be zipped
|
||||||
|
* @param string $destinationPath Path where the zip file will be created
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function zipFolder($folderPath, $destinationPath)
|
||||||
|
{
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($destinationPath, \ZipArchive::CREATE)) {
|
||||||
|
$this->addFolderToZip($zip, $folderPath);
|
||||||
|
$zip->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \ZipArchive $zip
|
||||||
|
* @param string $folderPath Path of the folder to add to the zip
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addFolderToZip($zip, $folderPath)
|
||||||
|
{
|
||||||
|
$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()) {
|
||||||
|
$zip->addFile($itemRealPath, $itemLocalPath);
|
||||||
|
} else if ($itemInfo->isDir()) {
|
||||||
|
$zip->addEmptyDir($itemLocalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the contents of the zip file into the given stream
|
||||||
|
*
|
||||||
|
* @param string $zipFilePath Path to the zip file
|
||||||
|
* @param resource $pointer Pointer to the stream to copy the zip
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function copyZipToStream($zipFilePath, $pointer)
|
||||||
|
{
|
||||||
|
$zipFilePointer = fopen($zipFilePath, 'r');
|
||||||
|
stream_copy_to_stream($zipFilePointer, $pointer);
|
||||||
|
fclose($zipFilePointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-91
@@ -1,87 +1,60 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Box\Spout\Writer\Internal\XLSX;
|
namespace Box\Spout\Writer\Common\Internal;
|
||||||
|
|
||||||
use Box\Spout\Writer\Exception\SheetNotFoundException;
|
use Box\Spout\Writer\Exception\SheetNotFoundException;
|
||||||
use Box\Spout\Writer\Helper\XLSX\FileSystemHelper;
|
|
||||||
use Box\Spout\Writer\Helper\XLSX\SharedStringsHelper;
|
|
||||||
use Box\Spout\Writer\Sheet;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Book
|
* Class Workbook
|
||||||
* Represents a workbook within a XLSX file.
|
* Represents a workbook within a spreadsheet file.
|
||||||
* It provides the functions to work with worksheets.
|
* It provides the functions to work with worksheets.
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Writer\Internal\XLSX
|
* @package Box\Spout\Writer\Common
|
||||||
*/
|
*/
|
||||||
class Workbook
|
abstract class AbstractWorkbook implements WorkbookInterface
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Maximum number of rows a XLSX sheet can contain
|
|
||||||
* @see http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx
|
|
||||||
*/
|
|
||||||
protected static $maxRowsPerWorksheet = 1048576;
|
|
||||||
|
|
||||||
/** @var bool Whether inline or shared strings should be used */
|
|
||||||
protected $shouldUseInlineStrings;
|
|
||||||
|
|
||||||
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
|
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
|
||||||
protected $shouldCreateNewSheetsAutomatically;
|
protected $shouldCreateNewSheetsAutomatically;
|
||||||
|
|
||||||
/** @var \Box\Spout\Writer\Helper\XLSX\FileSystemHelper Helper to perform file system operations */
|
/** @var WorksheetInterface[] Array containing the workbook's sheets */
|
||||||
protected $fileSystemHelper;
|
|
||||||
|
|
||||||
/** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
|
|
||||||
protected $sharedStringsHelper;
|
|
||||||
|
|
||||||
/** @var Worksheet[] Array containing the workbook's sheets */
|
|
||||||
protected $worksheets = [];
|
protected $worksheets = [];
|
||||||
|
|
||||||
/** @var Worksheet The worksheet where data will be written to */
|
/** @var WorksheetInterface The worksheet where data will be written to */
|
||||||
protected $currentWorksheet;
|
protected $currentWorksheet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $tempFolder
|
|
||||||
* @param bool $shouldUseInlineStrings
|
|
||||||
* @param bool $shouldCreateNewSheetsAutomatically
|
* @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
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
|
||||||
*/
|
*/
|
||||||
public function __construct($tempFolder, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically)
|
public function __construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle)
|
||||||
{
|
{
|
||||||
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
|
||||||
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
|
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
|
||||||
|
|
||||||
$this->fileSystemHelper = new FileSystemHelper($tempFolder);
|
|
||||||
$this->fileSystemHelper->createBaseFilesAndFolders();
|
|
||||||
|
|
||||||
// This helper will be shared by all sheets
|
|
||||||
$xlFolder = $this->fileSystemHelper->getXlFolder();
|
|
||||||
$this->sharedStringsHelper = new SharedStringsHelper($xlFolder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.
|
* Creates a new sheet in the workbook. The current sheet remains unchanged.
|
||||||
*
|
*
|
||||||
* @return Worksheet The created sheet
|
* @return WorksheetInterface The created sheet
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
||||||
*/
|
*/
|
||||||
public function addNewSheet()
|
abstract public function addNewSheet();
|
||||||
{
|
|
||||||
$newSheetNumber = count($this->worksheets);
|
|
||||||
$sheet = new Sheet($newSheetNumber);
|
|
||||||
|
|
||||||
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
|
|
||||||
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings);
|
|
||||||
$this->worksheets[] = $worksheet;
|
|
||||||
|
|
||||||
return $worksheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sheet in the workbook and make it the current sheet.
|
* 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).
|
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||||
*
|
*
|
||||||
* @return Worksheet The created sheet
|
* @return WorksheetInterface The created sheet
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
|
||||||
*/
|
*/
|
||||||
public function addNewSheetAndMakeItCurrent()
|
public function addNewSheetAndMakeItCurrent()
|
||||||
@@ -93,7 +66,7 @@ class Workbook
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Worksheet[] All the workbook's sheets
|
* @return WorksheetInterface[] All the workbook's sheets
|
||||||
*/
|
*/
|
||||||
public function getWorksheets()
|
public function getWorksheets()
|
||||||
{
|
{
|
||||||
@@ -103,7 +76,7 @@ class Workbook
|
|||||||
/**
|
/**
|
||||||
* Returns the current sheet
|
* Returns the current sheet
|
||||||
*
|
*
|
||||||
* @return Worksheet The current sheet
|
* @return WorksheetInterface The current sheet
|
||||||
*/
|
*/
|
||||||
public function getCurrentWorksheet()
|
public function getCurrentWorksheet()
|
||||||
{
|
{
|
||||||
@@ -114,7 +87,7 @@ class Workbook
|
|||||||
* Sets the given sheet as the current one. New data will be written to this sheet.
|
* 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).
|
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||||
*
|
*
|
||||||
* @param \Box\Spout\Writer\Sheet $sheet The "external" sheet to set as current
|
* @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
|
* @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
|
||||||
*/
|
*/
|
||||||
@@ -129,7 +102,7 @@ class Workbook
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Worksheet $worksheet
|
* @param WorksheetInterface $worksheet
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function setCurrentWorksheet($worksheet)
|
protected function setCurrentWorksheet($worksheet)
|
||||||
@@ -140,15 +113,15 @@ class Workbook
|
|||||||
/**
|
/**
|
||||||
* Returns the worksheet associated to the given external sheet.
|
* Returns the worksheet associated to the given external sheet.
|
||||||
*
|
*
|
||||||
* @param \Box\Spout\Writer\Sheet $sheet
|
* @param \Box\Spout\Writer\Common\Sheet $sheet
|
||||||
* @return Worksheet|null The worksheet associated to the given external sheet or null if not found.
|
* @return WorksheetInterface|null The worksheet associated to the given external sheet or null if not found.
|
||||||
*/
|
*/
|
||||||
protected function getWorksheetFromExternalSheet($sheet)
|
protected function getWorksheetFromExternalSheet($sheet)
|
||||||
{
|
{
|
||||||
$worksheetFound = null;
|
$worksheetFound = null;
|
||||||
|
|
||||||
foreach ($this->worksheets as $worksheet) {
|
foreach ($this->worksheets as $worksheet) {
|
||||||
if ($worksheet->getExternalSheet() == $sheet) {
|
if ($worksheet->getExternalSheet() === $sheet) {
|
||||||
$worksheetFound = $worksheet;
|
$worksheetFound = $worksheet;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -162,28 +135,35 @@ class Workbook
|
|||||||
* If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
|
* 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.
|
* with the creation of new worksheets if one worksheet has reached its maximum capicity.
|
||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be written.
|
* @param array $dataRow Array containing data to be written. Cannot be empty.
|
||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||||
|
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
|
||||||
* @return void
|
* @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\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
|
* @throws \Box\Spout\Writer\Exception\WriterException If unable to write data
|
||||||
*/
|
*/
|
||||||
public function addRowToCurrentWorksheet($dataRow)
|
public function addRowToCurrentWorksheet($dataRow, $style)
|
||||||
{
|
{
|
||||||
$currentWorksheet = $this->getCurrentWorksheet();
|
$currentWorksheet = $this->getCurrentWorksheet();
|
||||||
$hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows();
|
$hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows();
|
||||||
|
$styleHelper = $this->getStyleHelper();
|
||||||
|
|
||||||
// if we reached the maximum number of rows for the current sheet...
|
// if we reached the maximum number of rows for the current sheet...
|
||||||
if ($hasReachedMaxRows) {
|
if ($hasReachedMaxRows) {
|
||||||
// ... continue writing in a new sheet if option set
|
// ... continue writing in a new sheet if option set
|
||||||
if ($this->shouldCreateNewSheetsAutomatically) {
|
if ($this->shouldCreateNewSheetsAutomatically) {
|
||||||
$currentWorksheet = $this->addNewSheetAndMakeItCurrent();
|
$currentWorksheet = $this->addNewSheetAndMakeItCurrent();
|
||||||
$currentWorksheet->addRow($dataRow);
|
|
||||||
|
$updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
|
||||||
|
$registeredStyle = $styleHelper->registerStyle($updatedStyle);
|
||||||
|
$currentWorksheet->addRow($dataRow, $registeredStyle);
|
||||||
} else {
|
} else {
|
||||||
// otherwise, do nothing as the data won't be read anyways
|
// otherwise, do nothing as the data won't be read anyways
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$currentWorksheet->addRow($dataRow);
|
$updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
|
||||||
|
$registeredStyle = $styleHelper->registerStyle($updatedStyle);
|
||||||
|
$currentWorksheet->addRow($dataRow, $registeredStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,43 +173,16 @@ class Workbook
|
|||||||
protected function hasCurrentWorkseetReachedMaxRows()
|
protected function hasCurrentWorkseetReachedMaxRows()
|
||||||
{
|
{
|
||||||
$currentWorksheet = $this->getCurrentWorksheet();
|
$currentWorksheet = $this->getCurrentWorksheet();
|
||||||
return ($currentWorksheet->getLastWrittenRowIndex() >= self::$maxRowsPerWorksheet);
|
return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the workbook and all its associated sheets.
|
* Closes the workbook and all its associated sheets.
|
||||||
* All the necessary files are written to disk and zipped together to create the XLSX file.
|
* All the necessary files are written to disk and zipped together to create the ODS file.
|
||||||
* All the temporary files are then deleted.
|
* All the temporary files are then deleted.
|
||||||
*
|
*
|
||||||
* @param resource $finalFilePointer Pointer to the XLSX that will be created
|
* @param resource $finalFilePointer Pointer to the ODS that will be created
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function close($finalFilePointer)
|
abstract public function close($finalFilePointer);
|
||||||
{
|
|
||||||
foreach ($this->worksheets as $worksheet) {
|
|
||||||
$worksheet->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->sharedStringsHelper->close();
|
|
||||||
|
|
||||||
// Finish creating all the necessary files before zipping everything together
|
|
||||||
$this->fileSystemHelper
|
|
||||||
->createContentTypesFile($this->worksheets)
|
|
||||||
->createWorkbookFile($this->worksheets)
|
|
||||||
->createWorkbookRelsFile($this->worksheets)
|
|
||||||
->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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<?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 within a ODS file
|
||||||
|
*
|
||||||
|
* @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 [SHEET_INDEX] => [SHEET_NAME] keeping track of sheets' name to enforce uniqueness */
|
||||||
|
protected static $SHEETS_NAME_USED = [];
|
||||||
|
|
||||||
|
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||||
|
protected $index;
|
||||||
|
|
||||||
|
/** @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 of creation (zero-based)
|
||||||
|
*/
|
||||||
|
public function __construct($sheetIndex)
|
||||||
|
{
|
||||||
|
$this->index = $sheetIndex;
|
||||||
|
$this->stringHelper = new StringHelper();
|
||||||
|
$this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
* @return int Index of the sheet, based on order of creation (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)
|
||||||
|
{
|
||||||
|
if (!$this->isNameValid($name)) {
|
||||||
|
$errorMessage = "The sheet's name is invalid. It did not meet at least one of these requirements:\n";
|
||||||
|
$errorMessage .= " - It should not be blank\n";
|
||||||
|
$errorMessage .= " - It should not exceed 31 characters\n";
|
||||||
|
$errorMessage .= " - It should not contain these characters: \\ / ? * : [ or ]\n";
|
||||||
|
$errorMessage .= " - It should be unique";
|
||||||
|
throw new InvalidSheetNameException($errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->name = $name;
|
||||||
|
self::$SHEETS_NAME_USED[$this->index] = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given sheet's name is valid.
|
||||||
|
* @see Sheet::setName for validity rules.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return bool TRUE if the name is valid, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
protected function isNameValid($name)
|
||||||
|
{
|
||||||
|
if (!is_string($name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nameLength = $this->stringHelper->getStringLength($name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
$nameLength > 0 &&
|
||||||
|
$nameLength <= self::MAX_LENGTH_SHEET_NAME &&
|
||||||
|
!$this->doesContainInvalidCharacters($name) &&
|
||||||
|
$this->isNameUnique($name) &&
|
||||||
|
!$this->doesStartOrEndWithSingleQuote($name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 as $sheetIndex => $sheetName) {
|
||||||
|
if ($sheetIndex !== $this->index && $sheetName === $name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class InvalidColorException
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @package Box\Spout\Writer\Exception
|
||||||
|
*/
|
||||||
|
class InvalidColorException extends WriterException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class InvalidSheetNameException
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @package Box\Spout\Writer\Exception
|
||||||
|
*/
|
||||||
|
class InvalidSheetNameException extends WriterException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class SheetNotFoundException
|
* Class SheetNotFoundException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Writer\Exception
|
* @package Box\Spout\Writer\Exception
|
||||||
*/
|
*/
|
||||||
class SheetNotFoundException extends WriterException
|
class SheetNotFoundException extends WriterException
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class WriterAlreadyOpenedException
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @package Box\Spout\Writer\Exception
|
||||||
|
*/
|
||||||
|
class WriterAlreadyOpenedException extends WriterException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class WriterNotOpenedException
|
* Class WriterNotOpenedException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Writer\Exception
|
* @package Box\Spout\Writer\Exception
|
||||||
*/
|
*/
|
||||||
class WriterNotOpenedException extends WriterException
|
class WriterNotOpenedException extends WriterException
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Writer\Helper\XLSX;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class CellHelper
|
|
||||||
* This class provides helper functions when working with cells
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Writer\Helper\XLSX
|
|
||||||
*/
|
|
||||||
class CellHelper
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
{
|
|
||||||
$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);
|
|
||||||
|
|
||||||
return $cellIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Writer\Helper\XLSX;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class ZipHelper
|
|
||||||
* This class provides helper functions to create zip files
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Writer\Helper\XLSX
|
|
||||||
*/
|
|
||||||
class ZipHelper
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Zips the given folder
|
|
||||||
*
|
|
||||||
* @param string $folderPath Path of the folder to be zipped
|
|
||||||
* @param string $destinationPath Path where the zip file will be created
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function zipFolder($folderPath, $destinationPath)
|
|
||||||
{
|
|
||||||
$zip = new \ZipArchive();
|
|
||||||
if ($zip->open($destinationPath, \ZipArchive::CREATE)) {
|
|
||||||
$this->addFolderToZip($zip, $folderPath);
|
|
||||||
$zip->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \ZipArchive $zip
|
|
||||||
* @param string $folderPath Path of the folder to add to the zip
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function addFolderToZip($zip, $folderPath)
|
|
||||||
{
|
|
||||||
$folderRealPath = realpath($folderPath) . DIRECTORY_SEPARATOR;
|
|
||||||
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
|
|
||||||
|
|
||||||
foreach ($itemIterator as $itemInfo) {
|
|
||||||
$itemRealPath = realpath($itemInfo->getPathname());
|
|
||||||
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
|
|
||||||
|
|
||||||
if ($itemInfo->isFile()) {
|
|
||||||
$zip->addFile($itemInfo->getPathname(), $itemLocalPath);
|
|
||||||
} else if ($itemInfo->isDir()) {
|
|
||||||
$zip->addEmptyDir($itemLocalPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
<?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->baseFolderPath, 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" standalone="yes"?>
|
||||||
|
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
|
||||||
|
<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:file-entry manifest:full-path="mimetype" manifest:media-type="text/plain"/>
|
||||||
|
<manifest:file-entry manifest:full-path="META-INF/manifest.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();
|
||||||
|
$zipHelper->zipFolderAndCopyToStream($this->rootFolder, $streamPointer);
|
||||||
|
|
||||||
|
// once the zip is copied, remove it
|
||||||
|
$this->deleteFile($zipHelper->getZipFilePath($this->rootFolder));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\ODS\Helper;
|
||||||
|
|
||||||
|
use Box\Spout\Writer\Common\Helper\AbstractStyleHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
$defaultStyle = $this->getDefaultStyle();
|
||||||
|
$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">';
|
||||||
|
|
||||||
|
if ($style->shouldApplyFont()) {
|
||||||
|
$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 .= '/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($style->shouldWrapText()) {
|
||||||
|
$content .= '<style:table-cell-properties fo:wrap-option="wrap" style:vertical-align="automatic"/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$content .= '</style:style>';
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?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);
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<?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 XLSX 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 = new \Box\Spout\Common\Escaper\ODS();
|
||||||
|
$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)
|
||||||
|
{
|
||||||
|
$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 = $dataRow[$currentCellIndex];
|
||||||
|
|
||||||
|
if (!array_key_exists($nextCellIndex, $dataRow) || $currentCellValue !== $dataRow[$nextCellIndex]) {
|
||||||
|
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
|
||||||
|
$data .= $this->getCellContent($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 getCellContent($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()
|
||||||
|
{
|
||||||
|
fclose($this->sheetFilePointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?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 XLSX 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,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Writer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Sheet
|
|
||||||
* Represents a worksheet within a XLSX file
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Writer
|
|
||||||
*/
|
|
||||||
class Sheet
|
|
||||||
{
|
|
||||||
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
|
|
||||||
|
|
||||||
/** @var int Number of the sheet, based on order of creation (zero-based) */
|
|
||||||
protected $sheetNumber;
|
|
||||||
|
|
||||||
/** @var string Name of the sheet */
|
|
||||||
protected $name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $sheetNumber Number of the sheet, based on order of creation (zero-based)
|
|
||||||
*/
|
|
||||||
function __construct($sheetNumber)
|
|
||||||
{
|
|
||||||
$this->sheetNumber = $sheetNumber;
|
|
||||||
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetNumber + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int Number of the sheet, based on order of creation (zero-based)
|
|
||||||
*/
|
|
||||||
public function getSheetNumber()
|
|
||||||
{
|
|
||||||
return $this->sheetNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string Name of the sheet
|
|
||||||
*/
|
|
||||||
public function getName()
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\Style;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Style
|
||||||
|
* Represents a style to be applied to a cell
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Writer\Style
|
||||||
|
*/
|
||||||
|
class Style
|
||||||
|
{
|
||||||
|
/** Default font values */
|
||||||
|
const DEFAULT_FONT_SIZE = 11;
|
||||||
|
const DEFAULT_FONT_COLOR = Color::BLACK;
|
||||||
|
const DEFAULT_FONT_NAME = 'Arial';
|
||||||
|
|
||||||
|
/** @var int|null Style ID */
|
||||||
|
protected $id = null;
|
||||||
|
|
||||||
|
/** @var bool Whether the font should be bold */
|
||||||
|
protected $fontBold = false;
|
||||||
|
/** @var bool Whether the bold property was set */
|
||||||
|
protected $hasSetFontBold = false;
|
||||||
|
|
||||||
|
/** @var bool Whether the font should be italic */
|
||||||
|
protected $fontItalic = false;
|
||||||
|
/** @var bool Whether the italic property was set */
|
||||||
|
protected $hasSetFontItalic = false;
|
||||||
|
|
||||||
|
/** @var bool Whether the font should be underlined */
|
||||||
|
protected $fontUnderline = false;
|
||||||
|
/** @var bool Whether the underline property was set */
|
||||||
|
protected $hasSetFontUnderline = false;
|
||||||
|
|
||||||
|
/** @var bool Whether the font should be struck through */
|
||||||
|
protected $fontStrikethrough = false;
|
||||||
|
/** @var bool Whether the strikethrough property was set */
|
||||||
|
protected $hasSetFontStrikethrough = false;
|
||||||
|
|
||||||
|
/** @var int Font size */
|
||||||
|
protected $fontSize = self::DEFAULT_FONT_SIZE;
|
||||||
|
/** @var bool Whether the font size property was set */
|
||||||
|
protected $hasSetFontSize = false;
|
||||||
|
|
||||||
|
/** @var string Font color */
|
||||||
|
protected $fontColor = self::DEFAULT_FONT_COLOR;
|
||||||
|
/** @var bool Whether the font color property was set */
|
||||||
|
protected $hasSetFontColor = false;
|
||||||
|
|
||||||
|
/** @var string Font name */
|
||||||
|
protected $fontName = self::DEFAULT_FONT_NAME;
|
||||||
|
/** @var bool Whether the font name property was set */
|
||||||
|
protected $hasSetFontName = false;
|
||||||
|
|
||||||
|
/** @var bool Whether specific font properties should be applied */
|
||||||
|
protected $shouldApplyFont = false;
|
||||||
|
|
||||||
|
/** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */
|
||||||
|
protected $shouldWrapText = false;
|
||||||
|
/** @var bool Whether the wrap text property was set */
|
||||||
|
protected $hasSetWrapText = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public function getId()
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $id
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setId($id)
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function isFontBold()
|
||||||
|
{
|
||||||
|
return $this->fontBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFontBold()
|
||||||
|
{
|
||||||
|
$this->fontBold = true;
|
||||||
|
$this->hasSetFontBold = true;
|
||||||
|
$this->shouldApplyFont = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function isFontItalic()
|
||||||
|
{
|
||||||
|
return $this->fontItalic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFontItalic()
|
||||||
|
{
|
||||||
|
$this->fontItalic = true;
|
||||||
|
$this->hasSetFontItalic = true;
|
||||||
|
$this->shouldApplyFont = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function isFontUnderline()
|
||||||
|
{
|
||||||
|
return $this->fontUnderline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFontUnderline()
|
||||||
|
{
|
||||||
|
$this->fontUnderline = true;
|
||||||
|
$this->hasSetFontUnderline = true;
|
||||||
|
$this->shouldApplyFont = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function isFontStrikethrough()
|
||||||
|
{
|
||||||
|
return $this->fontStrikethrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFontStrikethrough()
|
||||||
|
{
|
||||||
|
$this->fontStrikethrough = true;
|
||||||
|
$this->hasSetFontStrikethrough = true;
|
||||||
|
$this->shouldApplyFont = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getFontSize()
|
||||||
|
{
|
||||||
|
return $this->fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $fontSize Font size, in pixels
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFontSize($fontSize)
|
||||||
|
{
|
||||||
|
$this->fontSize = $fontSize;
|
||||||
|
$this->hasSetFontSize = true;
|
||||||
|
$this->shouldApplyFont = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFontColor()
|
||||||
|
{
|
||||||
|
return $this->fontColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the font color.
|
||||||
|
*
|
||||||
|
* @param string $fontColor ARGB color (@see Color)
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFontColor($fontColor)
|
||||||
|
{
|
||||||
|
$this->fontColor = $fontColor;
|
||||||
|
$this->hasSetFontColor = true;
|
||||||
|
$this->shouldApplyFont = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFontName()
|
||||||
|
{
|
||||||
|
return $this->fontName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $fontName Name of the font to use
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFontName($fontName)
|
||||||
|
{
|
||||||
|
$this->fontName = $fontName;
|
||||||
|
$this->hasSetFontName = true;
|
||||||
|
$this->shouldApplyFont = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function shouldWrapText()
|
||||||
|
{
|
||||||
|
return $this->shouldWrapText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setShouldWrapText()
|
||||||
|
{
|
||||||
|
$this->shouldWrapText = true;
|
||||||
|
$this->hasSetWrapText = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool Whether specific font properties should be applied
|
||||||
|
*/
|
||||||
|
public function shouldApplyFont()
|
||||||
|
{
|
||||||
|
return $this->shouldApplyFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the style for future comparison with other styles.
|
||||||
|
* The ID is excluded from the comparison, as we only care about
|
||||||
|
* actual style properties.
|
||||||
|
*
|
||||||
|
* @return string The serialized style
|
||||||
|
*/
|
||||||
|
public function serialize()
|
||||||
|
{
|
||||||
|
// In order to be able to properly compare style, set static ID value
|
||||||
|
$currentId = $this->id;
|
||||||
|
$this->setId(0);
|
||||||
|
|
||||||
|
$serializedStyle = serialize($this);
|
||||||
|
|
||||||
|
$this->setId($currentId);
|
||||||
|
|
||||||
|
return $serializedStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the current style with the given style, using the given style as a base. This means that:
|
||||||
|
* - if current style and base style both have property A set, use current style property's value
|
||||||
|
* - if current style has property A set but base style does not, use current style property's value
|
||||||
|
* - if base style has property A set but current style does not, use base style property's value
|
||||||
|
*
|
||||||
|
* @NOTE: This function returns a new style.
|
||||||
|
*
|
||||||
|
* @param Style $baseStyle
|
||||||
|
* @return Style New style corresponding to the merge of the 2 styles
|
||||||
|
*/
|
||||||
|
public function mergeWith($baseStyle)
|
||||||
|
{
|
||||||
|
$mergedStyle = clone $this;
|
||||||
|
|
||||||
|
if (!$this->hasSetFontBold && $baseStyle->isFontBold()) {
|
||||||
|
$mergedStyle->setFontBold();
|
||||||
|
}
|
||||||
|
if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) {
|
||||||
|
$mergedStyle->setFontItalic();
|
||||||
|
}
|
||||||
|
if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) {
|
||||||
|
$mergedStyle->setFontUnderline();
|
||||||
|
}
|
||||||
|
if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) {
|
||||||
|
$mergedStyle->setFontStrikethrough();
|
||||||
|
}
|
||||||
|
if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
|
||||||
|
$mergedStyle->setFontSize($baseStyle->getFontSize());
|
||||||
|
}
|
||||||
|
if (!$this->hasSetFontSize && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) {
|
||||||
|
$mergedStyle->setFontColor($baseStyle->getFontColor());
|
||||||
|
}
|
||||||
|
if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
|
||||||
|
$mergedStyle->setFontName($baseStyle->getFontName());
|
||||||
|
}
|
||||||
|
if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) {
|
||||||
|
$mergedStyle->setShouldWrapText();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mergedStyle;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\Style;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class StyleBuilder
|
||||||
|
* Builder to create new styles
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Writer\Style
|
||||||
|
*/
|
||||||
|
class StyleBuilder
|
||||||
|
{
|
||||||
|
/** @var Style Style to be created */
|
||||||
|
protected $style;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->style = new Style();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the font bold.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFontBold()
|
||||||
|
{
|
||||||
|
$this->style->setFontBold();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the font italic.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFontItalic()
|
||||||
|
{
|
||||||
|
$this->style->setFontItalic();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the font underlined.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFontUnderline()
|
||||||
|
{
|
||||||
|
$this->style->setFontUnderline();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the font struck through.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFontStrikethrough()
|
||||||
|
{
|
||||||
|
$this->style->setFontStrikethrough();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the font size.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param int $fontSize Font size, in pixels
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFontSize($fontSize)
|
||||||
|
{
|
||||||
|
$this->style->setFontSize($fontSize);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the font color.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param string $fontColor ARGB color (@see Color)
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFontColor($fontColor)
|
||||||
|
{
|
||||||
|
$this->style->setFontColor($fontColor);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the font name.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param string $fontName Name of the font to use
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFontName($fontName)
|
||||||
|
{
|
||||||
|
$this->style->setFontName($fontName);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the text wrap in the cell if it's too long or
|
||||||
|
* on multiple lines.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setShouldWrapText()
|
||||||
|
{
|
||||||
|
$this->style->setShouldWrapText();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured style. The style is cached and can be reused.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
return $this->style;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use Box\Spout\Common\Type;
|
|||||||
/**
|
/**
|
||||||
* Class WriterFactory
|
* Class WriterFactory
|
||||||
* This factory is used to create writers, based on the type of the file to be read.
|
* This factory is used to create writers, based on the type of the file to be read.
|
||||||
* It supports CSV and XLSX formats.
|
* It supports CSV, XLSX and ODS formats.
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Writer
|
* @package Box\Spout\Writer
|
||||||
*/
|
*/
|
||||||
@@ -18,8 +18,9 @@ class WriterFactory
|
|||||||
/**
|
/**
|
||||||
* This creates an instance of the appropriate writer, given the type of the file to be read
|
* This creates an instance of the appropriate writer, given the type of the file to be read
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $writerType Type of the writer to instantiate
|
* @param string $writerType Type of the writer to instantiate
|
||||||
* @return \Box\Spout\Writer\CSV|\Box\Spout\Writer\XLSX
|
* @return WriterInterface
|
||||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||||
*/
|
*/
|
||||||
public static function create($writerType)
|
public static function create($writerType)
|
||||||
@@ -28,10 +29,13 @@ class WriterFactory
|
|||||||
|
|
||||||
switch ($writerType) {
|
switch ($writerType) {
|
||||||
case Type::CSV:
|
case Type::CSV:
|
||||||
$writer = new CSV();
|
$writer = new CSV\Writer();
|
||||||
break;
|
break;
|
||||||
case Type::XLSX:
|
case Type::XLSX:
|
||||||
$writer = new XLSX();
|
$writer = new XLSX\Writer();
|
||||||
|
break;
|
||||||
|
case Type::ODS:
|
||||||
|
$writer = new ODS\Writer();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType);
|
throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface WriterInterface
|
|||||||
* By using this method, the data will be written to a file.
|
* By using this method, the data will be written to a file.
|
||||||
*
|
*
|
||||||
* @param string $outputFilePath Path of the output file that will contain the data
|
* @param string $outputFilePath Path of the output file that will contain the data
|
||||||
* @return \Box\Spout\Writer\WriterInterface
|
* @return WriterInterface
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
|
||||||
*/
|
*/
|
||||||
public function openToFile($outputFilePath);
|
public function openToFile($outputFilePath);
|
||||||
@@ -24,7 +24,7 @@ interface WriterInterface
|
|||||||
* By using this method, the data will be outputted directly to the browser.
|
* By using this method, the data will be outputted directly to the browser.
|
||||||
*
|
*
|
||||||
* @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
|
* @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 \Box\Spout\Writer\WriterInterface
|
* @return WriterInterface
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
||||||
*/
|
*/
|
||||||
public function openToBrowser($outputFileName);
|
public function openToBrowser($outputFileName);
|
||||||
@@ -34,12 +34,25 @@ interface WriterInterface
|
|||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be streamed.
|
* @param array $dataRow Array containing data to be streamed.
|
||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||||
* @return \Box\Spout\Writer\WriterInterface
|
* @return WriterInterface
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yetthe writer
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yetthe writer
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||||
*/
|
*/
|
||||||
public function addRow(array $dataRow);
|
public function addRow(array $dataRow);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write given data to the output and apply the given style.
|
||||||
|
* @see addRow
|
||||||
|
*
|
||||||
|
* @param array $dataRow Array of array containing data to be streamed.
|
||||||
|
* @param Style\Style $style Style to be applied to the row.
|
||||||
|
* @return WriterInterface
|
||||||
|
* @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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write given data to the output. New data will be appended to end of stream.
|
* Write given data to the output. New data will be appended to end of stream.
|
||||||
*
|
*
|
||||||
@@ -48,13 +61,26 @@ interface WriterInterface
|
|||||||
* ['data11', 12, , '', 'data13'],
|
* ['data11', 12, , '', 'data13'],
|
||||||
* ['data21', 'data22', null],
|
* ['data21', 'data22', null],
|
||||||
* ];
|
* ];
|
||||||
* @return \Box\Spout\Writer\WriterInterface
|
* @return WriterInterface
|
||||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||||
*/
|
*/
|
||||||
public function addRows(array $dataRows);
|
public function addRows(array $dataRows);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write given data to the output and apply the given style.
|
||||||
|
* @see addRows
|
||||||
|
*
|
||||||
|
* @param array $dataRows Array of array containing data to be streamed.
|
||||||
|
* @param Style\Style $style Style to be applied to the rows.
|
||||||
|
* @return WriterInterface
|
||||||
|
* @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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the writer. This will close the streamer as well, preventing new data
|
* Closes the writer. This will close the streamer as well, preventing new data
|
||||||
* to be written to the file.
|
* to be written to the file.
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Writer;
|
|
||||||
|
|
||||||
use Box\Spout\Writer\Exception\WriterNotOpenedException;
|
|
||||||
use Box\Spout\Writer\Internal\XLSX\Workbook;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class XLSX
|
|
||||||
* This class provides base support to write data to XLSX files
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Writer
|
|
||||||
*/
|
|
||||||
class XLSX extends AbstractWriter
|
|
||||||
{
|
|
||||||
/** @var string Content-Type value for the header */
|
|
||||||
protected static $headerContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
||||||
|
|
||||||
/** @var string Temporary folder where the files to create the XLSX will be stored */
|
|
||||||
protected $tempFolder;
|
|
||||||
|
|
||||||
/** @var bool Whether inline or shared strings should be used - inline string is more memory efficient */
|
|
||||||
protected $shouldUseInlineStrings = true;
|
|
||||||
|
|
||||||
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
|
|
||||||
protected $shouldCreateNewSheetsAutomatically = true;
|
|
||||||
|
|
||||||
/** @var Internal\XLSX\Workbook The workbook for the XLSX file */
|
|
||||||
protected $book;
|
|
||||||
|
|
||||||
/** @var int */
|
|
||||||
protected $highestRowIndex = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
|
|
||||||
* @return XLSX
|
|
||||||
*/
|
|
||||||
public function setTempFolder($tempFolder)
|
|
||||||
{
|
|
||||||
$this->tempFolder = $tempFolder;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use inline string to be more memory efficient. If set to false, it will use shared strings.
|
|
||||||
*
|
|
||||||
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
|
||||||
* @return XLSX
|
|
||||||
*/
|
|
||||||
public function setShouldUseInlineStrings($shouldUseInlineStrings)
|
|
||||||
{
|
|
||||||
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
|
|
||||||
* @return XLSX
|
|
||||||
*/
|
|
||||||
public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically)
|
|
||||||
{
|
|
||||||
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
if (!$this->book) {
|
|
||||||
$tempFolder = ($this->tempFolder) ? : sys_get_temp_dir();
|
|
||||||
$this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically);
|
|
||||||
$this->book->addNewSheetAndMakeItCurrent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all the workbook's sheets
|
|
||||||
*
|
|
||||||
* @return 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->book->getWorksheets();
|
|
||||||
|
|
||||||
/** @var Internal\XLSX\Worksheet $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.
|
|
||||||
*
|
|
||||||
* @return 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->book->addNewSheetAndMakeItCurrent();
|
|
||||||
|
|
||||||
return $worksheet->getExternalSheet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current sheet
|
|
||||||
*
|
|
||||||
* @return 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->book->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).
|
|
||||||
*
|
|
||||||
* @param 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->book->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->book) {
|
|
||||||
throw new WriterNotOpenedException('The writer must be opened before performing this action.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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'];
|
|
||||||
* @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)
|
|
||||||
{
|
|
||||||
$this->throwIfBookIsNotAvailable();
|
|
||||||
$this->book->addRowToCurrentWorksheet($dataRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the writer, preventing any additional writing.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function closeWriter()
|
|
||||||
{
|
|
||||||
if ($this->book) {
|
|
||||||
$this->book->close($this->filePointer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+31
-47
@@ -1,15 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Box\Spout\Writer\Helper\XLSX;
|
namespace Box\Spout\Writer\XLSX\Helper;
|
||||||
|
|
||||||
use Box\Spout\Writer\Internal\XLSX\Worksheet;
|
use Box\Spout\Writer\Common\Helper\ZipHelper;
|
||||||
|
use Box\Spout\Writer\XLSX\Internal\Worksheet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class FileSystemHelper
|
* Class FileSystemHelper
|
||||||
* This class provides helper functions to help with the file system operations
|
* This class provides helper functions to help with the file system operations
|
||||||
* like files/folders creation & deletion for XLSX files
|
* like files/folders creation & deletion for XLSX files
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Writer\Helper\XLSX
|
* @package Box\Spout\Writer\XLSX\Helper
|
||||||
*/
|
*/
|
||||||
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
||||||
{
|
{
|
||||||
@@ -26,6 +27,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
|||||||
const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml';
|
const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml';
|
||||||
const WORKBOOK_XML_FILE_NAME = 'workbook.xml';
|
const WORKBOOK_XML_FILE_NAME = 'workbook.xml';
|
||||||
const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels';
|
const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels';
|
||||||
|
const STYLES_XML_FILE_NAME = 'styles.xml';
|
||||||
|
|
||||||
/** @var string Path to the root folder inside the temp folder where the files to create the XLSX will be stored */
|
/** @var string Path to the root folder inside the temp folder where the files to create the XLSX will be stored */
|
||||||
protected $rootFolder;
|
protected $rootFolder;
|
||||||
@@ -92,7 +94,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
|||||||
*/
|
*/
|
||||||
protected function createRootFolder()
|
protected function createRootFolder()
|
||||||
{
|
{
|
||||||
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx'));
|
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx', true));
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,10 +181,10 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
protected function createCoreXmlFile()
|
protected function createCoreXmlFile()
|
||||||
{
|
{
|
||||||
$createdDate = (new \DateTime())->format('c');
|
$createdDate = (new \DateTime())->format(\DateTime::W3C);
|
||||||
$coreXmlFileContents = <<<EOD
|
$coreXmlFileContents = <<<EOD
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<dcterms:created xsi:type="dcterms:W3CDTF">$createdDate</dcterms:created>
|
<dcterms:created xsi:type="dcterms:W3CDTF">$createdDate</dcterms:created>
|
||||||
<dcterms:modified xsi:type="dcterms:W3CDTF">$createdDate</dcterms:modified>
|
<dcterms:modified xsi:type="dcterms:W3CDTF">$createdDate</dcterms:modified>
|
||||||
<cp:revision>0</cp:revision>
|
<cp:revision>0</cp:revision>
|
||||||
@@ -247,15 +249,15 @@ EOD;
|
|||||||
<Default ContentType="application/xml" Extension="xml"/>
|
<Default ContentType="application/xml" Extension="xml"/>
|
||||||
<Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>
|
<Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>
|
||||||
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/>
|
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/>
|
||||||
|
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
/** @var Worksheet $worksheet */
|
/** @var Worksheet $worksheet */
|
||||||
foreach ($worksheets as $worksheet) {
|
foreach ($worksheets as $worksheet) {
|
||||||
$contentTypesXmlFileContents .= ' <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet' . $worksheet->getId() . '.xml"/>' . PHP_EOL;
|
$contentTypesXmlFileContents .= '<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet' . $worksheet->getId() . '.xml"/>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$contentTypesXmlFileContents .= <<<EOD
|
$contentTypesXmlFileContents .= <<<EOD
|
||||||
|
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/>
|
||||||
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>
|
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>
|
||||||
<Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>
|
<Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>
|
||||||
<Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>
|
<Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>
|
||||||
@@ -279,16 +281,16 @@ EOD;
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||||
<sheets>
|
<sheets>
|
||||||
|
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
|
||||||
/** @var Worksheet $worksheet */
|
/** @var Worksheet $worksheet */
|
||||||
foreach ($worksheets as $worksheet) {
|
foreach ($worksheets as $worksheet) {
|
||||||
$worksheetName = $worksheet->getExternalSheet()->getName();
|
$worksheetName = $worksheet->getExternalSheet()->getName();
|
||||||
$worksheetId = $worksheet->getId();
|
$worksheetId = $worksheet->getId();
|
||||||
$workbookXmlFileContents .= ' <sheet name="' . $escaper->escape($worksheetName) . '" sheetId="' . $worksheetId . '" r:id="rIdSheet' . $worksheetId . '"/>' . PHP_EOL;
|
$workbookXmlFileContents .= '<sheet name="' . $escaper->escape($worksheetName) . '" sheetId="' . $worksheetId . '" r:id="rIdSheet' . $worksheetId . '"/>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$workbookXmlFileContents .= <<<EOD
|
$workbookXmlFileContents .= <<<EOD
|
||||||
@@ -312,14 +314,14 @@ EOD;
|
|||||||
$workbookRelsXmlFileContents = <<<EOD
|
$workbookRelsXmlFileContents = <<<EOD
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rIdStyles" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/>
|
||||||
<Relationship Id="rIdSharedStrings" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"/>
|
<Relationship Id="rIdSharedStrings" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"/>
|
||||||
|
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
/** @var Worksheet $worksheet */
|
/** @var Worksheet $worksheet */
|
||||||
foreach ($worksheets as $worksheet) {
|
foreach ($worksheets as $worksheet) {
|
||||||
$worksheetId = $worksheet->getId();
|
$worksheetId = $worksheet->getId();
|
||||||
$workbookRelsXmlFileContents .= ' <Relationship Id="rIdSheet' . $worksheetId . '" Target="worksheets/sheet' . $worksheetId . '.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>' . PHP_EOL;
|
$workbookRelsXmlFileContents .= '<Relationship Id="rIdSheet' . $worksheetId . '" Target="worksheets/sheet' . $worksheetId . '.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$workbookRelsXmlFileContents .= '</Relationships>';
|
$workbookRelsXmlFileContents .= '</Relationships>';
|
||||||
@@ -329,6 +331,20 @@ EOD;
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the "styles.xml" file under the "xl" folder
|
||||||
|
*
|
||||||
|
* @param StyleHelper $styleHelper
|
||||||
|
* @return FileSystemHelper
|
||||||
|
*/
|
||||||
|
public function createStylesFile($styleHelper)
|
||||||
|
{
|
||||||
|
$stylesXmlFileContents = $styleHelper->getStylesXMLFileContent();
|
||||||
|
$this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zips the root folder and streams the contents of the zip into the given stream
|
* Zips the root folder and streams the contents of the zip into the given stream
|
||||||
*
|
*
|
||||||
@@ -337,42 +353,10 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function zipRootFolderAndCopyToStream($streamPointer)
|
public function zipRootFolderAndCopyToStream($streamPointer)
|
||||||
{
|
{
|
||||||
$this->zipRootFolder();
|
$zipHelper = new ZipHelper();
|
||||||
$this->copyZipToStream($streamPointer);
|
$zipHelper->zipFolderAndCopyToStream($this->rootFolder, $streamPointer);
|
||||||
|
|
||||||
// once the zip is copied, remove it
|
// once the zip is copied, remove it
|
||||||
$this->deleteFile($this->getZipFilePath());
|
$this->deleteFile($zipHelper->getZipFilePath($this->rootFolder));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zips the root folder
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function zipRootFolder()
|
|
||||||
{
|
|
||||||
$zipHelper = new ZipHelper();
|
|
||||||
$zipHelper->zipFolder($this->rootFolder, $this->getZipFilePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string Path of the zip file created from the root folder
|
|
||||||
*/
|
|
||||||
protected function getZipFilePath()
|
|
||||||
{
|
|
||||||
return $this->rootFolder . '.zip';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Streams the contents of the zip 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-5
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Box\Spout\Writer\Helper\XLSX;
|
namespace Box\Spout\Writer\XLSX\Helper;
|
||||||
|
|
||||||
use Box\Spout\Common\Exception\IOException;
|
use Box\Spout\Common\Exception\IOException;
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ use Box\Spout\Common\Exception\IOException;
|
|||||||
* Class SharedStringsHelper
|
* Class SharedStringsHelper
|
||||||
* This class provides helper functions to write shared strings
|
* This class provides helper functions to write shared strings
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Writer\Helper\XLSX
|
* @package Box\Spout\Writer\XLSX\Helper
|
||||||
*/
|
*/
|
||||||
class SharedStringsHelper
|
class SharedStringsHelper
|
||||||
{
|
{
|
||||||
@@ -25,6 +25,9 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
const DEFAULT_STRINGS_COUNT_PART = 'count="9999999999999" uniqueCount="9999999999999"';
|
const DEFAULT_STRINGS_COUNT_PART = 'count="9999999999999" uniqueCount="9999999999999"';
|
||||||
|
|
||||||
|
/** @var resource Pointer to the sharedStrings.xml file */
|
||||||
|
protected $sharedStringsFilePointer;
|
||||||
|
|
||||||
/** @var int Number of shared strings already written */
|
/** @var int Number of shared strings already written */
|
||||||
protected $numSharedStrings = 0;
|
protected $numSharedStrings = 0;
|
||||||
|
|
||||||
@@ -36,15 +39,16 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function __construct($xlFolder)
|
public function __construct($xlFolder)
|
||||||
{
|
{
|
||||||
$sharedStringsFilePath = $xlFolder . DIRECTORY_SEPARATOR . self::SHARED_STRINGS_FILE_NAME;
|
$sharedStringsFilePath = $xlFolder . '/' . self::SHARED_STRINGS_FILE_NAME;
|
||||||
$this->sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w');
|
$this->sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w');
|
||||||
|
|
||||||
$this->throwIfSharedStringsFilePointerIsNotAvailable();
|
$this->throwIfSharedStringsFilePointerIsNotAvailable();
|
||||||
|
|
||||||
// the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later
|
// the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later
|
||||||
$header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>' . PHP_EOL;
|
$header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>';
|
||||||
fwrite($this->sharedStringsFilePointer, $header);
|
fwrite($this->sharedStringsFilePointer, $header);
|
||||||
|
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
|
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +74,7 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function writeString($string)
|
public function writeString($string)
|
||||||
{
|
{
|
||||||
fwrite($this->sharedStringsFilePointer, ' <si><t xml:space="preserve">' . $this->stringsEscaper->escape($string) . '</t></si>' . PHP_EOL);
|
fwrite($this->sharedStringsFilePointer, '<si><t xml:space="preserve">' . $this->stringsEscaper->escape($string) . '</t></si>');
|
||||||
$this->numSharedStrings++;
|
$this->numSharedStrings++;
|
||||||
|
|
||||||
// Shared string ID is zero-based
|
// Shared string ID is zero-based
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\XLSX\Helper;
|
||||||
|
|
||||||
|
use Box\Spout\Writer\Common\Helper\AbstractStyleHelper;
|
||||||
|
use Box\Spout\Writer\Style\Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class StyleHelper
|
||||||
|
* This class provides helper functions to manage styles
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Writer\XLSX\Helper
|
||||||
|
*/
|
||||||
|
class StyleHelper extends AbstractStyleHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the content of the "styles.xml" file, given a list of styles.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getStylesXMLFileContent()
|
||||||
|
{
|
||||||
|
$content = <<<EOD
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||||
|
EOD;
|
||||||
|
|
||||||
|
$content .= $this->getFontsSectionContent();
|
||||||
|
$content .= $this->getFillsSectionContent();
|
||||||
|
$content .= $this->getBordersSectionContent();
|
||||||
|
$content .= $this->getCellStyleXfsSectionContent();
|
||||||
|
$content .= $this->getCellXfsSectionContent();
|
||||||
|
$content .= $this->getCellStylesSectionContent();
|
||||||
|
|
||||||
|
$content .= <<<EOD
|
||||||
|
</styleSheet>
|
||||||
|
EOD;
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of the "<fonts>" section.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getFontsSectionContent()
|
||||||
|
{
|
||||||
|
$content = '<fonts count="' . count($this->styleIdToStyleMappingTable) . '">';
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Writer\Style\Style $style */
|
||||||
|
foreach ($this->getRegisteredStyles() as $style) {
|
||||||
|
$content .= '<font>';
|
||||||
|
|
||||||
|
$content .= '<sz val="' . $style->getFontSize() . '"/>';
|
||||||
|
$content .= '<color rgb="' . Color::toARGB($style->getFontColor()) . '"/>';
|
||||||
|
$content .= '<name val="' . $style->getFontName() . '"/>';
|
||||||
|
|
||||||
|
if ($style->isFontBold()) {
|
||||||
|
$content .= '<b/>';
|
||||||
|
}
|
||||||
|
if ($style->isFontItalic()) {
|
||||||
|
$content .= '<i/>';
|
||||||
|
}
|
||||||
|
if ($style->isFontUnderline()) {
|
||||||
|
$content .= '<u/>';
|
||||||
|
}
|
||||||
|
if ($style->isFontStrikethrough()) {
|
||||||
|
$content .= '<strike/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$content .= '</font>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$content .= '</fonts>';
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of the "<fills>" section.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getFillsSectionContent()
|
||||||
|
{
|
||||||
|
return <<<EOD
|
||||||
|
<fills count="1">
|
||||||
|
<fill>
|
||||||
|
<patternFill patternType="none"/>
|
||||||
|
</fill>
|
||||||
|
</fills>
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of the "<borders>" section.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getBordersSectionContent()
|
||||||
|
{
|
||||||
|
return <<<EOD
|
||||||
|
<borders count="1">
|
||||||
|
<border>
|
||||||
|
<left/>
|
||||||
|
<right/>
|
||||||
|
<top/>
|
||||||
|
<bottom/>
|
||||||
|
<diagonal/>
|
||||||
|
</border>
|
||||||
|
</borders>
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of the "<cellStyleXfs>" section.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getCellStyleXfsSectionContent()
|
||||||
|
{
|
||||||
|
return <<<EOD
|
||||||
|
<cellStyleXfs count="1">
|
||||||
|
<xf borderId="0" fillId="0" fontId="0" numFmtId="0"/>
|
||||||
|
</cellStyleXfs>
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of the "<cellXfs>" section.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getCellXfsSectionContent()
|
||||||
|
{
|
||||||
|
$registeredStyles = $this->getRegisteredStyles();
|
||||||
|
|
||||||
|
$content = '<cellXfs count="' . count($registeredStyles) . '">';
|
||||||
|
|
||||||
|
foreach ($registeredStyles as $style) {
|
||||||
|
$content .= '<xf numFmtId="0" fontId="' . $style->getId() . '" fillId="0" borderId="0" xfId="0"';
|
||||||
|
|
||||||
|
if ($style->shouldApplyFont()) {
|
||||||
|
$content .= ' applyFont="1"';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($style->shouldWrapText()) {
|
||||||
|
$content .= ' applyAlignment="1">';
|
||||||
|
$content .= '<alignment wrapText="1"/>';
|
||||||
|
$content .= '</xf>';
|
||||||
|
} else {
|
||||||
|
$content .= '/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$content .= '</cellXfs>';
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of the "<cellStyles>" section.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getCellStylesSectionContent()
|
||||||
|
{
|
||||||
|
return <<<EOD
|
||||||
|
<cellStyles count="1">
|
||||||
|
<cellStyle builtinId="0" name="Normal" xfId="0"/>
|
||||||
|
</cellStyles>
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\XLSX\Internal;
|
||||||
|
|
||||||
|
use Box\Spout\Writer\Common\Internal\AbstractWorkbook;
|
||||||
|
use Box\Spout\Writer\XLSX\Helper\FileSystemHelper;
|
||||||
|
use Box\Spout\Writer\XLSX\Helper\SharedStringsHelper;
|
||||||
|
use Box\Spout\Writer\XLSX\Helper\StyleHelper;
|
||||||
|
use Box\Spout\Writer\Common\Sheet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Workbook
|
||||||
|
* Represents a workbook within a XLSX file.
|
||||||
|
* It provides the functions to work with worksheets.
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Writer\XLSX\Internal
|
||||||
|
*/
|
||||||
|
class Workbook extends AbstractWorkbook
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Maximum number of rows a XLSX sheet can contain
|
||||||
|
* @see http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx
|
||||||
|
*/
|
||||||
|
protected static $maxRowsPerWorksheet = 1048576;
|
||||||
|
|
||||||
|
/** @var bool Whether inline or shared strings should be used */
|
||||||
|
protected $shouldUseInlineStrings;
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Writer\XLSX\Helper\FileSystemHelper Helper to perform file system operations */
|
||||||
|
protected $fileSystemHelper;
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
|
||||||
|
protected $sharedStringsHelper;
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles */
|
||||||
|
protected $styleHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $tempFolder
|
||||||
|
* @param bool $shouldUseInlineStrings
|
||||||
|
* @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, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically, $defaultRowStyle)
|
||||||
|
{
|
||||||
|
parent::__construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle);
|
||||||
|
|
||||||
|
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
||||||
|
|
||||||
|
$this->fileSystemHelper = new FileSystemHelper($tempFolder);
|
||||||
|
$this->fileSystemHelper->createBaseFilesAndFolders();
|
||||||
|
|
||||||
|
$this->styleHelper = new StyleHelper($defaultRowStyle);
|
||||||
|
|
||||||
|
// This helper will be shared by all sheets
|
||||||
|
$xlFolder = $this->fileSystemHelper->getXlFolder();
|
||||||
|
$this->sharedStringsHelper = new SharedStringsHelper($xlFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles to XLSX 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);
|
||||||
|
|
||||||
|
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
|
||||||
|
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings);
|
||||||
|
$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 XLSX file.
|
||||||
|
* All the temporary files are then deleted.
|
||||||
|
*
|
||||||
|
* @param resource $finalFilePointer Pointer to the XLSX that will be created
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function close($finalFilePointer)
|
||||||
|
{
|
||||||
|
/** @var Worksheet[] $worksheets */
|
||||||
|
$worksheets = $this->worksheets;
|
||||||
|
|
||||||
|
foreach ($worksheets as $worksheet) {
|
||||||
|
$worksheet->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sharedStringsHelper->close();
|
||||||
|
|
||||||
|
// Finish creating all the necessary files before zipping everything together
|
||||||
|
$this->fileSystemHelper
|
||||||
|
->createContentTypesFile($worksheets)
|
||||||
|
->createWorkbookFile($worksheets)
|
||||||
|
->createWorkbookRelsFile($worksheets)
|
||||||
|
->createStylesFile($this->styleHelper)
|
||||||
|
->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-43
@@ -1,31 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Box\Spout\Writer\Internal\XLSX;
|
namespace Box\Spout\Writer\XLSX\Internal;
|
||||||
|
|
||||||
|
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||||
use Box\Spout\Common\Exception\IOException;
|
use Box\Spout\Common\Exception\IOException;
|
||||||
use Box\Spout\Writer\Helper\XLSX\CellHelper;
|
use Box\Spout\Writer\Common\Helper\CellHelper;
|
||||||
|
use Box\Spout\Writer\Common\Internal\WorksheetInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Worksheet
|
* Class Worksheet
|
||||||
* Represents a worksheet within a XLSX file. The difference with the Sheet object is
|
* Represents a worksheet within a XLSX file. The difference with the Sheet object is
|
||||||
* that this class provides an interface to write data
|
* that this class provides an interface to write data
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Writer\Internal\XLSX
|
* @package Box\Spout\Writer\XLSX\Internal
|
||||||
*/
|
*/
|
||||||
class Worksheet
|
class Worksheet implements WorksheetInterface
|
||||||
{
|
{
|
||||||
const SHEET_XML_FILE_HEADER = <<<EOD
|
const SHEET_XML_FILE_HEADER = <<<EOD
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
/** @var \Box\Spout\Writer\Sheet The "external" sheet */
|
/** @var \Box\Spout\Writer\Common\Sheet The "external" sheet */
|
||||||
protected $externalSheet;
|
protected $externalSheet;
|
||||||
|
|
||||||
/** @var string Path to the XML file that will contain the sheet data */
|
/** @var string Path to the XML file that will contain the sheet data */
|
||||||
protected $worksheetFilePath;
|
protected $worksheetFilePath;
|
||||||
|
|
||||||
/** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
|
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
|
||||||
protected $sharedStringsHelper;
|
protected $sharedStringsHelper;
|
||||||
|
|
||||||
/** @var bool Whether inline or shared strings should be used */
|
/** @var bool Whether inline or shared strings should be used */
|
||||||
@@ -37,13 +39,13 @@ EOD;
|
|||||||
/** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */
|
/** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */
|
||||||
protected $sheetFilePointer;
|
protected $sheetFilePointer;
|
||||||
|
|
||||||
/** @var int */
|
/** @var int Index of the last written row */
|
||||||
protected $lastWrittenRowIndex = 0;
|
protected $lastWrittenRowIndex = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \Box\Spout\Writer\Sheet $externalSheet The associated "external" sheet
|
* @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet
|
||||||
* @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
|
* @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
|
||||||
* @param \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper $sharedStringsHelper Helper for shared strings
|
* @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings
|
||||||
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
||||||
*/
|
*/
|
||||||
@@ -53,9 +55,10 @@ EOD;
|
|||||||
$this->sharedStringsHelper = $sharedStringsHelper;
|
$this->sharedStringsHelper = $sharedStringsHelper;
|
||||||
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
||||||
|
|
||||||
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
|
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
|
||||||
$this->worksheetFilePath = $worksheetFilesFolder . DIRECTORY_SEPARATOR . strtolower($this->externalSheet->getName()) . '.xml';
|
$this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml';
|
||||||
$this->startSheet();
|
$this->startSheet();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +73,25 @@ EOD;
|
|||||||
$this->sheetFilePointer = fopen($this->worksheetFilePath, 'w');
|
$this->sheetFilePointer = fopen($this->worksheetFilePath, 'w');
|
||||||
$this->throwIfSheetFilePointerIsNotAvailable();
|
$this->throwIfSheetFilePointerIsNotAvailable();
|
||||||
|
|
||||||
fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER . PHP_EOL);
|
fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER);
|
||||||
fwrite($this->sheetFilePointer, ' <sheetData>' . PHP_EOL);
|
fwrite($this->sheetFilePointer, '<sheetData>');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \Box\Spout\Writer\Sheet The "external" sheet
|
* 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 \Box\Spout\Writer\Common\Sheet The "external" sheet
|
||||||
*/
|
*/
|
||||||
public function getExternalSheet()
|
public function getExternalSheet()
|
||||||
{
|
{
|
||||||
@@ -95,66 +111,58 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function getId()
|
public function getId()
|
||||||
{
|
{
|
||||||
// sheet number is zero-based, while ID is 1-based
|
// sheet index is zero-based, while ID is 1-based
|
||||||
return $this->externalSheet->getSheetNumber() + 1;
|
return $this->externalSheet->getIndex() + 1;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds data to the worksheet.
|
* Adds data to the worksheet.
|
||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be written.
|
* @param array $dataRow Array containing data to be written. Cannot be empty.
|
||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* 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
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
|
* @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)
|
public function addRow($dataRow, $style)
|
||||||
{
|
{
|
||||||
$cellNumber = 0;
|
$cellNumber = 0;
|
||||||
$rowIndex = $this->lastWrittenRowIndex + 1;
|
$rowIndex = $this->lastWrittenRowIndex + 1;
|
||||||
$numCells = count($dataRow);
|
$numCells = count($dataRow);
|
||||||
|
|
||||||
$data = ' <row r="' . $rowIndex . '" spans="1:' . $numCells . '">' . PHP_EOL;
|
$data = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
|
||||||
|
|
||||||
foreach($dataRow as $cellValue) {
|
foreach($dataRow as $cellValue) {
|
||||||
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
||||||
$data .= '<c r="' . $columnIndex . $rowIndex . '"';
|
$data .= '<c r="' . $columnIndex . $rowIndex . '"';
|
||||||
|
$data .= ' s="' . $style->getId() . '"';
|
||||||
|
|
||||||
if (empty($cellValue)) {
|
if (CellHelper::isNonEmptyString($cellValue)) {
|
||||||
$data .= '/>' . PHP_EOL;
|
|
||||||
} else {
|
|
||||||
if (is_numeric($cellValue)) {
|
|
||||||
$data .= '><v>' . $cellValue . '</v></c>' . PHP_EOL;
|
|
||||||
} else {
|
|
||||||
if ($this->shouldUseInlineStrings) {
|
if ($this->shouldUseInlineStrings) {
|
||||||
$data .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>' . PHP_EOL;
|
$data .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
|
||||||
} else {
|
} else {
|
||||||
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
||||||
$data .= ' t="s"><v>' . $sharedStringId . '</v></c>' . PHP_EOL;
|
$data .= ' t="s"><v>' . $sharedStringId . '</v></c>';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (CellHelper::isBoolean($cellValue)) {
|
||||||
|
$data .= ' t="b"><v>' . $cellValue . '</v></c>';
|
||||||
|
} else if (CellHelper::isNumeric($cellValue)) {
|
||||||
|
$data .= '><v>' . $cellValue . '</v></c>';
|
||||||
|
} else if (empty($cellValue)) {
|
||||||
|
$data .= '/>';
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
$cellNumber++;
|
$cellNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data .= ' </row>' . PHP_EOL;
|
$data .= '</row>';
|
||||||
|
|
||||||
$wasWriteSuccessful = fwrite($this->sheetFilePointer, $data);
|
$wasWriteSuccessful = fwrite($this->sheetFilePointer, $data);
|
||||||
if ($wasWriteSuccessful === false) {
|
if ($wasWriteSuccessful === false) {
|
||||||
throw new IOException('Unable to write data in ' . $this->worksheetFilePath);
|
throw new IOException("Unable to write data in {$this->worksheetFilePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// only update the count if the write worked
|
// only update the count if the write worked
|
||||||
@@ -168,7 +176,7 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
fwrite($this->sheetFilePointer, ' </sheetData>' . PHP_EOL);
|
fwrite($this->sheetFilePointer, '</sheetData>');
|
||||||
fwrite($this->sheetFilePointer, '</worksheet>');
|
fwrite($this->sheetFilePointer, '</worksheet>');
|
||||||
fclose($this->sheetFilePointer);
|
fclose($this->sheetFilePointer);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer\XLSX;
|
||||||
|
|
||||||
|
use Box\Spout\Writer\AbstractMultiSheetsWriter;
|
||||||
|
use Box\Spout\Writer\Style\StyleBuilder;
|
||||||
|
use Box\Spout\Writer\XLSX\Internal\Workbook;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Writer
|
||||||
|
* This class provides base support to write data to XLSX files
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Writer\XLSX
|
||||||
|
*/
|
||||||
|
class Writer extends AbstractMultiSheetsWriter
|
||||||
|
{
|
||||||
|
/** Default style font values */
|
||||||
|
const DEFAULT_FONT_SIZE = 12;
|
||||||
|
const DEFAULT_FONT_NAME = 'Calibri';
|
||||||
|
|
||||||
|
/** @var string Content-Type value for the header */
|
||||||
|
protected static $headerContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
|
|
||||||
|
/** @var string Temporary folder where the files to create the XLSX will be stored */
|
||||||
|
protected $tempFolder;
|
||||||
|
|
||||||
|
/** @var bool Whether inline or shared strings should be used - inline string is more memory efficient */
|
||||||
|
protected $shouldUseInlineStrings = true;
|
||||||
|
|
||||||
|
/** @var Internal\Workbook The workbook for the XLSX 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 XLSX 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use inline string to be more memory efficient. If set to false, it will use shared strings.
|
||||||
|
* This must be set before opening the writer.
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
||||||
|
* @return Writer
|
||||||
|
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
||||||
|
*/
|
||||||
|
public function setShouldUseInlineStrings($shouldUseInlineStrings)
|
||||||
|
{
|
||||||
|
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
|
||||||
|
|
||||||
|
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (!$this->book) {
|
||||||
|
$tempFolder = ($this->tempFolder) ? : sys_get_temp_dir();
|
||||||
|
$this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default style to be applied to rows.
|
||||||
|
*
|
||||||
|
* @return \Box\Spout\Writer\Style\Style
|
||||||
|
*/
|
||||||
|
protected function getDefaultRowStyle()
|
||||||
|
{
|
||||||
|
return (new StyleBuilder())
|
||||||
|
->setFontSize(self::DEFAULT_FONT_SIZE)
|
||||||
|
->setFontName(self::DEFAULT_FONT_NAME)
|
||||||
|
->build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the writer, preventing any additional writing.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function closeWriter()
|
||||||
|
{
|
||||||
|
if ($this->book) {
|
||||||
|
$this->book->close($this->filePointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 177 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user