Compare commits

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

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

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

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

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

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

Possible cell alignments are: LEFT, RIGHT, CENTER and JUSTIFY.
2019-10-27 18:58:56 +01:00
Adrien Loison 0a0b1f7196 [docs] Fix site_url - take 2 2019-10-17 13:12:40 +00:00
Adrien Loison db32a7c7db [docs] Fix site_url 2019-10-17 11:15:55 +00:00
Adrien Loison 859b8d336e [Docs] Force using current protocol scheme for resources and links
If the docs is accessed with https, change all links to be https as well.
2019-10-17 10:32:39 +00:00
Adrien Loison 8a2dcc946b Improve docs for local dev 2019-10-17 09:29:03 +00:00
Adrien Loison 74146c6224 Switch documentation site to HTTPS
Accessing the website through HTTP may be blocked in some regions of the world.
Using HTTPS should help with this problem and is the good thing to do anyway.
2019-10-17 07:44:13 +00:00
Adrien Loison 6a6d1df9df Fix typo - Boder => Border 2019-10-17 07:37:37 +00:00
Alexander Rakushin 52312b7045 Added tests for StyleRegistry and StyleMerger 2019-10-01 20:08:54 +00:00
Alexander Rakushin d4e12b1812 Simplification of the code 2019-10-01 20:08:54 +00:00
Alexander Rakushin e8c6d83104 сode formatting (CS Fixer) 2019-10-01 20:08:54 +00:00
Alexander Rakushin 40aecd7b90 Added support for cell formats when writing Excel files 2019-10-01 20:08:54 +00:00
Adrien Loison 16a2f91a22 Cell indexes not being respected when rendering row
Fixes #682
When calling `Row::setCellIndex`, it's possible to create a Row with holes.
Instead of iterating over existing cells of a Row, we should instead use the cell indexes (from 0 to max cell index).
2019-09-28 14:02:23 +00:00
Ilya Troy 2d297e954b Update getting-started.md
Fix writer usage example
2019-07-30 22:14:20 +02:00
Adrien Loison 2716d7eeed [PHP 7.4] Updated way to disable the fgetcsv/fputcsv escape character
From PHP 7.4, the recommended way to disable the escape character for fgetcsv() and fputcsv() is an empty string, instead of "\0".
Discussed here: https://github.com/php/php-src/pull/3515
2019-07-21 23:15:34 +02:00
Adrien Loison 1bbfd45b82 Support for missing styles XML file in XLSX
Some files don't have a "styles.xml" file. Excel supports these files, Spout should do too.
2019-07-20 16:48:51 +02:00
165 changed files with 6555 additions and 1067 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
github: adrilo
+270
View File
@@ -0,0 +1,270 @@
name: Spout CI
on: [push, pull_request]
jobs:
tests-on-php-latest:
strategy:
matrix:
operating-system: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.operating-system }}
name: Tests - PHP 8.1 on ${{ matrix.operating-system }}
env:
extensions: zip, xmlreader, dom
cache-key: cache-matrix-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Test with phpunit
run: vendor/bin/phpunit --no-coverage
tests-on-older-php:
strategy:
matrix:
php-versions: ['7.3', '7.4', '8.0']
runs-on: ubuntu-latest
name: Tests - PHP ${{ matrix.php-versions }}
env:
extensions: zip, xmlreader, dom
cache-key: cache-matrix-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Test with phpunit
run: vendor/bin/phpunit --no-coverage
code-coverage:
name: Code coverage
runs-on: ubuntu-latest
env:
extensions: zip, xmlreader, dom
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run Tests with Code Coverage
run: |
mkdir -p build/logs
vendor/bin/phpunit --coverage-clover=build/logs/clover.xml
- name: Upload coverage results to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
composer global require php-coveralls/php-coveralls
php-coveralls --coverage_clover=build/logs/clover.xml -v --exclude-no-stmt
coding-style:
name: Coding Style
runs-on: ubuntu-latest
env:
extensions: zip, xmlreader, dom
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHP-CS-Fixer
run: vendor/bin/php-cs-fixer fix --verbose --diff --dry-run
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
env:
extensions: zip, xmlreader, dom
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
key: ${{ env.cache-key }}
- name: Cache extensions
uses: actions/cache@v2
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: ${{ env.extensions }}
tools: phpstan
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Static Analysis using PHPStan
run: ./vendor/bin/phpstan analyse -c phpstan.neon --no-progress
+2 -2
View File
@@ -4,5 +4,5 @@
/tests/resources/generated /tests/resources/generated
/tests/coverage /tests/coverage
/vendor /vendor
/composer.lock /.php-cs-fixer.cache
/.php_cs.cache /.phpunit.result.cache
+14 -18
View File
@@ -1,6 +1,12 @@
<?php <?php
$config = PhpCsFixer\Config::create() $finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->name('*.php')
->exclude('vendor');
$config = new PhpCsFixer\Config();
return $config
->setRiskyAllowed(true) ->setRiskyAllowed(true)
->setRules([ ->setRules([
'@Symfony' => true, '@Symfony' => true,
@@ -13,14 +19,15 @@ $config = PhpCsFixer\Config::create()
'declare_equal_normalize' => ['space' => 'single'], 'declare_equal_normalize' => ['space' => 'single'],
'heredoc_to_nowdoc' => true, 'heredoc_to_nowdoc' => true,
'increment_style' => ['style' => 'post'], 'increment_style' => ['style' => 'post'],
'is_null' => ['use_yoda_style' => false], 'is_null' => true,
'method_argument_space' => ['ensure_fully_multiline' => true], 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'modernize_types_casting' => true, 'modernize_types_casting' => true,
'no_break_comment' => ['comment_text' => 'do nothing'], 'no_break_comment' => ['comment_text' => 'do nothing'],
'no_empty_phpdoc' => false, 'no_empty_phpdoc' => false,
'no_null_property_initialization' => true, 'no_null_property_initialization' => true,
'no_short_echo_tag' => true, 'echo_tag_syntax' => false,
'no_superfluous_elseif' => true, 'no_superfluous_elseif' => true,
'no_superfluous_phpdoc_tags' => false,
'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'switch_case', 'yield']], 'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'switch_case', 'yield']],
'no_unneeded_curly_braces' => true, 'no_unneeded_curly_braces' => true,
'no_unneeded_final_method' => true, 'no_unneeded_final_method' => true,
@@ -36,23 +43,12 @@ $config = PhpCsFixer\Config::create()
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'phpdoc_separation' => false, 'phpdoc_separation' => false,
'protected_to_private' => true, 'protected_to_private' => true,
'psr4' => true, 'psr_autoloading' => true,
'return_type_declaration' => ['space_before' => 'one'], 'return_type_declaration' => ['space_before' => 'one'],
'semicolon_after_instruction' => true, 'semicolon_after_instruction' => true,
'simplified_null_return' => false, 'simplified_null_return' => false,
'single_line_comment_style' => ['comment_types' => ['hash']], 'single_line_comment_style' => ['comment_types' => ['hash']],
'strict_comparison' => true, 'strict_comparison' => true,
'yoda_style' => ['equal' => false, 'identical' => false], 'yoda_style' => ['equal' => false, 'identical' => false],
]); ])
->setFinder($finder);
$config->setFinder(
PhpCsFixer\Finder::create()
->exclude('vendor')
->in(__DIR__)
->name('*.php')
);
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
$config->setCacheFile($cacheDir . '/.php_cs.cache');
return $config;
-57
View File
@@ -1,57 +0,0 @@
filter:
excluded_paths: [vendor/*, tests/*]
tools:
external_code_coverage:
timeout: 600 # Wait 10 minutes for results
runs: 1 # Merge results for the 7.1 job
php_mess_detector: true
php_code_sniffer:
config:
standard: PSR4
filter:
paths: ['src']
sensiolabs_security_checker: true
php_pdepend: true
php_loc:
enabled: true
filter:
paths: ['src']
php_cpd: false # Must be disabled to use php_sim instead
php_sim:
enabled: true
filter:
paths: ['src']
build_failure_conditions:
- 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9
- 'project.metric_change("scrutinizer.test_coverage", < -0.005)' # Code Coverage decreased by more than 0.5%
- 'project.metric("scrutinizer.test_coverage", < 0.97)' # Code Coverage drops below 97%
checks:
php:
remove_extra_empty_lines: true
remove_php_closing_tag: true
remove_trailing_whitespace: true
fix_use_statements:
remove_unused: true
preserve_multiple: false
preserve_blanklines: true
fix_php_opening_tag: true
fix_linefeed: true
fix_line_ending: true
fix_identation_4spaces: true
fix_doc_comments: true
uppercase_constants: true
use_self_instead_of_fqcn: true
simplify_boolean_return: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
phpunit_assertions: true
parameters_in_camelcaps: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
optional_parameters_at_the_end: true
newline_at_end_of_file: true
encourage_single_quotes: true
-58
View File
@@ -1,58 +0,0 @@
dist: trusty
sudo: false
language: php
matrix:
include:
- php: 7.1
env: WITH_CS=true
- php: 7.1
env: WITH_PHPUNIT=true WITH_COVERAGE=true
- php: 7.2
env: WITH_PHPUNIT=true
- php: 7.3
env: WITH_PHPUNIT=true
cache:
directories:
- $HOME/.composer/cache
- $HOME/.php-cs-fixer
before_install:
- |
if [[ "$WITH_COVERAGE" != "true" ]]; then
phpenv config-rm xdebug.ini
fi
if [[ "$WITH_CS" != "true" ]]; then
composer remove friendsofphp/php-cs-fixer --dev --no-update
fi
- composer validate
install:
- composer install --no-interaction --prefer-source
before_script:
- mkdir -p "$HOME/.php-cs-fixer"
- mkdir -p build/logs
script:
- |
if [[ "$WITH_CS" == "true" ]]; then
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run
fi
- |
if [[ "$WITH_PHPUNIT" == "true" ]]; then
if [[ "$WITH_COVERAGE" == "true" ]]; then
vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover
else
vendor/bin/phpunit --no-coverage
fi
fi
after_script:
- |
if [[ "$WITH_COVERAGE" == "true" ]]; then
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover
fi
+16 -2
View File
@@ -6,7 +6,7 @@ All contributions are welcome to this project.
Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at: Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at:
http://opensource.box.com/cla https://opensource.box.com/cla
To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement). To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement).
@@ -68,7 +68,21 @@ This will add your changes on top of what's already in upstream, minimizing merg
Make sure that all tests are passing before submitting a pull request. Make sure that all tests are passing before submitting a pull request.
### Step 8: Send the pull request ### Step 8: Fix code style
Run the following command to check the code style of your changes:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff
```
This will print a diff of proposed code style changes. To apply these suggestions, run the following command:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist
```
### Step 9: Send the pull request
Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did.
+24 -14
View File
@@ -1,26 +1,39 @@
# Spout # Spout
[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout) [![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges) [![Project Status](https://opensource.box.com/badges/inactive.svg)](https://opensource.box.com/badges)
[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout) [![example workflow](https://github.com/box/spout/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/box/spout/actions/workflows/ci.yml?query=branch%3Amaster)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/box/spout/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master) [![Coverage Status](https://coveralls.io/repos/github/box/spout/badge.svg?branch=master)](https://coveralls.io/github/box/spout?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) [![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. ## 🪦 Archived project 🪦
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) This project has been archived and is no longer maintained. No bug fix and no additional features will be added.<br>
You won't be able to submit new issues or pull requests, and no additional features will be added
You can still use Spout as is in your projects though :)
> Thanks to everyone who contributed to this project, from a typo fix to the new cool feature.<br>
> It was great to see the involvement of this community!
<br>
## About
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB).
Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Documentation ## Documentation
Full documentation can be found at [http://opensource.box.com/spout/](http://opensource.box.com/spout/). Full documentation can be found at [https://opensource.box.com/spout/](https://opensource.box.com/spout/).
## Requirements ## Requirements
* PHP version 7.1 or higher * PHP version 7.2 or higher
* PHP extension `php_zip` enabled * PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled * PHP extension `php_xmlreader` enabled
@@ -38,17 +51,14 @@ If you just want to check that everything is working as expected, executing the
For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing). For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing).
> Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout)
## Support ## Support
You can ask questions, submit new features ideas or discuss about Spout in the chat room:<br> Spout is no longer actively supported. You can still ask questions, or discuss about it in the chat room:<br>
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Copyright and License ## Copyright and License
Copyright 2017 Box, Inc. All rights reserved. Copyright 2022 Box, Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+1 -1
View File
@@ -11,7 +11,7 @@ With the 3.0 version, this is now possible: each cell can have its own style.
Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value. Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
Finally, **_Spout 3.0 only supports PHP 7.1 and above_**, as other PHP versions are no longer supported by the community. Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community.
Reader changes Reader changes
-------------- --------------
-35
View File
@@ -1,35 +0,0 @@
build: false
clone_depth: 1
environment:
PHP_CHOCO_VERSION: 7.2.0
PHP_CACHE_DIR: C:\tools\php
cache:
- '%PHP_CACHE_DIR% -> appveyor.yml'
init:
- SET PATH=%PHP_CACHE_DIR%;%PATH%
- SET COMPOSER_CACHE_DIR=%PHP_CACHE_DIR%
- SET COMPOSER_NO_INTERACTION=1
- SET PHP=0
- SET ANSICON=121x90 (121x90)
install:
- IF EXIST %PHP_CACHE_DIR% (SET PHP=1)
- IF %PHP%==0 cinst php -y --version %PHP_CHOCO_VERSION% --params "/InstallDir:%PHP_CACHE_DIR%"
- IF %PHP%==0 cinst composer -y --ia "/DEV=%PHP_CACHE_DIR%"
- cd %PHP_CACHE_DIR%
- IF %PHP%==0 copy php.ini-production php.ini
- IF %PHP%==0 echo extension_dir=ext >> php.ini
- IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini
- IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini
- IF %PHP%==0 echo extension=php_openssl.dll >> php.ini
- php -v
- IF %PHP%==0 (composer --version) ELSE (composer self-update)
- cd %APPVEYOR_BUILD_FOLDER%
- composer install --prefer-dist --no-progress
test_script:
- cd %APPVEYOR_BUILD_FOLDER%
- vendor\bin\phpunit --colors=always
+7 -5
View File
@@ -12,13 +12,15 @@
} }
], ],
"require": { "require": {
"php": ">=7.1.0", "php": ">=7.3.0",
"ext-zip": "*", "ext-zip": "*",
"ext-xmlreader" : "*" "ext-xmlreader": "*",
"ext-dom": "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^7", "phpunit/phpunit": "^9",
"friendsofphp/php-cs-fixer": "^2" "friendsofphp/php-cs-fixer": "^3",
"phpstan/phpstan": "^1"
}, },
"suggest": { "suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
@@ -36,7 +38,7 @@
}, },
"config": { "config": {
"platform": { "platform": {
"php": "7.1" "php": "7.3"
} }
} }
} }
Generated
+4196
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -10,7 +10,7 @@ title: Spout
email: oss@box.com email: oss@box.com
description: "An open source PHP library to read and write spreadsheet files (XLSX, ODS and CSV), in a fast and scalable way." description: "An open source PHP library to read and write spreadsheet files (XLSX, ODS and CSV), in a fast and scalable way."
baseurl: "" # the subpath of your site, e.g. /blog baseurl: "" # the subpath of your site, e.g. /blog
url: "http://opensource.box.com" # the base hostname & protocol for your site url: "https://opensource.box.com" # the base hostname & protocol for your site
# Build settings # Build settings
markdown: kramdown markdown: kramdown
+7
View File
@@ -0,0 +1,7 @@
# Github Metadata plugin
repository: box/spout
plugins:
- "jekyll-github-metadata"
github:
url: http://localhost:8080
+1 -1
View File
@@ -11,7 +11,7 @@
<div class="footer-col footer-col-2"> <div class="footer-col footer-col-2">
<ul class="social-media-list"> <ul class="social-media-list">
<li><a href="https://github.com/box/spout/issues"><span class="icon icon--github">{% include icon-github.svg %}</span><span class="username">GitHub Issues</span></a></li> <li><a href="{{ site.github.issues_url }}"><span class="icon icon--github">{% include icon-github.svg %}</span><span class="username">GitHub Issues</span></a></li>
</ul> </ul>
</div> </div>
+1 -1
View File
@@ -6,7 +6,7 @@
<title>{% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title> <title>{% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title>
<meta name="description" content="{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines | truncate: 160 }}{% else %}{{ site.description }}{% endif %}"> <meta name="description" content="{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines | truncate: 160 }}{% else %}{{ site.description }}{% endif %}">
<link rel="stylesheet" href="{{ "/css/main.css" | prepend: site.github.url }}"> <link rel="stylesheet" href="{{ "/css/main.css" | prepend: site_url }}">
{% if site.algolia.enabled %} {% if site.algolia.enabled %}
<link rel="stylesheet" href="//cdn.jsdelivr.net/docsearch.js/1/docsearch.min.css" /> <link rel="stylesheet" href="//cdn.jsdelivr.net/docsearch.js/1/docsearch.min.css" />
{% endif %} {% endif %}
+6 -6
View File
@@ -1,7 +1,7 @@
<header class="site-header"> <header class="site-header">
<div class="wrapper"> <div class="wrapper">
<a class="site-title" href="{{ site.github.url }}/">{{ site.title }}</a> <a class="site-title" href="{{ site_url }}/">{{ site.title }}</a>
<nav class="site-nav"> <nav class="site-nav">
<a href="#" class="menu-icon"> <a href="#" class="menu-icon">
@@ -12,11 +12,11 @@
</svg> </svg>
</a> </a>
<div class="trigger"> <div class="trigger">
<a class="page-link" href="{{ site.github.url }}/getting-started/">Getting Started</a> <a class="page-link" href="{{ site_url }}/getting-started/">Getting Started</a>
<a class="page-link" href="{{ site.github.url }}/docs/">Documentation</a> <a class="page-link" href="{{ site_url }}/docs/">Documentation</a>
<a class="page-link" href="{{ site.github.url }}/guides/">Guides</a> <a class="page-link" href="{{ site_url }}/guides/">Guides</a>
<a class="page-link" href="{{ site.github.url }}/faq/">FAQ</a> <a class="page-link" href="{{ site_url }}/faq/">FAQ</a>
<a class="page-link" href="https://github.com/box/spout"> <a class="page-link" href="{{ site.github.repository_url }}">
<span class="icon icon--github">{% include icon-github.svg %}</span> <span class="icon icon--github">{% include icon-github.svg %}</span>
<span class="username">GitHub</span> <span class="username">GitHub</span>
</a> </a>
@@ -8,4 +8,4 @@
Writing an ODS file with millions of rows? {{ site.spout_html }} can do it in no time! Writing an ODS file with millions of rows? {{ site.spout_html }} can do it in no time!
{% endcapture %} {% endcapture %}
{% include section.html %} {% include section.html site_url = include.site_url %}
@@ -11,4 +11,4 @@
Switching from one type to another is ridiculously easy! Switching from one type to another is ridiculously easy!
{% endcapture %} {% endcapture %}
{% include section.html %} {% include section.html site_url=include.site_url %}
+3 -2
View File
@@ -13,8 +13,9 @@
<li class="feature-check">{{ site.spout_html }}'s API is {{emS}}developer-friendly{{emE}}.</li> <li class="feature-check">{{ site.spout_html }}'s API is {{emS}}developer-friendly{{emE}}.</li>
</ul><br> </ul><br>
<div class="btn-wrapper"> <div class="btn-wrapper">
<a href="{{ site.github.url }}/getting-started/" class="page-link"><button class="btn">Get started</button></a> <a href="{{ include.site_url }}/getting-started/" class="page-link"><button class="btn">Get started</button></a>
</div> </div>
{% endcapture %} {% endcapture %}
{% include section.html %} {% include section.html site_url=include.site_url %}
+1 -2
View File
@@ -1,4 +1,3 @@
<div class="section {{ sectionClass }} vertical-align-middle"> <div class="section {{ sectionClass }} vertical-align-middle">
<div class="wrapper"> <div class="wrapper">
<div class="description mbl"> <div class="description mbl">
@@ -9,7 +8,7 @@
{% assign sectionIconsAsArray = sectionIcons | split:'~~' %} {% assign sectionIconsAsArray = sectionIcons | split:'~~' %}
<div class="icons"> <div class="icons">
{% for icon in sectionIconsAsArray %} {% for icon in sectionIconsAsArray %}
<img src="{{ site.github.url }}/images/{{ icon }}"/> <img src="{{ include.site_url }}/images/{{ icon }}"/>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
+12
View File
@@ -0,0 +1,12 @@
{% comment %}
This is a hack to force using HTTPS...
The URL of the docs should be changed in the repo settings instead
{% endcomment %}
{% assign protocol_scheme = page.url | absolute_url | truncate: 5, "" %}
{%- capture site_url -%}
{%- if protocol_scheme == "https" -%}
{{ site.github.url | replace: "http://", "https://" }}
{%- else -%}
{{ site.github.url }}
{%- endif -%}
{%- endcapture -%}
+2 -8
View File
@@ -1,6 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
{% include set-global-site-url.html %}
{% include head.html %} {% include head.html %}
<body> <body>
@@ -13,14 +15,6 @@
{{ content }} {{ content }}
<!--
<div class="page-content">
<div class="wrapper">
</div>
</div>
-->
{% include footer.html %} {% include footer.html %}
{% include algolia.html %} {% include algolia.html %}
+1
View File
@@ -1,6 +1,7 @@
--- ---
layout: default layout: default
--- ---
<article class="post"> <article class="post">
<div class="wrapper"> <div class="wrapper">
<header class="post-header"> <header class="post-header">
-15
View File
@@ -1,15 +0,0 @@
---
layout: default
---
<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
<header class="post-header">
<h1 class="post-title" itemprop="name headline">{{ page.title }}</h1>
<p class="post-meta"><time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">{{ page.date | date: "%b %-d, %Y" }}</time>{% if page.author %} • <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">{{ page.author }}</span></span>{% endif %}</p>
</header>
<div class="post-content" itemprop="articleBody">
{{ content }}
</div>
</article>
+7 -3
View File
@@ -117,7 +117,7 @@ $reader->setShouldPreserveEmptyRows(true);
For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones: For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones:
| Category | Property | API | Category | Property | API
|:----------|:--------------|:-------------------------------------- |:---------------------|:---------------|:--------------------------------------
| Font | Bold | `StyleBuilder::setFontBold()` | Font | Bold | `StyleBuilder::setFontBold()`
| | Italic | `StyleBuilder::setFontItalic()` | | Italic | `StyleBuilder::setFontItalic()`
| | Underline | `StyleBuilder::setFontUnderline()` | | Underline | `StyleBuilder::setFontUnderline()`
@@ -125,8 +125,10 @@ For fonts and alignments, {{ site.spout_html }} does not support all the possibl
| | Font name | `StyleBuilder::setFontName('Arial')` | | Font name | `StyleBuilder::setFontName('Arial')`
| | Font size | `StyleBuilder::setFontSize(14)` | | Font size | `StyleBuilder::setFontSize(14)`
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` | | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
| Alignment | Wrap text | `StyleBuilder::setShouldWrapText(true|false)` | Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)`
| | Wrap text | `StyleBuilder::setShouldWrapText(true)`
| Format _(XLSX only)_ | Number format | `StyleBuilder::setFormat('0.000')`
| | Date format | `StyleBuilder::setFormat('m/d/yy h:mm')`
### Styling rows ### Styling rows
@@ -135,6 +137,7 @@ It is possible to apply some formatting options to a row. In this case, all cell
```php ```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder; use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
use Box\Spout\Common\Entity\Style\CellAlignment;
use Box\Spout\Common\Entity\Style\Color; use Box\Spout\Common\Entity\Style\Color;
$writer = WriterEntityFactory::createXLSXWriter(); $writer = WriterEntityFactory::createXLSXWriter();
@@ -146,6 +149,7 @@ $style = (new StyleBuilder())
->setFontSize(15) ->setFontSize(15)
->setFontColor(Color::BLUE) ->setFontColor(Color::BLUE)
->setShouldWrapText() ->setShouldWrapText()
->setCellAlignment(CellAlignment::RIGHT)
->setBackgroundColor(Color::YELLOW) ->setBackgroundColor(Color::YELLOW)
->build(); ->build();
+7 -3
View File
@@ -4,11 +4,13 @@ title: Getting Started
permalink: /getting-started/ permalink: /getting-started/
--- ---
{% include set-global-site-url.html %}
This guide will help you install {{ site.spout_html }} and teach you how to use it. This guide will help you install {{ site.spout_html }} and teach you how to use it.
## Requirements ## Requirements
* PHP version 7.1 or higher * PHP version 7.2 or higher
* PHP extension `ext-zip` enabled * PHP extension `ext-zip` enabled
* PHP extension `ext-xmlreader` enabled * PHP extension `ext-xmlreader` enabled
@@ -86,7 +88,9 @@ As with the reader, there is one common interface to write data to a file:
use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Common\Entity\Row; use Box\Spout\Common\Entity\Row;
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.ext'); $writer = WriterEntityFactory::createXLSXWriter();
// $writer = WriterEntityFactory::createODSWriter();
// $writer = WriterEntityFactory::createCSVWriter();
$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->openToBrowser($fileName); // stream data directly to the browser
@@ -132,4 +136,4 @@ For XLSX and ODS files, the number of rows per sheet is limited to *1,048,576*.
## Advanced usage ## Advanced usage
You can do a lot more with {{ site.spout_html }}! Check out the [full documentation]({{ site.github.url }}/docs/) to learn about all the features. You can do a lot more with {{ site.spout_html }}! Check out the [full documentation]({{ site_url }}/docs/) to learn about all the features.
+4 -2
View File
@@ -4,15 +4,17 @@ title: Guides
permalink: /guides/ permalink: /guides/
--- ---
{% include set-global-site-url.html %}
These guides focus on common and more advanced usages of {{ site.spout_html }}.<br> These guides focus on common and more advanced usages of {{ site.spout_html }}.<br>
If you are just starting with {{ site.spout_html }}, check out the [Getting Started page]({{ site.github.url }}/getting-started/) and the [Documentation]({{ site.github.url }}/docs/) first. If you are just starting with {{ site.spout_html }}, check out the [Getting Started page]({{ site_url }}/getting-started/) and the [Documentation]({{ site_url }}/docs/) first.
{% assign pages=site.pages | sort: 'path' %} {% assign pages=site.pages | sort: 'path' %}
<ul> <ul>
{% for page in pages %} {% for page in pages %}
{% if page.title and page.category contains 'guide' %} {% if page.title and page.category contains 'guide' %}
<li> <li>
<a class="page-link" href="{{ page.url | prepend: site.github.url }}">{{ page.title }}</a> <a class="page-link" href="{{ page.url | prepend: site_url }}">{{ page.title }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@@ -94,7 +94,7 @@ class MyStreamController extends Controller
$i++; $i++;
// Flushing the buffer every N rows to stream echo'ed content. // Flushing the buffer every N rows to stream echo'ed content.
if ($i % FLUSH_THRESHOLD === 0) { if ($i % self::FLUSH_THRESHOLD === 0) {
flush(); flush();
} }
} }
+5 -3
View File
@@ -5,6 +5,8 @@ title: Spout - Read and write spreadsheets, quickly and at scale
permalink: / permalink: /
--- ---
{% include section-supported-spreadsheet-types.html %} {% include set-global-site-url.html %}
{% include section-fast-and-scalable.html %}
{% include section-why-use-spout.html %} {% include section-supported-spreadsheet-types.html site_url=site_url %}
{% include section-fast-and-scalable.html site_url=site_url %}
{% include section-why-use-spout.html site_url=site_url %}
+3 -1
View File
@@ -1,7 +1,9 @@
version: "3" version: "3"
services: services:
site: site:
command: jekyll serve command: bash -c "
gem install 'jekyll-github-metadata'
&& jekyll serve --config _config.yml,_config_local.yml"
image: jekyll/jekyll:latest image: jekyll/jekyll:latest
volumes: volumes:
- $PWD:/srv/jekyll - $PWD:/srv/jekyll
+9
View File
@@ -0,0 +1,9 @@
parameters:
level: 4
paths:
- src
- tests
excludePaths:
# Exclude these files that are OK
- src/Spout/Reader/Common/Creator/ReaderEntityFactory.php
- src/Spout/Writer/Common/Creator/WriterEntityFactory.php
+9 -7
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="tests/bootstrap.php" bootstrap="tests/bootstrap.php"
colors="true" colors="true"
convertErrorsToExceptions="false" convertErrorsToExceptions="false"
@@ -8,12 +8,14 @@
defaultTestSuite="unit-tests" defaultTestSuite="unit-tests"
verbose="false"> verbose="false">
<testsuites> <php>
<ini name="error_reporting" value="-1"/>
</php>
<testsuites>
<testsuite name="unit-tests"> <testsuite name="unit-tests">
<directory>tests/</directory> <directory>tests/</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<groups> <groups>
@@ -22,13 +24,13 @@
</exclude> </exclude>
</groups> </groups>
<filter> <coverage>
<whitelist> <include>
<directory suffix=".php">src/</directory> <directory suffix=".php">src/</directory>
</include>
<exclude> <exclude>
<directory>src/Spout/Autoloader</directory> <directory>src/Spout/Autoloader</directory>
</exclude> </exclude>
</whitelist> </coverage>
</filter>
</phpunit> </phpunit>
+11 -11
View File
@@ -23,7 +23,7 @@ class Psr4Autoloader
*/ */
public function register() public function register()
{ {
spl_autoload_register([$this, 'loadClass']); \spl_autoload_register([$this, 'loadClass']);
} }
/** /**
@@ -40,10 +40,10 @@ class Psr4Autoloader
public function addNamespace($prefix, $baseDir, $prepend = false) public function addNamespace($prefix, $baseDir, $prepend = false)
{ {
// normalize namespace prefix // normalize namespace prefix
$prefix = trim($prefix, '\\') . '\\'; $prefix = \trim($prefix, '\\') . '\\';
// normalize the base directory with a trailing separator // normalize the base directory with a trailing separator
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/'; $baseDir = \rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
// initialize the namespace prefix array // initialize the namespace prefix array
if (isset($this->prefixes[$prefix]) === false) { if (isset($this->prefixes[$prefix]) === false) {
@@ -52,9 +52,9 @@ class Psr4Autoloader
// retain the base directory for the namespace prefix // retain the base directory for the namespace prefix
if ($prepend) { if ($prepend) {
array_unshift($this->prefixes[$prefix], $baseDir); \array_unshift($this->prefixes[$prefix], $baseDir);
} else { } else {
array_push($this->prefixes[$prefix], $baseDir); \array_push($this->prefixes[$prefix], $baseDir);
} }
} }
@@ -72,12 +72,12 @@ class Psr4Autoloader
// work backwards through the namespace names of the fully-qualified // work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name // class name to find a mapped file name
while (($pos = strrpos($prefix, '\\')) !== false) { while (($pos = \strrpos($prefix, '\\')) !== false) {
// retain the trailing namespace separator in the prefix // retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1); $prefix = \substr($class, 0, $pos + 1);
// the rest is the relative class name // the rest is the relative class name
$relativeClass = substr($class, $pos + 1); $relativeClass = \substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class // try to load a mapped file for the prefix and relative class
$mappedFile = $this->loadMappedFile($prefix, $relativeClass); $mappedFile = $this->loadMappedFile($prefix, $relativeClass);
@@ -87,7 +87,7 @@ class Psr4Autoloader
// remove the trailing namespace separator for the next iteration // remove the trailing namespace separator for the next iteration
// of strrpos() // of strrpos()
$prefix = rtrim($prefix, '\\'); $prefix = \rtrim($prefix, '\\');
} }
// never found a mapped file // never found a mapped file
@@ -115,7 +115,7 @@ class Psr4Autoloader
// replace namespace separators with directory separators // replace namespace separators with directory separators
// in the relative class name, append with .php // in the relative class name, append with .php
$file = $baseDir $file = $baseDir
. str_replace('\\', '/', $relativeClass) . \str_replace('\\', '/', $relativeClass)
. '.php'; . '.php';
// if the mapped file exists, require it // if the mapped file exists, require it
@@ -137,7 +137,7 @@ class Psr4Autoloader
*/ */
protected function requireFile($file) protected function requireFile($file)
{ {
if (file_exists($file)) { if (\file_exists($file)) {
require $file; require $file;
return true; return true;
+1 -1
View File
@@ -8,7 +8,7 @@ require_once 'Psr4Autoloader.php';
* @var string * @var string
* Full path to "src/Spout" which is what we want "Box\Spout" to map to. * Full path to "src/Spout" which is what we want "Box\Spout" to map to.
*/ */
$srcBaseDirectory = dirname(dirname(__FILE__)); $srcBaseDirectory = \dirname(\dirname(__FILE__));
$loader = new Psr4Autoloader(); $loader = new Psr4Autoloader();
$loader->register(); $loader->register();
+16 -8
View File
@@ -13,38 +13,38 @@ class Cell
/** /**
* Numeric cell type (whole numbers, fractional numbers, dates) * Numeric cell type (whole numbers, fractional numbers, dates)
*/ */
const TYPE_NUMERIC = 0; public const TYPE_NUMERIC = 0;
/** /**
* String (text) cell type * String (text) cell type
*/ */
const TYPE_STRING = 1; public const TYPE_STRING = 1;
/** /**
* Formula cell type * Formula cell type
* Not used at the moment * Not used at the moment
*/ */
const TYPE_FORMULA = 2; public const TYPE_FORMULA = 2;
/** /**
* Empty cell type * Empty cell type
*/ */
const TYPE_EMPTY = 3; public const TYPE_EMPTY = 3;
/** /**
* Boolean cell type * Boolean cell type
*/ */
const TYPE_BOOLEAN = 4; public const TYPE_BOOLEAN = 4;
/** /**
* Date cell type * Date cell type
*/ */
const TYPE_DATE = 5; public const TYPE_DATE = 5;
/** /**
* Error cell type * Error cell type
*/ */
const TYPE_ERROR = 6; public const TYPE_ERROR = 6;
/** /**
* The value of this cell * The value of this cell
@@ -65,7 +65,7 @@ class Cell
protected $style; protected $style;
/** /**
* @param $value mixed * @param mixed|null $value
* @param Style|null $style * @param Style|null $style
*/ */
public function __construct($value, Style $style = null) public function __construct($value, Style $style = null)
@@ -91,6 +91,14 @@ class Cell
return !$this->isError() ? $this->value : null; return !$this->isError() ? $this->value : null;
} }
/**
* @return mixed
*/
public function getValueEvenIfError()
{
return $this->value;
}
/** /**
* @param Style|null $style * @param Style|null $style
*/ */
+9 -3
View File
@@ -70,7 +70,7 @@ class Row
*/ */
public function getCellAtIndex($cellIndex) public function getCellAtIndex($cellIndex)
{ {
return isset($this->cells[$cellIndex]) ? $this->cells[$cellIndex] : null; return $this->cells[$cellIndex] ?? null;
} }
/** /**
@@ -89,7 +89,13 @@ class Row
*/ */
public function getNumCells() public function getNumCells()
{ {
return count($this->cells); // When using "setCellAtIndex", it's possible to
// have "$this->cells" contain holes.
if (empty($this->cells)) {
return 0;
}
return \max(\array_keys($this->cells)) + 1;
} }
/** /**
@@ -116,7 +122,7 @@ class Row
*/ */
public function toArray() public function toArray()
{ {
return array_map(function (Cell $cell) { return \array_map(function (Cell $cell) {
return $cell->getValue(); return $cell->getValue();
}, $this->cells); }, $this->cells);
} }
+12 -12
View File
@@ -7,20 +7,20 @@ namespace Box\Spout\Common\Entity\Style;
*/ */
class Border class Border
{ {
const LEFT = 'left'; public const LEFT = 'left';
const RIGHT = 'right'; public const RIGHT = 'right';
const TOP = 'top'; public const TOP = 'top';
const BOTTOM = 'bottom'; public const BOTTOM = 'bottom';
const STYLE_NONE = 'none'; public const STYLE_NONE = 'none';
const STYLE_SOLID = 'solid'; public const STYLE_SOLID = 'solid';
const STYLE_DASHED = 'dashed'; public const STYLE_DASHED = 'dashed';
const STYLE_DOTTED = 'dotted'; public const STYLE_DOTTED = 'dotted';
const STYLE_DOUBLE = 'double'; public const STYLE_DOUBLE = 'double';
const WIDTH_THIN = 'thin'; public const WIDTH_THIN = 'thin';
const WIDTH_MEDIUM = 'medium'; public const WIDTH_MEDIUM = 'medium';
const WIDTH_THICK = 'thick'; public const WIDTH_THICK = 'thick';
/** @var array A list of BorderPart objects for this border. */ /** @var array A list of BorderPart objects for this border. */
private $parts = []; private $parts = [];
+3 -3
View File
@@ -93,7 +93,7 @@ class BorderPart
*/ */
public function setName($name) public function setName($name)
{ {
if (!in_array($name, self::$allowedNames)) { if (!\in_array($name, self::$allowedNames)) {
throw new InvalidNameException($name); throw new InvalidNameException($name);
} }
$this->name = $name; $this->name = $name;
@@ -114,7 +114,7 @@ class BorderPart
*/ */
public function setStyle($style) public function setStyle($style)
{ {
if (!in_array($style, self::$allowedStyles)) { if (!\in_array($style, self::$allowedStyles)) {
throw new InvalidStyleException($style); throw new InvalidStyleException($style);
} }
$this->style = $style; $this->style = $style;
@@ -152,7 +152,7 @@ class BorderPart
*/ */
public function setWidth($width) public function setWidth($width)
{ {
if (!in_array($width, self::$allowedWidths)) { if (!\in_array($width, self::$allowedWidths)) {
throw new InvalidWidthException($width); throw new InvalidWidthException($width);
} }
$this->width = $width; $this->width = $width;
@@ -0,0 +1,32 @@
<?php
namespace Box\Spout\Common\Entity\Style;
/**
* Class Alignment
* This class provides constants to work with text alignment.
*/
abstract class CellAlignment
{
public const LEFT = 'left';
public const RIGHT = 'right';
public const CENTER = 'center';
public const JUSTIFY = 'justify';
private static $VALID_ALIGNMENTS = [
self::LEFT => 1,
self::RIGHT => 1,
self::CENTER => 1,
self::JUSTIFY => 1,
];
/**
* @param string $cellAlignment
*
* @return bool Whether the given cell alignment is valid
*/
public static function isValid($cellAlignment)
{
return isset(self::$VALID_ALIGNMENTS[$cellAlignment]);
}
}
+16 -16
View File
@@ -8,21 +8,21 @@ use Box\Spout\Common\Exception\InvalidColorException;
* Class Color * Class Color
* This class provides constants and functions to work with colors * This class provides constants and functions to work with colors
*/ */
class Color abstract class Color
{ {
/** Standard colors - based on Office Online */ /** Standard colors - based on Office Online */
const BLACK = '000000'; public const BLACK = '000000';
const WHITE = 'FFFFFF'; public const WHITE = 'FFFFFF';
const RED = 'FF0000'; public const RED = 'FF0000';
const DARK_RED = 'C00000'; public const DARK_RED = 'C00000';
const ORANGE = 'FFC000'; public const ORANGE = 'FFC000';
const YELLOW = 'FFFF00'; public const YELLOW = 'FFFF00';
const LIGHT_GREEN = '92D040'; public const LIGHT_GREEN = '92D040';
const GREEN = '00B050'; public const GREEN = '00B050';
const LIGHT_BLUE = '00B0E0'; public const LIGHT_BLUE = '00B0E0';
const BLUE = '0070C0'; public const BLUE = '0070C0';
const DARK_BLUE = '002060'; public const DARK_BLUE = '002060';
const PURPLE = '7030A0'; public const PURPLE = '7030A0';
/** /**
* Returns an RGB color from R, G and B values * Returns an RGB color from R, G and B values
@@ -38,7 +38,7 @@ class Color
self::throwIfInvalidColorComponentValue($green); self::throwIfInvalidColorComponentValue($green);
self::throwIfInvalidColorComponentValue($blue); self::throwIfInvalidColorComponentValue($blue);
return strtoupper( return \strtoupper(
self::convertColorComponentToHex($red) . self::convertColorComponentToHex($red) .
self::convertColorComponentToHex($green) . self::convertColorComponentToHex($green) .
self::convertColorComponentToHex($blue) self::convertColorComponentToHex($blue)
@@ -54,7 +54,7 @@ class Color
*/ */
protected static function throwIfInvalidColorComponentValue($colorComponent) protected static function throwIfInvalidColorComponentValue($colorComponent)
{ {
if (!is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) { if (!\is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent"); throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent");
} }
} }
@@ -67,7 +67,7 @@ class Color
*/ */
protected static function convertColorComponentToHex($colorComponent) protected static function convertColorComponentToHex($colorComponent)
{ {
return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT); return \str_pad(\dechex($colorComponent), 2, '0', STR_PAD_LEFT);
} }
/** /**
+129 -6
View File
@@ -8,10 +8,10 @@ namespace Box\Spout\Common\Entity\Style;
*/ */
class Style class Style
{ {
/** Default font values */ /** Default values */
const DEFAULT_FONT_SIZE = 11; public const DEFAULT_FONT_SIZE = 11;
const DEFAULT_FONT_COLOR = Color::BLACK; public const DEFAULT_FONT_COLOR = Color::BLACK;
const DEFAULT_FONT_NAME = 'Arial'; public const DEFAULT_FONT_NAME = 'Arial';
/** @var int|null Style ID */ /** @var int|null Style ID */
private $id; private $id;
@@ -54,12 +54,19 @@ class Style
/** @var bool Whether specific font properties should be applied */ /** @var bool Whether specific font properties should be applied */
private $shouldApplyFont = false; private $shouldApplyFont = false;
/** @var bool Whether specific cell alignment should be applied */
private $shouldApplyCellAlignment = false;
/** @var string Cell alignment */
private $cellAlignment;
/** @var bool Whether the cell alignment property was set */
private $hasSetCellAlignment = false;
/** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */ /** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */
private $shouldWrapText = false; private $shouldWrapText = false;
/** @var bool Whether the wrap text property was set */ /** @var bool Whether the wrap text property was set */
private $hasSetWrapText = false; private $hasSetWrapText = false;
/** @var Border */ /** @var Border|null */
private $border; private $border;
/** @var bool Whether border properties should be applied */ /** @var bool Whether border properties should be applied */
@@ -71,6 +78,18 @@ class Style
/** @var bool */ /** @var bool */
private $hasSetBackgroundColor = false; private $hasSetBackgroundColor = false;
/** @var string|null Format */
private $format;
/** @var bool */
private $hasSetFormat = false;
/** @var bool */
private $isRegistered = false;
/** @var bool */
private $isEmpty = true;
/** /**
* @return int|null * @return int|null
*/ */
@@ -91,7 +110,7 @@ class Style
} }
/** /**
* @return Border * @return Border|null
*/ */
public function getBorder() public function getBorder()
{ {
@@ -106,6 +125,7 @@ class Style
{ {
$this->shouldApplyBorder = true; $this->shouldApplyBorder = true;
$this->border = $border; $this->border = $border;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -134,6 +154,7 @@ class Style
$this->fontBold = true; $this->fontBold = true;
$this->hasSetFontBold = true; $this->hasSetFontBold = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -162,6 +183,7 @@ class Style
$this->fontItalic = true; $this->fontItalic = true;
$this->hasSetFontItalic = true; $this->hasSetFontItalic = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -190,6 +212,7 @@ class Style
$this->fontUnderline = true; $this->fontUnderline = true;
$this->hasSetFontUnderline = true; $this->hasSetFontUnderline = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -218,6 +241,7 @@ class Style
$this->fontStrikethrough = true; $this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true; $this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -247,6 +271,7 @@ class Style
$this->fontSize = $fontSize; $this->fontSize = $fontSize;
$this->hasSetFontSize = true; $this->hasSetFontSize = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -278,6 +303,7 @@ class Style
$this->fontColor = $fontColor; $this->fontColor = $fontColor;
$this->hasSetFontColor = true; $this->hasSetFontColor = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -307,6 +333,7 @@ class Style
$this->fontName = $fontName; $this->fontName = $fontName;
$this->hasSetFontName = true; $this->hasSetFontName = true;
$this->shouldApplyFont = true; $this->shouldApplyFont = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -319,6 +346,45 @@ class Style
return $this->hasSetFontName; return $this->hasSetFontName;
} }
/**
* @return string
*/
public function getCellAlignment()
{
return $this->cellAlignment;
}
/**
* @param string $cellAlignment The cell alignment
*
* @return Style
*/
public function setCellAlignment($cellAlignment)
{
$this->cellAlignment = $cellAlignment;
$this->hasSetCellAlignment = true;
$this->shouldApplyCellAlignment = true;
$this->isEmpty = false;
return $this;
}
/**
* @return bool
*/
public function hasSetCellAlignment()
{
return $this->hasSetCellAlignment;
}
/**
* @return bool Whether specific cell alignment should be applied
*/
public function shouldApplyCellAlignment()
{
return $this->shouldApplyCellAlignment;
}
/** /**
* @return bool * @return bool
*/ */
@@ -335,6 +401,7 @@ class Style
{ {
$this->shouldWrapText = $shouldWrap; $this->shouldWrapText = $shouldWrap;
$this->hasSetWrapText = true; $this->hasSetWrapText = true;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -364,6 +431,7 @@ class Style
{ {
$this->hasSetBackgroundColor = true; $this->hasSetBackgroundColor = true;
$this->backgroundColor = $color; $this->backgroundColor = $color;
$this->isEmpty = false;
return $this; return $this;
} }
@@ -383,4 +451,59 @@ class Style
{ {
return $this->hasSetBackgroundColor; return $this->hasSetBackgroundColor;
} }
/**
* Sets format
* @param string $format
* @return Style
*/
public function setFormat($format)
{
$this->hasSetFormat = true;
$this->format = $format;
$this->isEmpty = false;
return $this;
}
/**
* @return string|null
*/
public function getFormat()
{
return $this->format;
}
/**
* @return bool Whether format should be applied
*/
public function shouldApplyFormat()
{
return $this->hasSetFormat;
}
/**
* @return bool
*/
public function isRegistered() : bool
{
return $this->isRegistered;
}
public function markAsRegistered(?int $id) : void
{
$this->setId($id);
$this->isRegistered = true;
}
public function unmarkAsRegistered() : void
{
$this->setId(0);
$this->isRegistered = false;
}
public function isEmpty() : bool
{
return $this->isEmpty;
}
} }
+9 -9
View File
@@ -9,7 +9,7 @@ namespace Box\Spout\Common\Helper;
class CellTypeHelper class CellTypeHelper
{ {
/** /**
* @param $value * @param mixed|null $value
* @return bool Whether the given value is considered "empty" * @return bool Whether the given value is considered "empty"
*/ */
public static function isEmpty($value) public static function isEmpty($value)
@@ -18,24 +18,24 @@ class CellTypeHelper
} }
/** /**
* @param $value * @param mixed $value
* @return bool Whether the given value is a non empty string * @return bool Whether the given value is a non empty string
*/ */
public static function isNonEmptyString($value) public static function isNonEmptyString($value)
{ {
return (gettype($value) === 'string' && $value !== ''); return (\gettype($value) === 'string' && $value !== '');
} }
/** /**
* Returns whether the given value is numeric. * Returns whether the given value is numeric.
* A numeric value is from type "integer" or "double" ("float" is not returned by gettype). * A numeric value is from type "integer" or "double" ("float" is not returned by gettype).
* *
* @param $value * @param mixed $value
* @return bool Whether the given value is numeric * @return bool Whether the given value is numeric
*/ */
public static function isNumeric($value) public static function isNumeric($value)
{ {
$valueType = gettype($value); $valueType = \gettype($value);
return ($valueType === 'integer' || $valueType === 'double'); return ($valueType === 'integer' || $valueType === 'double');
} }
@@ -44,24 +44,24 @@ class CellTypeHelper
* Returns whether the given value is boolean. * Returns whether the given value is boolean.
* "true"/"false" and 0/1 are not booleans. * "true"/"false" and 0/1 are not booleans.
* *
* @param $value * @param mixed $value
* @return bool Whether the given value is boolean * @return bool Whether the given value is boolean
*/ */
public static function isBoolean($value) public static function isBoolean($value)
{ {
return gettype($value) === 'boolean'; return \gettype($value) === 'boolean';
} }
/** /**
* Returns whether the given value is a DateTime or DateInterval object. * Returns whether the given value is a DateTime or DateInterval object.
* *
* @param $value * @param mixed $value
* @return bool Whether the given value is a DateTime or DateInterval object * @return bool Whether the given value is a DateTime or DateInterval object
*/ */
public static function isDateTimeOrDateInterval($value) public static function isDateTimeOrDateInterval($value)
{ {
return ( return (
$value instanceof \DateTime || $value instanceof \DateTimeInterface ||
$value instanceof \DateInterval $value instanceof \DateInterval
); );
} }
+13 -13
View File
@@ -11,18 +11,18 @@ use Box\Spout\Common\Exception\EncodingConversionException;
class EncodingHelper class EncodingHelper
{ {
/** Definition of the encodings that can have a BOM */ /** Definition of the encodings that can have a BOM */
const ENCODING_UTF8 = 'UTF-8'; public const ENCODING_UTF8 = 'UTF-8';
const ENCODING_UTF16_LE = 'UTF-16LE'; public const ENCODING_UTF16_LE = 'UTF-16LE';
const ENCODING_UTF16_BE = 'UTF-16BE'; public const ENCODING_UTF16_BE = 'UTF-16BE';
const ENCODING_UTF32_LE = 'UTF-32LE'; public const ENCODING_UTF32_LE = 'UTF-32LE';
const ENCODING_UTF32_BE = 'UTF-32BE'; public const ENCODING_UTF32_BE = 'UTF-32BE';
/** Definition of the BOMs for the different encodings */ /** Definition of the BOMs for the different encodings */
const BOM_UTF8 = "\xEF\xBB\xBF"; public const BOM_UTF8 = "\xEF\xBB\xBF";
const BOM_UTF16_LE = "\xFF\xFE"; public const BOM_UTF16_LE = "\xFF\xFE";
const BOM_UTF16_BE = "\xFE\xFF"; public const BOM_UTF16_BE = "\xFE\xFF";
const BOM_UTF32_LE = "\xFF\xFE\x00\x00"; public const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
const BOM_UTF32_BE = "\x00\x00\xFE\xFF"; public const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
/** @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;
@@ -61,7 +61,7 @@ class EncodingHelper
$bomUsed = $this->supportedEncodingsWithBom[$encoding]; $bomUsed = $this->supportedEncodingsWithBom[$encoding];
// we skip the N first bytes // we skip the N first bytes
$byteOffsetToSkipBom = strlen($bomUsed); $byteOffsetToSkipBom = \strlen($bomUsed);
} }
return $byteOffsetToSkipBom; return $byteOffsetToSkipBom;
@@ -80,9 +80,9 @@ class EncodingHelper
$this->globalFunctionsHelper->rewind($filePointer); $this->globalFunctionsHelper->rewind($filePointer);
if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) { if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
$potentialBom = $this->supportedEncodingsWithBom[$encoding]; $potentialBom = $this->supportedEncodingsWithBom[$encoding];
$numBytesInBom = strlen($potentialBom); $numBytesInBom = \strlen($potentialBom);
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom); $hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
} }
+4 -4
View File
@@ -18,14 +18,14 @@ class ODS implements EscaperInterface
{ {
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as // @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
// single/double quotes (for XML attributes) need to be encoded. // single/double quotes (for XML attributes) need to be encoded.
if (defined('ENT_DISALLOWED')) { if (\defined('ENT_DISALLOWED')) {
// 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced. // 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced.
// Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor // Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor
// @link https://github.com/box/spout/issues/329 // @link https://github.com/box/spout/issues/329
$replacedString = htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8'); $replacedString = \htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
} else { } else {
// We are on hhvm or any other engine that does not support ENT_DISALLOWED. // We are on hhvm or any other engine that does not support ENT_DISALLOWED.
$escapedString = htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); $escapedString = \htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
// control characters values are from 0 to 1F (hex values) in the ASCII table // control characters values are from 0 to 1F (hex values) in the ASCII table
// some characters should not be escaped though: "\t", "\r" and "\n". // some characters should not be escaped though: "\t", "\r" and "\n".
@@ -34,7 +34,7 @@ class ODS implements EscaperInterface
'\x0B-\x0C' . '\x0B-\x0C' .
// skipping "\r" (0xD) // skipping "\r" (0xD)
'\x0E-\x1F]'; '\x0E-\x1F]';
$replacedString = preg_replace("/$regexPattern/", '�', $escapedString); $replacedString = \preg_replace("/$regexPattern/", '�', $escapedString);
} }
return $replacedString; return $replacedString;
+11 -11
View File
@@ -28,7 +28,7 @@ class XLSX implements EscaperInterface
if (!$this->isAlreadyInitialized) { if (!$this->isAlreadyInitialized) {
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern(); $this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap(); $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
$this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap); $this->controlCharactersEscapingReverseMap = \array_flip($this->controlCharactersEscapingMap);
$this->isAlreadyInitialized = true; $this->isAlreadyInitialized = true;
} }
@@ -47,7 +47,7 @@ class XLSX implements EscaperInterface
$escapedString = $this->escapeControlCharacters($string); $escapedString = $this->escapeControlCharacters($string);
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as // @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
// single/double quotes (for XML attributes) need to be encoded. // single/double quotes (for XML attributes) need to be encoded.
$escapedString = htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8'); $escapedString = \htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8');
return $escapedString; return $escapedString;
} }
@@ -103,10 +103,10 @@ class XLSX implements EscaperInterface
// control characters values are from 0 to 1F (hex values) in the ASCII table // control characters values are from 0 to 1F (hex values) in the ASCII table
for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) { for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) {
$character = chr($charValue); $character = \chr($charValue);
if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) { if (\preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
$charHexValue = dechex($charValue); $charHexValue = \dechex($charValue);
$escapedChar = '_x' . sprintf('%04s', strtoupper($charHexValue)) . '_'; $escapedChar = '_x' . \sprintf('%04s', \strtoupper($charHexValue)) . '_';
$controlCharactersEscapingMap[$escapedChar] = $character; $controlCharactersEscapingMap[$escapedChar] = $character;
} }
} }
@@ -132,11 +132,11 @@ class XLSX implements EscaperInterface
$escapedString = $this->escapeEscapeCharacter($string); $escapedString = $this->escapeEscapeCharacter($string);
// if no control characters // if no control characters
if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) { if (!\preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
return $escapedString; return $escapedString;
} }
return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function ($matches) { return \preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function ($matches) {
return $this->controlCharactersEscapingReverseMap[$matches[0]]; return $this->controlCharactersEscapingReverseMap[$matches[0]];
}, $escapedString); }, $escapedString);
} }
@@ -149,7 +149,7 @@ class XLSX implements EscaperInterface
*/ */
protected function escapeEscapeCharacter($string) protected function escapeEscapeCharacter($string)
{ {
return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string); return \preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
} }
/** /**
@@ -171,7 +171,7 @@ class XLSX implements EscaperInterface
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) { foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
// only unescape characters that don't contain the escaped escape character for now // only unescape characters that don't contain the escaped escape character for now
$unescapedString = preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString); $unescapedString = \preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
} }
return $this->unescapeEscapeCharacter($unescapedString); return $this->unescapeEscapeCharacter($unescapedString);
@@ -185,6 +185,6 @@ class XLSX implements EscaperInterface
*/ */
protected function unescapeEscapeCharacter($string) protected function unescapeEscapeCharacter($string)
{ {
return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string); return \preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
} }
} }
+17 -13
View File
@@ -17,9 +17,9 @@ class FileSystemHelper implements FileSystemHelperInterface
/** /**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur * @param string $baseFolderPath The path of the base folder where all the I/O can occur
*/ */
public function __construct($baseFolderPath) public function __construct(string $baseFolderPath)
{ {
$this->baseFolderRealPath = realpath($baseFolderPath); $this->baseFolderRealPath = \realpath($baseFolderPath);
} }
/** /**
@@ -36,7 +36,7 @@ class FileSystemHelper implements FileSystemHelperInterface
$folderPath = $parentFolderPath . '/' . $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");
} }
@@ -60,7 +60,7 @@ class FileSystemHelper implements FileSystemHelperInterface
$filePath = $parentFolderPath . '/' . $fileName; $filePath = $parentFolderPath . '/' . $fileName;
$wasCreationSuccessful = file_put_contents($filePath, $fileContents); $wasCreationSuccessful = \file_put_contents($filePath, $fileContents);
if ($wasCreationSuccessful === false) { if ($wasCreationSuccessful === false) {
throw new IOException("Unable to create file: $filePath"); throw new IOException("Unable to create file: $filePath");
} }
@@ -79,8 +79,8 @@ class FileSystemHelper implements FileSystemHelperInterface
{ {
$this->throwIfOperationNotInBaseFolder($filePath); $this->throwIfOperationNotInBaseFolder($filePath);
if (file_exists($filePath) && is_file($filePath)) { if (\file_exists($filePath) && \is_file($filePath)) {
unlink($filePath); \unlink($filePath);
} }
} }
@@ -102,13 +102,13 @@ class FileSystemHelper implements FileSystemHelperInterface
foreach ($itemIterator as $item) { foreach ($itemIterator as $item) {
if ($item->isDir()) { if ($item->isDir()) {
rmdir($item->getPathname()); \rmdir($item->getPathname());
} else { } else {
unlink($item->getPathname()); \unlink($item->getPathname());
} }
} }
rmdir($folderPath); \rmdir($folderPath);
} }
/** /**
@@ -117,13 +117,17 @@ class FileSystemHelper implements FileSystemHelperInterface
* should occur is not inside the base folder. * should occur is not inside the base folder.
* *
* @param string $operationFolderPath The path of the folder where the I/O operation should occur * @param string $operationFolderPath The path of the folder where the I/O operation should occur
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur
* is not inside the base folder or the base folder does not exist
* @return void * @return void
*/ */
protected function throwIfOperationNotInBaseFolder($operationFolderPath) protected function throwIfOperationNotInBaseFolder(string $operationFolderPath)
{ {
$operationFolderRealPath = realpath($operationFolderPath); $operationFolderRealPath = \realpath($operationFolderPath);
$isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0); if (!$this->baseFolderRealPath) {
throw new IOException("The base folder path is invalid: {$this->baseFolderRealPath}");
}
$isInBaseFolder = (\strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
if (!$isInBaseFolder) { if (!$isInBaseFolder) {
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}"); throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
} }
@@ -20,7 +20,7 @@ class GlobalFunctionsHelper
*/ */
public function fopen($fileName, $mode) public function fopen($fileName, $mode)
{ {
return fopen($fileName, $mode); return \fopen($fileName, $mode);
} }
/** /**
@@ -33,7 +33,7 @@ class GlobalFunctionsHelper
*/ */
public function fgets($handle, $length = null) public function fgets($handle, $length = null)
{ {
return fgets($handle, $length); return \fgets($handle, $length);
} }
/** /**
@@ -46,7 +46,7 @@ class GlobalFunctionsHelper
*/ */
public function fputs($handle, $string) public function fputs($handle, $string)
{ {
return fputs($handle, $string); return \fputs($handle, $string);
} }
/** /**
@@ -58,7 +58,7 @@ class GlobalFunctionsHelper
*/ */
public function fflush($handle) public function fflush($handle)
{ {
return fflush($handle); return \fflush($handle);
} }
/** /**
@@ -71,7 +71,7 @@ class GlobalFunctionsHelper
*/ */
public function fseek($handle, $offset) public function fseek($handle, $offset)
{ {
return fseek($handle, $offset); return \fseek($handle, $offset);
} }
/** /**
@@ -82,7 +82,7 @@ class GlobalFunctionsHelper
* @param int|null $length * @param int|null $length
* @param string|null $delimiter * @param string|null $delimiter
* @param string|null $enclosure * @param string|null $enclosure
* @return array * @return array|false
*/ */
public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null) public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null)
{ {
@@ -90,9 +90,9 @@ class GlobalFunctionsHelper
// To fix that, simply disable the escape character. // To fix that, simply disable the escape character.
// @see https://bugs.php.net/bug.php?id=43225 // @see https://bugs.php.net/bug.php?id=43225
// @see http://tools.ietf.org/html/rfc4180 // @see http://tools.ietf.org/html/rfc4180
$escapeCharacter = "\0"; $escapeCharacter = PHP_VERSION_ID >= 70400 ? '' : "\0";
return fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter); return \fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter);
} }
/** /**
@@ -103,7 +103,7 @@ class GlobalFunctionsHelper
* @param array $fields * @param array $fields
* @param string|null $delimiter * @param string|null $delimiter
* @param string|null $enclosure * @param string|null $enclosure
* @return int * @return int|false
*/ */
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null) public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
{ {
@@ -111,9 +111,9 @@ class GlobalFunctionsHelper
// To fix that, simply disable the escape character. // To fix that, simply disable the escape character.
// @see https://bugs.php.net/bug.php?id=43225 // @see https://bugs.php.net/bug.php?id=43225
// @see http://tools.ietf.org/html/rfc4180 // @see http://tools.ietf.org/html/rfc4180
$escapeCharacter = "\0"; $escapeCharacter = PHP_VERSION_ID >= 70400 ? '' : "\0";
return fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter); return \fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter);
} }
/** /**
@@ -126,7 +126,7 @@ class GlobalFunctionsHelper
*/ */
public function fwrite($handle, $string) public function fwrite($handle, $string)
{ {
return fwrite($handle, $string); return \fwrite($handle, $string);
} }
/** /**
@@ -138,7 +138,7 @@ class GlobalFunctionsHelper
*/ */
public function fclose($handle) public function fclose($handle)
{ {
return fclose($handle); return \fclose($handle);
} }
/** /**
@@ -150,7 +150,7 @@ class GlobalFunctionsHelper
*/ */
public function rewind($handle) public function rewind($handle)
{ {
return rewind($handle); return \rewind($handle);
} }
/** /**
@@ -162,7 +162,7 @@ class GlobalFunctionsHelper
*/ */
public function file_exists($fileName) public function file_exists($fileName)
{ {
return file_exists($fileName); return \file_exists($fileName);
} }
/** /**
@@ -176,7 +176,7 @@ class GlobalFunctionsHelper
{ {
$realFilePath = $this->convertToUseRealPath($filePath); $realFilePath = $this->convertToUseRealPath($filePath);
return file_get_contents($realFilePath); return \file_get_contents($realFilePath);
} }
/** /**
@@ -191,13 +191,13 @@ class GlobalFunctionsHelper
$realFilePath = $filePath; $realFilePath = $filePath;
if ($this->isZipStream($filePath)) { if ($this->isZipStream($filePath)) {
if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) { if (\preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
$documentPath = $matches[1]; $documentPath = $matches[1];
$documentInsideZipPath = $matches[2]; $documentInsideZipPath = $matches[2];
$realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath; $realFilePath = 'zip://' . \realpath($documentPath) . '#' . $documentInsideZipPath;
} }
} else { } else {
$realFilePath = realpath($filePath); $realFilePath = \realpath($filePath);
} }
return $realFilePath; return $realFilePath;
@@ -211,7 +211,7 @@ class GlobalFunctionsHelper
*/ */
protected function isZipStream($path) protected function isZipStream($path)
{ {
return (strpos($path, 'zip://') === 0); return (\strpos($path, 'zip://') === 0);
} }
/** /**
@@ -223,7 +223,7 @@ class GlobalFunctionsHelper
*/ */
public function feof($handle) public function feof($handle)
{ {
return feof($handle); return \feof($handle);
} }
/** /**
@@ -235,7 +235,7 @@ class GlobalFunctionsHelper
*/ */
public function is_readable($fileName) public function is_readable($fileName)
{ {
return is_readable($fileName); return \is_readable($fileName);
} }
/** /**
@@ -243,12 +243,12 @@ class GlobalFunctionsHelper
* @see basename() * @see basename()
* *
* @param string $path * @param string $path
* @param string|null $suffix * @param string $suffix
* @return string * @return string
*/ */
public function basename($path, $suffix = null) public function basename($path, $suffix = '')
{ {
return basename($path, $suffix); return \basename($path, $suffix);
} }
/** /**
@@ -260,7 +260,7 @@ class GlobalFunctionsHelper
*/ */
public function header($string) public function header($string)
{ {
header($string); \header($string);
} }
/** /**
@@ -271,8 +271,8 @@ class GlobalFunctionsHelper
*/ */
public function ob_end_clean() public function ob_end_clean()
{ {
if (ob_get_length() > 0) { if (\ob_get_length() > 0) {
ob_end_clean(); \ob_end_clean();
} }
} }
@@ -287,7 +287,7 @@ class GlobalFunctionsHelper
*/ */
public function iconv($string, $sourceEncoding, $targetEncoding) public function iconv($string, $sourceEncoding, $targetEncoding)
{ {
return iconv($sourceEncoding, $targetEncoding, $string); return \iconv($sourceEncoding, $targetEncoding, $string);
} }
/** /**
@@ -301,7 +301,7 @@ class GlobalFunctionsHelper
*/ */
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding) public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
{ {
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding); return \mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
} }
/** /**
@@ -312,7 +312,7 @@ class GlobalFunctionsHelper
*/ */
public function stream_get_wrappers() public function stream_get_wrappers()
{ {
return stream_get_wrappers(); return \stream_get_wrappers();
} }
/** /**
@@ -324,6 +324,6 @@ class GlobalFunctionsHelper
*/ */
public function function_exists($functionName) public function function_exists($functionName)
{ {
return function_exists($functionName); return \function_exists($functionName);
} }
} }
+38 -4
View File
@@ -13,12 +13,20 @@ class StringHelper
/** @var bool Whether the mbstring extension is loaded */ /** @var bool Whether the mbstring extension is loaded */
protected $hasMbstringSupport; protected $hasMbstringSupport;
/** @var bool Whether the code is running with PHP7 or older versions */
private $isRunningPhp7OrOlder;
/** @var array Locale info, used for number formatting */
private $localeInfo;
/** /**
* *
*/ */
public function __construct() public function __construct()
{ {
$this->hasMbstringSupport = extension_loaded('mbstring'); $this->hasMbstringSupport = \extension_loaded('mbstring');
$this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0;
$this->localeInfo = \localeconv();
} }
/** /**
@@ -32,7 +40,7 @@ class StringHelper
*/ */
public function getStringLength($string) public function getStringLength($string)
{ {
return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string); return $this->hasMbstringSupport ? \mb_strlen($string) : \strlen($string);
} }
/** /**
@@ -47,7 +55,7 @@ class StringHelper
*/ */
public function getCharFirstOccurrencePosition($char, $string) public function getCharFirstOccurrencePosition($char, $string)
{ {
$position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char); $position = $this->hasMbstringSupport ? \mb_strpos($string, $char) : \strpos($string, $char);
return ($position !== false) ? $position : -1; return ($position !== false) ? $position : -1;
} }
@@ -64,8 +72,34 @@ class StringHelper
*/ */
public function getCharLastOccurrencePosition($char, $string) public function getCharLastOccurrencePosition($char, $string)
{ {
$position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char); $position = $this->hasMbstringSupport ? \mb_strrpos($string, $char) : \strrpos($string, $char);
return ($position !== false) ? $position : -1; return ($position !== false) ? $position : -1;
} }
/**
* Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format.
*
* Formatting of float values is locale dependent in PHP < 8.
* Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34).
* However, float values must be formatted with no thousands separator and a "." as decimal point
* to work properly. This method can be used to convert the value to the correct format before storing it.
*
* @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8.
*
* @param int|float $numericValue
* @return int|float|string
*/
public function formatNumericValue($numericValue)
{
if ($this->isRunningPhp7OrOlder && is_float($numericValue)) {
return str_replace(
[$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
['', '.'],
$numericValue
);
}
return $numericValue;
}
} }
@@ -7,7 +7,7 @@ namespace Box\Spout\Common\Manager;
*/ */
abstract class OptionsManagerAbstract implements OptionsManagerInterface abstract class OptionsManagerAbstract implements OptionsManagerInterface
{ {
const PREFIX_OPTION = 'OPTION_'; public const PREFIX_OPTION = 'OPTION_';
/** @var string[] List of all supported option names */ /** @var string[] List of all supported option names */
private $supportedOptions = []; private $supportedOptions = [];
@@ -46,7 +46,7 @@ abstract class OptionsManagerAbstract implements OptionsManagerInterface
*/ */
public function setOption($optionName, $optionValue) public function setOption($optionName, $optionValue)
{ {
if (in_array($optionName, $this->supportedOptions)) { if (\in_array($optionName, $this->supportedOptions)) {
$this->options[$optionName] = $optionValue; $this->options[$optionName] = $optionValue;
} }
} }
+3 -3
View File
@@ -8,7 +8,7 @@ namespace Box\Spout\Common;
*/ */
abstract class Type abstract class Type
{ {
const CSV = 'csv'; public const CSV = 'csv';
const XLSX = 'xlsx'; public const XLSX = 'xlsx';
const ODS = 'ods'; public const ODS = 'ods';
} }
@@ -89,7 +89,7 @@ class InternalEntityFactory implements InternalEntityFactoryInterface
*/ */
public function createRowFromArray(array $cellValues = []) public function createRowFromArray(array $cellValues = [])
{ {
$cells = array_map(function ($cellValue) { $cells = \array_map(function ($cellValue) {
return $this->createCell($cellValue); return $this->createCell($cellValue);
}, $cellValues); }, $cellValues);
+30 -4
View File
@@ -3,6 +3,9 @@
namespace Box\Spout\Reader\CSV; namespace Box\Spout\Reader\CSV;
use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
use Box\Spout\Common\Manager\OptionsManagerInterface;
use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
use Box\Spout\Reader\Common\Entity\Options; use Box\Spout\Reader\Common\Entity\Options;
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory; use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
use Box\Spout\Reader\ReaderAbstract; use Box\Spout\Reader\ReaderAbstract;
@@ -22,6 +25,23 @@ class Reader extends ReaderAbstract
/** @var string Original value for the "auto_detect_line_endings" INI value */ /** @var string Original value for the "auto_detect_line_endings" INI value */
protected $originalAutoDetectLineEndings; protected $originalAutoDetectLineEndings;
/** @var bool Whether the code is running with PHP >= 8.1 */
private $isRunningAtLeastPhp81;
/**
* @param OptionsManagerInterface $optionsManager
* @param GlobalFunctionsHelper $globalFunctionsHelper
* @param InternalEntityFactoryInterface $entityFactory
*/
public function __construct(
OptionsManagerInterface $optionsManager,
GlobalFunctionsHelper $globalFunctionsHelper,
InternalEntityFactoryInterface $entityFactory
) {
parent::__construct($optionsManager, $globalFunctionsHelper, $entityFactory);
$this->isRunningAtLeastPhp81 = \version_compare(PHP_VERSION, '8.1.0') >= 0;
}
/** /**
* Sets the field delimiter for the CSV. * Sets the field delimiter for the CSV.
* Needs to be called before opening the reader. * Needs to be called before opening the reader.
@@ -84,8 +104,11 @@ class Reader extends ReaderAbstract
*/ */
protected function openReader($filePath) protected function openReader($filePath)
{ {
$this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings'); // "auto_detect_line_endings" is deprecated in PHP 8.1
ini_set('auto_detect_line_endings', '1'); if (!$this->isRunningAtLeastPhp81) {
$this->originalAutoDetectLineEndings = \ini_get('auto_detect_line_endings');
\ini_set('auto_detect_line_endings', '1');
}
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r'); $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) { if (!$this->filePointer) {
@@ -119,10 +142,13 @@ class Reader extends ReaderAbstract
*/ */
protected function closeReader() protected function closeReader()
{ {
if ($this->filePointer) { if (is_resource($this->filePointer)) {
$this->globalFunctionsHelper->fclose($this->filePointer); $this->globalFunctionsHelper->fclose($this->filePointer);
} }
ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings); // "auto_detect_line_endings" is deprecated in PHP 8.1
if (!$this->isRunningAtLeastPhp81) {
\ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
}
} }
} }
+13 -13
View File
@@ -19,9 +19,9 @@ class RowIterator implements IteratorInterface
/** /**
* Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines). * Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines).
*/ */
const MAX_READ_BYTES_PER_LINE = 0; public const MAX_READ_BYTES_PER_LINE = 0;
/** @var resource Pointer to the CSV file to read */ /** @var resource|null Pointer to the CSV file to read */
protected $filePointer; protected $filePointer;
/** @var int Number of read rows */ /** @var int Number of read rows */
@@ -84,7 +84,7 @@ class RowIterator implements IteratorInterface
* *
* @return void * @return void
*/ */
public function rewind() public function rewind() : void
{ {
$this->rewindAndSkipBom(); $this->rewindAndSkipBom();
@@ -114,7 +114,7 @@ class RowIterator implements IteratorInterface
* *
* @return bool * @return bool
*/ */
public function valid() public function valid() : bool
{ {
return ($this->filePointer && !$this->hasReachedEndOfFile); return ($this->filePointer && !$this->hasReachedEndOfFile);
} }
@@ -126,7 +126,7 @@ class RowIterator implements IteratorInterface
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void * @return void
*/ */
public function next() public function next() : void
{ {
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
@@ -146,8 +146,8 @@ class RowIterator implements IteratorInterface
} while ($this->shouldReadNextRow($rowData)); } while ($this->shouldReadNextRow($rowData));
if ($rowData !== false) { if ($rowData !== false) {
// str_replace will replace NULL values by empty strings // array_map will replace NULL values by empty strings
$rowDataBufferAsArray = str_replace(null, null, $rowData); $rowDataBufferAsArray = array_map(function ($value) { return (string) $value; }, $rowData);
$this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray); $this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
$this->numReadRows++; $this->numReadRows++;
} else { } else {
@@ -193,13 +193,13 @@ class RowIterator implements IteratorInterface
case EncodingHelper::ENCODING_UTF16_LE: case EncodingHelper::ENCODING_UTF16_LE:
case EncodingHelper::ENCODING_UTF32_LE: case EncodingHelper::ENCODING_UTF32_LE:
// remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
$cellValue = ltrim($cellValue); $cellValue = \ltrim($cellValue);
break; break;
case EncodingHelper::ENCODING_UTF16_BE: case EncodingHelper::ENCODING_UTF16_BE:
case EncodingHelper::ENCODING_UTF32_BE: case EncodingHelper::ENCODING_UTF32_BE:
// remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data // remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
$cellValue = rtrim($cellValue); $cellValue = \rtrim($cellValue);
break; break;
} }
@@ -215,7 +215,7 @@ class RowIterator implements IteratorInterface
*/ */
protected function isEmptyLine($lineData) protected function isEmptyLine($lineData)
{ {
return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null); return (\is_array($lineData) && \count($lineData) === 1 && $lineData[0] === null);
} }
/** /**
@@ -224,7 +224,7 @@ class RowIterator implements IteratorInterface
* *
* @return Row|null * @return Row|null
*/ */
public function current() public function current() : ?Row
{ {
return $this->rowBuffer; return $this->rowBuffer;
} }
@@ -235,7 +235,7 @@ class RowIterator implements IteratorInterface
* *
* @return int * @return int
*/ */
public function key() public function key() : int
{ {
return $this->numReadRows; return $this->numReadRows;
} }
@@ -245,7 +245,7 @@ class RowIterator implements IteratorInterface
* *
* @return void * @return void
*/ */
public function end() public function end() : void
{ {
// do nothing // do nothing
} }
+8 -8
View File
@@ -10,7 +10,7 @@ use Box\Spout\Reader\IteratorInterface;
*/ */
class SheetIterator implements IteratorInterface class SheetIterator implements IteratorInterface
{ {
/** @var \Box\Spout\Reader\CSV\Sheet The CSV unique "sheet" */ /** @var Sheet The CSV unique "sheet" */
protected $sheet; protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */ /** @var bool Whether the unique "sheet" has already been read */
@@ -30,7 +30,7 @@ class SheetIterator implements IteratorInterface
* *
* @return void * @return void
*/ */
public function rewind() public function rewind() : void
{ {
$this->hasReadUniqueSheet = false; $this->hasReadUniqueSheet = false;
} }
@@ -41,7 +41,7 @@ class SheetIterator implements IteratorInterface
* *
* @return bool * @return bool
*/ */
public function valid() public function valid() : bool
{ {
return (!$this->hasReadUniqueSheet); return (!$this->hasReadUniqueSheet);
} }
@@ -52,7 +52,7 @@ class SheetIterator implements IteratorInterface
* *
* @return void * @return void
*/ */
public function next() public function next() : void
{ {
$this->hasReadUniqueSheet = true; $this->hasReadUniqueSheet = true;
} }
@@ -61,9 +61,9 @@ class SheetIterator implements IteratorInterface
* Return the current element * Return the current element
* @see http://php.net/manual/en/iterator.current.php * @see http://php.net/manual/en/iterator.current.php
* *
* @return \Box\Spout\Reader\CSV\Sheet * @return Sheet
*/ */
public function current() public function current() : Sheet
{ {
return $this->sheet; return $this->sheet;
} }
@@ -74,7 +74,7 @@ class SheetIterator implements IteratorInterface
* *
* @return int * @return int
*/ */
public function key() public function key() : int
{ {
return 1; return 1;
} }
@@ -84,7 +84,7 @@ class SheetIterator implements IteratorInterface
* *
* @return void * @return void
*/ */
public function end() public function end() : void
{ {
// do nothing // do nothing
} }
@@ -35,6 +35,7 @@ class ReaderEntityFactory
return ReaderFactory::createFromType(Type::CSV); return ReaderFactory::createFromType(Type::CSV);
} catch (UnsupportedTypeException $e) { } catch (UnsupportedTypeException $e) {
// should never happen // should never happen
return null;
} }
} }
@@ -49,6 +50,7 @@ class ReaderEntityFactory
return ReaderFactory::createFromType(Type::XLSX); return ReaderFactory::createFromType(Type::XLSX);
} catch (UnsupportedTypeException $e) { } catch (UnsupportedTypeException $e) {
// should never happen // should never happen
return null;
} }
} }
@@ -63,6 +65,7 @@ class ReaderEntityFactory
return ReaderFactory::createFromType(Type::ODS); return ReaderFactory::createFromType(Type::ODS);
} catch (UnsupportedTypeException $e) { } catch (UnsupportedTypeException $e) {
// should never happen // should never happen
return null;
} }
} }
} }
@@ -37,7 +37,7 @@ class ReaderFactory
*/ */
public static function createFromFile(string $path) public static function createFromFile(string $path)
{ {
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $extension = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
return self::createFromType($extension); return self::createFromType($extension);
} }
+7 -7
View File
@@ -9,15 +9,15 @@ namespace Box\Spout\Reader\Common\Entity;
abstract class Options abstract class Options
{ {
// Common options // Common options
const SHOULD_FORMAT_DATES = 'shouldFormatDates'; public const SHOULD_FORMAT_DATES = 'shouldFormatDates';
const SHOULD_PRESERVE_EMPTY_ROWS = 'shouldPreserveEmptyRows'; public const SHOULD_PRESERVE_EMPTY_ROWS = 'shouldPreserveEmptyRows';
// CSV specific options // CSV specific options
const FIELD_DELIMITER = 'fieldDelimiter'; public const FIELD_DELIMITER = 'fieldDelimiter';
const FIELD_ENCLOSURE = 'fieldEnclosure'; public const FIELD_ENCLOSURE = 'fieldEnclosure';
const ENCODING = 'encoding'; public const ENCODING = 'encoding';
// XLSX specific options // XLSX specific options
const TEMP_FOLDER = 'tempFolder'; public const TEMP_FOLDER = 'tempFolder';
const SHOULD_USE_1904_DATES = 'shouldUse1904Dates'; public const SHOULD_USE_1904_DATES = 'shouldUse1904Dates';
} }
+17 -2
View File
@@ -47,19 +47,34 @@ class RowManager
*/ */
public function fillMissingIndexesWithEmptyCells(Row $row) public function fillMissingIndexesWithEmptyCells(Row $row)
{ {
if ($row->getNumCells() === 0) { $numCells = $row->getNumCells();
if ($numCells === 0) {
return $row; return $row;
} }
$rowCells = $row->getCells(); $rowCells = $row->getCells();
$maxCellIndex = max(array_keys($rowCells)); $maxCellIndex = $numCells;
// If the row has empty cells, calling "setCellAtIndex" will add the cell
// but in the wrong place (the new cell is added at the end of the array).
// Therefore, we need to sort the array using keys to have proper order.
// @see https://github.com/box/spout/issues/740
$needsSorting = false;
for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) { for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) {
if (!isset($rowCells[$cellIndex])) { if (!isset($rowCells[$cellIndex])) {
$row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex); $row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
$needsSorting = true;
} }
} }
if ($needsSorting) {
$rowCells = $row->getCells();
ksort($rowCells);
$row->setCells($rowCells);
}
return $row; return $row;
} }
} }
+7 -7
View File
@@ -11,16 +11,16 @@ use Box\Spout\Reader\Wrapper\XMLReader;
class XMLProcessor class XMLProcessor
{ {
/* Node types */ /* Node types */
const NODE_TYPE_START = XMLReader::ELEMENT; public const NODE_TYPE_START = XMLReader::ELEMENT;
const NODE_TYPE_END = XMLReader::END_ELEMENT; public const NODE_TYPE_END = XMLReader::END_ELEMENT;
/* Keys associated to reflection attributes to invoke a callback */ /* Keys associated to reflection attributes to invoke a callback */
const CALLBACK_REFLECTION_METHOD = 'reflectionMethod'; public const CALLBACK_REFLECTION_METHOD = 'reflectionMethod';
const CALLBACK_REFLECTION_OBJECT = 'reflectionObject'; public const CALLBACK_REFLECTION_OBJECT = 'reflectionObject';
/* Values returned by the callbacks to indicate what the processor should do next */ /* Values returned by the callbacks to indicate what the processor should do next */
const PROCESSING_CONTINUE = 1; public const PROCESSING_CONTINUE = 1;
const PROCESSING_STOP = 2; public const PROCESSING_STOP = 2;
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader; protected $xmlReader;
@@ -73,7 +73,7 @@ class XMLProcessor
{ {
$callbackObject = $callback[0]; $callbackObject = $callback[0];
$callbackMethodName = $callback[1]; $callbackMethodName = $callback[1];
$reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName); $reflectionMethod = new \ReflectionMethod(\get_class($callbackObject), $callbackMethodName);
$reflectionMethod->setAccessible(true); $reflectionMethod->setAccessible(true);
return [ return [
@@ -105,7 +105,7 @@ class InternalEntityFactory implements InternalEntityFactoryInterface
} }
/** /**
* @param $xmlReader * @param XMLReader $xmlReader
* @return XMLProcessor * @return XMLProcessor
*/ */
private function createXMLProcessor($xmlReader) private function createXMLProcessor($xmlReader)
@@ -11,29 +11,31 @@ use Box\Spout\Reader\Exception\InvalidValueException;
class CellValueFormatter class CellValueFormatter
{ {
/** Definition of all possible cell types */ /** Definition of all possible cell types */
const CELL_TYPE_STRING = 'string'; public const CELL_TYPE_STRING = 'string';
const CELL_TYPE_FLOAT = 'float'; public const CELL_TYPE_FLOAT = 'float';
const CELL_TYPE_BOOLEAN = 'boolean'; public const CELL_TYPE_BOOLEAN = 'boolean';
const CELL_TYPE_DATE = 'date'; public const CELL_TYPE_DATE = 'date';
const CELL_TYPE_TIME = 'time'; public const CELL_TYPE_TIME = 'time';
const CELL_TYPE_CURRENCY = 'currency'; public const CELL_TYPE_CURRENCY = 'currency';
const CELL_TYPE_PERCENTAGE = 'percentage'; public const CELL_TYPE_PERCENTAGE = 'percentage';
const CELL_TYPE_VOID = 'void'; public const CELL_TYPE_VOID = 'void';
/** Definition of XML nodes names used to parse data */ /** Definition of XML nodes names used to parse data */
const XML_NODE_P = 'p'; public const XML_NODE_P = 'p';
const XML_NODE_S = 'text:s'; public const XML_NODE_TEXT_A = 'text:a';
const XML_NODE_A = 'text:a'; public const XML_NODE_TEXT_SPAN = 'text:span';
const XML_NODE_SPAN = 'text:span'; public const XML_NODE_TEXT_S = 'text:s';
public const XML_NODE_TEXT_TAB = 'text:tab';
public const XML_NODE_TEXT_LINE_BREAK = 'text:line-break';
/** Definition of XML attributes used to parse data */ /** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_TYPE = 'office:value-type'; public const XML_ATTRIBUTE_TYPE = 'office:value-type';
const XML_ATTRIBUTE_VALUE = 'office:value'; public const XML_ATTRIBUTE_VALUE = 'office:value';
const XML_ATTRIBUTE_BOOLEAN_VALUE = 'office:boolean-value'; public const XML_ATTRIBUTE_BOOLEAN_VALUE = 'office:boolean-value';
const XML_ATTRIBUTE_DATE_VALUE = 'office:date-value'; public const XML_ATTRIBUTE_DATE_VALUE = 'office:date-value';
const XML_ATTRIBUTE_TIME_VALUE = 'office:time-value'; public const XML_ATTRIBUTE_TIME_VALUE = 'office:time-value';
const XML_ATTRIBUTE_CURRENCY = 'office:currency'; public const XML_ATTRIBUTE_CURRENCY = 'office:currency';
const XML_ATTRIBUTE_C = 'text:c'; public const XML_ATTRIBUTE_C = 'text:c';
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
protected $shouldFormatDates; protected $shouldFormatDates;
@@ -41,6 +43,13 @@ class CellValueFormatter
/** @var \Box\Spout\Common\Helper\Escaper\ODS Used to unescape XML data */ /** @var \Box\Spout\Common\Helper\Escaper\ODS Used to unescape XML data */
protected $escaper; protected $escaper;
/** @var array List of XML nodes representing whitespaces and their corresponding value */
private static $WHITESPACE_XML_NODES = [
self::XML_NODE_TEXT_S => ' ',
self::XML_NODE_TEXT_TAB => "\t",
self::XML_NODE_TEXT_LINE_BREAK => "\n",
];
/** /**
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @param \Box\Spout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data * @param \Box\Spout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data
@@ -55,7 +64,7 @@ class CellValueFormatter
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
* @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13 * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
* *
* @param \DOMNode $node * @param \DOMElement $node
* @throws InvalidValueException If the node value is not valid * @throws InvalidValueException If the node value is not valid
* @return string|int|float|bool|\DateTime|\DateInterval The value associated with the cell, empty string if cell's type is void/undefined * @return string|int|float|bool|\DateTime|\DateInterval The value associated with the cell, empty string if cell's type is void/undefined
*/ */
@@ -87,7 +96,7 @@ class CellValueFormatter
/** /**
* Returns the cell String value. * Returns the cell String value.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return string The value associated with the cell * @return string The value associated with the cell
*/ */
protected function formatStringCellValue($node) protected function formatStringCellValue($node)
@@ -96,33 +105,75 @@ class CellValueFormatter
$pNodes = $node->getElementsByTagName(self::XML_NODE_P); $pNodes = $node->getElementsByTagName(self::XML_NODE_P);
foreach ($pNodes as $pNode) { foreach ($pNodes as $pNode) {
$currentPValue = ''; $pNodeValues[] = $this->extractTextValueFromNode($pNode);
foreach ($pNode->childNodes as $childNode) {
if ($childNode instanceof \DOMText) {
$currentPValue .= $childNode->nodeValue;
} elseif ($childNode->nodeName === self::XML_NODE_S) {
$spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C);
$numSpaces = (!empty($spaceAttribute)) ? (int) $spaceAttribute : 1;
$currentPValue .= str_repeat(' ', $numSpaces);
} elseif ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) {
$currentPValue .= $childNode->nodeValue;
}
} }
$pNodeValues[] = $currentPValue; $escapedCellValue = \implode("\n", $pNodeValues);
}
$escapedCellValue = implode("\n", $pNodeValues);
$cellValue = $this->escaper->unescape($escapedCellValue); $cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue; return $cellValue;
} }
/**
* @param \DOMNode $pNode
* @return string
*/
private function extractTextValueFromNode($pNode)
{
$textValue = '';
foreach ($pNode->childNodes as $childNode) {
if ($childNode instanceof \DOMText) {
$textValue .= $childNode->nodeValue;
} elseif ($this->isWhitespaceNode($childNode->nodeName)) {
$textValue .= $this->transformWhitespaceNode($childNode);
} elseif ($childNode->nodeName === self::XML_NODE_TEXT_A || $childNode->nodeName === self::XML_NODE_TEXT_SPAN) {
$textValue .= $this->extractTextValueFromNode($childNode);
}
}
return $textValue;
}
/**
* Returns whether the given node is a whitespace node. It must be one of these:
* - <text:s />
* - <text:tab />
* - <text:line-break />
*
* @param string $nodeName
* @return bool
*/
private function isWhitespaceNode($nodeName)
{
return isset(self::$WHITESPACE_XML_NODES[$nodeName]);
}
/**
* The "<text:p>" node can contain the string value directly
* or contain child elements. In this case, whitespaces contain in
* the child elements should be replaced by their XML equivalent:
* - space => <text:s />
* - tab => <text:tab />
* - line break => <text:line-break />
*
* @see https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415200_253892949
*
* @param \DOMElement $node The XML node representing a whitespace
* @return string The corresponding whitespace value
*/
private function transformWhitespaceNode($node)
{
$countAttribute = $node->getAttribute(self::XML_ATTRIBUTE_C); // only defined for "<text:s>"
$numWhitespaces = (!empty($countAttribute)) ? (int) $countAttribute : 1;
return \str_repeat(self::$WHITESPACE_XML_NODES[$node->nodeName], $numWhitespaces);
}
/** /**
* Returns the cell Numeric value from the given node. * Returns the cell Numeric value from the given node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return int|float The value associated with the cell * @return int|float The value associated with the cell
*/ */
protected function formatFloatCellValue($node) protected function formatFloatCellValue($node)
@@ -139,7 +190,7 @@ class CellValueFormatter
/** /**
* Returns the cell Boolean value from the given node. * Returns the cell Boolean value from the given node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return bool The value associated with the cell * @return bool The value associated with the cell
*/ */
protected function formatBooleanCellValue($node) protected function formatBooleanCellValue($node)
@@ -152,7 +203,7 @@ class CellValueFormatter
/** /**
* Returns the cell Date value from the given node. * Returns the cell Date value from the given node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @throws InvalidValueException If the value is not a valid date * @throws InvalidValueException If the value is not a valid date
* @return \DateTime|string The value associated with the cell * @return \DateTime|string The value associated with the cell
*/ */
@@ -183,7 +234,7 @@ class CellValueFormatter
/** /**
* Returns the cell Time value from the given node. * Returns the cell Time value from the given node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @throws InvalidValueException If the value is not a valid time * @throws InvalidValueException If the value is not a valid time
* @return \DateInterval|string The value associated with the cell * @return \DateInterval|string The value associated with the cell
*/ */
@@ -214,7 +265,7 @@ class CellValueFormatter
/** /**
* Returns the cell Currency value from the given node. * Returns the cell Currency value from the given node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR") * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR")
*/ */
protected function formatCurrencyCellValue($node) protected function formatCurrencyCellValue($node)
@@ -228,7 +279,7 @@ class CellValueFormatter
/** /**
* Returns the cell Percentage value from the given node. * Returns the cell Percentage value from the given node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return int|float The value associated with the cell * @return int|float The value associated with the cell
*/ */
protected function formatPercentageCellValue($node) protected function formatPercentageCellValue($node)
@@ -11,12 +11,12 @@ use Box\Spout\Reader\ODS\Creator\InternalEntityFactory;
*/ */
class SettingsHelper class SettingsHelper
{ {
const SETTINGS_XML_FILE_PATH = 'settings.xml'; public const SETTINGS_XML_FILE_PATH = 'settings.xml';
/** Definition of XML nodes name and attribute used to parse settings data */ /** Definition of XML nodes name and attribute used to parse settings data */
const XML_NODE_CONFIG_ITEM = 'config:config-item'; public const XML_NODE_CONFIG_ITEM = 'config:config-item';
const XML_ATTRIBUTE_CONFIG_NAME = 'config:name'; public const XML_ATTRIBUTE_CONFIG_NAME = 'config:name';
const XML_ATTRIBUTE_VALUE_ACTIVE_TABLE = 'ActiveTable'; public const XML_ATTRIBUTE_VALUE_ACTIVE_TABLE = 'ActiveTable';
/** @var InternalEntityFactory Factory to create entities */ /** @var InternalEntityFactory Factory to create entities */
private $entityFactory; private $entityFactory;
+1 -1
View File
@@ -69,7 +69,7 @@ class Reader extends ReaderAbstract
*/ */
protected function closeReader() protected function closeReader()
{ {
if ($this->zip) { if ($this->zip !== null) {
$this->zip->close(); $this->zip->close();
} }
} }
+17 -16
View File
@@ -23,14 +23,14 @@ use Box\Spout\Reader\Wrapper\XMLReader;
class RowIterator implements IteratorInterface class RowIterator implements IteratorInterface
{ {
/** Definition of XML nodes names used to parse data */ /** Definition of XML nodes names used to parse data */
const XML_NODE_TABLE = 'table:table'; public const XML_NODE_TABLE = 'table:table';
const XML_NODE_ROW = 'table:table-row'; public const XML_NODE_ROW = 'table:table-row';
const XML_NODE_CELL = 'table:table-cell'; public const XML_NODE_CELL = 'table:table-cell';
const MAX_COLUMNS_EXCEL = 16384; public const MAX_COLUMNS_EXCEL = 16384;
/** Definition of XML attribute used to parse data */ /** Definition of XML attribute used to parse data */
const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated'; public const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated';
const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated'; public 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 */ /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader; protected $xmlReader;
@@ -56,7 +56,7 @@ class RowIterator implements IteratorInterface
/** @var Row The currently processed row */ /** @var Row The currently processed row */
protected $currentlyProcessedRow; protected $currentlyProcessedRow;
/** @var Row Buffer used to store the current row, while checking if there are more rows to read */ /** @var Row|null Buffer used to store the current row, while checking if there are more rows to read */
protected $rowBuffer; protected $rowBuffer;
/** @var bool Indicates whether all rows have been read */ /** @var bool Indicates whether all rows have been read */
@@ -68,7 +68,7 @@ class RowIterator implements IteratorInterface
/** @var int Row index to be processed next (one-based) */ /** @var int Row index to be processed next (one-based) */
protected $nextRowIndexToBeProcessed = 1; protected $nextRowIndexToBeProcessed = 1;
/** @var Cell Last processed cell (because when reading cell at column N+1, cell N is processed) */ /** @var Cell|null Last processed cell (because when reading cell at column N+1, cell N is processed) */
protected $lastProcessedCell; protected $lastProcessedCell;
/** @var int Number of times the last processed row should be repeated */ /** @var int Number of times the last processed row should be repeated */
@@ -118,7 +118,7 @@ class RowIterator implements IteratorInterface
* @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
* @return void * @return void
*/ */
public function rewind() public function rewind() : void
{ {
// Because sheet and row data is located in the file, we can't rewind both the // 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. // sheet iterator and the row iterator, as XML file cannot be read backwards.
@@ -142,7 +142,7 @@ class RowIterator implements IteratorInterface
* *
* @return bool * @return bool
*/ */
public function valid() public function valid() : bool
{ {
return (!$this->hasReachedEndOfFile); return (!$this->hasReachedEndOfFile);
} }
@@ -155,7 +155,7 @@ class RowIterator implements IteratorInterface
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
* @return void * @return void
*/ */
public function next() public function next() : void
{ {
if ($this->doesNeedDataForNextRowToBeProcessed()) { if ($this->doesNeedDataForNextRowToBeProcessed()) {
$this->readDataForNextRow(); $this->readDataForNextRow();
@@ -225,6 +225,7 @@ class RowIterator implements IteratorInterface
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader); $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
// NOTE: expand() will automatically decode all XML entities of the child nodes // NOTE: expand() will automatically decode all XML entities of the child nodes
/** @var \DOMElement $node */
$node = $xmlReader->expand(); $node = $xmlReader->expand();
$currentCell = $this->getCell($node); $currentCell = $this->getCell($node);
@@ -316,7 +317,7 @@ class RowIterator implements IteratorInterface
/** /**
* Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node. * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return Cell The cell set with the associated with the cell * @return Cell The cell set with the associated with the cell
*/ */
protected function getCell($node) protected function getCell($node)
@@ -339,7 +340,7 @@ class RowIterator implements IteratorInterface
* row data yet (as we still need to apply the "num-columns-repeated" attribute). * row data yet (as we still need to apply the "num-columns-repeated" attribute).
* *
* @param Row $currentRow * @param Row $currentRow
* @param Cell $lastReadCell The last read cell * @param Cell|null $lastReadCell The last read cell
* @return bool Whether the row is empty * @return bool Whether the row is empty
*/ */
protected function isEmptyRow($currentRow, $lastReadCell) protected function isEmptyRow($currentRow, $lastReadCell)
@@ -356,7 +357,7 @@ class RowIterator implements IteratorInterface
* *
* @return Row * @return Row
*/ */
public function current() public function current() : Row
{ {
return $this->rowBuffer; return $this->rowBuffer;
} }
@@ -367,7 +368,7 @@ class RowIterator implements IteratorInterface
* *
* @return int * @return int
*/ */
public function key() public function key() : int
{ {
return $this->lastRowIndexProcessed; return $this->lastRowIndexProcessed;
} }
@@ -377,7 +378,7 @@ class RowIterator implements IteratorInterface
* *
* @return void * @return void
*/ */
public function end() public function end() : void
{ {
$this->xmlReader->close(); $this->xmlReader->close();
} }
+13 -11
View File
@@ -15,26 +15,26 @@ use Box\Spout\Reader\Wrapper\XMLReader;
*/ */
class SheetIterator implements IteratorInterface class SheetIterator implements IteratorInterface
{ {
const CONTENT_XML_FILE_PATH = 'content.xml'; public const CONTENT_XML_FILE_PATH = 'content.xml';
const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'; public const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0';
/** Definition of XML nodes name and attribute used to parse sheet data */ /** Definition of XML nodes name and attribute used to parse sheet data */
const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles'; public const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles';
const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties'; public const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties';
const XML_NODE_TABLE = 'table:table'; public const XML_NODE_TABLE = 'table:table';
const XML_ATTRIBUTE_STYLE_NAME = 'style:name'; public const XML_ATTRIBUTE_STYLE_NAME = 'style:name';
const XML_ATTRIBUTE_TABLE_NAME = 'table:name'; public const XML_ATTRIBUTE_TABLE_NAME = 'table:name';
const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name'; public const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name';
const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display'; public const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display';
/** @var string $filePath Path of the file to be read */ /** @var string Path of the file to be read */
protected $filePath; protected $filePath;
/** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */ /** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */
protected $optionsManager; protected $optionsManager;
/** @var InternalEntityFactory $entityFactory Factory to create entities */ /** @var InternalEntityFactory Factory to create entities */
protected $entityFactory; protected $entityFactory;
/** @var XMLReader The XMLReader object that will help read sheet's XML data */ /** @var XMLReader The XMLReader object that will help read sheet's XML data */
@@ -108,6 +108,7 @@ class SheetIterator implements IteratorInterface
$sheetsVisibility = []; $sheetsVisibility = [];
$this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES); $this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES);
/** @var \DOMElement $automaticStylesNode */
$automaticStylesNode = $this->xmlReader->expand(); $automaticStylesNode = $this->xmlReader->expand();
$tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES); $tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES);
@@ -116,6 +117,7 @@ class SheetIterator implements IteratorInterface
foreach ($tableStyleNodes as $tableStyleNode) { foreach ($tableStyleNodes as $tableStyleNode) {
$isSheetVisible = ($tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY) !== 'false'); $isSheetVisible = ($tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY) !== 'false');
/** @var \DOMElement $parentStyleNode */
$parentStyleNode = $tableStyleNode->parentNode; $parentStyleNode = $tableStyleNode->parentNode;
$styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME); $styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME);
+5 -5
View File
@@ -53,7 +53,7 @@ abstract class ReaderAbstract implements ReaderInterface
/** /**
* Closes the reader. To be used after reading the file. * Closes the reader. To be used after reading the file.
* *
* @return ReaderAbstract * @return void
*/ */
abstract protected function closeReader(); abstract protected function closeReader();
@@ -145,7 +145,7 @@ abstract class ReaderAbstract implements ReaderInterface
} }
// Need to use realpath to fix "Can't open file" on some Windows setup // Need to use realpath to fix "Can't open file" on some Windows setup
return realpath($filePath); return \realpath($filePath);
} }
/** /**
@@ -158,7 +158,7 @@ abstract class ReaderAbstract implements ReaderInterface
protected function getStreamWrapperScheme($filePath) protected function getStreamWrapperScheme($filePath)
{ {
$streamScheme = null; $streamScheme = null;
if (preg_match('/^(\w+):\/\//', $filePath, $matches)) { if (\preg_match('/^(\w+):\/\//', $filePath, $matches)) {
$streamScheme = $matches[1]; $streamScheme = $matches[1];
} }
@@ -190,7 +190,7 @@ abstract class ReaderAbstract implements ReaderInterface
$streamScheme = $this->getStreamWrapperScheme($filePath); $streamScheme = $this->getStreamWrapperScheme($filePath);
return ($streamScheme !== null) ? return ($streamScheme !== null) ?
in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) : \in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) :
true; true;
} }
@@ -233,7 +233,7 @@ abstract class ReaderAbstract implements ReaderInterface
$this->closeReader(); $this->closeReader();
$sheetIterator = $this->getConcreteSheetIterator(); $sheetIterator = $this->getConcreteSheetIterator();
if ($sheetIterator) { if ($sheetIterator !== null) {
$sheetIterator->end(); $sheetIterator->end();
} }
@@ -20,8 +20,8 @@ trait XMLInternalErrorsHelper
*/ */
protected function useXMLInternalErrors() protected function useXMLInternalErrors()
{ {
libxml_clear_errors(); \libxml_clear_errors();
$this->initialUseInternalErrorsValue = libxml_use_internal_errors(true); $this->initialUseInternalErrorsValue = \libxml_use_internal_errors(true);
} }
/** /**
@@ -48,7 +48,7 @@ trait XMLInternalErrorsHelper
*/ */
private function hasXMLErrorOccured() private function hasXMLErrorOccured()
{ {
return (libxml_get_last_error() !== false); return (\libxml_get_last_error() !== false);
} }
/** /**
@@ -60,10 +60,10 @@ trait XMLInternalErrorsHelper
private function getLastXMLErrorMessage() private function getLastXMLErrorMessage()
{ {
$errorMessage = null; $errorMessage = null;
$error = libxml_get_last_error(); $error = \libxml_get_last_error();
if ($error !== false) { if ($error !== false) {
$errorMessage = trim($error->message); $errorMessage = \trim($error->message);
} }
return $errorMessage; return $errorMessage;
@@ -74,6 +74,6 @@ trait XMLInternalErrorsHelper
*/ */
protected function resetXMLInternalErrorsSetting() protected function resetXMLInternalErrorsSetting()
{ {
libxml_use_internal_errors($this->initialUseInternalErrorsValue); \libxml_use_internal_errors($this->initialUseInternalErrorsValue);
} }
} }
+5 -5
View File
@@ -11,7 +11,7 @@ class XMLReader extends \XMLReader
{ {
use XMLInternalErrorsHelper; use XMLInternalErrorsHelper;
const ZIP_WRAPPER = 'zip://'; public const ZIP_WRAPPER = 'zip://';
/** /**
* Opens the XML Reader to read a file located inside a ZIP file. * Opens the XML Reader to read a file located inside a ZIP file.
@@ -46,9 +46,9 @@ class XMLReader extends \XMLReader
public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath) public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath)
{ {
// The file path should not start with a '/', otherwise it won't be found // The file path should not start with a '/', otherwise it won't be found
$fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/'); $fileInsideZipPathWithoutLeadingSlash = \ltrim($fileInsideZipPath, '/');
return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPathWithoutLeadingSlash); return (self::ZIP_WRAPPER . \realpath($zipFilePath) . '#' . $fileInsideZipPathWithoutLeadingSlash);
} }
/** /**
@@ -62,7 +62,7 @@ class XMLReader extends \XMLReader
$doesFileExists = false; $doesFileExists = false;
$pattern = '/zip:\/\/([^#]+)#(.*)/'; $pattern = '/zip:\/\/([^#]+)#(.*)/';
if (preg_match($pattern, $zipStreamURI, $matches)) { if (\preg_match($pattern, $zipStreamURI, $matches)) {
$zipFilePath = $matches[1]; $zipFilePath = $matches[1];
$innerFilePath = $matches[2]; $innerFilePath = $matches[2];
@@ -158,7 +158,7 @@ class XMLReader extends \XMLReader
// In some cases, the node has a prefix (for instance, "<sheet>" can also be "<x:sheet>"). // In some cases, the node has a prefix (for instance, "<sheet>" can also be "<x:sheet>").
// So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName"). // So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName").
// @see https://github.com/box/spout/issues/233 // @see https://github.com/box/spout/issues/233
$hasPrefix = (strpos($nodeName, ':') !== false); $hasPrefix = (\strpos($nodeName, ':') !== false);
$currentNodeName = ($hasPrefix) ? $this->name : $this->localName; $currentNodeName = ($hasPrefix) ? $this->name : $this->localName;
return ($this->nodeType === $nodeType && $currentNodeName === $nodeName); return ($this->nodeType === $nodeType && $currentNodeName === $nodeName);
@@ -152,7 +152,7 @@ class InternalEntityFactory implements InternalEntityFactoryInterface
} }
/** /**
* @param $xmlReader * @param XMLReader $xmlReader
* @return XMLProcessor * @return XMLProcessor
*/ */
public function createXMLProcessor($xmlReader) public function createXMLProcessor($xmlReader)
@@ -21,7 +21,7 @@ class ManagerFactory
/** @var CachingStrategyFactory */ /** @var CachingStrategyFactory */
private $cachingStrategyFactory; private $cachingStrategyFactory;
/** @var WorkbookRelationshipsManager */ /** @var WorkbookRelationshipsManager|null */
private $cachedWorkbookRelationshipsManager; private $cachedWorkbookRelationshipsManager;
/** /**
+2 -2
View File
@@ -37,7 +37,7 @@ class CellHelper
$columnIndex = 0; $columnIndex = 0;
// Remove row information // Remove row information
$columnLetters = preg_replace('/\d/', '', $cellIndex); $columnLetters = \preg_replace('/\d/', '', $cellIndex);
// strlen() is super slow too... Using isset() is way faster and not too unreadable, // strlen() is super slow too... Using isset() is way faster and not too unreadable,
// since we checked before that there are between 1 and 3 letters. // since we checked before that there are between 1 and 3 letters.
@@ -75,6 +75,6 @@ class CellHelper
*/ */
protected static function isValidCellIndex($cellIndex) protected static function isValidCellIndex($cellIndex)
{ {
return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1); return (\preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1);
} }
} }
@@ -13,32 +13,24 @@ use Box\Spout\Reader\XLSX\Manager\StyleManager;
class CellValueFormatter class CellValueFormatter
{ {
/** Definition of all possible cell types */ /** Definition of all possible cell types */
const CELL_TYPE_INLINE_STRING = 'inlineStr'; public const CELL_TYPE_INLINE_STRING = 'inlineStr';
const CELL_TYPE_STR = 'str'; public const CELL_TYPE_STR = 'str';
const CELL_TYPE_SHARED_STRING = 's'; public const CELL_TYPE_SHARED_STRING = 's';
const CELL_TYPE_BOOLEAN = 'b'; public const CELL_TYPE_BOOLEAN = 'b';
const CELL_TYPE_NUMERIC = 'n'; public const CELL_TYPE_NUMERIC = 'n';
const CELL_TYPE_DATE = 'd'; public const CELL_TYPE_DATE = 'd';
const CELL_TYPE_ERROR = 'e'; public const CELL_TYPE_ERROR = 'e';
/** Definition of XML nodes names used to parse data */ /** Definition of XML nodes names used to parse data */
const XML_NODE_VALUE = 'v'; public const XML_NODE_VALUE = 'v';
const XML_NODE_INLINE_STRING_VALUE = 't'; public const XML_NODE_INLINE_STRING_VALUE = 't';
/** Definition of XML attributes used to parse data */ /** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_TYPE = 't'; public const XML_ATTRIBUTE_TYPE = 't';
const XML_ATTRIBUTE_STYLE_ID = 's'; public const XML_ATTRIBUTE_STYLE_ID = 's';
/** Constants used for date formatting */ /** Constants used for date formatting */
const NUM_SECONDS_IN_ONE_DAY = 86400; public const NUM_SECONDS_IN_ONE_DAY = 86400;
const NUM_SECONDS_IN_ONE_HOUR = 3600;
const NUM_SECONDS_IN_ONE_MINUTE = 60;
/**
* February 29th, 1900 is NOT a leap year but Excel thinks it is...
* @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
*/
const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
/** @var SharedStringsManager Manages shared strings */ /** @var SharedStringsManager Manages shared strings */
protected $sharedStringsManager; protected $sharedStringsManager;
@@ -74,7 +66,7 @@ class CellValueFormatter
/** /**
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @throws InvalidValueException If the value is not valid * @throws InvalidValueException If the value is not valid
* @return string|int|float|bool|\DateTime The value associated with the cell * @return string|int|float|bool|\DateTime The value associated with the cell
*/ */
@@ -110,7 +102,7 @@ class CellValueFormatter
/** /**
* Returns the cell's string value from a node's nested value node * Returns the cell's string value from a node's nested value node
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return string The value associated with the cell * @return string The value associated with the cell
*/ */
protected function getVNodeValue($node) protected function getVNodeValue($node)
@@ -125,15 +117,20 @@ class CellValueFormatter
/** /**
* Returns the cell String value where string is inline. * Returns the cell String value where string is inline.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return string The value associated with the cell * @return string The value associated with the cell
*/ */
protected function formatInlineStringCellValue($node) protected function formatInlineStringCellValue($node)
{ {
// inline strings are formatted this way: // inline strings are formatted this way (they can contain any number of <t> nodes):
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c> // <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t><t>[INLINE_STRING_2]</t></is></c>
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0); $tNodes = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE);
$cellValue = $this->escaper->unescape($tNode->nodeValue);
$cellValue = '';
for ($i = 0; $i < $tNodes->count(); $i++) {
$tNode = $tNodes->item($i);
$cellValue .= $this->escaper->unescape($tNode->nodeValue);
}
return $cellValue; return $cellValue;
} }
@@ -163,7 +160,7 @@ class CellValueFormatter
*/ */
protected function formatStrCellValue($nodeValue) protected function formatStrCellValue($nodeValue)
{ {
$escapedCellValue = trim($nodeValue); $escapedCellValue = \trim($nodeValue);
$cellValue = $this->escaper->unescape($escapedCellValue); $cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue; return $cellValue;
@@ -248,8 +245,8 @@ class CellValueFormatter
$baseDate = $this->shouldUse1904Dates ? '1904-01-01' : '1899-12-30'; $baseDate = $this->shouldUse1904Dates ? '1904-01-01' : '1899-12-30';
$daysSinceBaseDate = (int) $nodeValue; $daysSinceBaseDate = (int) $nodeValue;
$timeRemainder = fmod($nodeValue, 1); $timeRemainder = \fmod($nodeValue, 1);
$secondsRemainder = round($timeRemainder * self::NUM_SECONDS_IN_ONE_DAY, 0); $secondsRemainder = \round($timeRemainder * self::NUM_SECONDS_IN_ONE_DAY, 0);
$dateObj = \DateTime::createFromFormat('|Y-m-d', $baseDate); $dateObj = \DateTime::createFromFormat('|Y-m-d', $baseDate);
$dateObj->modify('+' . $daysSinceBaseDate . 'days'); $dateObj->modify('+' . $daysSinceBaseDate . 'days');
@@ -8,9 +8,9 @@ namespace Box\Spout\Reader\XLSX\Helper;
*/ */
class DateFormatHelper class DateFormatHelper
{ {
const KEY_GENERAL = 'general'; public const KEY_GENERAL = 'general';
const KEY_HOUR_12 = '12h'; public const KEY_HOUR_12 = '12h';
const KEY_HOUR_24 = '24h'; public const KEY_HOUR_24 = '24h';
/** /**
* This map is used to replace Excel format characters by their PHP equivalent. * This map is used to replace Excel format characters by their PHP equivalent.
@@ -62,13 +62,13 @@ class DateFormatHelper
// Remove brackets potentially present at the beginning of the format string // Remove brackets potentially present at the beginning of the format string
// and text portion of the format at the end of it (starting with ";") // and text portion of the format at the end of it (starting with ";")
// See §18.8.31 of ECMA-376 for more detail. // See §18.8.31 of ECMA-376 for more detail.
$dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat); $dateFormat = \preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat);
// Double quotes are used to escape characters that must not be interpreted. // Double quotes are used to escape characters that must not be interpreted.
// For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y" // For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y"
// By exploding the format string using double quote as a delimiter, we can get all parts // By exploding the format string using double quote as a delimiter, we can get all parts
// that must be transformed (even indexes) and all parts that must not be (odd indexes). // that must be transformed (even indexes) and all parts that must not be (odd indexes).
$dateFormatParts = explode('"', $dateFormat); $dateFormatParts = \explode('"', $dateFormat);
foreach ($dateFormatParts as $partIndex => $dateFormatPart) { foreach ($dateFormatParts as $partIndex => $dateFormatPart) {
// do not look at odd indexes // do not look at odd indexes
@@ -77,19 +77,19 @@ class DateFormatHelper
} }
// Make sure all characters are lowercase, as the mapping table is using lowercase characters // Make sure all characters are lowercase, as the mapping table is using lowercase characters
$transformedPart = strtolower($dateFormatPart); $transformedPart = \strtolower($dateFormatPart);
// Remove escapes related to non-format characters // Remove escapes related to non-format characters
$transformedPart = str_replace('\\', '', $transformedPart); $transformedPart = \str_replace('\\', '', $transformedPart);
// Apply general transformation first... // Apply general transformation first...
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]); $transformedPart = \strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]);
// ... then apply hour transformation, for 12-hour or 24-hour format // ... then apply hour transformation, for 12-hour or 24-hour format
if (self::has12HourFormatMarker($dateFormatPart)) { if (self::has12HourFormatMarker($dateFormatPart)) {
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]); $transformedPart = \strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]);
} else { } else {
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]); $transformedPart = \strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]);
} }
// overwrite the parts array with the new transformed part // overwrite the parts array with the new transformed part
@@ -97,16 +97,16 @@ class DateFormatHelper
} }
// Merge all transformed parts back together // Merge all transformed parts back together
$phpDateFormat = implode('"', $dateFormatParts); $phpDateFormat = \implode('"', $dateFormatParts);
// Finally, to have the date format compatible with the DateTime::format() function, we need to escape // Finally, to have the date format compatible with the DateTime::format() function, we need to escape
// all characters that are inside double quotes (and double quotes must be removed). // all characters that are inside double quotes (and double quotes must be removed).
// For instance, ["Day " dd] should become [\D\a\y\ dd] // For instance, ["Day " dd] should become [\D\a\y\ dd]
$phpDateFormat = preg_replace_callback('/"(.+?)"/', function ($matches) { $phpDateFormat = \preg_replace_callback('/"(.+?)"/', function ($matches) {
$stringToEscape = $matches[1]; $stringToEscape = $matches[1];
$letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY); $letters = \preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY);
return '\\' . implode('\\', $letters); return '\\' . \implode('\\', $letters);
}, $phpDateFormat); }, $phpDateFormat);
return $phpDateFormat; return $phpDateFormat;
@@ -118,6 +118,6 @@ class DateFormatHelper
*/ */
private static function has12HourFormatMarker($excelDateFormat) private static function has12HourFormatMarker($excelDateFormat)
{ {
return (stripos($excelDateFormat, 'am/pm') !== false); return (\stripos($excelDateFormat, 'am/pm') !== false);
} }
} }
@@ -29,7 +29,7 @@ class OptionsManager extends OptionsManagerAbstract
*/ */
protected function setDefaultOptions() protected function setDefaultOptions()
{ {
$this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir()); $this->setOption(Options::TEMP_FOLDER, \sys_get_temp_dir());
$this->setOption(Options::SHOULD_FORMAT_DATES, false); $this->setOption(Options::SHOULD_FORMAT_DATES, false);
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
$this->setOption(Options::SHOULD_USE_1904_DATES, false); $this->setOption(Options::SHOULD_USE_1904_DATES, false);
@@ -37,7 +37,7 @@ class CachingStrategyFactory
* This means that in order to store one shared string in memory, the memory amount needed is: * This means that in order to store one shared string in memory, the memory amount needed is:
* => 20 * 600 ≈ 12KB * => 20 * 600 ≈ 12KB
*/ */
const AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB = 12; public 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 * To avoid running out of memory when extracting a huge number of shared strings, they can be saved to temporary files
@@ -48,7 +48,7 @@ class CachingStrategyFactory
* best when the indexes of the shared strings are sorted in the sheet data. * 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. * 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; public const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
/** /**
* Returns the best caching strategy, given the number of unique shared strings * Returns the best caching strategy, given the number of unique shared strings
@@ -84,7 +84,7 @@ class CachingStrategyFactory
$memoryAvailable = $this->getMemoryLimitInKB(); $memoryAvailable = $this->getMemoryLimitInKB();
if ($memoryAvailable === -1) { if ((int) $memoryAvailable === -1) {
// if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe // if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe
$isInMemoryStrategyUsageSafe = ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE); $isInMemoryStrategyUsageSafe = ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE);
} else { } else {
@@ -103,14 +103,14 @@ class CachingStrategyFactory
protected function getMemoryLimitInKB() protected function getMemoryLimitInKB()
{ {
$memoryLimitFormatted = $this->getMemoryLimitFromIni(); $memoryLimitFormatted = $this->getMemoryLimitFromIni();
$memoryLimitFormatted = strtolower(trim($memoryLimitFormatted)); $memoryLimitFormatted = \strtolower(\trim($memoryLimitFormatted));
// No memory limit // No memory limit
if ($memoryLimitFormatted === '-1') { if ($memoryLimitFormatted === '-1') {
return -1; return -1;
} }
if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) { if (\preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) {
$amount = (int) ($matches[1]); $amount = (int) ($matches[1]);
$unit = $matches[2]; $unit = $matches[2];
@@ -133,6 +133,6 @@ class CachingStrategyFactory
*/ */
protected function getMemoryLimitFromIni() protected function getMemoryLimitFromIni()
{ {
return ini_get('memory_limit'); return \ini_get('memory_limit');
} }
} }
@@ -15,7 +15,7 @@ use Box\Spout\Reader\XLSX\Creator\HelperFactory;
class FileBasedStrategy implements CachingStrategyInterface class FileBasedStrategy implements CachingStrategyInterface
{ {
/** Value to use to escape the line feed character ("\n") */ /** Value to use to escape the line feed character ("\n") */
const ESCAPED_LINE_FEED_CHARACTER = '_x000A_'; public const ESCAPED_LINE_FEED_CHARACTER = '_x000A_';
/** @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;
@@ -32,7 +32,7 @@ class FileBasedStrategy implements CachingStrategyInterface
*/ */
protected $maxNumStringsPerTempFile; protected $maxNumStringsPerTempFile;
/** @var resource Pointer to the last temp file a shared string was written to */ /** @var resource|null Pointer to the last temp file a shared string was written to */
protected $tempFilePointer; protected $tempFilePointer;
/** /**
@@ -55,7 +55,7 @@ class FileBasedStrategy implements CachingStrategyInterface
public function __construct($tempFolder, $maxNumStringsPerTempFile, $helperFactory) public function __construct($tempFolder, $maxNumStringsPerTempFile, $helperFactory)
{ {
$this->fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder); $this->fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder);
$this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, uniqid('sharedstrings')); $this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, \uniqid('sharedstrings'));
$this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile; $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
@@ -135,7 +135,7 @@ class FileBasedStrategy implements CachingStrategyInterface
// free memory // free memory
unset($this->inMemoryTempFileContents); unset($this->inMemoryTempFileContents);
$this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath)); $this->inMemoryTempFileContents = \explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath));
$this->inMemoryTempFilePath = $tempFilePath; $this->inMemoryTempFilePath = $tempFilePath;
} }
@@ -151,7 +151,7 @@ class FileBasedStrategy implements CachingStrategyInterface
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
} }
return rtrim($sharedString, PHP_EOL); return \rtrim($sharedString, PHP_EOL);
} }
/** /**
@@ -162,7 +162,7 @@ class FileBasedStrategy implements CachingStrategyInterface
*/ */
private function escapeLineFeed($unescapedString) private function escapeLineFeed($unescapedString)
{ {
return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString); return \str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
} }
/** /**
@@ -173,7 +173,7 @@ class FileBasedStrategy implements CachingStrategyInterface
*/ */
private function unescapeLineFeed($escapedString) private function unescapeLineFeed($escapedString)
{ {
return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString); return \str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
} }
/** /**
@@ -16,20 +16,17 @@ use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface;
*/ */
class SharedStringsManager class SharedStringsManager
{ {
/** Main namespace for the sharedStrings.xml file */
const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
/** Definition of XML nodes names used to parse data */ /** Definition of XML nodes names used to parse data */
const XML_NODE_SST = 'sst'; public const XML_NODE_SST = 'sst';
const XML_NODE_SI = 'si'; public const XML_NODE_SI = 'si';
const XML_NODE_R = 'r'; public const XML_NODE_R = 'r';
const XML_NODE_T = 't'; public const XML_NODE_T = 't';
/** Definition of XML attributes used to parse data */ /** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_COUNT = 'count'; public const XML_ATTRIBUTE_COUNT = 'count';
const XML_ATTRIBUTE_UNIQUE_COUNT = 'uniqueCount'; public const XML_ATTRIBUTE_UNIQUE_COUNT = 'uniqueCount';
const XML_ATTRIBUTE_XML_SPACE = 'xml:space'; public const XML_ATTRIBUTE_XML_SPACE = 'xml:space';
const XML_ATTRIBUTE_VALUE_PRESERVE = 'preserve'; public const XML_ATTRIBUTE_VALUE_PRESERVE = 'preserve';
/** @var string Path of the XLSX file being read */ /** @var string Path of the XLSX file being read */
protected $filePath; protected $filePath;
@@ -43,7 +40,7 @@ class SharedStringsManager
/** @var InternalEntityFactory Factory to create entities */ /** @var InternalEntityFactory Factory to create entities */
protected $entityFactory; protected $entityFactory;
/** @var HelperFactory $helperFactory Factory to create helpers */ /** @var HelperFactory Factory to create helpers */
protected $helperFactory; protected $helperFactory;
/** @var CachingStrategyFactory Factory to create shared strings caching strategies */ /** @var CachingStrategyFactory Factory to create shared strings caching strategies */
@@ -182,6 +179,7 @@ class SharedStringsManager
$sharedStringValue = ''; $sharedStringValue = '';
// NOTE: expand() will automatically decode all XML entities of the child nodes // NOTE: expand() will automatically decode all XML entities of the child nodes
/** @var \DOMElement $siNode */
$siNode = $xmlReader->expand(); $siNode = $xmlReader->expand();
$textNodes = $siNode->getElementsByTagName(self::XML_NODE_T); $textNodes = $siNode->getElementsByTagName(self::XML_NODE_T);
@@ -190,7 +188,7 @@ class SharedStringsManager
$textNodeValue = $textNode->nodeValue; $textNodeValue = $textNode->nodeValue;
$shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode); $shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode);
$sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : trim($textNodeValue); $sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : \trim($textNodeValue);
} }
} }
@@ -244,7 +242,7 @@ class SharedStringsManager
*/ */
public function cleanup() public function cleanup()
{ {
if ($this->cachingStrategy) { if ($this->cachingStrategy !== null) {
$this->cachingStrategy->clearCache(); $this->cachingStrategy->clearCache();
} }
} }
+17 -17
View File
@@ -14,27 +14,27 @@ use Box\Spout\Reader\XLSX\Sheet;
class SheetManager class SheetManager
{ {
/** Paths of XML files relative to the XLSX file root */ /** Paths of XML files relative to the XLSX file root */
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels'; public const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml'; public const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
/** Definition of XML node names used to parse data */ /** Definition of XML node names used to parse data */
const XML_NODE_WORKBOOK_PROPERTIES = 'workbookPr'; public const XML_NODE_WORKBOOK_PROPERTIES = 'workbookPr';
const XML_NODE_WORKBOOK_VIEW = 'workbookView'; public const XML_NODE_WORKBOOK_VIEW = 'workbookView';
const XML_NODE_SHEET = 'sheet'; public const XML_NODE_SHEET = 'sheet';
const XML_NODE_SHEETS = 'sheets'; public const XML_NODE_SHEETS = 'sheets';
const XML_NODE_RELATIONSHIP = 'Relationship'; public const XML_NODE_RELATIONSHIP = 'Relationship';
/** Definition of XML attributes used to parse data */ /** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_DATE_1904 = 'date1904'; public const XML_ATTRIBUTE_DATE_1904 = 'date1904';
const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab'; public const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab';
const XML_ATTRIBUTE_R_ID = 'r:id'; public const XML_ATTRIBUTE_R_ID = 'r:id';
const XML_ATTRIBUTE_NAME = 'name'; public const XML_ATTRIBUTE_NAME = 'name';
const XML_ATTRIBUTE_STATE = 'state'; public const XML_ATTRIBUTE_STATE = 'state';
const XML_ATTRIBUTE_ID = 'Id'; public const XML_ATTRIBUTE_ID = 'Id';
const XML_ATTRIBUTE_TARGET = 'Target'; public const XML_ATTRIBUTE_TARGET = 'Target';
/** State value to represent a hidden sheet */ /** State value to represent a hidden sheet */
const SHEET_STATE_HIDDEN = 'hidden'; public const SHEET_STATE_HIDDEN = 'hidden';
/** @var string Path of the XLSX file being read */ /** @var string Path of the XLSX file being read */
protected $filePath; protected $filePath;
@@ -116,7 +116,7 @@ class SheetManager
{ {
// Using "filter_var($x, FILTER_VALIDATE_BOOLEAN)" here because the value of the "date1904" attribute // Using "filter_var($x, FILTER_VALIDATE_BOOLEAN)" here because the value of the "date1904" attribute
// may be the string "false", that is not mapped to the boolean "false" by default... // may be the string "false", that is not mapped to the boolean "false" by default...
$shouldUse1904Dates = filter_var($xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904), FILTER_VALIDATE_BOOLEAN); $shouldUse1904Dates = \filter_var($xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904), FILTER_VALIDATE_BOOLEAN);
$this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates); $this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates);
return XMLProcessor::PROCESSING_CONTINUE; return XMLProcessor::PROCESSING_CONTINUE;
@@ -211,7 +211,7 @@ class SheetManager
$sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET); $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
// sometimes, the sheet data file path already contains "/xl/"... // sometimes, the sheet data file path already contains "/xl/"...
if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) { if (\strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
$sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath; $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
break; break;
} }
+27 -17
View File
@@ -11,20 +11,20 @@ use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory;
class StyleManager class StyleManager
{ {
/** Nodes used to find relevant information in the styles XML file */ /** Nodes used to find relevant information in the styles XML file */
const XML_NODE_NUM_FMTS = 'numFmts'; public const XML_NODE_NUM_FMTS = 'numFmts';
const XML_NODE_NUM_FMT = 'numFmt'; public const XML_NODE_NUM_FMT = 'numFmt';
const XML_NODE_CELL_XFS = 'cellXfs'; public const XML_NODE_CELL_XFS = 'cellXfs';
const XML_NODE_XF = 'xf'; public const XML_NODE_XF = 'xf';
/** Attributes used to find relevant information in the styles XML file */ /** Attributes used to find relevant information in the styles XML file */
const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId'; public const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId';
const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode'; public const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode';
const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat'; public const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat';
/** By convention, default style ID is 0 */ /** By convention, default style ID is 0 */
const DEFAULT_STYLE_ID = 0; public const DEFAULT_STYLE_ID = 0;
const NUMBER_FORMAT_GENERAL = 'General'; public const NUMBER_FORMAT_GENERAL = 'General';
/** /**
* @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx * @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx
@@ -48,7 +48,10 @@ class StyleManager
/** @var string Path of the XLSX file being read */ /** @var string Path of the XLSX file being read */
protected $filePath; protected $filePath;
/** @var string Path of the styles XML file */ /** @var bool Whether the XLSX file contains a styles XML file */
protected $hasStylesXMLFile;
/** @var string|null Path of the styles XML file */
protected $stylesXMLFilePath; protected $stylesXMLFilePath;
/** @var InternalEntityFactory Factory to create entities */ /** @var InternalEntityFactory Factory to create entities */
@@ -57,10 +60,10 @@ class StyleManager
/** @var array Array containing the IDs of built-in number formats indicating a date */ /** @var array Array containing the IDs of built-in number formats indicating a date */
protected $builtinNumFmtIdIndicatingDates; protected $builtinNumFmtIdIndicatingDates;
/** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */ /** @var array|null Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
protected $customNumberFormats; protected $customNumberFormats;
/** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */ /** @var array|null Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
protected $stylesAttributes; protected $stylesAttributes;
/** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */ /** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */
@@ -75,9 +78,12 @@ class StyleManager
{ {
$this->filePath = $filePath; $this->filePath = $filePath;
$this->entityFactory = $entityFactory; $this->entityFactory = $entityFactory;
$this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping); $this->builtinNumFmtIdIndicatingDates = \array_keys(self::$builtinNumFmtIdToNumFormatMapping);
$this->hasStylesXMLFile = $workbookRelationshipsManager->hasStylesXMLFile();
if ($this->hasStylesXMLFile) {
$this->stylesXMLFilePath = $workbookRelationshipsManager->getStylesXMLFilePath(); $this->stylesXMLFilePath = $workbookRelationshipsManager->getStylesXMLFilePath();
} }
}
/** /**
* Returns whether the style with the given ID should consider * Returns whether the style with the given ID should consider
@@ -88,6 +94,10 @@ class StyleManager
*/ */
public function shouldFormatNumericValueAsDate($styleId) public function shouldFormatNumericValueAsDate($styleId)
{ {
if (!$this->hasStylesXMLFile) {
return false;
}
$stylesAttributes = $this->getStylesAttributes(); $stylesAttributes = $this->getStylesAttributes();
// Default style (0) does not format numeric values as timestamps. Only custom styles do. // Default style (0) does not format numeric values as timestamps. Only custom styles do.
@@ -263,7 +273,7 @@ class StyleManager
*/ */
protected function isNumFmtIdBuiltInDateFormat($numFmtId) protected function isNumFmtIdBuiltInDateFormat($numFmtId)
{ {
return in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates); return \in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates);
} }
/** /**
@@ -273,7 +283,7 @@ class StyleManager
protected function isFormatCodeCustomDateFormat($formatCode) protected function isFormatCodeCustomDateFormat($formatCode)
{ {
// if no associated format code or if using the default "General" format // if no associated format code or if using the default "General" format
if ($formatCode === null || strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) { if ($formatCode === null || \strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) {
return false; return false;
} }
@@ -288,7 +298,7 @@ class StyleManager
{ {
// Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\") // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
$pattern = '((?<!\\\)\[.+?(?<!\\\)\])'; $pattern = '((?<!\\\)\[.+?(?<!\\\)\])';
$formatCode = preg_replace($pattern, '', $formatCode); $formatCode = \preg_replace($pattern, '', $formatCode);
// custom date formats contain specific characters to represent the date: // custom date formats contain specific characters to represent the date:
// e - yy - m - d - h - s // e - yy - m - d - h - s
@@ -300,7 +310,7 @@ class StyleManager
// character not preceded by "\" (case insensitive) // character not preceded by "\" (case insensitive)
$pattern = '/(?<!\\\)' . $dateFormatCharacter . '/i'; $pattern = '/(?<!\\\)' . $dateFormatCharacter . '/i';
if (preg_match($pattern, $formatCode)) { if (\preg_match($pattern, $formatCode)) {
$hasFoundDateFormatCharacter = true; $hasFoundDateFormatCharacter = true;
break; break;
} }
@@ -12,20 +12,21 @@ use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory;
*/ */
class WorkbookRelationshipsManager class WorkbookRelationshipsManager
{ {
const BASE_PATH = 'xl/'; public const BASE_PATH = 'xl/';
/** Path of workbook relationships XML file inside the XLSX file */ /** Path of workbook relationships XML file inside the XLSX file */
const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels'; public const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels';
/** Relationships types */ /** Relationships types - For Transitional and Strict OOXML */
const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings'; public const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'; public const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
const RELATIONSHIP_TYPE_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; public const RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings';
public const RELATIONSHIP_TYPE_STYLES_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/styles';
/** Nodes and attributes used to find relevant information in the workbook relationships XML file */ /** Nodes and attributes used to find relevant information in the workbook relationships XML file */
const XML_NODE_RELATIONSHIP = 'Relationship'; public const XML_NODE_RELATIONSHIP = 'Relationship';
const XML_ATTRIBUTE_TYPE = 'Type'; public const XML_ATTRIBUTE_TYPE = 'Type';
const XML_ATTRIBUTE_TARGET = 'Target'; public const XML_ATTRIBUTE_TARGET = 'Target';
/** @var string Path of the XLSX file being read */ /** @var string Path of the XLSX file being read */
private $filePath; private $filePath;
@@ -33,7 +34,7 @@ class WorkbookRelationshipsManager
/** @var InternalEntityFactory Factory to create entities */ /** @var InternalEntityFactory Factory to create entities */
private $entityFactory; private $entityFactory;
/** @var array Cache of the already read workbook relationships: [TYPE] => [FILE_NAME] */ /** @var array|null Cache of the already read workbook relationships: [TYPE] => [FILE_NAME] */
private $cachedWorkbookRelationships; private $cachedWorkbookRelationships;
/** /**
@@ -52,10 +53,11 @@ class WorkbookRelationshipsManager
public function getSharedStringsXMLFilePath() public function getSharedStringsXMLFilePath()
{ {
$workbookRelationships = $this->getWorkbookRelationships(); $workbookRelationships = $this->getWorkbookRelationships();
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]; $sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]
?? $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml") // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false); $doesContainBasePath = (\strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false);
if (!$doesContainBasePath) { if (!$doesContainBasePath) {
// make sure we return an absolute file path // make sure we return an absolute file path
$sharedStringsXMLFilePath = self::BASE_PATH . $sharedStringsXMLFilePath; $sharedStringsXMLFilePath = self::BASE_PATH . $sharedStringsXMLFilePath;
@@ -71,19 +73,32 @@ class WorkbookRelationshipsManager
{ {
$workbookRelationships = $this->getWorkbookRelationships(); $workbookRelationships = $this->getWorkbookRelationships();
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]); return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS])
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]);
} }
/** /**
* @return string|null The path of the styles XML file * @return bool Whether the XLSX file contains a styles XML file
*/
public function hasStylesXMLFile()
{
$workbookRelationships = $this->getWorkbookRelationships();
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES])
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]);
}
/**
* @return string The path of the styles XML file
*/ */
public function getStylesXMLFilePath() public function getStylesXMLFilePath()
{ {
$workbookRelationships = $this->getWorkbookRelationships(); $workbookRelationships = $this->getWorkbookRelationships();
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]; $stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]
?? $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml") // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (strpos($stylesXMLFilePath, self::BASE_PATH) !== false); $doesContainBasePath = (\strpos($stylesXMLFilePath, self::BASE_PATH) !== false);
if (!$doesContainBasePath) { if (!$doesContainBasePath) {
// make sure we return a full path // make sure we return a full path
$stylesXMLFilePath = self::BASE_PATH . $stylesXMLFilePath; $stylesXMLFilePath = self::BASE_PATH . $stylesXMLFilePath;
+2 -2
View File
@@ -119,11 +119,11 @@ class Reader extends ReaderAbstract
*/ */
protected function closeReader() protected function closeReader()
{ {
if ($this->zip) { if ($this->zip !== null) {
$this->zip->close(); $this->zip->close();
} }
if ($this->sharedStringsManager) { if ($this->sharedStringsManager !== null) {
$this->sharedStringsManager->cleanup(); $this->sharedStringsManager->cleanup();
} }
} }
+21 -20
View File
@@ -21,21 +21,21 @@ use Box\Spout\Reader\XLSX\Helper\CellValueFormatter;
class RowIterator implements IteratorInterface class RowIterator implements IteratorInterface
{ {
/** Definition of XML nodes names used to parse data */ /** Definition of XML nodes names used to parse data */
const XML_NODE_DIMENSION = 'dimension'; public const XML_NODE_DIMENSION = 'dimension';
const XML_NODE_WORKSHEET = 'worksheet'; public const XML_NODE_WORKSHEET = 'worksheet';
const XML_NODE_ROW = 'row'; public const XML_NODE_ROW = 'row';
const XML_NODE_CELL = 'c'; public const XML_NODE_CELL = 'c';
/** Definition of XML attributes used to parse data */ /** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_REF = 'ref'; public const XML_ATTRIBUTE_REF = 'ref';
const XML_ATTRIBUTE_SPANS = 'spans'; public const XML_ATTRIBUTE_SPANS = 'spans';
const XML_ATTRIBUTE_ROW_INDEX = 'r'; public const XML_ATTRIBUTE_ROW_INDEX = 'r';
const XML_ATTRIBUTE_CELL_INDEX = 'r'; public const XML_ATTRIBUTE_CELL_INDEX = 'r';
/** @var string Path of the XLSX file being read */ /** @var string Path of the XLSX file being read */
protected $filePath; protected $filePath;
/** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */ /** @var string Path of the sheet data XML file as in [Content_Types].xml */
protected $sheetDataXMLFilePath; protected $sheetDataXMLFilePath;
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
@@ -127,7 +127,7 @@ class RowIterator implements IteratorInterface
*/ */
protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath) protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
{ {
return ltrim($sheetDataXMLFilePath, '/'); return \ltrim($sheetDataXMLFilePath, '/');
} }
/** /**
@@ -139,7 +139,7 @@ class RowIterator implements IteratorInterface
* @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read * @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
* @return void * @return void
*/ */
public function rewind() public function rewind() : void
{ {
$this->xmlReader->close(); $this->xmlReader->close();
@@ -163,7 +163,7 @@ class RowIterator implements IteratorInterface
* *
* @return bool * @return bool
*/ */
public function valid() public function valid() : bool
{ {
return (!$this->hasReachedEndOfFile); return (!$this->hasReachedEndOfFile);
} }
@@ -176,7 +176,7 @@ class RowIterator implements IteratorInterface
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
* @return void * @return void
*/ */
public function next() public function next() : void
{ {
$this->nextRowIndexToBeProcessed++; $this->nextRowIndexToBeProcessed++;
@@ -234,7 +234,7 @@ class RowIterator implements IteratorInterface
{ {
// Read dimensions of the sheet // Read dimensions of the sheet
$dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet) $dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
if (preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) { if (\preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) {
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1; $this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1;
} }
@@ -257,11 +257,11 @@ class RowIterator implements IteratorInterface
$numberOfColumnsForRow = $this->numColumns; $numberOfColumnsForRow = $this->numColumns;
$spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance $spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
if ($spans) { if ($spans) {
list(, $numberOfColumnsForRow) = explode(':', $spans); list(, $numberOfColumnsForRow) = \explode(':', $spans);
$numberOfColumnsForRow = (int) $numberOfColumnsForRow; $numberOfColumnsForRow = (int) $numberOfColumnsForRow;
} }
$cells = array_fill(0, $numberOfColumnsForRow, $this->entityFactory->createCell('')); $cells = \array_fill(0, $numberOfColumnsForRow, $this->entityFactory->createCell(''));
$this->currentlyProcessedRow->setCells($cells); $this->currentlyProcessedRow->setCells($cells);
return XMLProcessor::PROCESSING_CONTINUE; return XMLProcessor::PROCESSING_CONTINUE;
@@ -276,6 +276,7 @@ class RowIterator implements IteratorInterface
$currentColumnIndex = $this->getColumnIndex($xmlReader); $currentColumnIndex = $this->getColumnIndex($xmlReader);
// NOTE: expand() will automatically decode all XML entities of the child nodes // NOTE: expand() will automatically decode all XML entities of the child nodes
/** @var \DOMElement $node */
$node = $xmlReader->expand(); $node = $xmlReader->expand();
$cell = $this->getCell($node); $cell = $this->getCell($node);
@@ -352,7 +353,7 @@ class RowIterator implements IteratorInterface
/** /**
* Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node. * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
* *
* @param \DOMNode $node * @param \DOMElement $node
* @return Cell The cell set with the associated with the cell * @return Cell The cell set with the associated with the cell
*/ */
protected function getCell($node) protected function getCell($node)
@@ -374,7 +375,7 @@ class RowIterator implements IteratorInterface
* *
* @return Row|null * @return Row|null
*/ */
public function current() public function current() : ?Row
{ {
$rowToBeProcessed = $this->rowBuffer; $rowToBeProcessed = $this->rowBuffer;
@@ -399,7 +400,7 @@ class RowIterator implements IteratorInterface
* *
* @return int * @return int
*/ */
public function key() public function key() : int
{ {
// TODO: This should return $this->nextRowIndexToBeProcessed // TODO: This should return $this->nextRowIndexToBeProcessed
// but to avoid a breaking change, the return value for // but to avoid a breaking change, the return value for
@@ -414,7 +415,7 @@ class RowIterator implements IteratorInterface
* *
* @return void * @return void
*/ */
public function end() public function end() : void
{ {
$this->xmlReader->close(); $this->xmlReader->close();
} }
+2 -2
View File
@@ -27,7 +27,7 @@ class SheetIterator implements IteratorInterface
// Fetch all available sheets // Fetch all available sheets
$this->sheets = $sheetManager->getSheets(); $this->sheets = $sheetManager->getSheets();
if (count($this->sheets) === 0) { if (\count($this->sheets) === 0) {
throw new NoSheetsFoundException('The file must contain at least one sheet.'); throw new NoSheetsFoundException('The file must contain at least one sheet.');
} }
} }
@@ -51,7 +51,7 @@ class SheetIterator implements IteratorInterface
*/ */
public function valid() public function valid()
{ {
return ($this->currentSheetIndex < count($this->sheets)); return ($this->currentSheetIndex < \count($this->sheets));
} }
/** /**
+1 -1
View File
@@ -15,7 +15,7 @@ use Box\Spout\Writer\WriterAbstract;
class Writer extends WriterAbstract class Writer extends WriterAbstract
{ {
/** Number of rows to write before flushing */ /** Number of rows to write before flushing */
const FLUSH_THRESHOLD = 500; public const FLUSH_THRESHOLD = 500;
/** @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';
@@ -3,7 +3,9 @@
namespace Box\Spout\Writer\Common\Creator\Style; namespace Box\Spout\Writer\Common\Creator\Style;
use Box\Spout\Common\Entity\Style\Border; use Box\Spout\Common\Entity\Style\Border;
use Box\Spout\Common\Entity\Style\CellAlignment;
use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Entity\Style\Style;
use Box\Spout\Common\Exception\InvalidArgumentException;
/** /**
* Class StyleBuilder * Class StyleBuilder
@@ -122,6 +124,25 @@ class StyleBuilder
return $this; return $this;
} }
/**
* Sets the cell alignment.
*
* @param string $cellAlignment The cell alignment
*
* @throws InvalidArgumentException If the given cell alignment is not valid
* @return StyleBuilder
*/
public function setCellAlignment($cellAlignment)
{
if (!CellAlignment::isValid($cellAlignment)) {
throw new InvalidArgumentException('Invalid cell alignment value');
}
$this->style->setCellAlignment($cellAlignment);
return $this;
}
/** /**
* Set a border * Set a border
* *
@@ -148,6 +169,20 @@ class StyleBuilder
return $this; return $this;
} }
/**
* Sets a format
*
* @param string $format Format
* @return StyleBuilder
* @api
*/
public function setFormat($format)
{
$this->style->setFormat($format);
return $this;
}
/** /**
* Returns the configured style. The style is cached and can be reused. * Returns the configured style. The style is cached and can be reused.
* *
@@ -50,6 +50,7 @@ class WriterEntityFactory
return WriterFactory::createFromType(Type::CSV); return WriterFactory::createFromType(Type::CSV);
} catch (UnsupportedTypeException $e) { } catch (UnsupportedTypeException $e) {
// should never happen // should never happen
return null;
} }
} }
@@ -64,6 +65,7 @@ class WriterEntityFactory
return WriterFactory::createFromType(Type::XLSX); return WriterFactory::createFromType(Type::XLSX);
} catch (UnsupportedTypeException $e) { } catch (UnsupportedTypeException $e) {
// should never happen // should never happen
return null;
} }
} }
@@ -78,6 +80,7 @@ class WriterEntityFactory
return WriterFactory::createFromType(Type::ODS); return WriterFactory::createFromType(Type::ODS);
} catch (UnsupportedTypeException $e) { } catch (UnsupportedTypeException $e) {
// should never happen // should never happen
return null;
} }
} }
@@ -98,7 +101,7 @@ class WriterEntityFactory
*/ */
public static function createRowFromArray(array $cellValues = [], Style $rowStyle = null) public static function createRowFromArray(array $cellValues = [], Style $rowStyle = null)
{ {
$cells = array_map(function ($cellValue) { $cells = \array_map(function ($cellValue) {
return new Cell($cellValue); return new Cell($cellValue);
}, $cellValues); }, $cellValues);
@@ -35,7 +35,7 @@ class WriterFactory
*/ */
public static function createFromFile(string $path) public static function createFromFile(string $path)
{ {
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $extension = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
return self::createFromType($extension); return self::createFromType($extension);
} }
+7 -7
View File
@@ -9,15 +9,15 @@ namespace Box\Spout\Writer\Common\Entity;
abstract class Options abstract class Options
{ {
// CSV specific options // CSV specific options
const FIELD_DELIMITER = 'fieldDelimiter'; public const FIELD_DELIMITER = 'fieldDelimiter';
const FIELD_ENCLOSURE = 'fieldEnclosure'; public const FIELD_ENCLOSURE = 'fieldEnclosure';
const SHOULD_ADD_BOM = 'shouldAddBOM'; public const SHOULD_ADD_BOM = 'shouldAddBOM';
// Multisheets options // Multisheets options
const TEMP_FOLDER = 'tempFolder'; public const TEMP_FOLDER = 'tempFolder';
const DEFAULT_ROW_STYLE = 'defaultRowStyle'; public const DEFAULT_ROW_STYLE = 'defaultRowStyle';
const SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY = 'shouldCreateNewSheetsAutomatically'; public const SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY = 'shouldCreateNewSheetsAutomatically';
// XLSX specific options // XLSX specific options
const SHOULD_USE_INLINE_STRINGS = 'shouldUseInlineStrings'; public const SHOULD_USE_INLINE_STRINGS = 'shouldUseInlineStrings';
} }
+1 -1
View File
@@ -10,7 +10,7 @@ use Box\Spout\Writer\Common\Manager\SheetManager;
*/ */
class Sheet class Sheet
{ {
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet'; public const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
/** @var int Index of the sheet, based on order in the workbook (zero-based) */ /** @var int Index of the sheet, based on order in the workbook (zero-based) */
private $index; private $index;
+1 -1
View File
@@ -19,7 +19,7 @@ class Workbook
*/ */
public function __construct() public function __construct()
{ {
$this->internalId = uniqid(); $this->internalId = \uniqid();
} }
/** /**
+1 -1
View File
@@ -11,7 +11,7 @@ class Worksheet
/** @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 */
private $filePath; private $filePath;
/** @var resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */ /** @var resource|null Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */
private $filePointer; private $filePointer;
/** @var Sheet The "external" sheet */ /** @var Sheet The "external" sheet */
+16 -15
View File
@@ -8,38 +8,39 @@ namespace Box\Spout\Writer\Common\Helper;
*/ */
class CellHelper class CellHelper
{ {
/** @var array Cache containing the mapping column index => cell index */ /** @var array Cache containing the mapping column index => column letters */
private static $columnIndexToCellIndexCache = []; private static $columnIndexToColumnLettersCache = [];
/** /**
* Returns the cell index (base 26) associated to the base 10 column index. * Returns the column letters (base 26) associated to the base 10 column index.
* Excel uses A to Z letters for column indexing, where A is the 1st column, * Excel uses A to Z letters for column indexing, where A is the 1st column,
* Z is the 26th and AA is the 27th. * 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. * 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, ...) * @param int $columnIndexZeroBased The Excel column index (0, 42, ...)
*
* @return string The associated cell index ('A', 'BC', ...) * @return string The associated cell index ('A', 'BC', ...)
*/ */
public static function getCellIndexFromColumnIndex($columnIndex) public static function getColumnLettersFromColumnIndex($columnIndexZeroBased)
{ {
$originalColumnIndex = $columnIndex; $originalColumnIndex = $columnIndexZeroBased;
// Using isset here because it is way faster than array_key_exists... // Using isset here because it is way faster than array_key_exists...
if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) { if (!isset(self::$columnIndexToColumnLettersCache[$originalColumnIndex])) {
$cellIndex = ''; $columnLetters = '';
$capitalAAsciiValue = ord('A'); $capitalAAsciiValue = \ord('A');
do { do {
$modulus = $columnIndex % 26; $modulus = $columnIndexZeroBased % 26;
$cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex; $columnLetters = \chr($capitalAAsciiValue + $modulus) . $columnLetters;
// substracting 1 because it's zero-based // substracting 1 because it's zero-based
$columnIndex = (int) ($columnIndex / 26) - 1; $columnIndexZeroBased = (int) ($columnIndexZeroBased / 26) - 1;
} while ($columnIndex >= 0); } while ($columnIndexZeroBased >= 0);
self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex; self::$columnIndexToColumnLettersCache[$originalColumnIndex] = $columnLetters;
} }
return self::$columnIndexToCellIndexCache[$originalColumnIndex]; return self::$columnIndexToColumnLettersCache[$originalColumnIndex];
} }
} }
+10 -10
View File
@@ -10,11 +10,11 @@ use Box\Spout\Writer\Common\Creator\InternalEntityFactory;
*/ */
class ZipHelper class ZipHelper
{ {
const ZIP_EXTENSION = '.zip'; public const ZIP_EXTENSION = '.zip';
/** Controls what to do when trying to add an existing file */ /** Controls what to do when trying to add an existing file */
const EXISTING_FILES_SKIP = 'skip'; public const EXISTING_FILES_SKIP = 'skip';
const EXISTING_FILES_OVERWRITE = 'overwrite'; public const EXISTING_FILES_OVERWRITE = 'overwrite';
/** @var InternalEntityFactory Factory to create entities */ /** @var InternalEntityFactory Factory to create entities */
private $entityFactory; private $entityFactory;
@@ -135,7 +135,7 @@ class ZipHelper
public static function canChooseCompressionMethod() public static function canChooseCompressionMethod()
{ {
// setCompressionName() is a PHP7+ method... // setCompressionName() is a PHP7+ method...
return (method_exists(new \ZipArchive(), 'setCompressionName')); return (\method_exists(new \ZipArchive(), 'setCompressionName'));
} }
/** /**
@@ -151,7 +151,7 @@ class ZipHelper
foreach ($itemIterator as $itemInfo) { foreach ($itemIterator as $itemInfo) {
$itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname()); $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath); $itemLocalPath = \str_replace($folderRealPath, '', $itemRealPath);
if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) { if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) {
$zip->addFile($itemRealPath, $itemLocalPath); $zip->addFile($itemRealPath, $itemLocalPath);
@@ -181,9 +181,9 @@ class ZipHelper
*/ */
protected function getNormalizedRealPath($path) protected function getNormalizedRealPath($path)
{ {
$realPath = realpath($path); $realPath = \realpath($path);
return str_replace(DIRECTORY_SEPARATOR, '/', $realPath); return \str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
} }
/** /**
@@ -210,8 +210,8 @@ class ZipHelper
*/ */
protected function copyZipToStream($zipFilePath, $pointer) protected function copyZipToStream($zipFilePath, $pointer)
{ {
$zipFilePointer = fopen($zipFilePath, 'r'); $zipFilePointer = \fopen($zipFilePath, 'r');
stream_copy_to_stream($zipFilePointer, $pointer); \stream_copy_to_stream($zipFilePointer, $pointer);
fclose($zipFilePointer); \fclose($zipFilePointer);
} }
} }
@@ -0,0 +1,38 @@
<?php
namespace Box\Spout\Writer\Common\Manager;
use Box\Spout\Common\Entity\Style\Style;
/**
* Class RegisteredStyle
* Allow to know if this style must replace actual row style.
*/
class RegisteredStyle
{
/**
* @var Style
*/
private $style;
/**
* @var bool
*/
private $isMatchingRowStyle;
public function __construct(Style $style, bool $isMatchingRowStyle)
{
$this->style = $style;
$this->isMatchingRowStyle = $isMatchingRowStyle;
}
public function getStyle() : Style
{
return $this->style;
}
public function isMatchingRowStyle() : bool
{
return $this->isMatchingRowStyle;
}
}
@@ -13,7 +13,7 @@ use Box\Spout\Writer\Exception\InvalidSheetNameException;
class SheetManager class SheetManager
{ {
/** Sheet name should not exceed 31 characters */ /** Sheet name should not exceed 31 characters */
const MAX_LENGTH_SHEET_NAME = 31; public const MAX_LENGTH_SHEET_NAME = 31;
/** @var array Invalid characters that cannot be contained in the sheet name */ /** @var array Invalid characters that cannot be contained in the sheet name */
private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', ':', '[', ']']; private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', ':', '[', ']'];
@@ -45,8 +45,8 @@ class SheetManager
*/ */
public function throwIfNameIsInvalid($name, Sheet $sheet) public function throwIfNameIsInvalid($name, Sheet $sheet)
{ {
if (!is_string($name)) { if (!\is_string($name)) {
$actualType = gettype($name); $actualType = \gettype($name);
$errorMessage = "The sheet's name is invalid. It must be a string ($actualType given)."; $errorMessage = "The sheet's name is invalid. It must be a string ($actualType given).";
throw new InvalidSheetNameException($errorMessage); throw new InvalidSheetNameException($errorMessage);
} }
@@ -74,9 +74,9 @@ class SheetManager
} }
} }
if (count($failedRequirements) !== 0) { if (\count($failedRequirements) !== 0) {
$errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - "; $errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - ";
$errorMessage .= implode("\n - ", $failedRequirements); $errorMessage .= \implode("\n - ", $failedRequirements);
throw new InvalidSheetNameException($errorMessage); throw new InvalidSheetNameException($errorMessage);
} }
} }
@@ -90,7 +90,7 @@ class SheetManager
*/ */
private function doesContainInvalidCharacters($name) private function doesContainInvalidCharacters($name)
{ {
return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name); return (\str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name);
} }
/** /**
@@ -0,0 +1,32 @@
<?php
namespace Box\Spout\Writer\Common\Manager\Style;
use Box\Spout\Common\Entity\Style\Style;
/**
* Class PossiblyUpdatedStyle
* Indicates if style is updated.
* It allow to know if style registration must be done.
*/
class PossiblyUpdatedStyle
{
private $style;
private $isUpdated;
public function __construct(Style $style, bool $isUpdated)
{
$this->style = $style;
$this->isUpdated = $isUpdated;
}
public function getStyle() : Style
{
return $this->style;
}
public function isUpdated() : bool
{
return $this->isUpdated;
}
}
@@ -50,13 +50,11 @@ class StyleManager implements StyleManagerInterface
* Typically, set "wrap text" if a cell contains a new line. * Typically, set "wrap text" if a cell contains a new line.
* *
* @param Cell $cell * @param Cell $cell
* @return Style * @return PossiblyUpdatedStyle The eventually updated style
*/ */
public function applyExtraStylesIfNeeded(Cell $cell) public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle
{ {
$updatedStyle = $this->applyWrapTextIfCellContainsNewLine($cell); return $this->applyWrapTextIfCellContainsNewLine($cell);
return $updatedStyle;
} }
/** /**
@@ -69,21 +67,19 @@ class StyleManager implements StyleManagerInterface
* on the Windows version of Excel... * on the Windows version of Excel...
* *
* @param Cell $cell The cell the style should be applied to * @param Cell $cell The cell the style should be applied to
* @return \Box\Spout\Common\Entity\Style\Style The eventually updated style * @return PossiblyUpdatedStyle The eventually updated style
*/ */
protected function applyWrapTextIfCellContainsNewLine(Cell $cell) protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : PossiblyUpdatedStyle
{ {
$cellStyle = $cell->getStyle(); $cellStyle = $cell->getStyle();
// if the "wrap text" option is already set, no-op // if the "wrap text" option is already set, no-op
if ($cellStyle->hasSetWrapText()) { if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) {
return $cellStyle;
}
if ($cell->isString() && strpos($cell->getValue(), "\n") !== false) {
$cellStyle->setShouldWrapText(); $cellStyle->setShouldWrapText();
return new PossiblyUpdatedStyle($cellStyle, true);
} }
return $cellStyle; return new PossiblyUpdatedStyle($cellStyle, false);
} }
} }
@@ -24,7 +24,7 @@ interface StyleManagerInterface
* Typically, set "wrap text" if a cell contains a new line. * Typically, set "wrap text" if a cell contains a new line.
* *
* @param Cell $cell * @param Cell $cell
* @return Style The updated style * @return PossiblyUpdatedStyle The eventually updated style
*/ */
public function applyExtraStylesIfNeeded(Cell $cell); public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle;
} }
@@ -85,9 +85,15 @@ class StyleMerger
if (!$style->hasSetWrapText() && $baseStyle->shouldWrapText()) { if (!$style->hasSetWrapText() && $baseStyle->shouldWrapText()) {
$styleToUpdate->setShouldWrapText(); $styleToUpdate->setShouldWrapText();
} }
if (!$style->getBorder() && $baseStyle->shouldApplyBorder()) { if (!$style->hasSetCellAlignment() && $baseStyle->shouldApplyCellAlignment()) {
$styleToUpdate->setCellAlignment($baseStyle->getCellAlignment());
}
if ($style->getBorder() === null && $baseStyle->shouldApplyBorder()) {
$styleToUpdate->setBorder($baseStyle->getBorder()); $styleToUpdate->setBorder($baseStyle->getBorder());
} }
if ($style->getFormat() === null && $baseStyle->shouldApplyFormat()) {
$styleToUpdate->setFormat($baseStyle->getFormat());
}
if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) { if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) {
$styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor()); $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor());
} }
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more