Compare commits
381 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8459666841 | ||
|
ec8d53b13c | ||
|
0739e044da | ||
|
cc42c1d29f | ||
|
5926207012 | ||
|
550a6831f3 | ||
|
6f1b67b39d | ||
|
ea0a67d283 | ||
|
e95e0eeefd | ||
|
64a09a748d | ||
|
7517e5c4de | ||
|
e75f6f7301 | ||
|
27c6845b4b | ||
|
2499dc46b7 | ||
|
6b7366bb6f | ||
|
6a10ec3586 | ||
|
9882bf0946 | ||
|
75c06807af | ||
|
0345b369c7 | ||
|
f8595e9d63 | ||
|
9533accd73 | ||
|
9bdb027d31 | ||
|
76017f0949 | ||
|
fde8a495ca | ||
|
69eeeff478 | ||
|
2ff515c306 | ||
|
0837d49c2b | ||
|
110876e32c | ||
|
8c1f0cc447 | ||
|
eb84ec9364 | ||
|
8a17d6c71f | ||
|
c6f596c776 | ||
|
11d91e1740 | ||
|
197fb9987a | ||
|
a58b340835 | ||
|
57b6e87a65 | ||
|
91f756be0b | ||
|
03e1ce438a | ||
|
df9d96366f | ||
|
0f20c99a7f | ||
|
9ab0b10a0f | ||
|
ed9322e309 | ||
|
73347517f0 | ||
|
ad913f0100 | ||
|
c29d1877b8 | ||
|
816596183f | ||
|
ab973cab34 | ||
|
b8eb2bb814 | ||
|
f54f7a400c | ||
|
7964dadc21 | ||
|
eb88bb4c3a | ||
|
94b654175c | ||
|
dbdf5f7f38 | ||
|
9f4c094fa0 | ||
|
0a0b1f7196 | ||
|
db32a7c7db | ||
|
859b8d336e | ||
|
8a2dcc946b | ||
|
74146c6224 | ||
|
6a6d1df9df | ||
|
52312b7045 | ||
|
d4e12b1812 | ||
|
e8c6d83104 | ||
|
40aecd7b90 | ||
|
16a2f91a22 | ||
|
2d297e954b | ||
|
2716d7eeed | ||
|
1bbfd45b82 | ||
|
6c4086cf97 | ||
|
a296f73a98 | ||
|
0f0bf64802 | ||
|
9d33fcdd00 | ||
|
4ff9717b0a | ||
|
c62177f0e4 | ||
|
3beaa32021 | ||
|
5ce5a488d1 | ||
|
69b0fb9eaf | ||
|
40ee386edd | ||
|
6104d41857 | ||
|
4260c46b11 | ||
|
4a9d0398ad | ||
|
e8693834a0 | ||
|
171a2fab10 | ||
|
3d577197d2 | ||
|
71cf0fe339 | ||
|
ee998f7173 | ||
|
6c8344c025 | ||
|
a420e3fffa | ||
|
e99c80b3ad | ||
|
8f7f106555 | ||
|
738ea30f35 | ||
|
8a1c48b6b0 | ||
|
e1acdc1fc5 | ||
|
b105d15f08 | ||
|
b05ce01d3c | ||
|
195b0d4bda | ||
|
f7c483adbd | ||
|
1b64a06fbe | ||
|
d25a4ebd6d | ||
|
e83ac423dc | ||
|
799ad93d23 | ||
|
5c0030854f | ||
|
01ad5af2c5 | ||
|
29cf6245a1 | ||
|
21e0e9e6b1 | ||
|
e135b71473 | ||
|
d84f5168ec | ||
|
88eee3be72 | ||
|
4c7adbb33f | ||
|
9f4e28b3fd | ||
|
0efdf48119 | ||
|
cd0831ea8e | ||
|
5b1bcc1303 | ||
|
f5168114d0 | ||
|
4d1d1c1e87 | ||
|
102e17159c | ||
|
68a96367a8 | ||
|
78b6639480 | ||
|
a665b974fa | ||
|
139f7fdfb3 | ||
|
c826d15472 | ||
|
e2b519d6f9 | ||
|
0c8a53c821 | ||
|
5a470188a9 | ||
|
1c69dee9c9 | ||
|
b9206fcb4b | ||
|
ddfa40e8b3 | ||
|
111f82d35f | ||
|
8dd6487ea3 | ||
|
7367b89384 | ||
|
8aec9ea992 | ||
|
3d0f108b1d | ||
|
2d2151ac8d | ||
|
ca5962271e | ||
|
727a90fd06 | ||
|
3851e05f83 | ||
|
7274226b75 | ||
|
fec27e9056 | ||
|
c74c0d9127 | ||
|
e1ae3c8a81 | ||
|
0ab053dc6e | ||
|
28c1bea28c | ||
|
3681a3421a | ||
|
3bbff7ea7d | ||
|
b968513cb9 | ||
|
740fcfb8c1 | ||
|
554ebf987b | ||
|
668c10a30d | ||
|
61f2addefa | ||
|
4ec3a21170 | ||
|
b7e46740ce | ||
|
fd6dd46b25 | ||
|
6d44cd26cc | ||
|
ee5dee61c7 | ||
|
4d6437fa77 | ||
|
40b4a57e6b | ||
|
5d4166196a | ||
|
3bb4fd3d48 | ||
|
30366e6a5d | ||
|
762dd1573a | ||
|
7ec0f565fd | ||
|
cc9a0b526b | ||
|
238756ab6e | ||
|
c4e25a168e | ||
|
878c4a9c8b | ||
|
cebffbe80c | ||
|
69b091b37c | ||
|
bc17311f5f | ||
|
a366d0d0af | ||
|
99816b0b8e | ||
|
f9d8ad8be3 | ||
|
80553c6c52 | ||
|
606103f7fc | ||
|
4acd9ad087 | ||
|
4e6db6a8a1 | ||
|
048105461c | ||
|
1eb01a3d2a | ||
|
9f80ece73f | ||
|
7f8b95b2f3 | ||
|
4a65466b61 | ||
|
742780613a | ||
|
3128f86769 | ||
|
d898f91917 | ||
|
33c9d2f2ed | ||
|
36d3596f83 | ||
|
1ce931a424 | ||
|
6f4ddb1569 | ||
|
521f799366 | ||
|
984c9c1f67 | ||
|
e255895cff | ||
|
e276b4378e | ||
|
9ce77405e0 | ||
|
ef4a32eb5e | ||
|
4cb30bc36d | ||
|
bf616dee90 | ||
|
3a330debb3 | ||
|
a19231fb68 | ||
|
73d5d0ea17 | ||
|
687c321363 | ||
|
2fa01cd838 | ||
|
752f4bf64e | ||
|
b61323d7d2 | ||
|
179ab483d6 | ||
|
2fafb63115 | ||
|
5ef5647558 | ||
|
0978d340f0 | ||
|
77178122c3 | ||
|
23f8cc4f05 | ||
|
442a9837f1 | ||
|
cc07072cbb | ||
|
30aa1b87e2 | ||
|
ddb7365a79 | ||
|
a07a96f523 | ||
|
3e0afd858f | ||
|
d4e57b1f0d | ||
|
435a9a016e | ||
|
5e7a1745ac | ||
|
277c353984 | ||
|
ff2d54cc8d | ||
|
c94694cb60 | ||
|
584121d478 | ||
|
b2dc0c3fa9 | ||
|
7f65993c87 | ||
|
b75a3e34fc | ||
|
82605ab57b | ||
|
7a613eed8c | ||
|
54a1a09e29 | ||
|
a8eb7ad39c | ||
|
ffea8871a6 | ||
|
b02d13cd40 | ||
|
aa25678a83 | ||
|
192659cb24 | ||
|
e30bc37448 | ||
|
1891c0b053 | ||
|
a43c13a36f | ||
|
dc31d6e8c2 | ||
|
8edd8e2401 | ||
|
cd38ba093e | ||
|
70c81e809f | ||
|
1d3a9f939c | ||
|
efebfb2bc2 | ||
|
251c0bebc1 | ||
|
03866a6604 | ||
|
2ed30321b4 | ||
|
2c80b1f23a | ||
|
a24e794177 | ||
|
104cd9b811 | ||
|
a6a6b158de | ||
|
2d923c7e46 | ||
|
b4724906c4 | ||
|
bb20d2e6bb | ||
|
b8fd789ac0 | ||
|
5a7c2c1262 | ||
|
e9cd7a397e | ||
|
8bb42ebc23 | ||
|
71a6f6a937 | ||
|
616925148e | ||
|
6f0f7c9690 | ||
|
b69e28050b | ||
|
d6e8fe4b54 | ||
|
e321f30c3b | ||
|
c31373fb1a | ||
|
6c57125c0c | ||
|
30837f869d | ||
|
e60054f3c4 | ||
|
3ee7099c95 | ||
|
2b1160bb33 | ||
|
049fd990b9 | ||
|
d2ac54c578 | ||
|
0c90d102ef | ||
|
e39dcb3847 | ||
|
10d1140a95 | ||
|
48debbcbca | ||
|
f4d6fb87ee | ||
|
771afcb5f1 | ||
|
86e26632f6 | ||
|
8614f79da3 | ||
|
4827e56cac | ||
|
03e85ffc21 | ||
|
e4cc8b4eaa | ||
|
73341d06a9 | ||
|
209372462b | ||
|
4a5da2ad74 | ||
|
c48c07db99 | ||
|
a804be4844 | ||
|
4bfbb41c95 | ||
|
61b71bf379 | ||
|
3c3294061a | ||
|
1fde6d836a | ||
|
05489cda88 | ||
|
128db45f22 | ||
|
9a85d84a2e | ||
|
4407cffeff | ||
|
8d27b3097d | ||
|
2c6cb1ffe5 | ||
|
22daea5f9a | ||
|
ed0e8f79cc | ||
|
44d72d8245 | ||
|
728dd3b399 | ||
|
05a9a1b60a | ||
|
a76624a721 | ||
|
90cbb7b5a6 | ||
|
f55520661e | ||
|
cb5dae22e4 | ||
|
2f6193ce20 | ||
|
582da8403d | ||
|
8b666fc6cd | ||
|
f8aab6eefd | ||
|
c4c6dddb20 | ||
|
6c97141679 | ||
|
9c8cc05364 | ||
|
2a9400dfca | ||
|
8ef6bdac62 | ||
|
46c246e6a4 | ||
|
8fd606ae4f | ||
|
6767386daf | ||
|
3395d3abb3 | ||
|
d1c4d563c1 | ||
|
01cc8b3da0 | ||
|
45980195cd | ||
|
a1a1077677 | ||
|
16d0290a17 | ||
|
f8c39287ad | ||
|
118810de22 | ||
|
3720c9ea1c | ||
|
6124528161 | ||
|
818ec2488c | ||
|
aa7978146f | ||
|
d6e707c5fe | ||
|
03ae367fb3 | ||
|
ac48dce334 | ||
|
e147c580ca | ||
|
7615acf398 | ||
|
1d38bb00c5 | ||
|
79d947a6b3 | ||
|
dd3cc5bf47 | ||
|
0a5be41c53 | ||
|
d2ba6d884c | ||
|
e4154dfdc3 | ||
|
3f0016f753 | ||
|
bc009a3241 | ||
|
0f8e7a8f58 | ||
|
156fd29a44 | ||
|
ef171910b9 | ||
|
5949cb2442 | ||
|
108a92e259 | ||
|
9fb1427944 | ||
|
a8f1fba854 | ||
|
1812b4f996 | ||
|
b4ace972e7 | ||
|
c428daff5a | ||
|
5908443583 | ||
|
050672755d | ||
|
e1928eb68a | ||
|
9467b5a810 | ||
|
2699ffbcae | ||
|
3559bc8834 | ||
|
506d682e74 | ||
|
7efab5576d | ||
|
366f121eb0 | ||
|
f8d0ac2682 | ||
|
82377403ff | ||
|
e0c84f77b1 | ||
|
b78227c370 | ||
|
444308d42c | ||
|
353d4e86a5 | ||
|
f043f8d4d0 | ||
|
c8ddcf5441 | ||
|
4a346cef0c | ||
|
1c8934790d | ||
|
2a1925bc51 | ||
|
2183ff6738 | ||
|
380ee524a5 | ||
|
21263a0730 | ||
|
0104714cbd | ||
|
611a091290 | ||
|
40a86c4b6c | ||
|
8a3b895afc | ||
|
e3f7ecfa64 | ||
|
93d7aafe8b | ||
|
dc53b6aa20 |
12
.gitattributes
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Ignore all tests, documentation and dot files for archive
|
||||
/docs export-ignore
|
||||
/tests export-ignore
|
||||
/.editorconfig export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.php_cs.dist export-ignore
|
||||
/.scrutinizer.yml export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/CONTRIBUTING.md export-ignore
|
||||
/logo.png export-ignore
|
||||
/phpunit.xml export-ignore
|
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: adrilo
|
270
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,270 @@
|
||||
name: Spout CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
tests-on-php-latest:
|
||||
strategy:
|
||||
matrix:
|
||||
operating-system: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.operating-system }}
|
||||
name: Tests - PHP 8.1 on ${{ matrix.operating-system }}
|
||||
env:
|
||||
extensions: zip, xmlreader, dom
|
||||
cache-key: cache-matrix-v1 # can be any string, change to clear the extension cache.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache environment
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
key: ${{ env.cache-key }}
|
||||
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
restore-keys: ${{ steps.extcache.outputs.key }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
# Use composer.json for key, if composer.lock is not committed.
|
||||
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Test with phpunit
|
||||
run: vendor/bin/phpunit --no-coverage
|
||||
|
||||
tests-on-older-php:
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.3', '7.4', '8.0']
|
||||
runs-on: ubuntu-latest
|
||||
name: Tests - PHP ${{ matrix.php-versions }}
|
||||
env:
|
||||
extensions: zip, xmlreader, dom
|
||||
cache-key: cache-matrix-v1 # can be any string, change to clear the extension cache.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache environment
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: ${{ env.extensions }}
|
||||
key: ${{ env.cache-key }}
|
||||
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
restore-keys: ${{ steps.extcache.outputs.key }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: ${{ env.extensions }}
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
# Use composer.json for key, if composer.lock is not committed.
|
||||
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Test with phpunit
|
||||
run: vendor/bin/phpunit --no-coverage
|
||||
|
||||
code-coverage:
|
||||
name: Code coverage
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
extensions: zip, xmlreader, dom
|
||||
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache environment
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
key: ${{ env.cache-key }}
|
||||
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
restore-keys: ${{ steps.extcache.outputs.key }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
# Use composer.json for key, if composer.lock is not committed.
|
||||
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Run Tests with Code Coverage
|
||||
run: |
|
||||
mkdir -p build/logs
|
||||
vendor/bin/phpunit --coverage-clover=build/logs/clover.xml
|
||||
|
||||
- name: Upload coverage results to Coveralls
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
composer global require php-coveralls/php-coveralls
|
||||
php-coveralls --coverage_clover=build/logs/clover.xml -v --exclude-no-stmt
|
||||
|
||||
coding-style:
|
||||
name: Coding Style
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
extensions: zip, xmlreader, dom
|
||||
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache environment
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
key: ${{ env.cache-key }}
|
||||
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
restore-keys: ${{ steps.extcache.outputs.key }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
# Use composer.json for key, if composer.lock is not committed.
|
||||
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Run PHP-CS-Fixer
|
||||
run: vendor/bin/php-cs-fixer fix --verbose --diff --dry-run
|
||||
|
||||
static-analysis:
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
extensions: zip, xmlreader, dom
|
||||
cache-key: cache-single-v1 # can be any string, change to clear the extension cache.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache environment
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
key: ${{ env.cache-key }}
|
||||
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
restore-keys: ${{ steps.extcache.outputs.key }}
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: ${{ env.extensions }}
|
||||
tools: phpstan
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
# Use composer.json for key, if composer.lock is not committed.
|
||||
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Static Analysis using PHPStan
|
||||
run: ./vendor/bin/phpstan analyse -c phpstan.neon --no-progress
|
7
.gitignore
vendored
@ -1,5 +1,8 @@
|
||||
/.idea
|
||||
*.iml
|
||||
|
||||
/tests/resources/generated
|
||||
/tests/coverage
|
||||
/vendor
|
||||
/.idea
|
||||
*.iml
|
||||
/.php-cs-fixer.cache
|
||||
/.phpunit.result.cache
|
||||
|
54
.php-cs-fixer.dist.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in(__DIR__)
|
||||
->name('*.php')
|
||||
->exclude('vendor');
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
return $config
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'align_multiline_comment' => false,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'binary_operator_spaces' => ['default' => null],
|
||||
'blank_line_before_statement' => ['statements' => ['return']],
|
||||
'combine_consecutive_unsets' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'declare_equal_normalize' => ['space' => 'single'],
|
||||
'heredoc_to_nowdoc' => true,
|
||||
'increment_style' => ['style' => 'post'],
|
||||
'is_null' => true,
|
||||
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||
'modernize_types_casting' => true,
|
||||
'no_break_comment' => ['comment_text' => 'do nothing'],
|
||||
'no_empty_phpdoc' => false,
|
||||
'no_null_property_initialization' => true,
|
||||
'echo_tag_syntax' => false,
|
||||
'no_superfluous_elseif' => true,
|
||||
'no_superfluous_phpdoc_tags' => false,
|
||||
'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'switch_case', 'yield']],
|
||||
'no_unneeded_curly_braces' => true,
|
||||
'no_unneeded_final_method' => true,
|
||||
'no_useless_else' => false,
|
||||
'no_useless_return' => true,
|
||||
'ordered_imports' => true,
|
||||
'phpdoc_add_missing_param_annotation' => true,
|
||||
'phpdoc_align' => false,
|
||||
'phpdoc_annotation_without_dot' => false,
|
||||
'phpdoc_no_empty_return' => false,
|
||||
'phpdoc_order' => true,
|
||||
'phpdoc_summary' => false,
|
||||
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
|
||||
'phpdoc_separation' => false,
|
||||
'protected_to_private' => true,
|
||||
'psr_autoloading' => true,
|
||||
'return_type_declaration' => ['space_before' => 'one'],
|
||||
'semicolon_after_instruction' => true,
|
||||
'simplified_null_return' => false,
|
||||
'single_line_comment_style' => ['comment_types' => ['hash']],
|
||||
'strict_comparison' => true,
|
||||
'yoda_style' => ['equal' => false, 'identical' => false],
|
||||
])
|
||||
->setFinder($finder);
|
@ -1,54 +0,0 @@
|
||||
filter:
|
||||
excluded_paths: [vendor/*, tests/*]
|
||||
|
||||
tools:
|
||||
external_code_coverage:
|
||||
timeout: 600 # Wait 10 minutes for results
|
||||
runs: 3 # Merge results for 5.4, 5.5 and 5.6 jobs
|
||||
php_mess_detector: true
|
||||
php_code_sniffer:
|
||||
config:
|
||||
standard: PSR4
|
||||
filter:
|
||||
paths: ['src']
|
||||
sensiolabs_security_checker: true
|
||||
php_pdepend: true
|
||||
php_loc:
|
||||
enabled: true
|
||||
excluded_dirs: [vendor, tests]
|
||||
php_cpd:
|
||||
enabled: true
|
||||
excluded_dirs: [vendor, tests]
|
||||
|
||||
build_failure_conditions:
|
||||
- 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9
|
||||
- 'project.metric_change("scrutinizer.test_coverage", < -0.005)' # Code Coverage decreased by more than 0.5%
|
||||
- 'project.metric("scrutinizer.test_coverage", < 0.97)' # Code Coverage drops below 97%
|
||||
|
||||
checks:
|
||||
php:
|
||||
remove_extra_empty_lines: true
|
||||
remove_php_closing_tag: true
|
||||
remove_trailing_whitespace: true
|
||||
fix_use_statements:
|
||||
remove_unused: true
|
||||
preserve_multiple: false
|
||||
preserve_blanklines: true
|
||||
fix_php_opening_tag: true
|
||||
fix_linefeed: true
|
||||
fix_line_ending: true
|
||||
fix_identation_4spaces: true
|
||||
fix_doc_comments: true
|
||||
uppercase_constants: true
|
||||
use_self_instead_of_fqcn: true
|
||||
simplify_boolean_return: true
|
||||
return_doc_comments: true
|
||||
return_doc_comment_if_not_inferrable: true
|
||||
phpunit_assertions: true
|
||||
parameters_in_camelcaps: true
|
||||
parameter_doc_comments: true
|
||||
param_doc_comment_if_not_inferrable: true
|
||||
optional_parameters_at_the_end: true
|
||||
newline_at_end_of_file: true
|
||||
encourage_single_quotes: true
|
||||
|
18
.travis.yml
@ -1,18 +0,0 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- hhvm
|
||||
|
||||
install:
|
||||
- composer install --no-interaction --prefer-source
|
||||
|
||||
script:
|
||||
- mkdir -p build/logs
|
||||
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
|
||||
|
||||
after_script:
|
||||
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover build/logs/clover.xml; fi
|
@ -6,7 +6,7 @@ All contributions are welcome to this project.
|
||||
|
||||
Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at:
|
||||
|
||||
http://opensource.box.com/cla
|
||||
https://opensource.box.com/cla
|
||||
|
||||
To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement).
|
||||
|
||||
@ -68,7 +68,21 @@ This will add your changes on top of what's already in upstream, minimizing merg
|
||||
|
||||
Make sure that all tests are passing before submitting a pull request.
|
||||
|
||||
### Step 8: Send the pull request
|
||||
### Step 8: Fix code style
|
||||
|
||||
Run the following command to check the code style of your changes:
|
||||
|
||||
```
|
||||
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff
|
||||
```
|
||||
|
||||
This will print a diff of proposed code style changes. To apply these suggestions, run the following command:
|
||||
|
||||
```
|
||||
vendor/bin/php-cs-fixer fix --config=.php_cs.dist
|
||||
```
|
||||
|
||||
### Step 9: Send the pull request
|
||||
|
||||
Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did.
|
||||
|
||||
|
294
README.md
@ -1,290 +1,64 @@
|
||||
# Spout
|
||||
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
[](http://opensource.box.com/badges)
|
||||
[](http://travis-ci.org/box/spout)
|
||||
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
||||
[](https://opensource.box.com/badges)
|
||||
[](https://github.com/box/spout/actions/workflows/ci.yml?query=branch%3Amaster)
|
||||
[](https://coveralls.io/github/box/spout?branch=master)
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
|
||||
Spout is a PHP library to read and write CSV and XLSX files, in a fast and scalable way.
|
||||
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
|
||||
## 🪦 Archived project 🪦
|
||||
|
||||
This project has been archived and is no longer maintained. No bug fix and no additional features will be added.<br>
|
||||
You won't be able to submit new issues or pull requests, and no additional features will be added
|
||||
|
||||
You can still use Spout as is in your projects though :)
|
||||
|
||||
> Thanks to everyone who contributed to this project, from a typo fix to the new cool feature.<br>
|
||||
> It was great to see the involvement of this community!
|
||||
|
||||
<br>
|
||||
|
||||
## About
|
||||
|
||||
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
|
||||
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: [](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
|
||||
## Installation
|
||||
## Documentation
|
||||
|
||||
### Composer (recommended)
|
||||
|
||||
Spout can be installed directly from [Composer](https://getcomposer.org/).
|
||||
|
||||
Add "box/spout" as a dependency in your project's composer.json file:
|
||||
```json
|
||||
"require": {
|
||||
"box/spout": "~2.0"
|
||||
}
|
||||
```
|
||||
|
||||
Then run the install command from Composer:
|
||||
```
|
||||
php composer.phar install
|
||||
```
|
||||
|
||||
### Manual installation
|
||||
|
||||
If you can't use Composer, no worries! You can still install Spout manually.
|
||||
|
||||
> Before starting, make sure your system meets the [requirements](#requirements).
|
||||
|
||||
1. Download the source code from the [Releases page](https://github.com/box/spout/releases)
|
||||
2. Extract the downloaded content into your project.
|
||||
3. Add this code to the top controller (index.php) or wherever it may be more appropriate:
|
||||
```php
|
||||
require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path!
|
||||
```
|
||||
Full documentation can be found at [https://opensource.box.com/spout/](https://opensource.box.com/spout/).
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* PHP version 5.4.0 or higher
|
||||
* PHP version 7.2 or higher
|
||||
* PHP extension `php_zip` enabled
|
||||
* PHP extension `php_xmlreader` enabled
|
||||
* PHP extension `php_simplexml` enabled
|
||||
|
||||
## Upgrade guide
|
||||
|
||||
## Basic usage
|
||||
|
||||
### Reader
|
||||
|
||||
#### How to read a CSV file?
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\ReaderFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->open($filePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($reader->getRowIterator() as $row) {
|
||||
// do stuff
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
```
|
||||
|
||||
#### How to read a XLSX file?
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\ReaderFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->open($filePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($reader->getRowIterator() as $row) {
|
||||
// do stuff
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
```
|
||||
|
||||
If there are multiple sheets in the file, the reader will read through all of them sequentially.
|
||||
|
||||
### Writer
|
||||
|
||||
### How to create a CSV file?
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::CSV);
|
||||
$writer->openToFile($filePath); // write data to a file or to a PHP stream
|
||||
$writer->addRow($singleRow); // add a row at a time
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
### How to create a XLSX file?
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->openToBrowser($fileName); // stream data directly to the browser
|
||||
$writer->addRows($multipleRows); // add multiple rows at a time
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
For XLSX files, the number of rows per sheet is limited to 1,048,576 (see [Office OpenXML specs](http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx)). By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
|
||||
|
||||
|
||||
## Advanced usage
|
||||
|
||||
### Configuring the CSV reader and writer
|
||||
|
||||
It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
|
||||
```php
|
||||
use Box\Spout\Reader\ReaderFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->setFieldDelimiter('|');
|
||||
$reader->setFieldEnclosure('@');
|
||||
```
|
||||
|
||||
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
|
||||
```php
|
||||
$reader->setEncoding('UTF-16LE');
|
||||
```
|
||||
|
||||
The writer always generate CSV files encoded in UTF-8, with a BOM.
|
||||
|
||||
### Configuring the XLSX writer
|
||||
|
||||
#### Strings storage
|
||||
|
||||
XLSX files support different ways to store the string values:
|
||||
* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored)
|
||||
* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process
|
||||
|
||||
In order to keep the memory usage really low, Spout does not optimize strings when using shared strings. It is nevertheless possible to use this mode.
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setShouldUseInlineStrings(true); // default (and recommended) value
|
||||
$writer->setShouldUseInlineStrings(false); // will use shared strings
|
||||
```
|
||||
|
||||
#### New sheet creation
|
||||
|
||||
It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
|
||||
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
|
||||
```
|
||||
|
||||
#### Using custom temporary folder
|
||||
|
||||
Processing XLSX files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by sys_get_temp_dir()). It is possible to override this by explicitly setting it on the reader or writer:
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setTempFolder($customTempFolderPath);
|
||||
```
|
||||
|
||||
### Playing with XLSX sheets
|
||||
|
||||
When creating a XLSX file, it is possible to control in which sheet the data will be written to.
|
||||
At any point, you can retrieve the current sheet and set the current sheet:
|
||||
```php
|
||||
$firstSheet = $writer->getCurrentSheet();
|
||||
$writer->addRow($rowForSheet1); // writes the row to the first sheet
|
||||
|
||||
$newSheet = $writer->addNewSheetAndMakeItCurrent();
|
||||
$writer->addRow($rowForSheet2); // writes the row to the new sheet
|
||||
|
||||
$writer->setCurrentSheet($firstSheet);
|
||||
$writer->addRow($anotherRowForSheet1); // append the row to the first sheet
|
||||
```
|
||||
|
||||
It is also possible to retrieve all the sheets currently created:
|
||||
```php
|
||||
$sheets = $writer->getSheets();
|
||||
```
|
||||
|
||||
If you rely on the sheet's name in your application, you can access it and customize it this way:
|
||||
```php
|
||||
// Accessing the sheet name when reading
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
$sheetName = $sheet->getName();
|
||||
}
|
||||
|
||||
// Accessing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheetName = $sheet->getName();
|
||||
|
||||
// Customizing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheetName = $sheet->setName('My custom name');
|
||||
```
|
||||
|
||||
### Fluent interface
|
||||
|
||||
Because fluent interfaces are great, you can use them with Spout:
|
||||
```php
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setTempFolder($customTempFolderPath)
|
||||
->setShouldUseInlineStrings(true)
|
||||
->openToFile($filePath)
|
||||
->addRow($headerRow)
|
||||
->addRows($dataRows)
|
||||
->close();
|
||||
```
|
||||
|
||||
Version 3 introduced new functionality but also some breaking changes. If you want to upgrade your Spout codebase from version 2 please consult the [Upgrade guide](UPGRADE-3.0.md).
|
||||
|
||||
## Running tests
|
||||
|
||||
On the `master` branch, only unit and functional tests are included. The performance requires very large files and have been excluded.
|
||||
If you just want to check that everything is working as expected, executing the tests of the master branch is enough.
|
||||
The `master` branch includes unit, functional and performance tests.
|
||||
If you just want to check that everything is working as expected, executing the unit and functional tests is enough.
|
||||
|
||||
If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output:
|
||||
|
||||
* `phpunit` - runs the whole test suite (unit + functional + performance tests)
|
||||
* `phpunit --testuite no-perf-tests` - only runs the unit and functional tests
|
||||
* `phpunit --testuite perf-tests` - only runs the performance tests
|
||||
|
||||
For information, the performance tests take about one hour to run (processing 2 million rows files is not a quick thing).
|
||||
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
#### How can Spout handle such large data sets and still use less than 10MB of memory?
|
||||
|
||||
When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
|
||||
|
||||
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access.
|
||||
|
||||
#### How long does it take to generate a file with X rows?
|
||||
|
||||
Here are a few numbers regarding the performance of Spout:
|
||||
|
||||
| | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
|
||||
| :------------------------------- | :----------------------: | :--------------------------: | :------------------------------: |
|
||||
| Read CSV | < 1 second | 4 seconds | 2-3 minutes |
|
||||
| Write CSV | < 1 second | 2 seconds | 2-3 minutes |
|
||||
| Read XLSX (using inline strings) | < 1 second | 35-40 seconds | 18-20 minutes |
|
||||
| Read XLSX (using shared strings) | 1 second | 1-2 minutes | 35-40 minutes |
|
||||
| Write XLSX | 1 second | 20-25 seconds | 8-10 minutes |
|
||||
|
||||
#### Does Spout support charts or formulas?
|
||||
|
||||
No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used.
|
||||
So the larger the file would be, the more memory would be consumed, preventing your code to scale well.
|
||||
* `phpunit` - runs unit and functional tests
|
||||
* `phpunit --group perf-tests` - only runs the performance tests
|
||||
|
||||
For information, the performance tests take about 10 minutes to run (processing 1 million rows files is not a quick thing).
|
||||
|
||||
## Support
|
||||
|
||||
Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject.
|
||||
|
||||
Spout is no longer actively supported. You can still ask questions, or discuss about it in the chat room:<br>
|
||||
[](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
## Copyright and License
|
||||
|
||||
Copyright 2015 Box, Inc. All rights reserved.
|
||||
Copyright 2022 Box, Inc. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
89
UPGRADE-3.0.md
Normal file
@ -0,0 +1,89 @@
|
||||
Upgrading from 2.x to 3.0
|
||||
=========================
|
||||
|
||||
Spout 3.0 introduced several backwards-incompatible changes. The upgrade from Spout 2.x to 3.0 must therefore be done with caution.
|
||||
This guide is meant to ease this process.
|
||||
|
||||
Most notable changes
|
||||
--------------------
|
||||
In 2.x, styles were applied per row; it was therefore impossible to apply different styles to cells in the same row.
|
||||
With the 3.0 version, this is now possible: each cell can have its own style.
|
||||
|
||||
Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
|
||||
|
||||
Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community.
|
||||
|
||||
Reader changes
|
||||
--------------
|
||||
Creating a reader should now be done through the Reader `ReaderEntityFactory`, instead of using the `ReaderFactory`.
|
||||
Also, the `ReaderFactory::create($type)` method was removed and replaced by methods for each reader:
|
||||
```php
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory; // namespace is no longer "Box\Spout\Reader"
|
||||
...
|
||||
$reader = ReaderEntityFactory::createXLSXReader(); // replaces ReaderFactory::create(Type::XLSX)
|
||||
$reader = ReaderEntityFactory::createCSVReader(); // replaces ReaderFactory::create(Type::CSV)
|
||||
$reader = ReaderEntityFactory::createODSReader(); // replaces ReaderFactory::create(Type::ODS)
|
||||
```
|
||||
|
||||
When iterating over the spreadsheet rows, Spout now returns `Row` objects, instead of an array containing row values. Accessing the row values should now be done this way:
|
||||
```php
|
||||
...
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($sheet->getRowIterator() as $row) { // $row is a "Row" object, not an array
|
||||
$rowAsArray = $row->toArray(); // this is the 2.x equivalent
|
||||
// OR
|
||||
$cellsArray = $row->getCells(); // this can be used to get access to cells' details
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Writer changes
|
||||
--------------
|
||||
Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`, instead of using the `WriterFactory`.
|
||||
Also, the `WriterFactory::create($type)` method was removed and replaced by methods for each writer:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory; // namespace is no longer "Box\Spout\Writer"
|
||||
...
|
||||
$writer = WriterEntityFactory::createXLSXWriter(); // replaces WriterFactory::create(Type::XLSX)
|
||||
$writer = WriterEntityFactory::createCSVWriter(); // replaces WriterFactory::create(Type::CSV)
|
||||
$writer = WriterEntityFactory::createODSWriter(); // replaces WriterFactory::create(Type::ODS)
|
||||
```
|
||||
|
||||
Adding rows is also done differently: instead of passing an array, the writer now takes in a `Row` object (or an array of `Row`). Creating such objects can easily be done this way:
|
||||
```php
|
||||
// Adding a row from an array of values (2.x equivalent)
|
||||
$cellValues = ['foo', 12345];
|
||||
$row1 = WriterEntityFactory::createRowFromArray($cellValues, $rowStyle);
|
||||
|
||||
// Adding a row from an array of Cell
|
||||
$cell1 = WriterEntityFactory::createCell('foo', $cellStyle1); // this cell has its own style
|
||||
$cell2 = WriterEntityFactory::createCell(12345, $cellStyle2); // this cell has its own style
|
||||
$row2 = WriterEntityFactory::createRow([$cell1, $cell2]);
|
||||
|
||||
$writer->addRows([$row1, $row2]);
|
||||
```
|
||||
|
||||
Namespace changes for styles
|
||||
-----------------
|
||||
The namespaces for styles have changed. Styles are still created by using a `builder` class.
|
||||
|
||||
For the builder, please update your import statements to use the following namespaces:
|
||||
|
||||
Box\Spout\Writer\Common\Creator\Style\StyleBuilder
|
||||
Box\Spout\Writer\Common\Creator\Style\BorderBuilder
|
||||
|
||||
The `Style` base class and style definitions like `Border`, `BorderPart` and `Color` also have a new namespace.
|
||||
|
||||
If your are using these classes directly via an import statement in your code, please use the following namespaces:
|
||||
|
||||
Box\Spout\Common\Entity\Style\Border
|
||||
Box\Spout\Common\Entity\Style\BorderPart
|
||||
Box\Spout\Common\Entity\Style\Color
|
||||
Box\Spout\Common\Entity\Style\Style
|
||||
|
||||
Handling of empty rows
|
||||
----------------------
|
||||
In 2.x, empty rows were not added to the spreadsheet.
|
||||
In 3.0, `addRow` now always writes a row to the spreadsheet: when the row does not contain any cells, an empty row is created in the sheet.
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "box/spout",
|
||||
"description": "PHP Library to read and write CSV and XLSX files, in a fast and scalable way",
|
||||
"description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
|
||||
"type": "library",
|
||||
"keywords": ["php","read","write","csv","xlsx","excel","spreadsheet","scale","memory","stream","ooxml"],
|
||||
"keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"],
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://www.github.com/box/spout",
|
||||
"authors": [
|
||||
@ -12,14 +12,15 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"php": ">=7.3.0",
|
||||
"ext-zip": "*",
|
||||
"ext-xmlreader" : "*",
|
||||
"ext-simplexml": "*"
|
||||
"ext-xmlreader": "*",
|
||||
"ext-dom": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": ">=3.7",
|
||||
"scrutinizer/ocular": "~1.1"
|
||||
"phpunit/phpunit": "^9",
|
||||
"friendsofphp/php-cs-fixer": "^3",
|
||||
"phpstan/phpstan": "^1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
|
||||
@ -29,5 +30,15 @@
|
||||
"psr-4": {
|
||||
"Box\\Spout\\": "src/Spout"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.1.x-dev"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3937
composer.lock
generated
9
docs/.gitignore
vendored
Executable file
@ -0,0 +1,9 @@
|
||||
/.idea
|
||||
*.iml
|
||||
|
||||
/tests/resources/generated
|
||||
/tests/coverage
|
||||
/vendor
|
||||
|
||||
/.sass-cache
|
||||
/_site
|
17
docs/README.md
Executable file
@ -0,0 +1,17 @@
|
||||
# Spout Documentation
|
||||
|
||||
## Quickstart
|
||||
|
||||
1. Install `jekyll`: `sudo gem install jekyll`
|
||||
2. Run the site locally: `jekyll serve`
|
||||
|
||||
## Usage with Docker and Docker Compose
|
||||
|
||||
* Install Docker and Docker Compose
|
||||
* Run ```docker-compose up```
|
||||
* Point your browser at [http://127.0.0.1:8080/](http://127.0.0.1:8080/)
|
||||
|
||||
## Editing the documentation
|
||||
|
||||
* All documents relevant for the documentation are located in the ```_pages``` folder.
|
||||
|
37
docs/_config.yml
Executable file
@ -0,0 +1,37 @@
|
||||
# Welcome to Jekyll!
|
||||
#
|
||||
# This config file is meant for settings that affect your whole blog, values
|
||||
# which you are expected to set up once and rarely need to edit after that.
|
||||
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||
# 'jekyll serve'. If you change this file, please restart the server process.
|
||||
|
||||
# Site settings
|
||||
title: Spout
|
||||
email: oss@box.com
|
||||
description: "An open source PHP library to read and write spreadsheet files (XLSX, ODS and CSV), in a fast and scalable way."
|
||||
baseurl: "" # the subpath of your site, e.g. /blog
|
||||
url: "https://opensource.box.com" # the base hostname & protocol for your site
|
||||
|
||||
# Build settings
|
||||
markdown: kramdown
|
||||
highlighter: rouge
|
||||
kramdown:
|
||||
input: GFM
|
||||
syntax_highlighter: rouge
|
||||
|
||||
exclude: ["README.md"]
|
||||
collections:
|
||||
pages:
|
||||
output: true
|
||||
sections:
|
||||
output: true
|
||||
|
||||
# 3rd parties
|
||||
#google_analytics:
|
||||
algolia:
|
||||
enabled: false
|
||||
# apiKey:
|
||||
# indexName:
|
||||
|
||||
# Misc
|
||||
spout_html: <span class="spout">Spout</span>
|
7
docs/_config_local.yml
Normal file
@ -0,0 +1,7 @@
|
||||
# Github Metadata plugin
|
||||
repository: box/spout
|
||||
plugins:
|
||||
- "jekyll-github-metadata"
|
||||
|
||||
github:
|
||||
url: http://localhost:8080
|
9
docs/_includes/algolia.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% if site.algolia.enabled %}
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/docsearch.js/1/docsearch.min.js"></script>
|
||||
<script type="text/javascript"> docsearch({
|
||||
apiKey: '{{ site.algolia.apiKey }}',
|
||||
indexName: '{{ site.algolia.indexName }}',
|
||||
inputSelector: '#algolia-doc-search'
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
12
docs/_includes/analytics.html
Executable file
@ -0,0 +1,12 @@
|
||||
{% if site.google_analytics %}
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{ site.google_analytics }}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
||||
{% endif %}
|
9
docs/_includes/banner.html
Executable file
@ -0,0 +1,9 @@
|
||||
<div class="site-banner">
|
||||
<div class="wrapper vertical-align-middle">
|
||||
<p class="tag-line">
|
||||
Read and write spreadsheets
|
||||
<br />
|
||||
<strong>quickly</strong> and <strong>at scale</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
9
docs/_includes/base.html
Executable file
@ -0,0 +1,9 @@
|
||||
<!-- http://ricostacruz.com/til/relative-paths-in-jekyll.html -->
|
||||
<!-- _includes/base.html -->
|
||||
|
||||
{% assign base = '' %}
|
||||
{% assign depth = page.url | split: '/' | size | minus: 1 %}
|
||||
{% if depth == 1 %}{% assign base = '.' %}
|
||||
{% elsif depth == 2 %}{% assign base = '..' %}
|
||||
{% elsif depth == 3 %}{% assign base = '../..' %}
|
||||
{% elsif depth == 4 %}{% assign base = '../../..' %}{% endif %}
|
25
docs/_includes/footer.html
Executable file
@ -0,0 +1,25 @@
|
||||
<footer class="site-footer">
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
<div class="footer-col-wrapper">
|
||||
<div class="footer-col footer-col-1">
|
||||
<ul class="contact-list">
|
||||
<li>Need to contact us?<br><a href="https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/box/spout.svg" alt="Join the chat at https://gitter.im/box/spout" /></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-col footer-col-2">
|
||||
<ul class="social-media-list">
|
||||
<li><a href="{{ site.github.issues_url }}"><span class="icon icon--github">{% include icon-github.svg %}</span><span class="username">GitHub Issues</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-col footer-col-3">
|
||||
<p>{{ site.title }} - {{ site.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</footer>
|
17
docs/_includes/head.html
Executable file
@ -0,0 +1,17 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>{% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title>
|
||||
<meta name="description" content="{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines | truncate: 160 }}{% else %}{{ site.description }}{% endif %}">
|
||||
|
||||
<link rel="stylesheet" href="{{ "/css/main.css" | prepend: site_url }}">
|
||||
{% if site.algolia.enabled %}
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/docsearch.js/1/docsearch.min.css" />
|
||||
{% endif %}
|
||||
|
||||
{% if jekyll.environment == 'production' %}
|
||||
{% include analytics.html %}
|
||||
{% endif %}
|
||||
</head>
|
33
docs/_includes/header.html
Executable file
@ -0,0 +1,33 @@
|
||||
<header class="site-header">
|
||||
|
||||
<div class="wrapper">
|
||||
<a class="site-title" href="{{ site_url }}/">{{ site.title }}</a>
|
||||
|
||||
<nav class="site-nav">
|
||||
<a href="#" class="menu-icon">
|
||||
<svg viewBox="0 0 18 15">
|
||||
<path fill="#424242" d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z"/>
|
||||
<path fill="#424242" d="M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z"/>
|
||||
<path fill="#424242" d="M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="trigger">
|
||||
<a class="page-link" href="{{ site_url }}/getting-started/">Getting Started</a>
|
||||
<a class="page-link" href="{{ site_url }}/docs/">Documentation</a>
|
||||
<a class="page-link" href="{{ site_url }}/guides/">Guides</a>
|
||||
<a class="page-link" href="{{ site_url }}/faq/">FAQ</a>
|
||||
<a class="page-link" href="{{ site.github.repository_url }}">
|
||||
<span class="icon icon--github">{% include icon-github.svg %}</span>
|
||||
<span class="username">GitHub</span>
|
||||
</a>
|
||||
{% if site.algolia.enabled %}
|
||||
<span class="site-search">
|
||||
<input type="text" id="algolia-doc-search">
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
1
docs/_includes/icon-github.html
Executable file
@ -0,0 +1 @@
|
||||
<a href="https://github.com/{{ include.username }}"><span class="icon icon--github">{% include icon-github.svg %}</span><span class="username">{{ include.username }}</span></a>
|
1
docs/_includes/icon-github.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 16 16"><path fill="#828282" d="M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388,0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159,0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481,0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258,0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847,0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209,2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943,0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497,0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713,0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028,5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z"/></svg>
|
After Width: | Height: | Size: 926 B |
11
docs/_includes/section-fast-and-scalable.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% assign sectionClass = "section-even centered" %}
|
||||
{% assign sectionTitle = "Fast and Scalable" %}
|
||||
{% assign sectionIcons = "icon-lightning-bolt.png" %}
|
||||
|
||||
{% capture sectionContent %}
|
||||
Reading a small CSV file? No problem!<br>
|
||||
Reading a huge XLSX file? No extra code needed!<br>
|
||||
Writing an ODS file with millions of rows? {{ site.spout_html }} can do it in no time!
|
||||
{% endcapture %}
|
||||
|
||||
{% include section.html site_url = include.site_url %}
|
14
docs/_includes/section-supported-spreadsheet-types.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% assign sectionClass = "section-odd centered" %}
|
||||
{% assign sectionTitle = "Supported Spreadsheet Types" %}
|
||||
{% assign sectionIcons = "icon-xlsx.png~~icon-ods.png~~icon-csv.png" %}
|
||||
|
||||
{% assign emS = '<span class="light-em">' %}
|
||||
{% assign emE = '</span>' %}
|
||||
|
||||
{% capture sectionContent %}
|
||||
{{ site.spout_html }} supports 3 types of spreadsheets: {{emS}}XLSX{{emE}}, {{emS}}ODS{{emE}} and {{emS}}CSV{{emE}}.<br>
|
||||
{{ site.spout_html }} provides a simple and unified API to read or create these different types of spreadsheets.
|
||||
Switching from one type to another is ridiculously easy!
|
||||
{% endcapture %}
|
||||
|
||||
{% include section.html site_url=include.site_url %}
|
21
docs/_includes/section-why-use-spout.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% assign sectionClass = "section-odd last-section" %}
|
||||
{% capture sectionTitle %}Why use {{ site.spout_html }}?{% endcapture %}
|
||||
{% assign sectionIcons = "" %}
|
||||
|
||||
{% assign emS = '<span class="light-em">' %}
|
||||
{% assign emE = '</span>' %}
|
||||
|
||||
{% capture sectionContent %}
|
||||
<ul class="feature-list">
|
||||
<li class="feature-check">{{ site.spout_html }} is capable of processing files of {{emS}}any size{{emE}}.</li>
|
||||
<li class="feature-check">{{ site.spout_html }} needs {{emS}}only 3MB{{emE}} of memory to process any file.</li>
|
||||
<li class="feature-check">{{ site.spout_html }}'s streaming mechanism makes it {{emS}}incredibly fast{{emE}}.</li>
|
||||
<li class="feature-check">{{ site.spout_html }}'s API is {{emS}}developer-friendly{{emE}}.</li>
|
||||
</ul><br>
|
||||
<div class="btn-wrapper">
|
||||
<a href="{{ include.site_url }}/getting-started/" class="page-link"><button class="btn">Get started</button></a>
|
||||
</div>
|
||||
|
||||
{% endcapture %}
|
||||
|
||||
{% include section.html site_url=include.site_url %}
|
16
docs/_includes/section.html
Executable file
@ -0,0 +1,16 @@
|
||||
<div class="section {{ sectionClass }} vertical-align-middle">
|
||||
<div class="wrapper">
|
||||
<div class="description mbl">
|
||||
<h2 class="section-title">{{ sectionTitle }}</h2>
|
||||
{{ sectionContent }}
|
||||
</div>
|
||||
{% if sectionIcons %}
|
||||
{% assign sectionIconsAsArray = sectionIcons | split:'~~' %}
|
||||
<div class="icons">
|
||||
{% for icon in sectionIconsAsArray %}
|
||||
<img src="{{ include.site_url }}/images/{{ icon }}"/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
12
docs/_includes/set-global-site-url.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% comment %}
|
||||
This is a hack to force using HTTPS...
|
||||
The URL of the docs should be changed in the repo settings instead
|
||||
{% endcomment %}
|
||||
{% assign protocol_scheme = page.url | absolute_url | truncate: 5, "" %}
|
||||
{%- capture site_url -%}
|
||||
{%- if protocol_scheme == "https" -%}
|
||||
{{ site.github.url | replace: "http://", "https://" }}
|
||||
{%- else -%}
|
||||
{{ site.github.url }}
|
||||
{%- endif -%}
|
||||
{%- endcapture -%}
|
23
docs/_layouts/default.html
Executable file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
{% include set-global-site-url.html %}
|
||||
|
||||
{% include head.html %}
|
||||
|
||||
<body>
|
||||
|
||||
{% include header.html %}
|
||||
|
||||
{% if page.banner %}
|
||||
{% include banner.html %}
|
||||
{% endif %}
|
||||
|
||||
{{ content }}
|
||||
|
||||
{% include footer.html %}
|
||||
{% include algolia.html %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
40
docs/_layouts/doc.html
Executable file
@ -0,0 +1,40 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.slim.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/anchor-js/4.1.0/anchor.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
anchors.add();
|
||||
|
||||
$('.post-title, .doc-content h1, .doc-content h2, .doc-content h3').each(function(index, element) {
|
||||
var el = $(element);
|
||||
var title = el.text();
|
||||
var id = el.attr('id');
|
||||
var tag = el.prop('tagName');
|
||||
var titleEl = $('<div>').append(
|
||||
$('<a>')
|
||||
.attr('href', '#' + id)
|
||||
.addClass(tag.toLowerCase())
|
||||
.text(title));
|
||||
$('.table-of-content').append(titleEl);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="table-of-content" data-spy="affix" data-offset-top="40" data-offset-bottom="300">
|
||||
</div>
|
||||
|
||||
<article class="post">
|
||||
<div class="wrapper">
|
||||
<header class="post-header">
|
||||
<h1 class="post-title">{{ page.title }}</h1>
|
||||
</header>
|
||||
<div class="post-content">
|
||||
<div class="doc-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
15
docs/_layouts/page.html
Executable file
@ -0,0 +1,15 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<article class="post">
|
||||
<div class="wrapper">
|
||||
<header class="post-header">
|
||||
<h1 class="post-title">{{ page.title }}</h1>
|
||||
</header>
|
||||
<div class="post-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
323
docs/_pages/documentation.md
Executable file
@ -0,0 +1,323 @@
|
||||
---
|
||||
layout: doc
|
||||
title: Documentation
|
||||
permalink: /docs/
|
||||
---
|
||||
|
||||
## Configuration for CSV
|
||||
|
||||
It is possible to configure both the CSV reader and writer to adapt them to your requirements:
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
|
||||
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.csv');
|
||||
/** All of these methods have to be called before opening the reader. */
|
||||
$reader->setFieldDelimiter('|');
|
||||
$reader->setFieldEnclosure('@');
|
||||
|
||||
```
|
||||
|
||||
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
|
||||
|
||||
```php
|
||||
$reader->setEncoding('UTF-16LE');
|
||||
```
|
||||
|
||||
By default, the writer generates CSV files encoded in UTF-8, with a BOM.
|
||||
It is however possible to not include the BOM:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$writer = WriterEntityFactory::createWriterFromFile('/path/to/file.csv');
|
||||
$writer->setShouldAddBOM(false);
|
||||
```
|
||||
|
||||
|
||||
## Configuration for XLSX and ODS
|
||||
|
||||
### New sheet creation
|
||||
|
||||
It is possible to change the behavior of the writers when the maximum number of rows (*1,048,576*) has been written in the current sheet. By default, a new sheet is automatically created so that writing can keep going but that may not always be preferable.
|
||||
|
||||
```php
|
||||
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$writer = WriterEntityFactory::createODSWriter();
|
||||
$writer->setShouldCreateNewSheetsAutomatically(true); // default value
|
||||
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
|
||||
```
|
||||
|
||||
### Using a custom temporary folder
|
||||
|
||||
Processing XLSX and ODS files requires temporary files to be created. By default, {{ site.spout_html }} will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
$writer->setTempFolder($customTempFolderPath);
|
||||
```
|
||||
|
||||
### Strings storage (XLSX writer)
|
||||
|
||||
XLSX files support different ways to store the string values:
|
||||
* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored)
|
||||
* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process
|
||||
|
||||
In order to keep the memory usage really low, {{ site.spout_html }} does not de-duplicate strings when using shared strings. It is nevertheless possible to use this mode.
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
$writer->setShouldUseInlineStrings(true); // default (and recommended) value
|
||||
$writer->setShouldUseInlineStrings(false); // will use shared strings
|
||||
```
|
||||
|
||||
> #### Note on Apple Numbers and iOS support
|
||||
>
|
||||
> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings!
|
||||
|
||||
### Date/Time formatting
|
||||
|
||||
When reading a spreadsheet containing dates or times, {{ site.spout_html }} returns the values by default as `DateTime` objects.
|
||||
It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet.
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
|
||||
$reader = ReaderEntityFactory::createXLSXReader();
|
||||
$reader->setShouldFormatDates(false); // default value
|
||||
$reader->setShouldFormatDates(true); // will return formatted dates
|
||||
```
|
||||
|
||||
|
||||
## Empty rows
|
||||
|
||||
By default, when {{ site.spout_html }} reads a spreadsheet it skips empty rows and only return rows containing data.
|
||||
This behavior can be changed so that {{ site.spout_html }} returns all rows:
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
|
||||
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.ext');
|
||||
$reader->setShouldPreserveEmptyRows(true);
|
||||
```
|
||||
|
||||
|
||||
## Styling
|
||||
|
||||
### Available styles
|
||||
|
||||
{{ site.spout_html }} supports styling at a row and cell level. It is possible to customize the fonts, backgrounds, alignment as well as borders.
|
||||
|
||||
For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones:
|
||||
|
||||
| Category | Property | API
|
||||
|:---------------------|:---------------|:--------------------------------------
|
||||
| Font | Bold | `StyleBuilder::setFontBold()`
|
||||
| | Italic | `StyleBuilder::setFontItalic()`
|
||||
| | Underline | `StyleBuilder::setFontUnderline()`
|
||||
| | Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
||||
| | Font name | `StyleBuilder::setFontName('Arial')`
|
||||
| | Font size | `StyleBuilder::setFontSize(14)`
|
||||
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
|
||||
| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)`
|
||||
| | Wrap text | `StyleBuilder::setShouldWrapText(true)`
|
||||
| Format _(XLSX only)_ | Number format | `StyleBuilder::setFormat('0.000')`
|
||||
| | Date format | `StyleBuilder::setFormat('m/d/yy h:mm')`
|
||||
|
||||
### Styling rows
|
||||
|
||||
It is possible to apply some formatting options to a row. In this case, all cells of the row will have the same style:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
|
||||
use Box\Spout\Common\Entity\Style\CellAlignment;
|
||||
use Box\Spout\Common\Entity\Style\Color;
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
$writer->openToFile($filePath);
|
||||
|
||||
/** Create a style with the StyleBuilder */
|
||||
$style = (new StyleBuilder())
|
||||
->setFontBold()
|
||||
->setFontSize(15)
|
||||
->setFontColor(Color::BLUE)
|
||||
->setShouldWrapText()
|
||||
->setCellAlignment(CellAlignment::RIGHT)
|
||||
->setBackgroundColor(Color::YELLOW)
|
||||
->build();
|
||||
|
||||
/** Create a row with cells and apply the style to all cells */
|
||||
$row = WriterEntityFactory::createRowFromArray(['Carl', 'is', 'great'], $style);
|
||||
|
||||
/** Add the row to the writer */
|
||||
$writer->addRow($row);
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
Adding borders to a row requires a ```Border``` object.
|
||||
|
||||
```php
|
||||
use Box\Spout\Common\Entity\Style\Border;
|
||||
use Box\Spout\Writer\Common\Creator\Style\BorderBuilder;
|
||||
use Box\Spout\Common\Entity\Style\Color;
|
||||
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$border = (new BorderBuilder())
|
||||
->setBorderBottom(Color::GREEN, Border::WIDTH_THIN, Border::STYLE_DASHED)
|
||||
->build();
|
||||
|
||||
$style = (new StyleBuilder())
|
||||
->setBorder($border)
|
||||
->build();
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
$writer->openToFile($filePath);
|
||||
|
||||
$cells = WriterEntityFactory::createCell('Border Bottom Green Thin Dashed');
|
||||
$row = WriterEntityFactory::createRow($cells);
|
||||
$row->setStyle($style);
|
||||
$writer->addRow($row);
|
||||
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
### Styling cells
|
||||
|
||||
The same styling techniques as described in [Styling rows](#styling-rows) can be applied to individual cells of a row as well.
|
||||
|
||||
Cell styles are inherited from the parent row and the default row style respectively.
|
||||
|
||||
The styles applied to a specific cell will override any parent styles if present.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
use Box\Spout\Common\Entity\Style\Color;
|
||||
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$defaultStyle = (new StyleBuilder())
|
||||
->setFontSize(8)
|
||||
->build();
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
$writer->setDefaultRowStyle($defaultStyle)
|
||||
->openToFile($filePath);
|
||||
|
||||
$zebraBlackStyle = (new StyleBuilder())
|
||||
->setBackgroundColor(Color::BLACK)
|
||||
->setFontColor(Color::WHITE)
|
||||
->setFontSize(10)
|
||||
->build();
|
||||
|
||||
$zebraWhiteStyle = (new StyleBuilder())
|
||||
->setBackgroundColor(Color::WHITE)
|
||||
->setFontColor(Color::BLACK)
|
||||
->setFontItalic()
|
||||
->build();
|
||||
|
||||
$cells = [
|
||||
WriterEntityFactory::createCell('Ze', $zebraBlackStyle),
|
||||
WriterEntityFactory::createCell('bra', $zebraWhiteStyle)
|
||||
];
|
||||
|
||||
$rowStyle = (new StyleBuilder())
|
||||
->setFontBold()
|
||||
->build();
|
||||
|
||||
$row = WriterEntityFactory::createRow($cells, $rowStyle);
|
||||
|
||||
$writer->addRow($row);
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
### Default style
|
||||
|
||||
{{ site.spout_html }} will use a default style for all created rows. This style can be overridden this way:
|
||||
|
||||
```php
|
||||
$defaultStyle = (new StyleBuilder())
|
||||
->setFontName('Arial')
|
||||
->setFontSize(11)
|
||||
->build();
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
$writer->setDefaultRowStyle($defaultStyle)
|
||||
->openToFile($filePath);
|
||||
```
|
||||
|
||||
|
||||
## Playing with sheets
|
||||
|
||||
When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet:
|
||||
|
||||
```php
|
||||
$firstSheet = $writer->getCurrentSheet();
|
||||
$writer->addRow($rowForSheet1); // writes the row to the first sheet
|
||||
|
||||
$newSheet = $writer->addNewSheetAndMakeItCurrent();
|
||||
$writer->addRow($rowForSheet2); // writes the row to the new sheet
|
||||
|
||||
$writer->setCurrentSheet($firstSheet);
|
||||
$writer->addRow($anotherRowForSheet1); // append the row to the first sheet
|
||||
```
|
||||
|
||||
It is also possible to retrieve all the sheets currently created:
|
||||
```php
|
||||
$sheets = $writer->getSheets();
|
||||
```
|
||||
|
||||
It is possible to retrieve some sheet's attributes when reading:
|
||||
```php
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
$sheetName = $sheet->getName();
|
||||
$isSheetVisible = $sheet->isVisible();
|
||||
$isSheetActive = $sheet->isActive(); // active sheet when spreadsheet last saved
|
||||
}
|
||||
```
|
||||
|
||||
If you rely on the sheet's name in your application, you can customize it this way:
|
||||
|
||||
```php
|
||||
// Accessing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheetName = $sheet->getName();
|
||||
|
||||
// Customizing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheet->setName('My custom name');
|
||||
```
|
||||
|
||||
> Please note that Excel has some restrictions on the sheet's name:
|
||||
> * it must not be blank
|
||||
> * it must not exceed 31 characters
|
||||
> * it must not contain these characters: \ / ? * : [ or ]
|
||||
> * it must not start or end with a single quote
|
||||
> * it must be unique
|
||||
>
|
||||
> Handling these restrictions is the developer's responsibility. {{ site.spout_html }} does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in.
|
||||
|
||||
|
||||
## Fluent interface
|
||||
|
||||
Because fluent interfaces are great, you can use them with {{ site.spout_html }}:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\WriterEntityFactory;
|
||||
|
||||
$writer = WriterEntityFactory::createWriterFromFile('path/to/file.ext');
|
||||
$writer->setTempFolder($customTempFolderPath)
|
||||
->setShouldUseInlineStrings(true)
|
||||
->openToFile($filePath)
|
||||
->addRow($headerRow)
|
||||
->addRows($dataRows)
|
||||
->close();
|
||||
```
|
30
docs/_pages/faq.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
layout: page
|
||||
title: Frequently Asked Questions
|
||||
permalink: /faq/
|
||||
---
|
||||
|
||||
### How can {{ site.spout_html }} handle such large data sets and still use less than 3MB of memory?
|
||||
|
||||
When writing data, {{ site.spout_html }} is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
|
||||
|
||||
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access.
|
||||
|
||||
### How long does it take to generate a file with X rows?
|
||||
|
||||
Here are a few numbers regarding the performance of {{ site.spout_html }}:
|
||||
|
||||
| Type | Action | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) |
|
||||
|------|-------------------------------|--------------------------|------------------------------|----------------------------------|
|
||||
| CSV | Read | < 1 second | 4 seconds | 2-3 minutes |
|
||||
| | Write | < 1 second | 2 seconds | 2-3 minutes |
|
||||
| XLSX | Read<br>*inline strings* | < 1 second | 35-40 seconds | 18-20 minutes |
|
||||
| | Read<br>*shared strings* | 1 second | 1-2 minutes | 35-40 minutes |
|
||||
| | Write | 1 second | 20-25 seconds | 8-10 minutes |
|
||||
| ODS | Read | 1 second | 1-2 minutes | 5-6 minutes |
|
||||
| | Write | < 1 second | 35-40 seconds | 5-6 minutes |
|
||||
|
||||
### Does {{ site.spout_html }} support charts or formulas?
|
||||
|
||||
No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used.
|
||||
So the larger the file would be, the more memory would be consumed, preventing your code to scale well.
|
139
docs/_pages/getting-started.md
Executable file
@ -0,0 +1,139 @@
|
||||
---
|
||||
layout: doc
|
||||
title: Getting Started
|
||||
permalink: /getting-started/
|
||||
---
|
||||
|
||||
{% include set-global-site-url.html %}
|
||||
|
||||
This guide will help you install {{ site.spout_html }} and teach you how to use it.
|
||||
|
||||
## Requirements
|
||||
|
||||
* PHP version 7.2 or higher
|
||||
* PHP extension `ext-zip` enabled
|
||||
* PHP extension `ext-xmlreader` enabled
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### Composer (recommended)
|
||||
|
||||
{{ site.spout_html }} can be installed directly from [Composer](https://getcomposer.org/).
|
||||
|
||||
Run the following command:
|
||||
```powershell
|
||||
$ composer require box/spout
|
||||
```
|
||||
|
||||
### Manual installation
|
||||
|
||||
If you can't use Composer, no worries! You can still install {{ site.spout_html }} manually.
|
||||
|
||||
> Before starting, make sure your system meets the [requirements](#requirements).
|
||||
|
||||
1. Download the source code from the [Releases page](https://github.com/box/spout/releases)
|
||||
2. Extract the downloaded content into your project.
|
||||
3. Add this code to the top controller (e.g. index.php) or wherever it may be more appropriate:
|
||||
|
||||
```php
|
||||
// don't forget to change the path!
|
||||
require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php';
|
||||
```
|
||||
|
||||
|
||||
## Basic usage
|
||||
|
||||
### Reader
|
||||
|
||||
Regardless of the file type, the interface to read a file is always the same:
|
||||
|
||||
```php
|
||||
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
|
||||
$reader = ReaderEntityFactory::createReaderFromFile('/path/to/file.ext');
|
||||
|
||||
$reader->open($filePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
// do stuff with the row
|
||||
$cells = $row->getCells();
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
```
|
||||
|
||||
If there are multiple sheets in the file, the reader will read all of them sequentially.
|
||||
|
||||
|
||||
Note that {{ site.spout_html }} guesses the reader type based on the file extension. If the extension is not standard (`.csv`, `.ods`, `.xlsx` _- lower/uppercase_), a specific reader can be created directly:
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
|
||||
$reader = ReaderEntityFactory::createXLSXReader();
|
||||
// $reader = ReaderEntityFactory::createODSReader();
|
||||
// $reader = ReaderEntityFactory::createCSVReader();
|
||||
```
|
||||
|
||||
### Writer
|
||||
|
||||
As with the reader, there is one common interface to write data to a file:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
use Box\Spout\Common\Entity\Row;
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
// $writer = WriterEntityFactory::createODSWriter();
|
||||
// $writer = WriterEntityFactory::createCSVWriter();
|
||||
|
||||
$writer->openToFile($filePath); // write data to a file or to a PHP stream
|
||||
//$writer->openToBrowser($fileName); // stream data directly to the browser
|
||||
|
||||
$cells = [
|
||||
WriterEntityFactory::createCell('Carl'),
|
||||
WriterEntityFactory::createCell('is'),
|
||||
WriterEntityFactory::createCell('great!'),
|
||||
];
|
||||
|
||||
/** add a row at a time */
|
||||
$singleRow = WriterEntityFactory::createRow($cells);
|
||||
$writer->addRow($singleRow);
|
||||
|
||||
/** add multiple rows at a time */
|
||||
$multipleRows = [
|
||||
WriterEntityFactory::createRow($cells),
|
||||
WriterEntityFactory::createRow($cells),
|
||||
];
|
||||
$writer->addRows($multipleRows);
|
||||
|
||||
/** Shortcut: add a row from an array of values */
|
||||
$values = ['Carl', 'is', 'great!'];
|
||||
$rowFromValues = WriterEntityFactory::createRowFromArray($values);
|
||||
$writer->addRow($rowFromValues);
|
||||
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
Similar to the reader, if the file extension of the file to be written is not standard, specific writers can be created this way:
|
||||
|
||||
```php
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
use Box\Spout\Common\Entity\Row;
|
||||
|
||||
$writer = WriterEntityFactory::createXLSXWriter();
|
||||
// $writer = WriterEntityFactory::createODSWriter();
|
||||
// $writer = WriterEntityFactory::createCSVWriter();
|
||||
```
|
||||
|
||||
For XLSX and ODS files, the number of rows per sheet is limited to *1,048,576*. By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
|
||||
|
||||
|
||||
## Advanced usage
|
||||
|
||||
You can do a lot more with {{ site.spout_html }}! Check out the [full documentation]({{ site_url }}/docs/) to learn about all the features.
|
21
docs/_pages/guides.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
layout: page
|
||||
title: Guides
|
||||
permalink: /guides/
|
||||
---
|
||||
|
||||
{% include set-global-site-url.html %}
|
||||
|
||||
These guides focus on common and more advanced usages of {{ site.spout_html }}.<br>
|
||||
If you are just starting with {{ site.spout_html }}, check out the [Getting Started page]({{ site_url }}/getting-started/) and the [Documentation]({{ site_url }}/docs/) first.
|
||||
|
||||
{% assign pages=site.pages | sort: 'path' %}
|
||||
<ul>
|
||||
{% for page in pages %}
|
||||
{% if page.title and page.category contains 'guide' %}
|
||||
<li>
|
||||
<a class="page-link" href="{{ page.url | prepend: site_url }}">{{ page.title }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
BIN
docs/_pages/guides/.DS_Store
vendored
Normal file
62
docs/_pages/guides/1-add-data-existing-spreadsheet.md
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
layout: page
|
||||
title: "Add data to an existing spreadsheet"
|
||||
category: guide
|
||||
permalink: /guides/add-data-to-existing-spreadsheet/
|
||||
---
|
||||
|
||||
A common use case when using spreadsheets is to add data to an existing spreadsheet. For instance, let's assume you built a spreadsheet containing the last orders on your favorite website and want to update it as you make a new order.
|
||||
|
||||
We'll start with a file called "orders.xlsx" and add a new row, containing the last order's info, at the end.
|
||||
|
||||
In order to avoid memory issues when dealing with large spreadsheets, {{ site.spout_html }} does not hold the whole representation of the spreadsheet in memory. So to alter an existing spreadsheet, we'll have to create a new one that is similar to the existing one and add the new data in the new one.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$existingFilePath = 'path/to/orders.xlsx';
|
||||
$newFilePath = 'path/to/new-orders.xlsx';
|
||||
|
||||
// we need a reader to read the existing file...
|
||||
$reader = ReaderEntityFactory::createReaderFromFile($existingFilePath);
|
||||
$reader->setShouldFormatDates(true); // this is to be able to copy dates
|
||||
$reader->open($existingFilePath);
|
||||
|
||||
// ... and a writer to create the new file
|
||||
$writer = WriterEntityFactory::createWriterFromFile($newFilePath);
|
||||
$writer->openToFile($newFilePath);
|
||||
|
||||
// let's read the entire spreadsheet...
|
||||
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
|
||||
// Add sheets in the new file, as we read new sheets in the existing one
|
||||
if ($sheetIndex !== 1) {
|
||||
$writer->addNewSheetAndMakeItCurrent();
|
||||
}
|
||||
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
// ... and copy each row into the new spreadsheet
|
||||
$writer->addRow($row);
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, the new spreadsheet contains the same data as the existing one.
|
||||
// So let's add the new data:
|
||||
$writer->addRow(
|
||||
WriterEntityFactory::createRowFromArray(['2015-12-25', 'Christmas gift', 29, 'USD'])
|
||||
);
|
||||
|
||||
$reader->close();
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
Optionally, if you rely on the file name or want to keep only one file, simple remove the old file and rename the new one:
|
||||
|
||||
```php?start_inline=1
|
||||
unlink($existingFilePath);
|
||||
rename($newFilePath, $existingFilePath);
|
||||
```
|
||||
|
||||
That's it! The created file now contains the updated data and is ready to be used.
|
89
docs/_pages/guides/2-edit-existing-spreadsheet.md
Normal file
@ -0,0 +1,89 @@
|
||||
---
|
||||
layout: page
|
||||
title: "Edit an existing spreadsheet"
|
||||
category: guide
|
||||
permalink: /guides/edit-existing-spreadsheet/
|
||||
---
|
||||
|
||||
Editing an existing spreadsheet is a pretty common task that {{ site.spout_html }} is totally capable of doing.
|
||||
|
||||
With {{ site.spout_html }}, it is not possible to do things like `deleteRow(3)` or `insertRowAfter(5, $newRow)`. This is because {{ site.spout_html }} does not keep an in-memory representation of the entire spreadsheet, to avoid consuming all the memory available with large spreadsheets. This means, {{ site.spout_html }} does not know how to jump to the 3rd row directly and has especially no way of moving backwards (changing row 3 after having changed row 5). So let's see how this can be done, in a scalable way.
|
||||
|
||||
For this example, let's assume we have an existing ODS spreadsheet called "my-music.ods" that looks like this:
|
||||
|
||||
| Song title | Artist | Album | Year |
|
||||
| ---------------- | --------------- | --------------- | ---- |
|
||||
| Yesterday | The Beatles | The White Album | 1968 |
|
||||
| Yellow Submarine | The Beatles | Unknown | 1968 |
|
||||
| Space Oddity | David Bowie | David Bowie | 1969 |
|
||||
| Thriller | Michael Jackson | Thriller | 1982 |
|
||||
| No Woman No Cry | Bob Marley | Legend | 1984 |
|
||||
| Buffalo Soldier | Bob Marley | Legend | 1984 |
|
||||
|
||||
> Note that the album for "Yellow Submarine" is "Unknown" and that the songs are ordered by year (most recent last).
|
||||
|
||||
We'd like to update the missing album for "Yellow Submarine", remove the Bob Marley's songs and add a new song: "Hotel California" from "The Eagles", released in 1976. Here is how this can be done:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
|
||||
|
||||
$existingFilePath = '/path/to/my-music.ods';
|
||||
$newFilePath = '/path/to/my-new-music.ods';
|
||||
|
||||
// we need a reader to read the existing file...
|
||||
$reader = ReaderEntityFactory::createReaderFromFile($existingFilePath);
|
||||
$reader->open($existingFilePath);
|
||||
$reader->setShouldFormatDates(true); // this is to be able to copy dates
|
||||
|
||||
// ... and a writer to create the new file
|
||||
$writer = WriterEntityFactory::createWriterFromFile($newFilePath);
|
||||
$writer->openToFile($newFilePath);
|
||||
|
||||
// let's read the entire spreadsheet
|
||||
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
|
||||
// Add sheets in the new file, as you read new sheets in the existing one
|
||||
if ($sheetIndex !== 1) {
|
||||
$writer->addNewSheetAndMakeItCurrent();
|
||||
}
|
||||
|
||||
foreach ($sheet->getRowIterator() as $rowIndex => $row) {
|
||||
$songTitle = $row->getCellAtIndex(0);
|
||||
$artist = $row->getCellAtIndex(1);
|
||||
|
||||
// Change the album name for "Yellow Submarine"
|
||||
if ($songTitle === 'Yellow Submarine') {
|
||||
$row->setCellAtIndex(WriterEntityFactory::createCell('The White Album'), 2);
|
||||
}
|
||||
|
||||
// skip Bob Marley's songs
|
||||
if ($artist === 'Bob Marley') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// write the edited row to the new file
|
||||
$writer->addRow($row);
|
||||
|
||||
// insert new song at the right position, between the 3rd and 4th rows
|
||||
if ($rowIndex === 3) {
|
||||
$writer->addRow(
|
||||
WriterEntityFactory::createRowFromArray(['Hotel California', 'The Eagles', 'Hotel California', 1976])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
$writer->close();
|
||||
```
|
||||
|
||||
Optionally, if you rely on the file name or want to keep only one file, simple remove the old file and rename the new one:
|
||||
|
||||
```php?start_inline=1
|
||||
unlink($existingFilePath);
|
||||
rename($newFilePath, $existingFilePath);
|
||||
```
|
||||
|
||||
That's it! The created file now contains the updated data and is ready to be used.
|
54
docs/_pages/guides/3-read-data-from-specific-sheet.md
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
layout: page
|
||||
title: "Read data from a specific sheet only"
|
||||
category: guide
|
||||
permalink: /guides/read-data-from-specific-sheet/
|
||||
---
|
||||
|
||||
Even though a spreadsheet contains multiple sheets, you may be interested in reading only one of them and skip the other ones. Here is how you can do it with {{ site.spout_html }}:
|
||||
|
||||
* If you know the name of the sheet
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
|
||||
$reader = ReaderEntityFactory::createXLSXReader();
|
||||
$reader->open($filePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
// only read data from "summary" sheet
|
||||
if ($sheet->getName() === 'summary') {
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
// do something with the row
|
||||
}
|
||||
break; // no need to read more sheets
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
```
|
||||
|
||||
* If you know the position of the sheet
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
|
||||
|
||||
$reader = ReaderEntityFactory::createXLSXReader();
|
||||
$reader->open($filePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
// only read data from 3rd sheet
|
||||
if ($sheet->getIndex() === 2) { // index is 0-based
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
// do something with the row
|
||||
}
|
||||
break; // no need to read more sheets
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
```
|
110
docs/_pages/guides/4-symfony-stream-content-large-spreadsheet.md
Normal file
@ -0,0 +1,110 @@
|
||||
---
|
||||
layout: page
|
||||
title: "[Symfony] Stream content of a large spreadsheet"
|
||||
category: guide
|
||||
permalink: /guides/symfony-stream-content-large-spreadsheet/
|
||||
---
|
||||
|
||||
> This tutorial is for the PHP framework [Symfony](http://symfony.com/).
|
||||
|
||||
The main benefit of streaming content is that this content can be rendered as soon as it is available. No matter how big the content is, the browser will be able to start rendering it as soon as the first byte is sent.
|
||||
|
||||
Reading a static spreadsheet to display its content to a user is a great use case for streaming. The spreadsheet can contain from a few rows to thousands of them and we don't want to wait until the whole file has been read (which can take a long time) before showing something to the user. Let's see how [Symfony's StreamedResponse](http://symfony.com/doc/current/components/http_foundation/introduction.html#streaming-a-response) let us easily stream the content of the spreadsheet.
|
||||
|
||||
A regular controller usually builds the content to be displayed and encapsulate it into a `Response` object. Everything happens synchronously. Such a controller may look like this:
|
||||
|
||||
```php?start_inline=1
|
||||
class MyRegularController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/spreadsheet/read")
|
||||
*/
|
||||
public function readAction()
|
||||
{
|
||||
$filePath = '/path/to/static/file.xlsx';
|
||||
|
||||
// The content to be displayed has to be built entirely
|
||||
// before it can be sent to the browser.
|
||||
$content = '';
|
||||
|
||||
$reader = ReaderEntityFactory::createReaderFromFile($filePath);
|
||||
$reader->open($filePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
$content .= '<table>';
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
$content .= '<tr>';
|
||||
$content .= implode(array_map(function($cell) {
|
||||
return '<td>' . $cell . '</td>';
|
||||
}, $row->getCells()));
|
||||
$content .= '</tr>';
|
||||
}
|
||||
$content .= '</table><br>';
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
|
||||
// The response is sent to the browser
|
||||
// once the entire file has been read.
|
||||
$response = new Response($content);
|
||||
$response->headers->set('Content-Type', 'text/html');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Converting a regular controller to return a `StreamedResponse` is super easy! This is what it looks like after conversion:
|
||||
|
||||
```php?start_inline=1
|
||||
class MyStreamController extends Controller
|
||||
{
|
||||
// See below how it is used.
|
||||
const FLUSH_THRESHOLD = 100;
|
||||
|
||||
/**
|
||||
* @Route("/spreadsheet/stream")
|
||||
*/
|
||||
public function readAction()
|
||||
{
|
||||
$filePath = '/path/to/static/file.xlsx';
|
||||
|
||||
// We'll now return a StreamedResponse.
|
||||
$response = new StreamedResponse();
|
||||
$response->headers->set('Content-Type', 'text/html');
|
||||
|
||||
// Instead of a string, the streamed response will execute
|
||||
// a callback function to retrieve data chunks.
|
||||
$response->setCallback(function() use ($filePath) {
|
||||
// Same code goes inside the callback.
|
||||
$reader = ReaderEntityFactory::createXLSXReader();
|
||||
$reader->open($filePath);
|
||||
|
||||
$i = 0;
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
// The main difference with the regular response is
|
||||
// that the content is now echo'ed, not appended.
|
||||
echo '<table>';
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
echo '<tr>';
|
||||
echo implode(array_map(function($cell) {
|
||||
return '<td>' . $cell . '</td>';
|
||||
}, $row->getCells()));
|
||||
echo '</tr>';
|
||||
|
||||
$i++;
|
||||
// Flushing the buffer every N rows to stream echo'ed content.
|
||||
if ($i % self::FLUSH_THRESHOLD === 0) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
echo '</table><br>';
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
});
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
```
|
12
docs/_pages/index.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: default
|
||||
banner: true
|
||||
title: Spout - Read and write spreadsheets, quickly and at scale
|
||||
permalink: /
|
||||
---
|
||||
|
||||
{% include set-global-site-url.html %}
|
||||
|
||||
{% include section-supported-spreadsheet-types.html site_url=site_url %}
|
||||
{% include section-fast-and-scalable.html site_url=site_url %}
|
||||
{% include section-why-use-spout.html site_url=site_url %}
|
278
docs/_sass/_base.scss
Executable file
@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Reset some basic elements
|
||||
*/
|
||||
body, h1, h2, h3, h4, h5, h6,
|
||||
p, blockquote, pre, hr,
|
||||
dl, dd, ol, ul, figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Basic styling
|
||||
*/
|
||||
body {
|
||||
font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;
|
||||
color: $text-color;
|
||||
background-color: $background-color;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-font-feature-settings: "kern" 1;
|
||||
-moz-font-feature-settings: "kern" 1;
|
||||
-o-font-feature-settings: "kern" 1;
|
||||
font-feature-settings: "kern" 1;
|
||||
font-kerning: normal;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set `margin-bottom` to maintain vertical rhythm
|
||||
*/
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
p, blockquote, pre,
|
||||
ul, ol, dl, figure,
|
||||
%vertical-rhythm {
|
||||
margin-bottom: $spacing-unit / 2;
|
||||
}
|
||||
|
||||
h1:not(:nth-child(1)), h2:not(:nth-child(1)), h3:not(:nth-child(1)), h4:not(:nth-child(1)), h5:not(:nth-child(1)), h6:not(:nth-child(1)) {
|
||||
margin-top: $spacing-unit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tables
|
||||
*/
|
||||
table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #c6cbd1;
|
||||
|
||||
&:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Images
|
||||
*/
|
||||
img {
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Figures
|
||||
*/
|
||||
figure > img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Lists
|
||||
*/
|
||||
ul, ol {
|
||||
margin-left: $spacing-unit;
|
||||
}
|
||||
|
||||
li {
|
||||
> ul,
|
||||
> ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Headings
|
||||
*/
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: $base-font-weight;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Links
|
||||
*/
|
||||
a {
|
||||
color: $brand-color;
|
||||
text-decoration: none;
|
||||
font-weight: $base-font-weight;
|
||||
|
||||
&:visited {
|
||||
color: lighten($brand-color, 15%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $text-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Blockquotes
|
||||
*/
|
||||
blockquote {
|
||||
color: $grey-color;
|
||||
border-left: 4px solid lighten($brand-color, 65%);
|
||||
padding: $spacing-unit / 3 $spacing-unit / 2;
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
|
||||
> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Code formatting
|
||||
*/
|
||||
pre,
|
||||
code {
|
||||
font-size: 15px;
|
||||
// border: 1px solid darken($code-color, 5%);
|
||||
border-radius: 3px;
|
||||
// background-color: $code-color;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px 12px;
|
||||
overflow-x: auto;
|
||||
|
||||
> code {
|
||||
border: 0;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper
|
||||
*/
|
||||
.wrapper {
|
||||
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));
|
||||
max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-right: $spacing-unit;
|
||||
padding-left: $spacing-unit;
|
||||
@extend %clearfix;
|
||||
|
||||
@include media-query($on-laptop) {
|
||||
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));
|
||||
max-width: calc(#{$content-width} - (#{$spacing-unit}));
|
||||
padding-right: $spacing-unit / 2;
|
||||
padding-left: $spacing-unit / 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Clearfix
|
||||
*/
|
||||
%clearfix {
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Icons
|
||||
*/
|
||||
.icon {
|
||||
|
||||
> svg {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
|
||||
path {
|
||||
fill: $grey-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
.vertical-align-middle {
|
||||
// position: relative;
|
||||
// top: 50%;
|
||||
// transform: translateY(50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.mrl { margin-right: 20px; }
|
||||
.mll { margin-left: 20px; }
|
||||
.mbl { margin-bottom: 20px; }
|
||||
.text-center { text-align: center; }
|
||||
|
||||
.light-em {
|
||||
font-weight: $base-font-weight + 100;
|
||||
}
|
79
docs/_sass/_index.scss
Executable file
@ -0,0 +1,79 @@
|
||||
.spout {
|
||||
font-variant: small-caps;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: $spacing-unit 0;
|
||||
border-bottom: 1px solid $grey-color-light;
|
||||
|
||||
&.last-section {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
&.centered .description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icons {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.section-odd {
|
||||
background-color: rgba($grey-color, 0.0);
|
||||
}
|
||||
|
||||
&.section-even {
|
||||
background-color: $grey-color-very-light;
|
||||
}
|
||||
|
||||
.btn-wrapper {
|
||||
margin: $spacing-unit 0;
|
||||
text-align: center;
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
border: 1px solid;
|
||||
background: $brand-color;
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
padding: 15px 30px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: darken($brand-color, 10%);
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feature-check {
|
||||
background: url("../images/blue-check-mark.png") no-repeat left top;
|
||||
background-size: 15px 15px;
|
||||
background-position-y: 4px;
|
||||
padding-left: 25px;
|
||||
}
|
410
docs/_sass/_layout.scss
Executable file
@ -0,0 +1,410 @@
|
||||
/**
|
||||
* Site header
|
||||
*/
|
||||
.site-header {
|
||||
border-bottom: 1px solid lighten($brand-color, 15%);
|
||||
min-height: 56px;
|
||||
|
||||
// Positioning context for the mobile navigation icon
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.site-search {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 26px;
|
||||
font-weight: 300;
|
||||
line-height: 56px;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 0;
|
||||
float: left;
|
||||
|
||||
&,
|
||||
&:visited {
|
||||
color: $grey-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
float: right;
|
||||
line-height: 56px;
|
||||
|
||||
.menu-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
color: $grey-color;
|
||||
line-height: $base-line-height;
|
||||
|
||||
// Gaps between nav items, but not on the last one
|
||||
&:not(:last-child) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
svg {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-query($on-palm) {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: $spacing-unit / 2;
|
||||
background-color: $background-color;
|
||||
border: 1px solid $grey-color-light;
|
||||
border-radius: 5px;
|
||||
text-align: right;
|
||||
|
||||
.menu-icon {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 36px;
|
||||
height: 26px;
|
||||
line-height: 0;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
> svg {
|
||||
width: 18px;
|
||||
height: 15px;
|
||||
|
||||
path {
|
||||
fill: $grey-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trigger {
|
||||
clear: both;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .trigger {
|
||||
display: block;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.site-banner {
|
||||
width: 100%;
|
||||
background: $brand-color;
|
||||
height: 340px;
|
||||
color: white;
|
||||
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
background: url('../images/logo.png');
|
||||
background-size: 30%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 95% center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tag-line {
|
||||
font-size: 2.8em;
|
||||
line-height: 1.1em;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@include media-query($on-palm) {
|
||||
height: 240px;
|
||||
|
||||
.tag-line {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Site footer
|
||||
*/
|
||||
.site-footer {
|
||||
background: $grey-color-dark;
|
||||
color: $grey-color-light;
|
||||
border-top: 1px solid $grey-color-light;
|
||||
padding: $spacing-unit 0;
|
||||
margin-top: $spacing-unit;
|
||||
|
||||
a {
|
||||
color: $grey-color-very-light;
|
||||
}
|
||||
|
||||
.contact-list,
|
||||
.social-media-list {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-col-wrapper {
|
||||
font-size: 15px;
|
||||
// color: $grey-color;
|
||||
margin-left: -$spacing-unit / 2;
|
||||
@extend %clearfix;
|
||||
}
|
||||
|
||||
.footer-col {
|
||||
float: left;
|
||||
margin-bottom: $spacing-unit / 2;
|
||||
padding-left: $spacing-unit / 2;
|
||||
}
|
||||
|
||||
.footer-col-1 {
|
||||
width: -webkit-calc(35% - (#{$spacing-unit} / 2));
|
||||
width: calc(35% - (#{$spacing-unit} / 2));
|
||||
}
|
||||
|
||||
.footer-col-2 {
|
||||
width: -webkit-calc(25% - (#{$spacing-unit} / 2));
|
||||
width: calc(25% - (#{$spacing-unit} / 2));
|
||||
}
|
||||
|
||||
.footer-col-3 {
|
||||
width: -webkit-calc(40% - (#{$spacing-unit} / 2));
|
||||
width: calc(40% - (#{$spacing-unit} / 2));
|
||||
}
|
||||
|
||||
@include media-query($on-laptop) {
|
||||
.footer-col-1,
|
||||
.footer-col-2 {
|
||||
width: -webkit-calc(50% - (#{$spacing-unit} / 2));
|
||||
width: calc(50% - (#{$spacing-unit} / 2));
|
||||
}
|
||||
|
||||
.footer-col-3 {
|
||||
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
|
||||
width: calc(100% - (#{$spacing-unit} / 2));
|
||||
}
|
||||
}
|
||||
|
||||
@include media-query($on-palm) {
|
||||
.footer-col {
|
||||
float: none;
|
||||
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
|
||||
width: calc(100% - (#{$spacing-unit} / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Page content
|
||||
*/
|
||||
.page-content {
|
||||
padding: $spacing-unit 0;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.post-list {
|
||||
margin-left: 0;
|
||||
list-style: none;
|
||||
|
||||
> li {
|
||||
margin-bottom: $spacing-unit;
|
||||
}
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
font-size: $small-font-size;
|
||||
color: $grey-color;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Posts
|
||||
*/
|
||||
.post-header {
|
||||
margin-bottom: $spacing-unit;
|
||||
padding-top: $spacing-unit;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 42px;
|
||||
letter-spacing: -1px;
|
||||
line-height: 1;
|
||||
|
||||
@include media-query($on-laptop) {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-content {
|
||||
margin-bottom: $spacing-unit;
|
||||
|
||||
h2 {
|
||||
font-size: 32px;
|
||||
|
||||
@include media-query($on-laptop) {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 26px;
|
||||
|
||||
@include media-query($on-laptop) {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
|
||||
@include media-query($on-laptop) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$table-of-content-width: 250px;
|
||||
|
||||
.table-of-content {
|
||||
position: absolute;
|
||||
left: 100px;
|
||||
width: $table-of-content-width;
|
||||
}
|
||||
|
||||
.table-of-content.affix-top {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
}
|
||||
|
||||
.table-of-content.affix-bottom {
|
||||
position: absolute;
|
||||
bottom: 300;
|
||||
}
|
||||
|
||||
.table-of-content.affix {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
.table-of-content a.h1 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-of-content a.h2 {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.table-of-content a.h3 {
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
|
||||
// TODO
|
||||
@include media-query($on-laptop-big+$table-of-content-width) {
|
||||
.table-of-content {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-query($on-laptop-big) {
|
||||
.table-of-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-query($on-laptop) {
|
||||
.table-of-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* search */
|
||||
input#algolia-doc-search {
|
||||
background: transparent url("/images/search.png") no-repeat 10px center;
|
||||
background-size: 16px 16px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
margin-left: 10px;
|
||||
padding: 0 10px;
|
||||
padding-left: 35px;
|
||||
height: 30px;
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
color: #333;
|
||||
outline: none;
|
||||
width: 100px;
|
||||
transition: width .2s ease;
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
input#algolia-doc-search:focus {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* Bottom border of each suggestion */
|
||||
.algolia-docsearch-suggestion {
|
||||
border-bottom-color: $brand-color;
|
||||
}
|
||||
/* Main category headers */
|
||||
.algolia-docsearch-suggestion--category-header {
|
||||
background-color: lighten($brand-color, 20%);
|
||||
}
|
||||
/* Highlighted search terms */
|
||||
.algolia-docsearch-suggestion--highlight {
|
||||
color: lighten($brand-color, 10%);
|
||||
}
|
||||
/* Highligted search terms in the main category headers */
|
||||
.algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--highlight {
|
||||
background-color: $brand-color;
|
||||
}
|
||||
/* Currently selected suggestion */
|
||||
.aa-cursor .algolia-docsearch-suggestion--content {
|
||||
color: darken($brand-color, 10%);
|
||||
}
|
||||
.aa-cursor .algolia-docsearch-suggestion {
|
||||
background: ligten($brand-color, 30%);
|
||||
}
|
||||
|
||||
/* For bigger screens, when displaying results in two columns */
|
||||
@media (min-width: 768px) {
|
||||
/* Bottom border of each suggestion */
|
||||
.algolia-docsearch-suggestion {
|
||||
border-bottom-color: lighten($brand-color, 15%);
|
||||
}
|
||||
/* Left column, with secondary category header */
|
||||
.algolia-docsearch-suggestion--subcategory-column {
|
||||
border-right-color: lighten($brand-color, 25%);
|
||||
background-color: lighten($brand-color, 45%);
|
||||
color: darken($brand-color, 35%);
|
||||
}
|
||||
}
|
109
docs/_sass/_syntax-highlighting.scss
Executable file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Syntax highlighting styles
|
||||
*/
|
||||
.highlight {
|
||||
// markdown editing
|
||||
$default: #989898;
|
||||
$bg: #f5f5f5;
|
||||
$caret: #00bdff;
|
||||
$black: #000;
|
||||
$white: #fff;
|
||||
$invisible: #E0E0E0;
|
||||
$highlight: #e6e6e6;
|
||||
$inserted: #DDFFDD;
|
||||
$output: #7F7F7F;
|
||||
$promt: #555;
|
||||
$traceback: #F93232;
|
||||
$deleted: #fdd;
|
||||
$selection: #C2E8FF;
|
||||
$found: #FFE792;
|
||||
$shadow: #808080;
|
||||
$comment: #bbbaba;
|
||||
$invalid: #F9F2CE;
|
||||
$operator: #626FC9;
|
||||
$keyword: #7aad36;
|
||||
$symbol: #E8FFD5;
|
||||
$type: #6700B9;
|
||||
$constant: #9870EC;
|
||||
$var: #4C8FC7;
|
||||
$attribute: #d42a57;
|
||||
$function: $keyword;
|
||||
$built-in: $attribute;
|
||||
$class: #3A1D72;
|
||||
$exception: #F93232;
|
||||
$section: #333333;
|
||||
$number: $constant;
|
||||
$literal: #de8325;
|
||||
$string_re: #699D36;
|
||||
$tag: $var;
|
||||
$name-class: #3A77BF;
|
||||
$entity: #6d98cf;
|
||||
$punctuation: $black;
|
||||
|
||||
color: $default;
|
||||
background-color: $bg;
|
||||
//border: 1px solid darken($bg, 5%);
|
||||
|
||||
.bp { color: $caret; } // Name.Builtin.Pseudo
|
||||
.c { color: $comment; } // Comment
|
||||
.c1 { @extend .c; } // Comment.Single
|
||||
.cm { @extend .c; } // Comment.Multiline
|
||||
.cp { color: $shadow } // Comment.Preproc
|
||||
.cs { @extend .c; } // Comment.Special
|
||||
.err { color: $exception; background-color: $invalid } // Error
|
||||
.gd { color: $black; background-color: $deleted; } // Generic.Deleted
|
||||
.ge { color: $default; background-color: $highlight; } // Generic.Emph
|
||||
.gh { color: $section;} // Generic.Heading
|
||||
.gi { color: $black; background-color: $inserted; } // Generic.Inserted
|
||||
.go { color: $output } // Generic.Output
|
||||
.gp { color: $promt } // Generic.Prompt
|
||||
.gr { color: $exception } // Generic.Error
|
||||
.gs { background-color: $white; } // Generic.Strong
|
||||
.gt { color: $traceback } // Generic.Traceback
|
||||
.gu { color: $black; } // Generic.Subheading
|
||||
.hll { background-color: $found }
|
||||
.k { color: $keyword } // Keyword
|
||||
.kc { color: $constant } // Keyword.Constant
|
||||
.kd { @extend .k; } // Keyword.Declaration
|
||||
.kn { @extend .k; } // Keyword.Namespace
|
||||
.kp { @extend .k; } // Keyword.Pseudo
|
||||
.kr { @extend .k; } // Keyword.Reserved
|
||||
.kt { color: $type } // Keyword.Type
|
||||
.m { color: $number; } // Literal.Number
|
||||
.mf { @extend .m; } // Literal.Number.Float
|
||||
.mh { @extend .m; } // Literal.Number.Hex
|
||||
.mi { @extend .m; } // Literal.Number.Integer
|
||||
.mo { @extend .m; } // Literal.Number.Oct
|
||||
.il { @extend .m; } // Literal.Number.Integer.Long
|
||||
.n { color: $default } // Name
|
||||
.na { color: $attribute } // Name.Attribute
|
||||
.nb { color: $built-in } // Name.Builtin
|
||||
.nc { color: $name-class; } // Name.Class
|
||||
.nd { color: $shadow } // Name.Decorator
|
||||
.nf { color: $function } // Name.Function
|
||||
.ni { color: $entity;} // Name.Entity
|
||||
.nn { color: $class; text-decoration: underline } // Name.Namespace
|
||||
.no { color: $constant } // Name.Constant
|
||||
.nt { color: $tag;} // Name.Tag
|
||||
.nv { @extend .v; } // Name.Variable
|
||||
.nx { color: $default; }
|
||||
.o { color: $punctuation } // Operator.Word
|
||||
.ow { color: $operator } // Operator.Word
|
||||
.s { color: $literal } // Literal.String
|
||||
.s1 { @extend .s; } // Literal.String.Single
|
||||
.s2 { @extend .s; } // Literal.String.Double
|
||||
.sb { @extend .s; } // Literal.String.Backtick
|
||||
.sc { @extend .s; } // Literal.String.Char
|
||||
.sd { @extend .s; } // Literal.String.Doc
|
||||
.se { @extend .s; } // Literal.String.Escape
|
||||
.sh { @extend .s; } // Literal.String.Heredoc
|
||||
.si { @extend .s; } // Literal.String.Interpol
|
||||
.sr { color: $string_re; } // Literal.String.Regex
|
||||
.ss { color: $caret; } // Literal.String.Symbol
|
||||
.sx { @extend .s; } // Literal.String.Other
|
||||
.v { color: $var }
|
||||
.vc { color: $class; } // Name.Variable.Class
|
||||
.vg { @extend .v; } // Name.Variable.Global
|
||||
.vi { @extend .v; } // Name.Variable.Instance
|
||||
.w { color: $invisible; } // Text.Whitespace
|
||||
}
|
56
docs/css/main.scss
Executable file
@ -0,0 +1,56 @@
|
||||
---
|
||||
# Only the main Sass file needs front matter (the dashes are enough)
|
||||
---
|
||||
@charset "utf-8";
|
||||
|
||||
|
||||
|
||||
// Our variables
|
||||
$base-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$base-font-size: 16px;
|
||||
$base-font-weight: 300;
|
||||
$small-font-size: $base-font-size * 0.875;
|
||||
$base-line-height: 1.5;
|
||||
|
||||
$spacing-unit: 30px;
|
||||
|
||||
$text-color: #111;
|
||||
$background-color: #fdfdfd;
|
||||
$brand-color: #104c81;
|
||||
|
||||
$grey-color: #828282;
|
||||
$grey-color-light: lighten($grey-color, 40%);
|
||||
$grey-color-very-light: lighten($grey-color, 45%);
|
||||
$grey-color-dark: darken($grey-color, 25%);
|
||||
|
||||
// Width of the content area
|
||||
$content-width: 800px;
|
||||
|
||||
$on-palm: 600px;
|
||||
$on-laptop: 800px;
|
||||
$on-laptop-big: 1400px;
|
||||
|
||||
|
||||
|
||||
// Use media queries like this:
|
||||
// @include media-query($on-palm) {
|
||||
// .wrapper {
|
||||
// padding-right: $spacing-unit / 2;
|
||||
// padding-left: $spacing-unit / 2;
|
||||
// }
|
||||
// }
|
||||
@mixin media-query($device) {
|
||||
@media screen and (max-width: $device) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Import partials from `sass_dir` (defaults to `_sass`)
|
||||
@import
|
||||
"base",
|
||||
"layout",
|
||||
"syntax-highlighting",
|
||||
"index"
|
||||
;
|
15
docs/docker-compose.yml
Normal file
@ -0,0 +1,15 @@
|
||||
version: "3"
|
||||
services:
|
||||
site:
|
||||
command: bash -c "
|
||||
gem install 'jekyll-github-metadata'
|
||||
&& jekyll serve --config _config.yml,_config_local.yml"
|
||||
image: jekyll/jekyll:latest
|
||||
volumes:
|
||||
- $PWD:/srv/jekyll
|
||||
- $PWD/vendor/bundle:/usr/local/bundle
|
||||
ports:
|
||||
- 4000:4000
|
||||
- 35729:35729
|
||||
- 3000:3000
|
||||
- 8080:4000
|
BIN
docs/favicon.ico
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
docs/images/.DS_Store
vendored
Normal file
BIN
docs/images/blue-check-mark.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/images/icon-csv.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
docs/images/icon-lightning-bolt.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
docs/images/icon-ods.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
docs/images/icon-xlsx.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
docs/images/logo.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
9
phpstan.neon
Normal 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
|
53
phpunit.xml
@ -1,25 +1,36 @@
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.3/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="false"
|
||||
convertWarningsToExceptions="false"
|
||||
verbose="false">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="false"
|
||||
convertWarningsToExceptions="false"
|
||||
defaultTestSuite="unit-tests"
|
||||
verbose="false">
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="all-tests">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
</php>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">src/</directory>
|
||||
<exclude>
|
||||
<directory>src/Spout/Autoloader</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<testsuites>
|
||||
<testsuite name="unit-tests">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>perf-tests</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">src/</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory>src/Spout/Autoloader</directory>
|
||||
</exclude>
|
||||
</coverage>
|
||||
|
||||
</phpunit>
|
||||
|
@ -5,8 +5,6 @@ namespace Box\Spout\Autoloader;
|
||||
/**
|
||||
* Class Psr4Autoloader
|
||||
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
|
||||
*
|
||||
* @package Box\Spout\Autoloader
|
||||
*/
|
||||
class Psr4Autoloader
|
||||
{
|
||||
@ -16,7 +14,7 @@ class Psr4Autoloader
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $prefixes = array();
|
||||
protected $prefixes = [];
|
||||
|
||||
/**
|
||||
* Register loader with SPL autoloader stack.
|
||||
@ -25,7 +23,7 @@ class Psr4Autoloader
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'));
|
||||
\spl_autoload_register([$this, 'loadClass']);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,21 +40,21 @@ class Psr4Autoloader
|
||||
public function addNamespace($prefix, $baseDir, $prepend = false)
|
||||
{
|
||||
// normalize namespace prefix
|
||||
$prefix = trim($prefix, '\\') . '\\';
|
||||
$prefix = \trim($prefix, '\\') . '\\';
|
||||
|
||||
// normalize the base directory with a trailing separator
|
||||
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
|
||||
$baseDir = \rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
|
||||
|
||||
// initialize the namespace prefix array
|
||||
if (isset($this->prefixes[$prefix]) === false) {
|
||||
$this->prefixes[$prefix] = array();
|
||||
$this->prefixes[$prefix] = [];
|
||||
}
|
||||
|
||||
// retain the base directory for the namespace prefix
|
||||
if ($prepend) {
|
||||
array_unshift($this->prefixes[$prefix], $baseDir);
|
||||
\array_unshift($this->prefixes[$prefix], $baseDir);
|
||||
} else {
|
||||
array_push($this->prefixes[$prefix], $baseDir);
|
||||
\array_push($this->prefixes[$prefix], $baseDir);
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,13 +72,12 @@ class Psr4Autoloader
|
||||
|
||||
// work backwards through the namespace names of the fully-qualified
|
||||
// class name to find a mapped file name
|
||||
while (false !== $pos = strrpos($prefix, '\\')) {
|
||||
|
||||
while (($pos = \strrpos($prefix, '\\')) !== false) {
|
||||
// retain the trailing namespace separator in the prefix
|
||||
$prefix = substr($class, 0, $pos + 1);
|
||||
$prefix = \substr($class, 0, $pos + 1);
|
||||
|
||||
// the rest is the relative class name
|
||||
$relativeClass = substr($class, $pos + 1);
|
||||
$relativeClass = \substr($class, $pos + 1);
|
||||
|
||||
// try to load a mapped file for the prefix and relative class
|
||||
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
|
||||
@ -90,7 +87,7 @@ class Psr4Autoloader
|
||||
|
||||
// remove the trailing namespace separator for the next iteration
|
||||
// of strrpos()
|
||||
$prefix = rtrim($prefix, '\\');
|
||||
$prefix = \rtrim($prefix, '\\');
|
||||
}
|
||||
|
||||
// never found a mapped file
|
||||
@ -114,12 +111,11 @@ class Psr4Autoloader
|
||||
|
||||
// look through base directories for this namespace prefix
|
||||
foreach ($this->prefixes[$prefix] as $baseDir) {
|
||||
|
||||
// replace the namespace prefix with the base directory,
|
||||
// replace namespace separators with directory separators
|
||||
// in the relative class name, append with .php
|
||||
$file = $baseDir
|
||||
. str_replace('\\', '/', $relativeClass)
|
||||
. \str_replace('\\', '/', $relativeClass)
|
||||
. '.php';
|
||||
|
||||
// if the mapped file exists, require it
|
||||
@ -141,10 +137,12 @@ class Psr4Autoloader
|
||||
*/
|
||||
protected function requireFile($file)
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
if (\file_exists($file)) {
|
||||
require $file;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,10 @@ namespace Box\Spout\Autoloader;
|
||||
require_once 'Psr4Autoloader.php';
|
||||
|
||||
/**
|
||||
* @var string $srcBaseDirectory
|
||||
* @var string
|
||||
* Full path to "src/Spout" which is what we want "Box\Spout" to map to.
|
||||
*/
|
||||
$srcBaseDirectory = dirname(dirname(__FILE__));
|
||||
$srcBaseDirectory = \dirname(\dirname(__FILE__));
|
||||
|
||||
$loader = new Psr4Autoloader();
|
||||
$loader->register();
|
||||
|
49
src/Spout/Common/Creator/HelperFactory.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Creator;
|
||||
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
use Box\Spout\Common\Helper\FileSystemHelper;
|
||||
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
|
||||
use Box\Spout\Common\Helper\StringHelper;
|
||||
|
||||
/**
|
||||
* Class HelperFactory
|
||||
* Factory to create helpers
|
||||
*/
|
||||
class HelperFactory
|
||||
{
|
||||
/**
|
||||
* @return GlobalFunctionsHelper
|
||||
*/
|
||||
public function createGlobalFunctionsHelper()
|
||||
{
|
||||
return new GlobalFunctionsHelper();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
|
||||
* @return FileSystemHelper
|
||||
*/
|
||||
public function createFileSystemHelper($baseFolderPath)
|
||||
{
|
||||
return new FileSystemHelper($baseFolderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @return EncodingHelper
|
||||
*/
|
||||
public function createEncodingHelper(GlobalFunctionsHelper $globalFunctionsHelper)
|
||||
{
|
||||
return new EncodingHelper($globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StringHelper
|
||||
*/
|
||||
public function createStringHelper()
|
||||
{
|
||||
return new StringHelper();
|
||||
}
|
||||
}
|
216
src/Spout/Common/Entity/Cell.php
Normal file
@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Entity;
|
||||
|
||||
use Box\Spout\Common\Entity\Style\Style;
|
||||
use Box\Spout\Common\Helper\CellTypeHelper;
|
||||
|
||||
/**
|
||||
* Class Cell
|
||||
*/
|
||||
class Cell
|
||||
{
|
||||
/**
|
||||
* Numeric cell type (whole numbers, fractional numbers, dates)
|
||||
*/
|
||||
public const TYPE_NUMERIC = 0;
|
||||
|
||||
/**
|
||||
* String (text) cell type
|
||||
*/
|
||||
public const TYPE_STRING = 1;
|
||||
|
||||
/**
|
||||
* Formula cell type
|
||||
* Not used at the moment
|
||||
*/
|
||||
public const TYPE_FORMULA = 2;
|
||||
|
||||
/**
|
||||
* Empty cell type
|
||||
*/
|
||||
public const TYPE_EMPTY = 3;
|
||||
|
||||
/**
|
||||
* Boolean cell type
|
||||
*/
|
||||
public const TYPE_BOOLEAN = 4;
|
||||
|
||||
/**
|
||||
* Date cell type
|
||||
*/
|
||||
public const TYPE_DATE = 5;
|
||||
|
||||
/**
|
||||
* Error cell type
|
||||
*/
|
||||
public const TYPE_ERROR = 6;
|
||||
|
||||
/**
|
||||
* The value of this cell
|
||||
* @var mixed|null
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* The cell type
|
||||
* @var int|null
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* The cell style
|
||||
* @var Style
|
||||
*/
|
||||
protected $style;
|
||||
|
||||
/**
|
||||
* @param mixed|null $value
|
||||
* @param Style|null $style
|
||||
*/
|
||||
public function __construct($value, Style $style = null)
|
||||
{
|
||||
$this->setValue($value);
|
||||
$this->setStyle($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|null $value
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->type = $this->detectType($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return !$this->isError() ? $this->value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValueEvenIfError()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Style|null $style
|
||||
*/
|
||||
public function setStyle($style)
|
||||
{
|
||||
$this->style = $style ?: new Style();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Style
|
||||
*/
|
||||
public function getStyle()
|
||||
{
|
||||
return $this->style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $type
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value type
|
||||
*
|
||||
* @param mixed|null $value
|
||||
* @return int
|
||||
*/
|
||||
protected function detectType($value)
|
||||
{
|
||||
if (CellTypeHelper::isBoolean($value)) {
|
||||
return self::TYPE_BOOLEAN;
|
||||
}
|
||||
if (CellTypeHelper::isEmpty($value)) {
|
||||
return self::TYPE_EMPTY;
|
||||
}
|
||||
if (CellTypeHelper::isNumeric($value)) {
|
||||
return self::TYPE_NUMERIC;
|
||||
}
|
||||
if (CellTypeHelper::isDateTimeOrDateInterval($value)) {
|
||||
return self::TYPE_DATE;
|
||||
}
|
||||
if (CellTypeHelper::isNonEmptyString($value)) {
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
|
||||
return self::TYPE_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isBoolean()
|
||||
{
|
||||
return $this->type === self::TYPE_BOOLEAN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return $this->type === self::TYPE_EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isNumeric()
|
||||
{
|
||||
return $this->type === self::TYPE_NUMERIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isString()
|
||||
{
|
||||
return $this->type === self::TYPE_STRING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isDate()
|
||||
{
|
||||
return $this->type === self::TYPE_DATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isError()
|
||||
{
|
||||
return $this->type === self::TYPE_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->getValue();
|
||||
}
|
||||
}
|
129
src/Spout/Common/Entity/Row.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Entity;
|
||||
|
||||
use Box\Spout\Common\Entity\Style\Style;
|
||||
|
||||
class Row
|
||||
{
|
||||
/**
|
||||
* The cells in this row
|
||||
* @var Cell[]
|
||||
*/
|
||||
protected $cells = [];
|
||||
|
||||
/**
|
||||
* The row style
|
||||
* @var Style
|
||||
*/
|
||||
protected $style;
|
||||
|
||||
/**
|
||||
* Row constructor.
|
||||
* @param Cell[] $cells
|
||||
* @param Style|null $style
|
||||
*/
|
||||
public function __construct(array $cells, $style)
|
||||
{
|
||||
$this
|
||||
->setCells($cells)
|
||||
->setStyle($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Cell[] $cells
|
||||
*/
|
||||
public function getCells()
|
||||
{
|
||||
return $this->cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cell[] $cells
|
||||
* @return Row
|
||||
*/
|
||||
public function setCells(array $cells)
|
||||
{
|
||||
$this->cells = [];
|
||||
foreach ($cells as $cell) {
|
||||
$this->addCell($cell);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cell $cell
|
||||
* @param int $cellIndex
|
||||
* @return Row
|
||||
*/
|
||||
public function setCellAtIndex(Cell $cell, $cellIndex)
|
||||
{
|
||||
$this->cells[$cellIndex] = $cell;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $cellIndex
|
||||
* @return Cell|null
|
||||
*/
|
||||
public function getCellAtIndex($cellIndex)
|
||||
{
|
||||
return $this->cells[$cellIndex] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cell $cell
|
||||
* @return Row
|
||||
*/
|
||||
public function addCell(Cell $cell)
|
||||
{
|
||||
$this->cells[] = $cell;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getNumCells()
|
||||
{
|
||||
// When using "setCellAtIndex", it's possible to
|
||||
// have "$this->cells" contain holes.
|
||||
if (empty($this->cells)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return \max(\array_keys($this->cells)) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Style
|
||||
*/
|
||||
public function getStyle()
|
||||
{
|
||||
return $this->style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Style|null $style
|
||||
* @return Row
|
||||
*/
|
||||
public function setStyle($style)
|
||||
{
|
||||
$this->style = $style ?: new Style();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array The row values, as array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return \array_map(function (Cell $cell) {
|
||||
return $cell->getValue();
|
||||
}, $this->cells);
|
||||
}
|
||||
}
|
85
src/Spout/Common/Entity/Style/Border.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Entity\Style;
|
||||
|
||||
/**
|
||||
* Class Border
|
||||
*/
|
||||
class Border
|
||||
{
|
||||
public const LEFT = 'left';
|
||||
public const RIGHT = 'right';
|
||||
public const TOP = 'top';
|
||||
public const BOTTOM = 'bottom';
|
||||
|
||||
public const STYLE_NONE = 'none';
|
||||
public const STYLE_SOLID = 'solid';
|
||||
public const STYLE_DASHED = 'dashed';
|
||||
public const STYLE_DOTTED = 'dotted';
|
||||
public const STYLE_DOUBLE = 'double';
|
||||
|
||||
public const WIDTH_THIN = 'thin';
|
||||
public const WIDTH_MEDIUM = 'medium';
|
||||
public const WIDTH_THICK = 'thick';
|
||||
|
||||
/** @var array A list of BorderPart objects for this border. */
|
||||
private $parts = [];
|
||||
|
||||
/**
|
||||
* @param array $borderParts
|
||||
*/
|
||||
public function __construct(array $borderParts = [])
|
||||
{
|
||||
$this->setParts($borderParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name The name of the border part
|
||||
* @return BorderPart|null
|
||||
*/
|
||||
public function getPart($name)
|
||||
{
|
||||
return $this->hasPart($name) ? $this->parts[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name The name of the border part
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPart($name)
|
||||
{
|
||||
return isset($this->parts[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParts()
|
||||
{
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set BorderParts
|
||||
* @param array $parts
|
||||
* @return void
|
||||
*/
|
||||
public function setParts($parts)
|
||||
{
|
||||
unset($this->parts);
|
||||
foreach ($parts as $part) {
|
||||
$this->addPart($part);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BorderPart $borderPart
|
||||
* @return Border
|
||||
*/
|
||||
public function addPart(BorderPart $borderPart)
|
||||
{
|
||||
$this->parts[$borderPart->getName()] = $borderPart;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
184
src/Spout/Common/Entity/Style/BorderPart.php
Normal file
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Entity\Style;
|
||||
|
||||
use Box\Spout\Writer\Exception\Border\InvalidNameException;
|
||||
use Box\Spout\Writer\Exception\Border\InvalidStyleException;
|
||||
use Box\Spout\Writer\Exception\Border\InvalidWidthException;
|
||||
|
||||
/**
|
||||
* Class BorderPart
|
||||
*/
|
||||
class BorderPart
|
||||
{
|
||||
/**
|
||||
* @var string The style of this border part.
|
||||
*/
|
||||
protected $style;
|
||||
|
||||
/**
|
||||
* @var string The name of this border part.
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var string The color of this border part.
|
||||
*/
|
||||
protected $color;
|
||||
|
||||
/**
|
||||
* @var string The width of this border part.
|
||||
*/
|
||||
protected $width;
|
||||
|
||||
/**
|
||||
* @var array Allowed style constants for parts.
|
||||
*/
|
||||
protected static $allowedStyles = [
|
||||
'none',
|
||||
'solid',
|
||||
'dashed',
|
||||
'dotted',
|
||||
'double',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Allowed names constants for border parts.
|
||||
*/
|
||||
protected static $allowedNames = [
|
||||
'left',
|
||||
'right',
|
||||
'top',
|
||||
'bottom',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Allowed width constants for border parts.
|
||||
*/
|
||||
protected static $allowedWidths = [
|
||||
'thin',
|
||||
'medium',
|
||||
'thick',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $name @see BorderPart::$allowedNames
|
||||
* @param string $color A RGB color code
|
||||
* @param string $width @see BorderPart::$allowedWidths
|
||||
* @param string $style @see BorderPart::$allowedStyles
|
||||
* @throws InvalidNameException
|
||||
* @throws InvalidStyleException
|
||||
* @throws InvalidWidthException
|
||||
*/
|
||||
public function __construct($name, $color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
|
||||
{
|
||||
$this->setName($name);
|
||||
$this->setColor($color);
|
||||
$this->setWidth($width);
|
||||
$this->setStyle($style);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name The name of the border part @see BorderPart::$allowedNames
|
||||
* @throws InvalidNameException
|
||||
* @return void
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
if (!\in_array($name, self::$allowedNames)) {
|
||||
throw new InvalidNameException($name);
|
||||
}
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStyle()
|
||||
{
|
||||
return $this->style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $style The style of the border part @see BorderPart::$allowedStyles
|
||||
* @throws InvalidStyleException
|
||||
* @return void
|
||||
*/
|
||||
public function setStyle($style)
|
||||
{
|
||||
if (!\in_array($style, self::$allowedStyles)) {
|
||||
throw new InvalidStyleException($style);
|
||||
}
|
||||
$this->style = $style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getColor()
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color The color of the border part @see Color::rgb()
|
||||
* @return void
|
||||
*/
|
||||
public function setColor($color)
|
||||
{
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getWidth()
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $width The width of the border part @see BorderPart::$allowedWidths
|
||||
* @throws InvalidWidthException
|
||||
* @return void
|
||||
*/
|
||||
public function setWidth($width)
|
||||
{
|
||||
if (!\in_array($width, self::$allowedWidths)) {
|
||||
throw new InvalidWidthException($width);
|
||||
}
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllowedStyles()
|
||||
{
|
||||
return self::$allowedStyles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllowedNames()
|
||||
{
|
||||
return self::$allowedNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllowedWidths()
|
||||
{
|
||||
return self::$allowedWidths;
|
||||
}
|
||||
}
|
32
src/Spout/Common/Entity/Style/CellAlignment.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Entity\Style;
|
||||
|
||||
/**
|
||||
* Class Alignment
|
||||
* This class provides constants to work with text alignment.
|
||||
*/
|
||||
abstract class CellAlignment
|
||||
{
|
||||
public const LEFT = 'left';
|
||||
public const RIGHT = 'right';
|
||||
public const CENTER = 'center';
|
||||
public const JUSTIFY = 'justify';
|
||||
|
||||
private static $VALID_ALIGNMENTS = [
|
||||
self::LEFT => 1,
|
||||
self::RIGHT => 1,
|
||||
self::CENTER => 1,
|
||||
self::JUSTIFY => 1,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $cellAlignment
|
||||
*
|
||||
* @return bool Whether the given cell alignment is valid
|
||||
*/
|
||||
public static function isValid($cellAlignment)
|
||||
{
|
||||
return isset(self::$VALID_ALIGNMENTS[$cellAlignment]);
|
||||
}
|
||||
}
|
84
src/Spout/Common/Entity/Style/Color.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Entity\Style;
|
||||
|
||||
use Box\Spout\Common\Exception\InvalidColorException;
|
||||
|
||||
/**
|
||||
* Class Color
|
||||
* This class provides constants and functions to work with colors
|
||||
*/
|
||||
abstract class Color
|
||||
{
|
||||
/** Standard colors - based on Office Online */
|
||||
public const BLACK = '000000';
|
||||
public const WHITE = 'FFFFFF';
|
||||
public const RED = 'FF0000';
|
||||
public const DARK_RED = 'C00000';
|
||||
public const ORANGE = 'FFC000';
|
||||
public const YELLOW = 'FFFF00';
|
||||
public const LIGHT_GREEN = '92D040';
|
||||
public const GREEN = '00B050';
|
||||
public const LIGHT_BLUE = '00B0E0';
|
||||
public const BLUE = '0070C0';
|
||||
public const DARK_BLUE = '002060';
|
||||
public const PURPLE = '7030A0';
|
||||
|
||||
/**
|
||||
* Returns an RGB color from R, G and B values
|
||||
*
|
||||
* @param int $red Red component, 0 - 255
|
||||
* @param int $green Green component, 0 - 255
|
||||
* @param int $blue Blue component, 0 - 255
|
||||
* @return string RGB color
|
||||
*/
|
||||
public static function rgb($red, $green, $blue)
|
||||
{
|
||||
self::throwIfInvalidColorComponentValue($red);
|
||||
self::throwIfInvalidColorComponentValue($green);
|
||||
self::throwIfInvalidColorComponentValue($blue);
|
||||
|
||||
return \strtoupper(
|
||||
self::convertColorComponentToHex($red) .
|
||||
self::convertColorComponentToHex($green) .
|
||||
self::convertColorComponentToHex($blue)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception is the color component value is outside of bounds (0 - 255)
|
||||
*
|
||||
* @param int $colorComponent
|
||||
* @throws \Box\Spout\Common\Exception\InvalidColorException
|
||||
* @return void
|
||||
*/
|
||||
protected static function throwIfInvalidColorComponentValue($colorComponent)
|
||||
{
|
||||
if (!\is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
|
||||
throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the color component to its corresponding hexadecimal value
|
||||
*
|
||||
* @param int $colorComponent Color component, 0 - 255
|
||||
* @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d"
|
||||
*/
|
||||
protected static function convertColorComponentToHex($colorComponent)
|
||||
{
|
||||
return \str_pad(\dechex($colorComponent), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ARGB color of the given RGB color,
|
||||
* assuming that alpha value is always 1.
|
||||
*
|
||||
* @param string $rgbColor RGB color like "FF08B2"
|
||||
* @return string ARGB color
|
||||
*/
|
||||
public static function toARGB($rgbColor)
|
||||
{
|
||||
return 'FF' . $rgbColor;
|
||||
}
|
||||
}
|
509
src/Spout/Common/Entity/Style/Style.php
Normal file
@ -0,0 +1,509 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Entity\Style;
|
||||
|
||||
/**
|
||||
* Class Style
|
||||
* Represents a style to be applied to a cell
|
||||
*/
|
||||
class Style
|
||||
{
|
||||
/** Default values */
|
||||
public const DEFAULT_FONT_SIZE = 11;
|
||||
public const DEFAULT_FONT_COLOR = Color::BLACK;
|
||||
public const DEFAULT_FONT_NAME = 'Arial';
|
||||
|
||||
/** @var int|null Style ID */
|
||||
private $id;
|
||||
|
||||
/** @var bool Whether the font should be bold */
|
||||
private $fontBold = false;
|
||||
/** @var bool Whether the bold property was set */
|
||||
private $hasSetFontBold = false;
|
||||
|
||||
/** @var bool Whether the font should be italic */
|
||||
private $fontItalic = false;
|
||||
/** @var bool Whether the italic property was set */
|
||||
private $hasSetFontItalic = false;
|
||||
|
||||
/** @var bool Whether the font should be underlined */
|
||||
private $fontUnderline = false;
|
||||
/** @var bool Whether the underline property was set */
|
||||
private $hasSetFontUnderline = false;
|
||||
|
||||
/** @var bool Whether the font should be struck through */
|
||||
private $fontStrikethrough = false;
|
||||
/** @var bool Whether the strikethrough property was set */
|
||||
private $hasSetFontStrikethrough = false;
|
||||
|
||||
/** @var int Font size */
|
||||
private $fontSize = self::DEFAULT_FONT_SIZE;
|
||||
/** @var bool Whether the font size property was set */
|
||||
private $hasSetFontSize = false;
|
||||
|
||||
/** @var string Font color */
|
||||
private $fontColor = self::DEFAULT_FONT_COLOR;
|
||||
/** @var bool Whether the font color property was set */
|
||||
private $hasSetFontColor = false;
|
||||
|
||||
/** @var string Font name */
|
||||
private $fontName = self::DEFAULT_FONT_NAME;
|
||||
/** @var bool Whether the font name property was set */
|
||||
private $hasSetFontName = false;
|
||||
|
||||
/** @var bool Whether specific font properties should be applied */
|
||||
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) */
|
||||
private $shouldWrapText = false;
|
||||
/** @var bool Whether the wrap text property was set */
|
||||
private $hasSetWrapText = false;
|
||||
|
||||
/** @var Border|null */
|
||||
private $border;
|
||||
|
||||
/** @var bool Whether border properties should be applied */
|
||||
private $shouldApplyBorder = false;
|
||||
|
||||
/** @var string Background color */
|
||||
private $backgroundColor;
|
||||
|
||||
/** @var bool */
|
||||
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
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Style
|
||||
*/
|
||||
public function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Border|null
|
||||
*/
|
||||
public function getBorder()
|
||||
{
|
||||
return $this->border;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Border $border
|
||||
* @return Style
|
||||
*/
|
||||
public function setBorder(Border $border)
|
||||
{
|
||||
$this->shouldApplyBorder = true;
|
||||
$this->border = $border;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldApplyBorder()
|
||||
{
|
||||
return $this->shouldApplyBorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontBold()
|
||||
{
|
||||
return $this->fontBold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Style
|
||||
*/
|
||||
public function setFontBold()
|
||||
{
|
||||
$this->fontBold = true;
|
||||
$this->hasSetFontBold = true;
|
||||
$this->shouldApplyFont = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetFontBold()
|
||||
{
|
||||
return $this->hasSetFontBold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontItalic()
|
||||
{
|
||||
return $this->fontItalic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Style
|
||||
*/
|
||||
public function setFontItalic()
|
||||
{
|
||||
$this->fontItalic = true;
|
||||
$this->hasSetFontItalic = true;
|
||||
$this->shouldApplyFont = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetFontItalic()
|
||||
{
|
||||
return $this->hasSetFontItalic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontUnderline()
|
||||
{
|
||||
return $this->fontUnderline;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Style
|
||||
*/
|
||||
public function setFontUnderline()
|
||||
{
|
||||
$this->fontUnderline = true;
|
||||
$this->hasSetFontUnderline = true;
|
||||
$this->shouldApplyFont = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetFontUnderline()
|
||||
{
|
||||
return $this->hasSetFontUnderline;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontStrikethrough()
|
||||
{
|
||||
return $this->fontStrikethrough;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Style
|
||||
*/
|
||||
public function setFontStrikethrough()
|
||||
{
|
||||
$this->fontStrikethrough = true;
|
||||
$this->hasSetFontStrikethrough = true;
|
||||
$this->shouldApplyFont = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetFontStrikethrough()
|
||||
{
|
||||
return $this->hasSetFontStrikethrough;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getFontSize()
|
||||
{
|
||||
return $this->fontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $fontSize Font size, in pixels
|
||||
* @return Style
|
||||
*/
|
||||
public function setFontSize($fontSize)
|
||||
{
|
||||
$this->fontSize = $fontSize;
|
||||
$this->hasSetFontSize = true;
|
||||
$this->shouldApplyFont = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetFontSize()
|
||||
{
|
||||
return $this->hasSetFontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFontColor()
|
||||
{
|
||||
return $this->fontColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the font color.
|
||||
*
|
||||
* @param string $fontColor ARGB color (@see Color)
|
||||
* @return Style
|
||||
*/
|
||||
public function setFontColor($fontColor)
|
||||
{
|
||||
$this->fontColor = $fontColor;
|
||||
$this->hasSetFontColor = true;
|
||||
$this->shouldApplyFont = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetFontColor()
|
||||
{
|
||||
return $this->hasSetFontColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFontName()
|
||||
{
|
||||
return $this->fontName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fontName Name of the font to use
|
||||
* @return Style
|
||||
*/
|
||||
public function setFontName($fontName)
|
||||
{
|
||||
$this->fontName = $fontName;
|
||||
$this->hasSetFontName = true;
|
||||
$this->shouldApplyFont = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetFontName()
|
||||
{
|
||||
return $this->hasSetFontName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCellAlignment()
|
||||
{
|
||||
return $this->cellAlignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cellAlignment The cell alignment
|
||||
*
|
||||
* @return Style
|
||||
*/
|
||||
public function setCellAlignment($cellAlignment)
|
||||
{
|
||||
$this->cellAlignment = $cellAlignment;
|
||||
$this->hasSetCellAlignment = true;
|
||||
$this->shouldApplyCellAlignment = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetCellAlignment()
|
||||
{
|
||||
return $this->hasSetCellAlignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether specific cell alignment should be applied
|
||||
*/
|
||||
public function shouldApplyCellAlignment()
|
||||
{
|
||||
return $this->shouldApplyCellAlignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldWrapText()
|
||||
{
|
||||
return $this->shouldWrapText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $shouldWrap Should the text be wrapped
|
||||
* @return Style
|
||||
*/
|
||||
public function setShouldWrapText($shouldWrap = true)
|
||||
{
|
||||
$this->shouldWrapText = $shouldWrap;
|
||||
$this->hasSetWrapText = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetWrapText()
|
||||
{
|
||||
return $this->hasSetWrapText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether specific font properties should be applied
|
||||
*/
|
||||
public function shouldApplyFont()
|
||||
{
|
||||
return $this->shouldApplyFont;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background color
|
||||
* @param string $color ARGB color (@see Color)
|
||||
* @return Style
|
||||
*/
|
||||
public function setBackgroundColor($color)
|
||||
{
|
||||
$this->hasSetBackgroundColor = true;
|
||||
$this->backgroundColor = $color;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBackgroundColor()
|
||||
{
|
||||
return $this->backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the background color should be applied
|
||||
*/
|
||||
public function shouldApplyBackgroundColor()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Escaper;
|
||||
|
||||
/**
|
||||
* Class XLSX
|
||||
* Provides functions to escape and unescape data for XLSX files
|
||||
*
|
||||
* @package Box\Spout\Common\Escaper
|
||||
*/
|
||||
class XLSX implements EscaperInterface
|
||||
{
|
||||
/** @var string[] Control characters to be escaped */
|
||||
protected $controlCharactersEscapingMap;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
public function escape($string)
|
||||
{
|
||||
$escapedString = $this->escapeControlCharacters($string);
|
||||
$escapedString = htmlspecialchars($escapedString, ENT_QUOTES);
|
||||
|
||||
return $escapedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
public function unescape($string)
|
||||
{
|
||||
$unescapedString = htmlspecialchars_decode($string, ENT_QUOTES);
|
||||
$unescapedString = $this->unescapeControlCharacters($unescapedString);
|
||||
|
||||
return $unescapedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the map containing control characters to be escaped
|
||||
* mapped to their escaped values.
|
||||
* "\t", "\r" and "\n" don't need to be escaped.
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getControlCharactersEscapingMap()
|
||||
{
|
||||
$controlCharactersEscapingMap = [];
|
||||
$whitelistedControlCharacters = ["\t", "\r", "\n"];
|
||||
|
||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||
for ($charValue = 0x0; $charValue <= 0x1F; $charValue++) {
|
||||
if (!in_array(chr($charValue), $whitelistedControlCharacters)) {
|
||||
$charHexValue = dechex($charValue);
|
||||
$escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
|
||||
$controlCharactersEscapingMap[$escapedChar] = chr($charValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $controlCharactersEscapingMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PHP control characters from the given string to OpenXML escaped control characters
|
||||
*
|
||||
* Excel escapes control characters with _xHHHH_ and also escapes any
|
||||
* literal strings of that type by encoding the leading underscore.
|
||||
* So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @param string $string String to escape
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeControlCharacters($string)
|
||||
{
|
||||
$escapedString = $this->escapeEscapeCharacter($string);
|
||||
return str_replace(array_values($this->controlCharactersEscapingMap), array_keys($this->controlCharactersEscapingMap), $escapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the escape character: "_x0000_" -> "_x005F_x0000_"
|
||||
*
|
||||
* @param string $string String to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
protected function escapeEscapeCharacter($string)
|
||||
{
|
||||
return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts OpenXML escaped control characters from the given string to PHP control characters
|
||||
*
|
||||
* Excel escapes control characters with _xHHHH_ and also escapes any
|
||||
* literal strings of that type by encoding the leading underscore.
|
||||
* So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @param string $string String to unescape
|
||||
* @return string
|
||||
*/
|
||||
protected function unescapeControlCharacters($string)
|
||||
{
|
||||
$unescapedString = $string;
|
||||
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
|
||||
// only unescape characters that don't contain the escaped escape character for now
|
||||
$unescapedString = preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
|
||||
}
|
||||
|
||||
return $this->unescapeEscapeCharacter($unescapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unecapes the escape character: "_x005F_x0000_" => "_x0000_"
|
||||
*
|
||||
* @param string $string String to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
protected function unescapeEscapeCharacter($string)
|
||||
{
|
||||
return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class BadUsageException
|
||||
*
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class BadUsageException extends SpoutException
|
||||
{
|
||||
}
|
@ -4,8 +4,6 @@ namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class EncodingConversionException
|
||||
*
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class EncodingConversionException extends SpoutException
|
||||
{
|
||||
|
@ -4,8 +4,6 @@ namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class IOException
|
||||
*
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class IOException extends SpoutException
|
||||
{
|
||||
|
@ -4,8 +4,6 @@ namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class InvalidArgumentException
|
||||
*
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class InvalidArgumentException extends SpoutException
|
||||
{
|
||||
|
10
src/Spout/Common/Exception/InvalidColorException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class InvalidColorException
|
||||
*/
|
||||
class InvalidColorException extends SpoutException
|
||||
{
|
||||
}
|
@ -5,7 +5,7 @@ namespace Box\Spout\Common\Exception;
|
||||
/**
|
||||
* Class SpoutException
|
||||
*
|
||||
* @package Box\Spout\Common\Exception
|
||||
* @abstract
|
||||
*/
|
||||
abstract class SpoutException extends \Exception
|
||||
{
|
||||
|
@ -4,8 +4,6 @@ namespace Box\Spout\Common\Exception;
|
||||
|
||||
/**
|
||||
* Class UnsupportedTypeException
|
||||
*
|
||||
* @package Box\Spout\Common\Exception
|
||||
*/
|
||||
class UnsupportedTypeException extends SpoutException
|
||||
{
|
||||
|
68
src/Spout/Common/Helper/CellTypeHelper.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class CellTypeHelper
|
||||
* This class provides helper functions to determine the type of the cell value
|
||||
*/
|
||||
class CellTypeHelper
|
||||
{
|
||||
/**
|
||||
* @param mixed|null $value
|
||||
* @return bool Whether the given value is considered "empty"
|
||||
*/
|
||||
public static function isEmpty($value)
|
||||
{
|
||||
return ($value === null || $value === '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return bool Whether the given value is a non empty string
|
||||
*/
|
||||
public static function isNonEmptyString($value)
|
||||
{
|
||||
return (\gettype($value) === 'string' && $value !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given value is numeric.
|
||||
* A numeric value is from type "integer" or "double" ("float" is not returned by gettype).
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool Whether the given value is numeric
|
||||
*/
|
||||
public static function isNumeric($value)
|
||||
{
|
||||
$valueType = \gettype($value);
|
||||
|
||||
return ($valueType === 'integer' || $valueType === 'double');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given value is boolean.
|
||||
* "true"/"false" and 0/1 are not booleans.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool Whether the given value is boolean
|
||||
*/
|
||||
public static function isBoolean($value)
|
||||
{
|
||||
return \gettype($value) === 'boolean';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given value is a DateTime or DateInterval object.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool Whether the given value is a DateTime or DateInterval object
|
||||
*/
|
||||
public static function isDateTimeOrDateInterval($value)
|
||||
{
|
||||
return (
|
||||
$value instanceof \DateTimeInterface ||
|
||||
$value instanceof \DateInterval
|
||||
);
|
||||
}
|
||||
}
|
@ -7,24 +7,22 @@ use Box\Spout\Common\Exception\EncodingConversionException;
|
||||
/**
|
||||
* Class EncodingHelper
|
||||
* This class provides helper functions to work with encodings.
|
||||
*
|
||||
* @package Box\Spout\Common\Helper
|
||||
*/
|
||||
class EncodingHelper
|
||||
{
|
||||
/** Definition of the encodings that can have a BOM */
|
||||
const ENCODING_UTF8 = 'UTF-8';
|
||||
const ENCODING_UTF16_LE = 'UTF-16LE';
|
||||
const ENCODING_UTF16_BE = 'UTF-16BE';
|
||||
const ENCODING_UTF32_LE = 'UTF-32LE';
|
||||
const ENCODING_UTF32_BE = 'UTF-32BE';
|
||||
public const ENCODING_UTF8 = 'UTF-8';
|
||||
public const ENCODING_UTF16_LE = 'UTF-16LE';
|
||||
public const ENCODING_UTF16_BE = 'UTF-16BE';
|
||||
public const ENCODING_UTF32_LE = 'UTF-32LE';
|
||||
public const ENCODING_UTF32_BE = 'UTF-32BE';
|
||||
|
||||
/** Definition of the BOMs for the different encodings */
|
||||
const BOM_UTF8 = "\xEF\xBB\xBF";
|
||||
const BOM_UTF16_LE = "\xFF\xFE";
|
||||
const BOM_UTF16_BE = "\xFE\xFF";
|
||||
const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
|
||||
const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
|
||||
public const BOM_UTF8 = "\xEF\xBB\xBF";
|
||||
public const BOM_UTF16_LE = "\xFF\xFE";
|
||||
public const BOM_UTF16_BE = "\xFE\xFF";
|
||||
public const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
|
||||
public const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
@ -59,11 +57,11 @@ class EncodingHelper
|
||||
{
|
||||
$byteOffsetToSkipBom = 0;
|
||||
|
||||
if ($this->hasBom($filePointer, $encoding)) {
|
||||
if ($this->hasBOM($filePointer, $encoding)) {
|
||||
$bomUsed = $this->supportedEncodingsWithBom[$encoding];
|
||||
|
||||
// we skip the N first bytes
|
||||
$byteOffsetToSkipBom = strlen($bomUsed);
|
||||
$byteOffsetToSkipBom = \strlen($bomUsed);
|
||||
}
|
||||
|
||||
return $byteOffsetToSkipBom;
|
||||
@ -82,9 +80,9 @@ class EncodingHelper
|
||||
|
||||
$this->globalFunctionsHelper->rewind($filePointer);
|
||||
|
||||
if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
|
||||
if (\array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
|
||||
$potentialBom = $this->supportedEncodingsWithBom[$encoding];
|
||||
$numBytesInBom = strlen($potentialBom);
|
||||
$numBytesInBom = \strlen($potentialBom);
|
||||
|
||||
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
|
||||
}
|
||||
@ -97,8 +95,8 @@ class EncodingHelper
|
||||
*
|
||||
* @param string $string Non UTF-8 string to be converted
|
||||
* @param string $sourceEncoding The encoding used to encode the source string
|
||||
* @return string The converted, UTF-8 string
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
|
||||
* @return string The converted, UTF-8 string
|
||||
*/
|
||||
public function attemptConversionToUTF8($string, $sourceEncoding)
|
||||
{
|
||||
@ -110,8 +108,8 @@ class EncodingHelper
|
||||
*
|
||||
* @param string $string UTF-8 string to be converted
|
||||
* @param string $targetEncoding The encoding the string should be re-encoded into
|
||||
* @return string The converted string, encoded with the given encoding
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
|
||||
* @return string The converted string, encoded with the given encoding
|
||||
*/
|
||||
public function attemptConversionFromUTF8($string, $targetEncoding)
|
||||
{
|
||||
@ -125,8 +123,8 @@ class EncodingHelper
|
||||
* @param string $string string to be converted
|
||||
* @param string $sourceEncoding The encoding used to encode the source string
|
||||
* @param string $targetEncoding The encoding the string should be re-encoded into
|
||||
* @return string The converted string, encoded with the given encoding
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
|
||||
* @return string The converted string, encoded with the given encoding
|
||||
*/
|
||||
protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
|
||||
{
|
||||
@ -139,7 +137,7 @@ class EncodingHelper
|
||||
|
||||
if ($this->canUseIconv()) {
|
||||
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
|
||||
} else if ($this->canUseMbString()) {
|
||||
} elseif ($this->canUseMbString()) {
|
||||
$convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
|
||||
} else {
|
||||
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\".");
|
||||
|
@ -1,12 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Escaper;
|
||||
namespace Box\Spout\Common\Helper\Escaper;
|
||||
|
||||
/**
|
||||
* Class CSV
|
||||
* Provides functions to escape and unescape data for CSV files
|
||||
*
|
||||
* @package Box\Spout\Common\Escaper
|
||||
*/
|
||||
class CSV implements EscaperInterface
|
||||
{
|
@ -1,11 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Escaper;
|
||||
namespace Box\Spout\Common\Helper\Escaper;
|
||||
|
||||
/**
|
||||
* Interface EscaperInterface
|
||||
*
|
||||
* @package Box\Spout\Common\Escaper
|
||||
*/
|
||||
interface EscaperInterface
|
||||
{
|
59
src/Spout/Common/Helper/Escaper/ODS.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper\Escaper;
|
||||
|
||||
/**
|
||||
* Class ODS
|
||||
* Provides functions to escape and unescape data for ODS files
|
||||
*/
|
||||
class ODS implements EscaperInterface
|
||||
{
|
||||
/**
|
||||
* Escapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
public function escape($string)
|
||||
{
|
||||
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
|
||||
// single/double quotes (for XML attributes) need to be encoded.
|
||||
if (\defined('ENT_DISALLOWED')) {
|
||||
// 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced.
|
||||
// Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor
|
||||
// @link https://github.com/box/spout/issues/329
|
||||
$replacedString = \htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
|
||||
} else {
|
||||
// We are on hhvm or any other engine that does not support ENT_DISALLOWED.
|
||||
$escapedString = \htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||
// some characters should not be escaped though: "\t", "\r" and "\n".
|
||||
$regexPattern = '[\x00-\x08' .
|
||||
// skipping "\t" (0x9) and "\n" (0xA)
|
||||
'\x0B-\x0C' .
|
||||
// skipping "\r" (0xD)
|
||||
'\x0E-\x1F]';
|
||||
$replacedString = \preg_replace("/$regexPattern/", '<27>', $escapedString);
|
||||
}
|
||||
|
||||
return $replacedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
public function unescape($string)
|
||||
{
|
||||
// ==============
|
||||
// = WARNING =
|
||||
// ==============
|
||||
// It is assumed that the given string has already had its XML entities decoded.
|
||||
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
|
||||
// Therefore there is no need to call "htmlspecialchars_decode()".
|
||||
return $string;
|
||||
}
|
||||
}
|
190
src/Spout/Common/Helper/Escaper/XLSX.php
Normal file
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper\Escaper;
|
||||
|
||||
/**
|
||||
* Class XLSX
|
||||
* Provides functions to escape and unescape data for XLSX files
|
||||
*/
|
||||
class XLSX implements EscaperInterface
|
||||
{
|
||||
/** @var bool Whether the escaper has already been initialized */
|
||||
private $isAlreadyInitialized = false;
|
||||
|
||||
/** @var string Regex pattern to detect control characters that need to be escaped */
|
||||
private $escapableControlCharactersPattern;
|
||||
|
||||
/** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
|
||||
private $controlCharactersEscapingMap;
|
||||
|
||||
/** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
|
||||
private $controlCharactersEscapingReverseMap;
|
||||
|
||||
/**
|
||||
* Initializes the control characters if not already done
|
||||
*/
|
||||
protected function initIfNeeded()
|
||||
{
|
||||
if (!$this->isAlreadyInitialized) {
|
||||
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
|
||||
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
|
||||
$this->controlCharactersEscapingReverseMap = \array_flip($this->controlCharactersEscapingMap);
|
||||
|
||||
$this->isAlreadyInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
public function escape($string)
|
||||
{
|
||||
$this->initIfNeeded();
|
||||
|
||||
$escapedString = $this->escapeControlCharacters($string);
|
||||
// @NOTE: Using ENT_QUOTES as XML entities ('<', '>', '&') as well as
|
||||
// single/double quotes (for XML attributes) need to be encoded.
|
||||
$escapedString = \htmlspecialchars($escapedString, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
return $escapedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes the given string to make it compatible with XLSX
|
||||
*
|
||||
* @param string $string The string to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
public function unescape($string)
|
||||
{
|
||||
$this->initIfNeeded();
|
||||
|
||||
// ==============
|
||||
// = WARNING =
|
||||
// ==============
|
||||
// It is assumed that the given string has already had its XML entities decoded.
|
||||
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
|
||||
// Therefore there is no need to call "htmlspecialchars_decode()".
|
||||
$unescapedString = $this->unescapeControlCharacters($string);
|
||||
|
||||
return $unescapedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Regex pattern containing all escapable control characters
|
||||
*/
|
||||
protected function getEscapableControlCharactersPattern()
|
||||
{
|
||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||
// some characters should not be escaped though: "\t", "\r" and "\n".
|
||||
return '[\x00-\x08' .
|
||||
// skipping "\t" (0x9) and "\n" (0xA)
|
||||
'\x0B-\x0C' .
|
||||
// skipping "\r" (0xD)
|
||||
'\x0E-\x1F]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the map containing control characters to be escaped
|
||||
* mapped to their escaped values.
|
||||
* "\t", "\r" and "\n" don't need to be escaped.
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getControlCharactersEscapingMap()
|
||||
{
|
||||
$controlCharactersEscapingMap = [];
|
||||
|
||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||
for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) {
|
||||
$character = \chr($charValue);
|
||||
if (\preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
|
||||
$charHexValue = \dechex($charValue);
|
||||
$escapedChar = '_x' . \sprintf('%04s', \strtoupper($charHexValue)) . '_';
|
||||
$controlCharactersEscapingMap[$escapedChar] = $character;
|
||||
}
|
||||
}
|
||||
|
||||
return $controlCharactersEscapingMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PHP control characters from the given string to OpenXML escaped control characters
|
||||
*
|
||||
* Excel escapes control characters with _xHHHH_ and also escapes any
|
||||
* literal strings of that type by encoding the leading underscore.
|
||||
* So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @param string $string String to escape
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeControlCharacters($string)
|
||||
{
|
||||
$escapedString = $this->escapeEscapeCharacter($string);
|
||||
|
||||
// if no control characters
|
||||
if (!\preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
|
||||
return $escapedString;
|
||||
}
|
||||
|
||||
return \preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function ($matches) {
|
||||
return $this->controlCharactersEscapingReverseMap[$matches[0]];
|
||||
}, $escapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the escape character: "_x0000_" -> "_x005F_x0000_"
|
||||
*
|
||||
* @param string $string String to escape
|
||||
* @return string The escaped string
|
||||
*/
|
||||
protected function escapeEscapeCharacter($string)
|
||||
{
|
||||
return \preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts OpenXML escaped control characters from the given string to PHP control characters
|
||||
*
|
||||
* Excel escapes control characters with _xHHHH_ and also escapes any
|
||||
* literal strings of that type by encoding the leading underscore.
|
||||
* So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
|
||||
*
|
||||
* NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
|
||||
* @see https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
|
||||
*
|
||||
* @param string $string String to unescape
|
||||
* @return string
|
||||
*/
|
||||
protected function unescapeControlCharacters($string)
|
||||
{
|
||||
$unescapedString = $string;
|
||||
|
||||
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
|
||||
// only unescape characters that don't contain the escaped escape character for now
|
||||
$unescapedString = \preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
|
||||
}
|
||||
|
||||
return $this->unescapeEscapeCharacter($unescapedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unecapes the escape character: "_x005F_x0000_" => "_x0000_"
|
||||
*
|
||||
* @param string $string String to unescape
|
||||
* @return string The unescaped string
|
||||
*/
|
||||
protected function unescapeEscapeCharacter($string)
|
||||
{
|
||||
return \preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
|
||||
}
|
||||
}
|
@ -8,20 +8,18 @@ use Box\Spout\Common\Exception\IOException;
|
||||
* Class FileSystemHelper
|
||||
* This class provides helper functions to help with the file system operations
|
||||
* like files/folders creation & deletion
|
||||
*
|
||||
* @package Box\Spout\Common\Helper
|
||||
*/
|
||||
class FileSystemHelper
|
||||
class FileSystemHelper implements FileSystemHelperInterface
|
||||
{
|
||||
/** @var string Path of the base folder where all the I/O can occur */
|
||||
protected $baseFolderPath;
|
||||
/** @var string Real path of the base folder where all the I/O can occur */
|
||||
protected $baseFolderRealPath;
|
||||
|
||||
/**
|
||||
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
|
||||
*/
|
||||
public function __construct($baseFolderPath)
|
||||
public function __construct(string $baseFolderPath)
|
||||
{
|
||||
$this->baseFolderPath = $baseFolderPath;
|
||||
$this->baseFolderRealPath = \realpath($baseFolderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,8 +27,8 @@ class FileSystemHelper
|
||||
*
|
||||
* @param string $parentFolderPath The parent folder path under which the folder is going to be created
|
||||
* @param string $folderName The name of the folder to create
|
||||
* @return string Path of the created folder
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
|
||||
* @return string Path of the created folder
|
||||
*/
|
||||
public function createFolder($parentFolderPath, $folderName)
|
||||
{
|
||||
@ -38,7 +36,7 @@ class FileSystemHelper
|
||||
|
||||
$folderPath = $parentFolderPath . '/' . $folderName;
|
||||
|
||||
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
|
||||
$wasCreationSuccessful = \mkdir($folderPath, 0777, true);
|
||||
if (!$wasCreationSuccessful) {
|
||||
throw new IOException("Unable to create folder: $folderPath");
|
||||
}
|
||||
@ -53,8 +51,8 @@ class FileSystemHelper
|
||||
* @param string $parentFolderPath The parent folder path where the file is going to be created
|
||||
* @param string $fileName The name of the file to create
|
||||
* @param string $fileContents The contents of the file to create
|
||||
* @return string Path of the created file
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
|
||||
* @return string Path of the created file
|
||||
*/
|
||||
public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
|
||||
{
|
||||
@ -62,7 +60,7 @@ class FileSystemHelper
|
||||
|
||||
$filePath = $parentFolderPath . '/' . $fileName;
|
||||
|
||||
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
|
||||
$wasCreationSuccessful = \file_put_contents($filePath, $fileContents);
|
||||
if ($wasCreationSuccessful === false) {
|
||||
throw new IOException("Unable to create file: $filePath");
|
||||
}
|
||||
@ -74,15 +72,15 @@ class FileSystemHelper
|
||||
* Delete the file at the given path
|
||||
*
|
||||
* @param string $filePath Path of the file to delete
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
|
||||
* @return void
|
||||
*/
|
||||
public function deleteFile($filePath)
|
||||
{
|
||||
$this->throwIfOperationNotInBaseFolder($filePath);
|
||||
|
||||
if (file_exists($filePath) && is_file($filePath)) {
|
||||
unlink($filePath);
|
||||
if (\file_exists($filePath) && \is_file($filePath)) {
|
||||
\unlink($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,8 +88,8 @@ class FileSystemHelper
|
||||
* Delete the folder at the given path as well as all its contents
|
||||
*
|
||||
* @param string $folderPath Path of the folder to delete
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
|
||||
* @return void
|
||||
*/
|
||||
public function deleteFolderRecursively($folderPath)
|
||||
{
|
||||
@ -104,13 +102,13 @@ class FileSystemHelper
|
||||
|
||||
foreach ($itemIterator as $item) {
|
||||
if ($item->isDir()) {
|
||||
rmdir($item->getPathname());
|
||||
\rmdir($item->getPathname());
|
||||
} else {
|
||||
unlink($item->getPathname());
|
||||
\unlink($item->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($folderPath);
|
||||
\rmdir($folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,14 +117,19 @@ class FileSystemHelper
|
||||
* should occur is not inside the base folder.
|
||||
*
|
||||
* @param string $operationFolderPath The path of the folder where the I/O operation should occur
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur
|
||||
* is not inside the base folder or the base folder does not exist
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder
|
||||
*/
|
||||
protected function throwIfOperationNotInBaseFolder($operationFolderPath)
|
||||
protected function throwIfOperationNotInBaseFolder(string $operationFolderPath)
|
||||
{
|
||||
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0);
|
||||
$operationFolderRealPath = \realpath($operationFolderPath);
|
||||
if (!$this->baseFolderRealPath) {
|
||||
throw new IOException("The base folder path is invalid: {$this->baseFolderRealPath}");
|
||||
}
|
||||
$isInBaseFolder = (\strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
|
||||
if (!$isInBaseFolder) {
|
||||
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderPath}");
|
||||
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
51
src/Spout/Common/Helper/FileSystemHelperInterface.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class FileSystemHelperInterface
|
||||
* This interface describes helper functions to help with the file system operations
|
||||
* like files/folders creation & deletion
|
||||
*/
|
||||
interface FileSystemHelperInterface
|
||||
{
|
||||
/**
|
||||
* Creates an empty folder with the given name under the given parent folder.
|
||||
*
|
||||
* @param string $parentFolderPath The parent folder path under which the folder is going to be created
|
||||
* @param string $folderName The name of the folder to create
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
|
||||
* @return string Path of the created folder
|
||||
*/
|
||||
public function createFolder($parentFolderPath, $folderName);
|
||||
|
||||
/**
|
||||
* Creates a file with the given name and content in the given folder.
|
||||
* The parent folder must exist.
|
||||
*
|
||||
* @param string $parentFolderPath The parent folder path where the file is going to be created
|
||||
* @param string $fileName The name of the file to create
|
||||
* @param string $fileContents The contents of the file to create
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
|
||||
* @return string Path of the created file
|
||||
*/
|
||||
public function createFileWithContents($parentFolderPath, $fileName, $fileContents);
|
||||
|
||||
/**
|
||||
* Delete the file at the given path
|
||||
*
|
||||
* @param string $filePath Path of the file to delete
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
|
||||
* @return void
|
||||
*/
|
||||
public function deleteFile($filePath);
|
||||
|
||||
/**
|
||||
* Delete the folder at the given path as well as all its contents
|
||||
*
|
||||
* @param string $folderPath Path of the folder to delete
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
|
||||
* @return void
|
||||
*/
|
||||
public function deleteFolderRecursively($folderPath);
|
||||
}
|
@ -7,8 +7,6 @@ namespace Box\Spout\Common\Helper;
|
||||
* This class wraps global functions to facilitate testing
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @package Box\Spout\Common\Helper
|
||||
*/
|
||||
class GlobalFunctionsHelper
|
||||
{
|
||||
@ -22,7 +20,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function fopen($fileName, $mode)
|
||||
{
|
||||
return fopen($fileName, $mode);
|
||||
return \fopen($fileName, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,12 +28,12 @@ class GlobalFunctionsHelper
|
||||
* @see fgets()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int|void $length
|
||||
* @param int|null $length
|
||||
* @return string
|
||||
*/
|
||||
public function fgets($handle, $length = null)
|
||||
{
|
||||
return fgets($handle, $length);
|
||||
return \fgets($handle, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,7 +46,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function fputs($handle, $string)
|
||||
{
|
||||
return fputs($handle, $string);
|
||||
return \fputs($handle, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,7 +58,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function fflush($handle)
|
||||
{
|
||||
return fflush($handle);
|
||||
return \fflush($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +71,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function fseek($handle, $offset)
|
||||
{
|
||||
return fseek($handle, $offset);
|
||||
return \fseek($handle, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,14 +79,20 @@ class GlobalFunctionsHelper
|
||||
* @see fgetcsv()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int|void $length
|
||||
* @param string|void $delimiter
|
||||
* @param string|void $enclosure
|
||||
* @return array
|
||||
* @param int|null $length
|
||||
* @param string|null $delimiter
|
||||
* @param string|null $enclosure
|
||||
* @return array|false
|
||||
*/
|
||||
public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null)
|
||||
{
|
||||
return fgetcsv($handle, $length, $delimiter, $enclosure);
|
||||
// PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
|
||||
// To fix that, simply disable the escape character.
|
||||
// @see https://bugs.php.net/bug.php?id=43225
|
||||
// @see http://tools.ietf.org/html/rfc4180
|
||||
$escapeCharacter = PHP_VERSION_ID >= 70400 ? '' : "\0";
|
||||
|
||||
return \fgetcsv($handle, $length, $delimiter, $enclosure, $escapeCharacter);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,13 +101,19 @@ class GlobalFunctionsHelper
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param array $fields
|
||||
* @param string|void $delimiter
|
||||
* @param string|void $enclosure
|
||||
* @return int
|
||||
* @param string|null $delimiter
|
||||
* @param string|null $enclosure
|
||||
* @return int|false
|
||||
*/
|
||||
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
|
||||
{
|
||||
return fputcsv($handle, $fields, $delimiter, $enclosure);
|
||||
// PHP uses '\' as the default escape character. This is not RFC-4180 compliant...
|
||||
// To fix that, simply disable the escape character.
|
||||
// @see https://bugs.php.net/bug.php?id=43225
|
||||
// @see http://tools.ietf.org/html/rfc4180
|
||||
$escapeCharacter = PHP_VERSION_ID >= 70400 ? '' : "\0";
|
||||
|
||||
return \fputcsv($handle, $fields, $delimiter, $enclosure, $escapeCharacter);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,7 +126,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function fwrite($handle, $string)
|
||||
{
|
||||
return fwrite($handle, $string);
|
||||
return \fwrite($handle, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,7 +138,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function fclose($handle)
|
||||
{
|
||||
return fclose($handle);
|
||||
return \fclose($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,7 +150,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function rewind($handle)
|
||||
{
|
||||
return rewind($handle);
|
||||
return \rewind($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,7 +162,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function file_exists($fileName)
|
||||
{
|
||||
return file_exists($fileName);
|
||||
return \file_exists($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,7 +174,56 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function file_get_contents($filePath)
|
||||
{
|
||||
return file_get_contents($filePath);
|
||||
$realFilePath = $this->convertToUseRealPath($filePath);
|
||||
|
||||
return \file_get_contents($realFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given file path to use a real path.
|
||||
* This is to avoid issues on some Windows setup.
|
||||
*
|
||||
* @param string $filePath File path
|
||||
* @return string The file path using a real path
|
||||
*/
|
||||
protected function convertToUseRealPath($filePath)
|
||||
{
|
||||
$realFilePath = $filePath;
|
||||
|
||||
if ($this->isZipStream($filePath)) {
|
||||
if (\preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
|
||||
$documentPath = $matches[1];
|
||||
$documentInsideZipPath = $matches[2];
|
||||
$realFilePath = 'zip://' . \realpath($documentPath) . '#' . $documentInsideZipPath;
|
||||
}
|
||||
} else {
|
||||
$realFilePath = \realpath($filePath);
|
||||
}
|
||||
|
||||
return $realFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given path is a zip stream.
|
||||
*
|
||||
* @param string $path Path pointing to a document
|
||||
* @return bool TRUE if path is a zip stream, FALSE otherwise
|
||||
*/
|
||||
protected function isZipStream($path)
|
||||
{
|
||||
return (\strpos($path, 'zip://') === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function feof()
|
||||
* @see feof()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @return bool
|
||||
*/
|
||||
public function feof($handle)
|
||||
{
|
||||
return \feof($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,7 +235,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function is_readable($fileName)
|
||||
{
|
||||
return is_readable($fileName);
|
||||
return \is_readable($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,12 +243,12 @@ class GlobalFunctionsHelper
|
||||
* @see basename()
|
||||
*
|
||||
* @param string $path
|
||||
* @param string|void $suffix
|
||||
* @param string $suffix
|
||||
* @return string
|
||||
*/
|
||||
public function basename($path, $suffix = null)
|
||||
public function basename($path, $suffix = '')
|
||||
{
|
||||
return basename($path, $suffix);
|
||||
return \basename($path, $suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -201,7 +260,20 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function header($string)
|
||||
{
|
||||
header($string);
|
||||
\header($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function ob_end_clean()
|
||||
* @see ob_end_clean()
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ob_end_clean()
|
||||
{
|
||||
if (\ob_get_length() > 0) {
|
||||
\ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,7 +287,7 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function iconv($string, $sourceEncoding, $targetEncoding)
|
||||
{
|
||||
return iconv($sourceEncoding, $targetEncoding, $string);
|
||||
return \iconv($sourceEncoding, $targetEncoding, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,36 +301,18 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
|
||||
{
|
||||
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
|
||||
return \mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function stream_get_line()
|
||||
* @see stream_get_line()
|
||||
* Wrapper around global function stream_get_wrappers()
|
||||
* @see stream_get_wrappers()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $length
|
||||
* @param string|void $ending
|
||||
* @return string|bool
|
||||
*/
|
||||
public function stream_get_line($handle, $length, $ending = null)
|
||||
{
|
||||
return stream_get_line($handle, $length, $ending);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function str_getcsv()
|
||||
* @see str_getcsv()
|
||||
*
|
||||
* @param string $input
|
||||
* @param string|void $delimiter
|
||||
* @param string|void $enclosure
|
||||
* @param string|void $escape
|
||||
* @return array
|
||||
*/
|
||||
public function str_getcsv($input, $delimiter = null, $enclosure = null, $escape = null)
|
||||
public function stream_get_wrappers()
|
||||
{
|
||||
return str_getcsv($input, $delimiter, $enclosure, $escape);
|
||||
return \stream_get_wrappers();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,6 +324,6 @@ class GlobalFunctionsHelper
|
||||
*/
|
||||
public function function_exists($functionName)
|
||||
{
|
||||
return function_exists($functionName);
|
||||
return \function_exists($functionName);
|
||||
}
|
||||
}
|
||||
|
105
src/Spout/Common/Helper/StringHelper.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Helper;
|
||||
|
||||
/**
|
||||
* Class StringHelper
|
||||
* This class provides helper functions to work with strings and multibyte strings.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class StringHelper
|
||||
{
|
||||
/** @var bool Whether the mbstring extension is loaded */
|
||||
protected $hasMbstringSupport;
|
||||
|
||||
/** @var bool Whether the code is running with PHP7 or older versions */
|
||||
private $isRunningPhp7OrOlder;
|
||||
|
||||
/** @var array Locale info, used for number formatting */
|
||||
private $localeInfo;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->hasMbstringSupport = \extension_loaded('mbstring');
|
||||
$this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0;
|
||||
$this->localeInfo = \localeconv();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the given string.
|
||||
* It uses the multi-bytes function is available.
|
||||
* @see strlen
|
||||
* @see mb_strlen
|
||||
*
|
||||
* @param string $string
|
||||
* @return int
|
||||
*/
|
||||
public function getStringLength($string)
|
||||
{
|
||||
return $this->hasMbstringSupport ? \mb_strlen($string) : \strlen($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the first occurrence of the given character/substring within the given string.
|
||||
* It uses the multi-bytes function is available.
|
||||
* @see strpos
|
||||
* @see mb_strpos
|
||||
*
|
||||
* @param string $char Needle
|
||||
* @param string $string Haystack
|
||||
* @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found
|
||||
*/
|
||||
public function getCharFirstOccurrencePosition($char, $string)
|
||||
{
|
||||
$position = $this->hasMbstringSupport ? \mb_strpos($string, $char) : \strpos($string, $char);
|
||||
|
||||
return ($position !== false) ? $position : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the last occurrence of the given character/substring within the given string.
|
||||
* It uses the multi-bytes function is available.
|
||||
* @see strrpos
|
||||
* @see mb_strrpos
|
||||
*
|
||||
* @param string $char Needle
|
||||
* @param string $string Haystack
|
||||
* @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found
|
||||
*/
|
||||
public function getCharLastOccurrencePosition($char, $string)
|
||||
{
|
||||
$position = $this->hasMbstringSupport ? \mb_strrpos($string, $char) : \strrpos($string, $char);
|
||||
|
||||
return ($position !== false) ? $position : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
68
src/Spout/Common/Manager/OptionsManagerAbstract.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Manager;
|
||||
|
||||
/**
|
||||
* Class OptionsManager
|
||||
*/
|
||||
abstract class OptionsManagerAbstract implements OptionsManagerInterface
|
||||
{
|
||||
public const PREFIX_OPTION = 'OPTION_';
|
||||
|
||||
/** @var string[] List of all supported option names */
|
||||
private $supportedOptions = [];
|
||||
|
||||
/** @var array Associative array [OPTION_NAME => OPTION_VALUE] */
|
||||
private $options = [];
|
||||
|
||||
/**
|
||||
* OptionsManagerAbstract constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->supportedOptions = $this->getSupportedOptions();
|
||||
$this->setDefaultOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array List of supported options
|
||||
*/
|
||||
abstract protected function getSupportedOptions();
|
||||
|
||||
/**
|
||||
* Sets the default options.
|
||||
* To be overriden by child classes
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function setDefaultOptions();
|
||||
|
||||
/**
|
||||
* Sets the given option, if this option is supported.
|
||||
*
|
||||
* @param string $optionName
|
||||
* @param mixed $optionValue
|
||||
* @return void
|
||||
*/
|
||||
public function setOption($optionName, $optionValue)
|
||||
{
|
||||
if (\in_array($optionName, $this->supportedOptions)) {
|
||||
$this->options[$optionName] = $optionValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $optionName
|
||||
* @return mixed|null The set option or NULL if no option with given name found
|
||||
*/
|
||||
public function getOption($optionName)
|
||||
{
|
||||
$optionValue = null;
|
||||
|
||||
if (isset($this->options[$optionName])) {
|
||||
$optionValue = $this->options[$optionName];
|
||||
}
|
||||
|
||||
return $optionValue;
|
||||
}
|
||||
}
|
22
src/Spout/Common/Manager/OptionsManagerInterface.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Common\Manager;
|
||||
|
||||
/**
|
||||
* Interface OptionsManagerInterface
|
||||
*/
|
||||
interface OptionsManagerInterface
|
||||
{
|
||||
/**
|
||||
* @param string $optionName
|
||||
* @param mixed $optionValue
|
||||
* @return void
|
||||
*/
|
||||
public function setOption($optionName, $optionValue);
|
||||
|
||||
/**
|
||||
* @param string $optionName
|
||||
* @return mixed|null The set option or NULL if no option with given name found
|
||||
*/
|
||||
public function getOption($optionName);
|
||||
}
|
@ -8,6 +8,7 @@ namespace Box\Spout\Common;
|
||||
*/
|
||||
abstract class Type
|
||||
{
|
||||
const CSV = "csv";
|
||||
const XLSX = "xlsx";
|
||||
public const CSV = 'csv';
|
||||
public const XLSX = 'xlsx';
|
||||
public const ODS = 'ods';
|
||||
}
|
||||
|
@ -1,125 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
|
||||
|
||||
/**
|
||||
* Class AbstractReader
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
* @abstract
|
||||
*/
|
||||
abstract class AbstractReader implements ReaderInterface
|
||||
{
|
||||
/** @var bool Indicates whether the stream is currently open */
|
||||
protected $isStreamOpened = false;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function openReader($filePath);
|
||||
|
||||
/**
|
||||
* Returns an iterator to iterate over sheets.
|
||||
*
|
||||
* @return \Iterator To iterate over sheets
|
||||
*/
|
||||
abstract public function getConcreteSheetIterator();
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
*
|
||||
* @return AbstractReader
|
||||
*/
|
||||
abstract protected function closeReader();
|
||||
|
||||
/**
|
||||
* @param $globalFunctionsHelper
|
||||
* @return AbstractReader
|
||||
*/
|
||||
public function setGlobalFunctionsHelper($globalFunctionsHelper)
|
||||
{
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the reader to read the given file. It also makes sure
|
||||
* that the file exists and is readable.
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
||||
*/
|
||||
public function open($filePath)
|
||||
{
|
||||
if (!$this->isPhpStream($filePath)) {
|
||||
// we skip the checks if the provided file path points to a PHP stream
|
||||
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
||||
throw new IOException("Could not open $filePath for reading! File does not exist.");
|
||||
} else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
|
||||
throw new IOException("Could not open $filePath for reading! File is not readable.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->openReader($filePath);
|
||||
$this->isStreamOpened = true;
|
||||
} catch (\Exception $exception) {
|
||||
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path maps to a PHP stream
|
||||
*/
|
||||
protected function isPhpStream($filePath)
|
||||
{
|
||||
return (strpos($filePath, 'php://') === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator to iterate over sheets.
|
||||
*
|
||||
* @return \Iterator To iterate over sheets
|
||||
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
|
||||
*/
|
||||
public function getSheetIterator()
|
||||
{
|
||||
if (!$this->isStreamOpened) {
|
||||
throw new ReaderNotOpenedException('Reader should be opened first.');
|
||||
}
|
||||
|
||||
return $this->getConcreteSheetIterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader, preventing any additional reading
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if ($this->isStreamOpened) {
|
||||
$this->closeReader();
|
||||
|
||||
$sheetIterator = $this->getConcreteSheetIterator();
|
||||
if ($sheetIterator) {
|
||||
$sheetIterator->end();
|
||||
}
|
||||
|
||||
$this->isStreamOpened = false;
|
||||
}
|
||||
}
|
||||
}
|
98
src/Spout/Reader/CSV/Creator/InternalEntityFactory.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV\Creator;
|
||||
|
||||
use Box\Spout\Common\Creator\HelperFactory;
|
||||
use Box\Spout\Common\Entity\Cell;
|
||||
use Box\Spout\Common\Entity\Row;
|
||||
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
|
||||
use Box\Spout\Common\Manager\OptionsManagerInterface;
|
||||
use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
|
||||
use Box\Spout\Reader\CSV\RowIterator;
|
||||
use Box\Spout\Reader\CSV\Sheet;
|
||||
use Box\Spout\Reader\CSV\SheetIterator;
|
||||
|
||||
/**
|
||||
* Class EntityFactory
|
||||
* Factory to create entities
|
||||
*/
|
||||
class InternalEntityFactory implements InternalEntityFactoryInterface
|
||||
{
|
||||
/** @var HelperFactory */
|
||||
private $helperFactory;
|
||||
|
||||
/**
|
||||
* @param HelperFactory $helperFactory
|
||||
*/
|
||||
public function __construct(HelperFactory $helperFactory)
|
||||
{
|
||||
$this->helperFactory = $helperFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param OptionsManagerInterface $optionsManager
|
||||
* @param GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @return SheetIterator
|
||||
*/
|
||||
public function createSheetIterator($filePointer, $optionsManager, $globalFunctionsHelper)
|
||||
{
|
||||
$rowIterator = $this->createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper);
|
||||
$sheet = $this->createSheet($rowIterator);
|
||||
|
||||
return new SheetIterator($sheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RowIterator $rowIterator
|
||||
* @return Sheet
|
||||
*/
|
||||
private function createSheet($rowIterator)
|
||||
{
|
||||
return new Sheet($rowIterator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param OptionsManagerInterface $optionsManager
|
||||
* @param GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @return RowIterator
|
||||
*/
|
||||
private function createRowIterator($filePointer, $optionsManager, $globalFunctionsHelper)
|
||||
{
|
||||
$encodingHelper = $this->helperFactory->createEncodingHelper($globalFunctionsHelper);
|
||||
|
||||
return new RowIterator($filePointer, $optionsManager, $encodingHelper, $this, $globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cell[] $cells
|
||||
* @return Row
|
||||
*/
|
||||
public function createRow(array $cells = [])
|
||||
{
|
||||
return new Row($cells, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $cellValue
|
||||
* @return Cell
|
||||
*/
|
||||
public function createCell($cellValue)
|
||||
{
|
||||
return new Cell($cellValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $cellValues
|
||||
* @return Row
|
||||
*/
|
||||
public function createRowFromArray(array $cellValues = [])
|
||||
{
|
||||
$cells = \array_map(function ($cellValue) {
|
||||
return $this->createCell($cellValue);
|
||||
}, $cellValues);
|
||||
|
||||
return $this->createRow($cells);
|
||||
}
|
||||
}
|
40
src/Spout/Reader/CSV/Manager/OptionsManager.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV\Manager;
|
||||
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
use Box\Spout\Common\Manager\OptionsManagerAbstract;
|
||||
use Box\Spout\Reader\Common\Entity\Options;
|
||||
|
||||
/**
|
||||
* Class OptionsManager
|
||||
* CSV Reader options manager
|
||||
*/
|
||||
class OptionsManager extends OptionsManagerAbstract
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSupportedOptions()
|
||||
{
|
||||
return [
|
||||
Options::SHOULD_FORMAT_DATES,
|
||||
Options::SHOULD_PRESERVE_EMPTY_ROWS,
|
||||
Options::FIELD_DELIMITER,
|
||||
Options::FIELD_ENCLOSURE,
|
||||
Options::ENCODING,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setDefaultOptions()
|
||||
{
|
||||
$this->setOption(Options::SHOULD_FORMAT_DATES, false);
|
||||
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
|
||||
$this->setOption(Options::FIELD_DELIMITER, ',');
|
||||
$this->setOption(Options::FIELD_ENCLOSURE, '"');
|
||||
$this->setOption(Options::ENCODING, EncodingHelper::ENCODING_UTF8);
|
||||
}
|
||||
}
|
@ -2,17 +2,19 @@
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Reader\AbstractReader;
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
|
||||
use Box\Spout\Common\Manager\OptionsManagerInterface;
|
||||
use Box\Spout\Reader\Common\Creator\InternalEntityFactoryInterface;
|
||||
use Box\Spout\Reader\Common\Entity\Options;
|
||||
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
|
||||
use Box\Spout\Reader\ReaderAbstract;
|
||||
|
||||
/**
|
||||
* Class Reader
|
||||
* This class provides support to read data from a CSV file.
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class Reader extends AbstractReader
|
||||
class Reader extends ReaderAbstract
|
||||
{
|
||||
/** @var resource Pointer to the file to be written */
|
||||
protected $filePointer;
|
||||
@ -20,14 +22,25 @@ class Reader extends AbstractReader
|
||||
/** @var SheetIterator To iterator over the CSV unique "sheet" */
|
||||
protected $sheetIterator;
|
||||
|
||||
/** @var string Defines the character used to delimit fields (one character only) */
|
||||
protected $fieldDelimiter = ',';
|
||||
/** @var string Original value for the "auto_detect_line_endings" INI value */
|
||||
protected $originalAutoDetectLineEndings;
|
||||
|
||||
/** @var string Defines the character used to enclose fields (one character only) */
|
||||
protected $fieldEnclosure = '"';
|
||||
/** @var bool Whether the code is running with PHP >= 8.1 */
|
||||
private $isRunningAtLeastPhp81;
|
||||
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
||||
/**
|
||||
* @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.
|
||||
@ -38,7 +51,8 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
public function setFieldDelimiter($fieldDelimiter)
|
||||
{
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
$this->optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -51,7 +65,8 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
public function setFieldEnclosure($fieldEnclosure)
|
||||
{
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
$this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -64,30 +79,48 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
$this->encoding = $encoding;
|
||||
$this->optionsManager->setOption(Options::ENCODING, $encoding);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given path to make it ready to be read.
|
||||
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
|
||||
*
|
||||
* @param string $filePath Path of the CSV file to be read
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException
|
||||
* @return void
|
||||
*/
|
||||
protected function openReader($filePath)
|
||||
{
|
||||
// "auto_detect_line_endings" is deprecated in PHP 8.1
|
||||
if (!$this->isRunningAtLeastPhp81) {
|
||||
$this->originalAutoDetectLineEndings = \ini_get('auto_detect_line_endings');
|
||||
\ini_set('auto_detect_line_endings', '1');
|
||||
}
|
||||
|
||||
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
|
||||
if (!$this->filePointer) {
|
||||
throw new IOException("Could not open file $filePath for reading.");
|
||||
}
|
||||
|
||||
$this->sheetIterator = new SheetIterator(
|
||||
/** @var InternalEntityFactory $entityFactory */
|
||||
$entityFactory = $this->entityFactory;
|
||||
|
||||
$this->sheetIterator = $entityFactory->createSheetIterator(
|
||||
$this->filePointer,
|
||||
$this->fieldDelimiter,
|
||||
$this->fieldEnclosure,
|
||||
$this->encoding,
|
||||
$this->optionsManager,
|
||||
$this->globalFunctionsHelper
|
||||
);
|
||||
}
|
||||
@ -97,12 +130,11 @@ class Reader extends AbstractReader
|
||||
*
|
||||
* @return SheetIterator To iterate over sheets
|
||||
*/
|
||||
public function getConcreteSheetIterator()
|
||||
protected function getConcreteSheetIterator()
|
||||
{
|
||||
return $this->sheetIterator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
*
|
||||
@ -110,8 +142,13 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
protected function closeReader()
|
||||
{
|
||||
if ($this->filePointer) {
|
||||
if (is_resource($this->filePointer)) {
|
||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||
}
|
||||
|
||||
// "auto_detect_line_endings" is deprecated in PHP 8.1
|
||||
if (!$this->isRunningAtLeastPhp81) {
|
||||
\ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,25 +2,33 @@
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
use Box\Spout\Common\Entity\Row;
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
|
||||
use Box\Spout\Common\Manager\OptionsManagerInterface;
|
||||
use Box\Spout\Reader\Common\Entity\Options;
|
||||
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory;
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
|
||||
/**
|
||||
* Class RowIterator
|
||||
* Iterate over CSV rows.
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class RowIterator implements IteratorInterface
|
||||
{
|
||||
/** @var resource Pointer to the CSV file to read */
|
||||
/**
|
||||
* Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines).
|
||||
*/
|
||||
public const MAX_READ_BYTES_PER_LINE = 0;
|
||||
|
||||
/** @var resource|null Pointer to the CSV file to read */
|
||||
protected $filePointer;
|
||||
|
||||
/** @var int Number of read rows */
|
||||
protected $numReadRows = 0;
|
||||
|
||||
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
|
||||
protected $rowDataBuffer = null;
|
||||
/** @var Row|null Buffer used to store the current row, while checking if there are more rows to read */
|
||||
protected $rowBuffer;
|
||||
|
||||
/** @var bool Indicates whether all rows have been read */
|
||||
protected $hasReachedEndOfFile = false;
|
||||
@ -34,42 +42,54 @@ class RowIterator implements IteratorInterface
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */
|
||||
protected $encodingHelper;
|
||||
|
||||
/** @var \Box\Spout\Reader\CSV\Creator\InternalEntityFactory Factory to create entities */
|
||||
protected $entityFactory;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @param OptionsManagerInterface $optionsManager
|
||||
* @param EncodingHelper $encodingHelper
|
||||
* @param InternalEntityFactory $entityFactory
|
||||
* @param GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||
{
|
||||
public function __construct(
|
||||
$filePointer,
|
||||
OptionsManagerInterface $optionsManager,
|
||||
EncodingHelper $encodingHelper,
|
||||
InternalEntityFactory $entityFactory,
|
||||
GlobalFunctionsHelper $globalFunctionsHelper
|
||||
) {
|
||||
$this->filePointer = $filePointer;
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
$this->encoding = $encoding;
|
||||
$this->fieldDelimiter = $optionsManager->getOption(Options::FIELD_DELIMITER);
|
||||
$this->fieldEnclosure = $optionsManager->getOption(Options::FIELD_ENCLOSURE);
|
||||
$this->encoding = $optionsManager->getOption(Options::ENCODING);
|
||||
$this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
|
||||
$this->encodingHelper = $encodingHelper;
|
||||
$this->entityFactory = $entityFactory;
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
|
||||
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
* @see http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
public function rewind() : void
|
||||
{
|
||||
$this->rewindAndSkipBom();
|
||||
|
||||
$this->numReadRows = 0;
|
||||
$this->rowDataBuffer = null;
|
||||
$this->rowBuffer = null;
|
||||
|
||||
$this->next();
|
||||
}
|
||||
@ -90,86 +110,132 @@ class RowIterator implements IteratorInterface
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
* @see http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
public function valid() : bool
|
||||
{
|
||||
return ($this->filePointer && !$this->hasReachedEndOfFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element. Empty rows are skipped.
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
* Move forward to next element. Reads data for the next unprocessed row.
|
||||
* @see http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
public function next() : void
|
||||
{
|
||||
$lineData = null;
|
||||
$this->hasReachedEndOfFile = feof($this->filePointer);
|
||||
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
|
||||
if (!$this->hasReachedEndOfFile) {
|
||||
do {
|
||||
$utf8EncodedLineData = $this->getNextUTF8EncodedLine();
|
||||
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
} while ($lineData === false || ($lineData !== null && $this->isEmptyLine($lineData)));
|
||||
|
||||
if ($lineData !== false && $lineData !== null) {
|
||||
$this->rowDataBuffer = $lineData;
|
||||
$this->numReadRows++;
|
||||
}
|
||||
$this->readDataForNextRow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next line, converted if necessary to UTF-8.
|
||||
* Neither fgets nor fgetcsv don't work with non UTF-8 data... so we need to do some things manually.
|
||||
*
|
||||
* @return string The next line for the current file pointer, encoded in UTF-8
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
* @return void
|
||||
*/
|
||||
protected function getNextUTF8EncodedLine()
|
||||
protected function readDataForNextRow()
|
||||
{
|
||||
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
|
||||
$encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8("\n", $this->encoding);
|
||||
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, 0, $encodedEOLDelimiter);
|
||||
do {
|
||||
$rowData = $this->getNextUTF8EncodedRow();
|
||||
} while ($this->shouldReadNextRow($rowData));
|
||||
|
||||
// Once the line has been read, it can be converted to UTF-8
|
||||
$utf8EncodedLineData = $this->encodingHelper->attemptConversionToUTF8($encodedLineData, $this->encoding);
|
||||
|
||||
return $utf8EncodedLineData;
|
||||
if ($rowData !== false) {
|
||||
// array_map will replace NULL values by empty strings
|
||||
$rowDataBufferAsArray = array_map(function ($value) { return (string) $value; }, $rowData);
|
||||
$this->rowBuffer = $this->entityFactory->createRowFromArray($rowDataBufferAsArray);
|
||||
$this->numReadRows++;
|
||||
} else {
|
||||
// If we reach this point, it means end of file was reached.
|
||||
// This happens when the last lines are empty lines.
|
||||
$this->hasReachedEndOfFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $lineData Array containing the cells value for the line
|
||||
* @param array|bool $currentRowData
|
||||
* @return bool Whether the data for the current row can be returned or if we need to keep reading
|
||||
*/
|
||||
protected function shouldReadNextRow($currentRowData)
|
||||
{
|
||||
$hasSuccessfullyFetchedRowData = ($currentRowData !== false);
|
||||
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
$isEmptyLine = $this->isEmptyLine($currentRowData);
|
||||
|
||||
return (
|
||||
(!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) ||
|
||||
(!$this->shouldPreserveEmptyRows && $isEmptyLine)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next row, converted if necessary to UTF-8.
|
||||
* As fgetcsv() does not manage correctly encoding for non UTF-8 data,
|
||||
* we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes)
|
||||
*
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
* @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
|
||||
*/
|
||||
protected function getNextUTF8EncodedRow()
|
||||
{
|
||||
$encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
if ($encodedRowData === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($encodedRowData as $cellIndex => $cellValue) {
|
||||
switch ($this->encoding) {
|
||||
case EncodingHelper::ENCODING_UTF16_LE:
|
||||
case EncodingHelper::ENCODING_UTF32_LE:
|
||||
// remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
|
||||
$cellValue = \ltrim($cellValue);
|
||||
break;
|
||||
|
||||
case EncodingHelper::ENCODING_UTF16_BE:
|
||||
case EncodingHelper::ENCODING_UTF32_BE:
|
||||
// remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
|
||||
$cellValue = \rtrim($cellValue);
|
||||
break;
|
||||
}
|
||||
|
||||
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
|
||||
}
|
||||
|
||||
return $encodedRowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|bool $lineData Array containing the cells value for the line
|
||||
* @return bool Whether the given line is empty
|
||||
*/
|
||||
protected function isEmptyLine($lineData)
|
||||
{
|
||||
return (count($lineData) === 1 && $lineData[0] === null);
|
||||
return (\is_array($lineData) && \count($lineData) === 1 && $lineData[0] === null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element from the buffer
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
* @see http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return array|null
|
||||
* @return Row|null
|
||||
*/
|
||||
public function current()
|
||||
public function current() : ?Row
|
||||
{
|
||||
return $this->rowDataBuffer;
|
||||
return $this->rowBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
* @see http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
public function key() : int
|
||||
{
|
||||
return $this->numReadRows;
|
||||
}
|
||||
@ -179,7 +245,7 @@ class RowIterator implements IteratorInterface
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
public function end() : void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
@ -6,31 +6,57 @@ use Box\Spout\Reader\SheetInterface;
|
||||
|
||||
/**
|
||||
* Class Sheet
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class Sheet implements SheetInterface
|
||||
{
|
||||
/** @var RowIterator To iterate over the CSV's rows */
|
||||
/** @var \Box\Spout\Reader\CSV\RowIterator To iterate over the CSV's rows */
|
||||
protected $rowIterator;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @param RowIterator $rowIterator Corresponding row iterator
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||
public function __construct(RowIterator $rowIterator)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
|
||||
$this->rowIterator = $rowIterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RowIterator
|
||||
* @return \Box\Spout\Reader\CSV\RowIterator
|
||||
*/
|
||||
public function getRowIterator()
|
||||
{
|
||||
return $this->rowIterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Index of the sheet
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Name of the sheet - empty string since CSV does not support that
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Always TRUE as there is only one sheet
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Always TRUE as the only sheet is always visible
|
||||
*/
|
||||
public function isVisible()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ use Box\Spout\Reader\IteratorInterface;
|
||||
/**
|
||||
* Class SheetIterator
|
||||
* Iterate over CSV unique "sheet".
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class SheetIterator implements IteratorInterface
|
||||
{
|
||||
@ -19,68 +17,64 @@ class SheetIterator implements IteratorInterface
|
||||
protected $hasReadUniqueSheet = false;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @param Sheet $sheet Corresponding unique sheet
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||
public function __construct($sheet)
|
||||
{
|
||||
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
|
||||
$this->sheet = $sheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
* @see http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
public function rewind() : void
|
||||
{
|
||||
$this->hasReadUniqueSheet = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
* @see http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
public function valid() : bool
|
||||
{
|
||||
return (!$this->hasReadUniqueSheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
* @see http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
public function next() : void
|
||||
{
|
||||
$this->hasReadUniqueSheet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
* @see http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return Sheet
|
||||
*/
|
||||
public function current()
|
||||
public function current() : Sheet
|
||||
{
|
||||
return $this->sheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
* @see http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
public function key() : int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
@ -90,7 +84,7 @@ class SheetIterator implements IteratorInterface
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function end()
|
||||
public function end() : void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Common\Creator;
|
||||
|
||||
use Box\Spout\Common\Entity\Cell;
|
||||
use Box\Spout\Common\Entity\Row;
|
||||
|
||||
/**
|
||||
* Interface EntityFactoryInterface
|
||||
*/
|
||||
interface InternalEntityFactoryInterface
|
||||
{
|
||||
/**
|
||||
* @param Cell[] $cells
|
||||
* @return Row
|
||||
*/
|
||||
public function createRow(array $cells = []);
|
||||
|
||||
/**
|
||||
* @param mixed $cellValue
|
||||
* @return Cell
|
||||
*/
|
||||
public function createCell($cellValue);
|
||||
}
|
71
src/Spout/Reader/Common/Creator/ReaderEntityFactory.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Common\Creator;
|
||||
|
||||
use Box\Spout\Common\Exception\UnsupportedTypeException;
|
||||
use Box\Spout\Common\Type;
|
||||
use Box\Spout\Reader\ReaderInterface;
|
||||
|
||||
/**
|
||||
* Class ReaderEntityFactory
|
||||
* Factory to create external entities
|
||||
*/
|
||||
class ReaderEntityFactory
|
||||
{
|
||||
/**
|
||||
* Creates a reader by file extension
|
||||
*
|
||||
* @param string $path The path to the spreadsheet file. Supported extensions are .csv, .ods and .xlsx
|
||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||
* @return ReaderInterface
|
||||
*/
|
||||
public static function createReaderFromFile(string $path)
|
||||
{
|
||||
return ReaderFactory::createFromFile($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates an instance of a CSV reader
|
||||
*
|
||||
* @return \Box\Spout\Reader\CSV\Reader
|
||||
*/
|
||||
public static function createCSVReader()
|
||||
{
|
||||
try {
|
||||
return ReaderFactory::createFromType(Type::CSV);
|
||||
} catch (UnsupportedTypeException $e) {
|
||||
// should never happen
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates an instance of a XLSX reader
|
||||
*
|
||||
* @return \Box\Spout\Reader\XLSX\Reader
|
||||
*/
|
||||
public static function createXLSXReader()
|
||||
{
|
||||
try {
|
||||
return ReaderFactory::createFromType(Type::XLSX);
|
||||
} catch (UnsupportedTypeException $e) {
|
||||
// should never happen
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates an instance of a ODS reader
|
||||
*
|
||||
* @return \Box\Spout\Reader\ODS\Reader
|
||||
*/
|
||||
public static function createODSReader()
|
||||
{
|
||||
try {
|
||||
return ReaderFactory::createFromType(Type::ODS);
|
||||
} catch (UnsupportedTypeException $e) {
|
||||
// should never happen
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
103
src/Spout/Reader/Common/Creator/ReaderFactory.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Common\Creator;
|
||||
|
||||
use Box\Spout\Common\Creator\HelperFactory;
|
||||
use Box\Spout\Common\Exception\UnsupportedTypeException;
|
||||
use Box\Spout\Common\Type;
|
||||
use Box\Spout\Reader\CSV\Creator\InternalEntityFactory as CSVInternalEntityFactory;
|
||||
use Box\Spout\Reader\CSV\Manager\OptionsManager as CSVOptionsManager;
|
||||
use Box\Spout\Reader\CSV\Reader as CSVReader;
|
||||
use Box\Spout\Reader\ODS\Creator\HelperFactory as ODSHelperFactory;
|
||||
use Box\Spout\Reader\ODS\Creator\InternalEntityFactory as ODSInternalEntityFactory;
|
||||
use Box\Spout\Reader\ODS\Creator\ManagerFactory as ODSManagerFactory;
|
||||
use Box\Spout\Reader\ODS\Manager\OptionsManager as ODSOptionsManager;
|
||||
use Box\Spout\Reader\ODS\Reader as ODSReader;
|
||||
use Box\Spout\Reader\ReaderInterface;
|
||||
use Box\Spout\Reader\XLSX\Creator\HelperFactory as XLSXHelperFactory;
|
||||
use Box\Spout\Reader\XLSX\Creator\InternalEntityFactory as XLSXInternalEntityFactory;
|
||||
use Box\Spout\Reader\XLSX\Creator\ManagerFactory as XLSXManagerFactory;
|
||||
use Box\Spout\Reader\XLSX\Manager\OptionsManager as XLSXOptionsManager;
|
||||
use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyFactory;
|
||||
use Box\Spout\Reader\XLSX\Reader as XLSXReader;
|
||||
|
||||
/**
|
||||
* Class ReaderFactory
|
||||
* This factory is used to create readers, based on the type of the file to be read.
|
||||
* It supports CSV, XLSX and ODS formats.
|
||||
*/
|
||||
class ReaderFactory
|
||||
{
|
||||
/**
|
||||
* Creates a reader by file extension
|
||||
*
|
||||
* @param string $path The path to the spreadsheet file. Supported extensions are .csv,.ods and .xlsx
|
||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||
* @return ReaderInterface
|
||||
*/
|
||||
public static function createFromFile(string $path)
|
||||
{
|
||||
$extension = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
|
||||
return self::createFromType($extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates an instance of the appropriate reader, given the type of the file to be read
|
||||
*
|
||||
* @param string $readerType Type of the reader to instantiate
|
||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||
* @return ReaderInterface
|
||||
*/
|
||||
public static function createFromType($readerType)
|
||||
{
|
||||
switch ($readerType) {
|
||||
case Type::CSV: return self::createCSVReader();
|
||||
case Type::XLSX: return self::createXLSXReader();
|
||||
case Type::ODS: return self::createODSReader();
|
||||
default:
|
||||
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CSVReader
|
||||
*/
|
||||
private static function createCSVReader()
|
||||
{
|
||||
$optionsManager = new CSVOptionsManager();
|
||||
$helperFactory = new HelperFactory();
|
||||
$entityFactory = new CSVInternalEntityFactory($helperFactory);
|
||||
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
|
||||
|
||||
return new CSVReader($optionsManager, $globalFunctionsHelper, $entityFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return XLSXReader
|
||||
*/
|
||||
private static function createXLSXReader()
|
||||
{
|
||||
$optionsManager = new XLSXOptionsManager();
|
||||
$helperFactory = new XLSXHelperFactory();
|
||||
$managerFactory = new XLSXManagerFactory($helperFactory, new CachingStrategyFactory());
|
||||
$entityFactory = new XLSXInternalEntityFactory($managerFactory, $helperFactory);
|
||||
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
|
||||
|
||||
return new XLSXReader($optionsManager, $globalFunctionsHelper, $entityFactory, $managerFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ODSReader
|
||||
*/
|
||||
private static function createODSReader()
|
||||
{
|
||||
$optionsManager = new ODSOptionsManager();
|
||||
$helperFactory = new ODSHelperFactory();
|
||||
$managerFactory = new ODSManagerFactory();
|
||||
$entityFactory = new ODSInternalEntityFactory($helperFactory, $managerFactory);
|
||||
$globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
|
||||
|
||||
return new ODSReader($optionsManager, $globalFunctionsHelper, $entityFactory);
|
||||
}
|
||||
}
|