LICENSE 0000666 00000022546 13476413670 0005601 0 ustar 00 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
README.md 0000666 00000005743 13476413670 0006053 0 ustar 00 # Spout
[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges)
[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/box/spout/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Documentation
Full documentation can be found at [http://opensource.box.com/spout/](http://opensource.box.com/spout/).
## Requirements
* PHP version 7.1 or higher
* PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled
## Upgrade guide
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
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.
* `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).
> Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout)
## Support
You can ask questions, submit new features ideas or discuss about Spout in the chat room:
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Copyright and License
Copyright 2017 Box, Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
UPGRADE-3.0.md 0000666 00000010351 13476413670 0006472 0 ustar 00 Upgrading from 2.x to 3.0
=========================
Spout 3.0 introduced several backwards-incompatible changes. The upgrade from Spout 2.x to 3.0 must therefore be done with caution.
This guide is meant to ease this process.
Most notable changes
--------------------
In 2.x, styles were applied per row; it was therefore impossible to apply different styles to cells in the same row.
With the 3.0 version, this is now possible: each cell can have its own style.
Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
Finally, **_Spout 3.0 only supports PHP 7.1 and above_**, as other PHP versions are no longer supported by the community.
Reader changes
--------------
Creating a reader should now be done through the Reader `ReaderEntityFactory`, instead of using the `ReaderFactory`.
Also, the `ReaderFactory::create($type)` method was removed and replaced by methods for each reader:
```php
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory; // namespace is no longer "Box\Spout\Reader"
...
$reader = ReaderEntityFactory::createXLSXReader(); // replaces ReaderFactory::create(Type::XLSX)
$reader = ReaderEntityFactory::createCSVReader(); // replaces ReaderFactory::create(Type::CSV)
$reader = ReaderEntityFactory::createODSReader(); // replaces ReaderFactory::create(Type::ODS)
```
When iterating over the spreadsheet rows, Spout now returns `Row` objects, instead of an array containing row values. Accessing the row values should now be done this way:
```php
...
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) { // $row is a "Row" object, not an array
$rowAsArray = $row->toArray(); // this is the 2.x equivalent
// OR
$cellsArray = $row->getCells(); // this can be used to get access to cells' details
...
}
}
```
Writer changes
--------------
Writer creation follows the same change as the reader. It should now be done through the Writer `WriterEntityFactory`, instead of using the `WriterFactory`.
Also, the `WriterFactory::create($type)` method was removed and replaced by methods for each writer:
```php
use Box\Spout\Writer\Common\Creator\WriterEntityFactory; // namespace is no longer "Box\Spout\Writer"
...
$writer = WriterEntityFactory::createXLSXWriter(); // replaces WriterFactory::create(Type::XLSX)
$writer = WriterEntityFactory::createCSVWriter(); // replaces WriterFactory::create(Type::CSV)
$writer = WriterEntityFactory::createODSWriter(); // replaces WriterFactory::create(Type::ODS)
```
Adding rows is also done differently: instead of passing an array, the writer now takes in a `Row` object (or an array of `Row`). Creating such objects can easily be done this way:
```php
// Adding a row from an array of values (2.x equivalent)
$cellValues = ['foo', 12345];
$row1 = WriterEntityFactory::createRowFromArray($cellValues, $rowStyle);
// Adding a row from an array of Cell
$cell1 = WriterEntityFactory::createCell('foo', $cellStyle1); // this cell has its own style
$cell2 = WriterEntityFactory::createCell(12345, $cellStyle2); // this cell has its own style
$row2 = WriterEntityFactory::createRow([$cell1, $cell2]);
$writer->addRows([$row1, $row2]);
```
Namespace changes for styles
-----------------
The namespaces for styles have changed. Styles are still created by using a `builder` class.
For the builder, please update your import statements to use the following namespaces:
Box\Spout\Writer\Common\Creator\Style\StyleBuilder
Box\Spout\Writer\Common\Creator\Style\BorderBuilder
The `Style` base class and style definitions like `Border`, `BorderPart` and `Color` also have a new namespace.
If your are using these classes directly via an import statement in your code, please use the following namespaces:
Box\Spout\Common\Entity\Style\Border
Box\Spout\Common\Entity\Style\BorderPart
Box\Spout\Common\Entity\Style\Color
Box\Spout\Common\Entity\Style\Style
Handling of empty rows
----------------------
In 2.x, empty rows were not added to the spreadsheet.
In 3.0, `addRow` now always writes a row to the spreadsheet: when the row does not contain any cells, an empty row is created in the sheet.
appveyor.yml 0000666 00000002044 13476413670 0007153 0 ustar 00 build: false
clone_depth: 1
environment:
PHP_CHOCO_VERSION: 7.2.0
PHP_CACHE_DIR: C:\tools\php
cache:
- '%PHP_CACHE_DIR% -> appveyor.yml'
init:
- SET PATH=%PHP_CACHE_DIR%;%PATH%
- SET COMPOSER_CACHE_DIR=%PHP_CACHE_DIR%
- SET COMPOSER_NO_INTERACTION=1
- SET PHP=0
- SET ANSICON=121x90 (121x90)
install:
- IF EXIST %PHP_CACHE_DIR% (SET PHP=1)
- IF %PHP%==0 cinst php -y --version %PHP_CHOCO_VERSION% --params "/InstallDir:%PHP_CACHE_DIR%"
- IF %PHP%==0 cinst composer -y --ia "/DEV=%PHP_CACHE_DIR%"
- cd %PHP_CACHE_DIR%
- IF %PHP%==0 copy php.ini-production php.ini
- IF %PHP%==0 echo extension_dir=ext >> php.ini
- IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini
- IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini
- IF %PHP%==0 echo extension=php_openssl.dll >> php.ini
- php -v
- IF %PHP%==0 (composer --version) ELSE (composer self-update)
- cd %APPVEYOR_BUILD_FOLDER%
- composer install --prefer-dist --no-progress
test_script:
- cd %APPVEYOR_BUILD_FOLDER%
- vendor\bin\phpunit --colors=always
composer.json 0000666 00000002276 13476413670 0007314 0 ustar 00 {
"name": "box/spout",
"description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
"type": "library",
"keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"],
"license": "Apache-2.0",
"homepage": "https://www.github.com/box/spout",
"authors": [
{
"name": "Adrien Loison",
"email": "adrien@box.com"
}
],
"require": {
"php": ">=7.1.0",
"ext-zip": "*",
"ext-xmlreader" : "*"
},
"require-dev": {
"phpunit/phpunit": "^7",
"friendsofphp/php-cs-fixer": "^2"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
"ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
},
"autoload": {
"psr-4": {
"Box\\Spout\\": "src/Spout"
}
},
"extra": {
"branch-alias": {
"dev-master": "3.1.x-dev"
}
},
"config": {
"platform": {
"php": "7.1"
}
}
}
src/Spout/Autoloader/Psr4Autoloader.php 0000666 00000010677 13476413670 0014157 0 ustar 00 prefixes[$prefix]) === false) {
$this->prefixes[$prefix] = [];
}
// retain the base directory for the namespace prefix
if ($prepend) {
array_unshift($this->prefixes[$prefix], $baseDir);
} else {
array_push($this->prefixes[$prefix], $baseDir);
}
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name.
* @return mixed The mapped file name on success, or boolean false on
* failure.
*/
public function loadClass($class)
{
// the current namespace prefix
$prefix = $class;
// work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name
while (($pos = strrpos($prefix, '\\')) !== false) {
// retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1);
// the rest is the relative class name
$relativeClass = substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
if ($mappedFile !== false) {
return $mappedFile;
}
// remove the trailing namespace separator for the next iteration
// of strrpos()
$prefix = rtrim($prefix, '\\');
}
// never found a mapped file
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix.
* @param string $relativeClass The relative class name.
* @return mixed Boolean false if no mapped file can be loaded, or the
* name of the mapped file that was loaded.
*/
protected function loadMappedFile($prefix, $relativeClass)
{
// are there any base directories for this namespace prefix?
if (isset($this->prefixes[$prefix]) === false) {
return false;
}
// look through base directories for this namespace prefix
foreach ($this->prefixes[$prefix] as $baseDir) {
// replace the namespace prefix with the base directory,
// replace namespace separators with directory separators
// in the relative class name, append with .php
$file = $baseDir
. str_replace('\\', '/', $relativeClass)
. '.php';
// if the mapped file exists, require it
if ($this->requireFile($file)) {
// yes, we're done
return $file;
}
}
// never found it
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file The file to require.
* @return bool True if the file exists, false if not.
*/
protected function requireFile($file)
{
if (file_exists($file)) {
require $file;
return true;
}
return false;
}
}
src/Spout/Autoloader/autoload.php 0000666 00000000511 13476413670 0013101 0 ustar 00 register();
$loader->addNamespace('Box\Spout', $srcBaseDirectory);
src/Spout/Common/Creator/HelperFactory.php 0000666 00000002160 13476413670 0014572 0 ustar 00 setValue($value);
$this->setStyle($style);
}
/**
* @param mixed|null $value
*/
public function setValue($value)
{
$this->value = $value;
$this->type = $this->detectType($value);
}
/**
* @return mixed|null
*/
public function getValue()
{
return !$this->isError() ? $this->value : null;
}
/**
* @param Style|null $style
*/
public function setStyle($style)
{
$this->style = $style ?: new Style();
}
/**
* @return Style
*/
public function getStyle()
{
return $this->style;
}
/**
* @return int|null
*/
public function getType()
{
return $this->type;
}
/**
* @param int $type
*/
public function setType($type)
{
$this->type = $type;
}
/**
* Get the current value type
*
* @param mixed|null $value
* @return int
*/
protected function detectType($value)
{
if (CellTypeHelper::isBoolean($value)) {
return self::TYPE_BOOLEAN;
}
if (CellTypeHelper::isEmpty($value)) {
return self::TYPE_EMPTY;
}
if (CellTypeHelper::isNumeric($value)) {
return self::TYPE_NUMERIC;
}
if (CellTypeHelper::isDateTimeOrDateInterval($value)) {
return self::TYPE_DATE;
}
if (CellTypeHelper::isNonEmptyString($value)) {
return self::TYPE_STRING;
}
return self::TYPE_ERROR;
}
/**
* @return bool
*/
public function isBoolean()
{
return $this->type === self::TYPE_BOOLEAN;
}
/**
* @return bool
*/
public function isEmpty()
{
return $this->type === self::TYPE_EMPTY;
}
/**
* @return bool
*/
public function isNumeric()
{
return $this->type === self::TYPE_NUMERIC;
}
/**
* @return bool
*/
public function isString()
{
return $this->type === self::TYPE_STRING;
}
/**
* @return bool
*/
public function isDate()
{
return $this->type === self::TYPE_DATE;
}
/**
* @return bool
*/
public function isError()
{
return $this->type === self::TYPE_ERROR;
}
/**
* @return string
*/
public function __toString()
{
return (string) $this->getValue();
}
}
src/Spout/Common/Entity/Row.php 0000666 00000004106 13476413670 0012451 0 ustar 00 setCells($cells)
->setStyle($style);
}
/**
* @return Cell[] $cells
*/
public function getCells()
{
return $this->cells;
}
/**
* @param Cell[] $cells
* @return Row
*/
public function setCells(array $cells)
{
$this->cells = [];
foreach ($cells as $cell) {
$this->addCell($cell);
}
return $this;
}
/**
* @param Cell $cell
* @param int $cellIndex
* @return Row
*/
public function setCellAtIndex(Cell $cell, $cellIndex)
{
$this->cells[$cellIndex] = $cell;
return $this;
}
/**
* @param int $cellIndex
* @return Cell|null
*/
public function getCellAtIndex($cellIndex)
{
return isset($this->cells[$cellIndex]) ? $this->cells[$cellIndex] : null;
}
/**
* @param Cell $cell
* @return Row
*/
public function addCell(Cell $cell)
{
$this->cells[] = $cell;
return $this;
}
/**
* @return int
*/
public function getNumCells()
{
return count($this->cells);
}
/**
* @return Style
*/
public function getStyle()
{
return $this->style;
}
/**
* @param Style|null $style
* @return Row
*/
public function setStyle($style)
{
$this->style = $style ?: new Style();
return $this;
}
/**
* @return array The row values, as array
*/
public function toArray()
{
return array_map(function (Cell $cell) {
return $cell->getValue();
}, $this->cells);
}
}
src/Spout/Common/Entity/Style/Border.php 0000666 00000003236 13476413670 0014222 0 ustar 00 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;
}
}
src/Spout/Common/Entity/Style/BorderPart.php 0000666 00000007520 13476413670 0015051 0 ustar 00 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;
}
}
src/Spout/Common/Entity/Style/Color.php 0000666 00000004772 13476413670 0014071 0 ustar 00 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;
}
}
src/Spout/Common/Entity/Style/Style.php 0000666 00000017263 13476413670 0014112 0 ustar 00 id;
}
/**
* @param int $id
* @return Style
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return Border
*/
public function getBorder()
{
return $this->border;
}
/**
* @param Border $border
* @return Style
*/
public function setBorder(Border $border)
{
$this->shouldApplyBorder = true;
$this->border = $border;
return $this;
}
/**
* @return bool
*/
public function shouldApplyBorder()
{
return $this->shouldApplyBorder;
}
/**
* @return bool
*/
public function isFontBold()
{
return $this->fontBold;
}
/**
* @return Style
*/
public function setFontBold()
{
$this->fontBold = true;
$this->hasSetFontBold = true;
$this->shouldApplyFont = true;
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;
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;
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;
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;
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;
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;
return $this;
}
/**
* @return bool
*/
public function hasSetFontName()
{
return $this->hasSetFontName;
}
/**
* @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;
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;
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;
}
}
src/Spout/Common/Exception/EncodingConversionException.php 0000666 00000000230 13476413670 0020031 0 ustar 00 globalFunctionsHelper = $globalFunctionsHelper;
$this->supportedEncodingsWithBom = [
self::ENCODING_UTF8 => self::BOM_UTF8,
self::ENCODING_UTF16_LE => self::BOM_UTF16_LE,
self::ENCODING_UTF16_BE => self::BOM_UTF16_BE,
self::ENCODING_UTF32_LE => self::BOM_UTF32_LE,
self::ENCODING_UTF32_BE => self::BOM_UTF32_BE,
];
}
/**
* Returns the number of bytes to use as offset in order to skip the BOM.
*
* @param resource $filePointer Pointer to the file to check
* @param string $encoding Encoding of the file to check
* @return int Bytes offset to apply to skip the BOM (0 means no BOM)
*/
public function getBytesOffsetToSkipBOM($filePointer, $encoding)
{
$byteOffsetToSkipBom = 0;
if ($this->hasBOM($filePointer, $encoding)) {
$bomUsed = $this->supportedEncodingsWithBom[$encoding];
// we skip the N first bytes
$byteOffsetToSkipBom = strlen($bomUsed);
}
return $byteOffsetToSkipBom;
}
/**
* Returns whether the file identified by the given pointer has a BOM.
*
* @param resource $filePointer Pointer to the file to check
* @param string $encoding Encoding of the file to check
* @return bool TRUE if the file has a BOM, FALSE otherwise
*/
protected function hasBOM($filePointer, $encoding)
{
$hasBOM = false;
$this->globalFunctionsHelper->rewind($filePointer);
if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
$potentialBom = $this->supportedEncodingsWithBom[$encoding];
$numBytesInBom = strlen($potentialBom);
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
}
return $hasBOM;
}
/**
* Attempts to convert a non UTF-8 string into UTF-8.
*
* @param string $string Non UTF-8 string to be converted
* @param string $sourceEncoding The encoding used to encode the source string
* @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)
{
return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8);
}
/**
* Attempts to convert a UTF-8 string into the given encoding.
*
* @param string $string UTF-8 string to be converted
* @param string $targetEncoding The encoding the string should be re-encoded into
* @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)
{
return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding);
}
/**
* Attempts to convert the given string to the given encoding.
* Depending on what is installed on the server, we will try to iconv or mbstring.
*
* @param string $string string to be converted
* @param string $sourceEncoding The encoding used to encode the source string
* @param string $targetEncoding The encoding the string should be re-encoded into
* @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)
{
// if source and target encodings are the same, it's a no-op
if ($sourceEncoding === $targetEncoding) {
return $string;
}
$convertedString = null;
if ($this->canUseIconv()) {
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
} 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\".");
}
if ($convertedString === false) {
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed.");
}
return $convertedString;
}
/**
* Returns whether "iconv" can be used.
*
* @return bool TRUE if "iconv" is available and can be used, FALSE otherwise
*/
protected function canUseIconv()
{
return $this->globalFunctionsHelper->function_exists('iconv');
}
/**
* Returns whether "mb_string" functions can be used.
* These functions come with the PHP Intl package.
*
* @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise
*/
protected function canUseMbString()
{
return $this->globalFunctionsHelper->function_exists('mb_convert_encoding');
}
}
src/Spout/Common/Helper/Escaper/CSV.php 0000666 00000001361 13476413670 0013662 0 ustar 00 ', '&') as well as
// single/double quotes (for XML attributes) need to be encoded.
if (defined('ENT_DISALLOWED')) {
// 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced.
// Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor
// @link https://github.com/box/spout/issues/329
$replacedString = htmlspecialchars($string, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8');
} else {
// We are on hhvm or any other engine that does not support ENT_DISALLOWED.
$escapedString = htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
// control characters values are from 0 to 1F (hex values) in the ASCII table
// some characters should not be escaped though: "\t", "\r" and "\n".
$regexPattern = '[\x00-\x08' .
// skipping "\t" (0x9) and "\n" (0xA)
'\x0B-\x0C' .
// skipping "\r" (0xD)
'\x0E-\x1F]';
$replacedString = preg_replace("/$regexPattern/", '�', $escapedString);
}
return $replacedString;
}
/**
* Unescapes the given string to make it compatible with XLSX
*
* @param string $string The string to unescape
* @return string The unescaped string
*/
public function unescape($string)
{
// ==============
// = WARNING =
// ==============
// It is assumed that the given string has already had its XML entities decoded.
// This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
// Therefore there is no need to call "htmlspecialchars_decode()".
return $string;
}
}
src/Spout/Common/Helper/Escaper/XLSX.php 0000666 00000015562 13476413670 0014035 0 ustar 00 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("/(?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);
}
}
src/Spout/Common/Helper/FileSystemHelper.php 0000666 00000011321 13476413670 0015066 0 ustar 00 baseFolderRealPath = realpath($baseFolderPath);
}
/**
* Creates an empty folder with the given name under the given parent folder.
*
* @param string $parentFolderPath The parent folder path under which the folder is going to be created
* @param string $folderName The name of the folder to create
* @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)
{
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
$folderPath = $parentFolderPath . '/' . $folderName;
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
if (!$wasCreationSuccessful) {
throw new IOException("Unable to create folder: $folderPath");
}
return $folderPath;
}
/**
* Creates a file with the given name and content in the given folder.
* The parent folder must exist.
*
* @param string $parentFolderPath The parent folder path where the file is going to be created
* @param string $fileName The name of the file to create
* @param string $fileContents The contents of the file to create
* @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)
{
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
$filePath = $parentFolderPath . '/' . $fileName;
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
if ($wasCreationSuccessful === false) {
throw new IOException("Unable to create file: $filePath");
}
return $filePath;
}
/**
* Delete the file at the given path
*
* @param string $filePath Path of the file to delete
* @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);
}
}
/**
* 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)
{
$this->throwIfOperationNotInBaseFolder($folderPath);
$itemIterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($itemIterator as $item) {
if ($item->isDir()) {
rmdir($item->getPathname());
} else {
unlink($item->getPathname());
}
}
rmdir($folderPath);
}
/**
* All I/O operations must occur inside the base folder, for security reasons.
* This function will throw an exception if the folder where the I/O operation
* should occur is not inside the base folder.
*
* @param string $operationFolderPath The path of the folder where the I/O operation should occur
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder
* @return void
*/
protected function throwIfOperationNotInBaseFolder($operationFolderPath)
{
$operationFolderRealPath = realpath($operationFolderPath);
$isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
if (!$isInBaseFolder) {
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
}
}
}
src/Spout/Common/Helper/FileSystemHelperInterface.php 0000666 00000004016 13476413670 0016712 0 ustar 00 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);
}
/**
* Wrapper around global function is_readable()
* @see is_readable()
*
* @param string $fileName
* @return bool
*/
public function is_readable($fileName)
{
return is_readable($fileName);
}
/**
* Wrapper around global function basename()
* @see basename()
*
* @param string $path
* @param string|null $suffix
* @return string
*/
public function basename($path, $suffix = null)
{
return basename($path, $suffix);
}
/**
* Wrapper around global function header()
* @see header()
*
* @param string $string
* @return void
*/
public function header($string)
{
header($string);
}
/**
* Wrapper around global function ob_end_clean()
* @see ob_end_clean()
*
* @return void
*/
public function ob_end_clean()
{
if (ob_get_length() > 0) {
ob_end_clean();
}
}
/**
* Wrapper around global function iconv()
* @see iconv()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
* @return string|bool the converted string or FALSE on failure.
*/
public function iconv($string, $sourceEncoding, $targetEncoding)
{
return iconv($sourceEncoding, $targetEncoding, $string);
}
/**
* Wrapper around global function mb_convert_encoding()
* @see mb_convert_encoding()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
* @return string|bool the converted string or FALSE on failure.
*/
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
{
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
}
/**
* Wrapper around global function stream_get_wrappers()
* @see stream_get_wrappers()
*
* @return array
*/
public function stream_get_wrappers()
{
return stream_get_wrappers();
}
/**
* Wrapper around global function function_exists()
* @see function_exists()
*
* @param string $functionName
* @return bool
*/
public function function_exists($functionName)
{
return function_exists($functionName);
}
}
src/Spout/Common/Helper/StringHelper.php 0000666 00000004074 13476413670 0014257 0 ustar 00 hasMbstringSupport = extension_loaded('mbstring');
}
/**
* Returns the length of the given string.
* It uses the multi-bytes function is available.
* @see strlen
* @see mb_strlen
*
* @param string $string
* @return int
*/
public function getStringLength($string)
{
return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string);
}
/**
* Returns the position of the first occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
* @see strpos
* @see mb_strpos
*
* @param string $char Needle
* @param string $string Haystack
* @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharFirstOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char);
return ($position !== false) ? $position : -1;
}
/**
* Returns the position of the last occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
* @see strrpos
* @see mb_strrpos
*
* @param string $char Needle
* @param string $string Haystack
* @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharLastOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char);
return ($position !== false) ? $position : -1;
}
}
src/Spout/Common/Manager/OptionsManagerAbstract.php 0000666 00000003113 13476413670 0016407 0 ustar 00 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;
}
}
src/Spout/Common/Manager/OptionsManagerInterface.php 0000666 00000000721 13476413670 0016546 0 ustar 00 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);
}
}
src/Spout/Reader/CSV/Manager/OptionsManager.php 0000666 00000002024 13476413670 0015330 0 ustar 00 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);
}
}
src/Spout/Reader/CSV/Reader.php 0000666 00000006770 13476413670 0012246 0 ustar 00 optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
return $this;
}
/**
* Sets the field enclosure for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldEnclosure Character that enclose fields
* @return Reader
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
return $this;
}
/**
* Sets the encoding of the CSV file to be read.
* Needs to be called before opening the reader.
*
* @param string $encoding Encoding of the CSV file to be read
* @return Reader
*/
public function setEncoding($encoding)
{
$this->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
* @throws \Box\Spout\Common\Exception\IOException
* @return void
*/
protected function openReader($filePath)
{
$this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings');
ini_set('auto_detect_line_endings', '1');
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException("Could not open file $filePath for reading.");
}
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->sheetIterator = $entityFactory->createSheetIterator(
$this->filePointer,
$this->optionsManager,
$this->globalFunctionsHelper
);
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
protected function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
}
}
src/Spout/Reader/CSV/RowIterator.php 0000666 00000020710 13476413670 0013313 0 ustar 00 filePointer = $filePointer;
$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;
}
/**
* Rewind the Iterator to the first element
* @see http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->rewindAndSkipBom();
$this->numReadRows = 0;
$this->rowBuffer = null;
$this->next();
}
/**
* This rewinds and skips the BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*
* @return void
*/
protected function rewindAndSkipBom()
{
$byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding);
// sets the cursor after the BOM (0 means no BOM, so rewind it)
$this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom);
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid()
{
return ($this->filePointer && !$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Reads data for the next unprocessed row.
* @see http://php.net/manual/en/iterator.next.php
*
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void
*/
public function next()
{
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if (!$this->hasReachedEndOfFile) {
$this->readDataForNextRow();
}
}
/**
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
* @return void
*/
protected function readDataForNextRow()
{
do {
$rowData = $this->getNextUTF8EncodedRow();
} while ($this->shouldReadNextRow($rowData));
if ($rowData !== false) {
// str_replace will replace NULL values by empty strings
$rowDataBufferAsArray = str_replace(null, null, $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|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 (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null);
}
/**
* Return the current element from the buffer
* @see http://php.net/manual/en/iterator.current.php
*
* @return Row|null
*/
public function current()
{
return $this->rowBuffer;
}
/**
* Return the key of the current element
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->numReadRows;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// do nothing
}
}
src/Spout/Reader/CSV/Sheet.php 0000666 00000002221 13476413670 0012077 0 ustar 00 rowIterator = $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;
}
}
src/Spout/Reader/CSV/SheetIterator.php 0000666 00000003445 13476413670 0013622 0 ustar 00 sheet = $sheet;
}
/**
* Rewind the Iterator to the first element
* @see http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->hasReadUniqueSheet = false;
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid()
{
return (!$this->hasReadUniqueSheet);
}
/**
* Move forward to next element
* @see http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
$this->hasReadUniqueSheet = true;
}
/**
* Return the current element
* @see http://php.net/manual/en/iterator.current.php
*
* @return \Box\Spout\Reader\CSV\Sheet
*/
public function current()
{
return $this->sheet;
}
/**
* Return the key of the current element
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// do nothing
}
}
src/Spout/Reader/Common/Creator/InternalEntityFactoryInterface.php 0000666 00000000661 13476413670 0021353 0 ustar 00 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);
}
}
src/Spout/Reader/Common/Entity/Options.php 0000666 00000001044 13476413670 0014535 0 ustar 00 entityFactory = $entityFactory;
}
/**
* Detect whether a row is considered empty.
* An empty row has all of its cells empty.
*
* @param Row $row
* @return bool
*/
public function isEmpty(Row $row)
{
foreach ($row->getCells() as $cell) {
if (!$cell->isEmpty()) {
return false;
}
}
return true;
}
/**
* Fills the missing indexes of a row with empty cells.
*
* @param Row $row
* @return Row
*/
public function fillMissingIndexesWithEmptyCells(Row $row)
{
if ($row->getNumCells() === 0) {
return $row;
}
$rowCells = $row->getCells();
$maxCellIndex = max(array_keys($rowCells));
for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) {
if (!isset($rowCells[$cellIndex])) {
$row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
}
}
return $row;
}
}
src/Spout/Reader/Common/XMLProcessor.php 0000666 00000013506 13476413670 0014174 0 ustar 00 xmlReader = $xmlReader;
}
/**
* @param string $nodeName A callback may be triggered when a node with this name is read
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* @param callable $callback Callback to execute when the read node has the given name and type
* @return XMLProcessor
*/
public function registerCallback($nodeName, $nodeType, $callback)
{
$callbackKey = $this->getCallbackKey($nodeName, $nodeType);
$this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback);
return $this;
}
/**
* @param string $nodeName Name of the node
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* @return string Key used to store the associated callback
*/
private function getCallbackKey($nodeName, $nodeType)
{
return "$nodeName$nodeType";
}
/**
* Because the callback can be a "protected" function, we don't want to use call_user_func() directly
* but instead invoke the callback using Reflection. This allows the invocation of "protected" functions.
* Since some functions can be called a lot, we pre-process the callback to only return the elements that
* will be needed to invoke the callback later.
*
* @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME]
* @return array Associative array containing the elements needed to invoke the callback using Reflection
*/
private function getInvokableCallbackData($callback)
{
$callbackObject = $callback[0];
$callbackMethodName = $callback[1];
$reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName);
$reflectionMethod->setAccessible(true);
return [
self::CALLBACK_REFLECTION_METHOD => $reflectionMethod,
self::CALLBACK_REFLECTION_OBJECT => $callbackObject,
];
}
/**
* Resumes the reading of the XML file where it was left off.
* Stops whenever a callback indicates that reading should stop or at the end of the file.
*
* @throws \Box\Spout\Reader\Exception\XMLProcessingException
* @return void
*/
public function readUntilStopped()
{
while ($this->xmlReader->read()) {
$nodeType = $this->xmlReader->nodeType;
$nodeNamePossiblyWithPrefix = $this->xmlReader->name;
$nodeNameWithoutPrefix = $this->xmlReader->localName;
$callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType);
if ($callbackData !== null) {
$callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]);
if ($callbackResponse === self::PROCESSING_STOP) {
// stop reading
break;
}
}
}
}
/**
* @param string $nodeNamePossiblyWithPrefix Name of the node, possibly prefixed
* @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
* @return array|null Callback data to be used for execution when a node of the given name/type is read or NULL if none found
*/
private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType)
{
// With prefixed nodes, we should match if (by order of preference):
// 1. the callback was registered with the prefixed node name (e.g. "x:worksheet")
// 2. the callback was registered with the un-prefixed node name (e.g. "worksheet")
$callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType);
$callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType);
$hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix);
$callbackKeyToUse = $callbackKeyForUnPrefixedName;
if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) {
$callbackKeyToUse = $callbackKeyForPossiblyPrefixedName;
}
// Using isset here because it is way faster than array_key_exists...
return isset($this->callbacks[$callbackKeyToUse]) ? $this->callbacks[$callbackKeyToUse] : null;
}
/**
* @param array $callbackData Associative array containing data to invoke the callback using Reflection
* @param array $args Arguments to pass to the callback
* @return int Callback response
*/
private function invokeCallback($callbackData, $args)
{
$reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
$callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT];
return $reflectionMethod->invokeArgs($callbackObject, $args);
}
}
src/Spout/Reader/Exception/InvalidValueException.php 0000666 00000001261 13476413670 0016577 0 ustar 00 invalidValue = $invalidValue;
parent::__construct($message, $code, $previous);
}
/**
* @return mixed
*/
public function getInvalidValue()
{
return $this->invalidValue;
}
}
src/Spout/Reader/Exception/IteratorNotRewindableException.php 0000666 00000000237 13476413670 0020465 0 ustar 00 createStringsEscaper();
return new CellValueFormatter($shouldFormatDates, $escaper);
}
/**
* @param InternalEntityFactory $entityFactory
* @return SettingsHelper
*/
public function createSettingsHelper($entityFactory)
{
return new SettingsHelper($entityFactory);
}
/**
* @return \Box\Spout\Common\Helper\Escaper\ODS
*/
public function createStringsEscaper()
{
/* @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
return new \Box\Spout\Common\Helper\Escaper\ODS();
}
}
src/Spout/Reader/ODS/Creator/InternalEntityFactory.php 0000666 00000007253 13476413670 0016733 0 ustar 00 helperFactory = $helperFactory;
$this->managerFactory = $managerFactory;
}
/**
* @param string $filePath Path of the file to be read
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @return SheetIterator
*/
public function createSheetIterator($filePath, $optionsManager)
{
$escaper = $this->helperFactory->createStringsEscaper();
$settingsHelper = $this->helperFactory->createSettingsHelper($this);
return new SheetIterator($filePath, $optionsManager, $escaper, $settingsHelper, $this);
}
/**
* @param XMLReader $xmlReader XML Reader
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
* @param string $sheetName Name of the sheet
* @param bool $isSheetActive Whether the sheet was defined as active
* @param bool $isSheetVisible Whether the sheet is visible
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @return Sheet
*/
public function createSheet($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible, $optionsManager)
{
$rowIterator = $this->createRowIterator($xmlReader, $optionsManager);
return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible);
}
/**
* @param XMLReader $xmlReader XML Reader
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @return RowIterator
*/
private function createRowIterator($xmlReader, $optionsManager)
{
$shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES);
$cellValueFormatter = $this->helperFactory->createCellValueFormatter($shouldFormatDates);
$xmlProcessor = $this->createXMLProcessor($xmlReader);
$rowManager = $this->managerFactory->createRowManager($this);
return new RowIterator($xmlReader, $optionsManager, $cellValueFormatter, $xmlProcessor, $rowManager, $this);
}
/**
* @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);
}
/**
* @return XMLReader
*/
public function createXMLReader()
{
return new XMLReader();
}
/**
* @param $xmlReader
* @return XMLProcessor
*/
private function createXMLProcessor($xmlReader)
{
return new XMLProcessor($xmlReader);
}
/**
* @return \ZipArchive
*/
public function createZipArchive()
{
return new \ZipArchive();
}
}
src/Spout/Reader/ODS/Creator/ManagerFactory.php 0000666 00000000640 13476413670 0015325 0 ustar 00 shouldFormatDates = $shouldFormatDates;
$this->escaper = $escaper;
}
/**
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
* @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
*
* @param \DOMNode $node
* @throws InvalidValueException If the node value is not valid
* @return string|int|float|bool|\DateTime|\DateInterval The value associated with the cell, empty string if cell's type is void/undefined
*/
public function extractAndFormatNodeValue($node)
{
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE);
switch ($cellType) {
case self::CELL_TYPE_STRING:
return $this->formatStringCellValue($node);
case self::CELL_TYPE_FLOAT:
return $this->formatFloatCellValue($node);
case self::CELL_TYPE_BOOLEAN:
return $this->formatBooleanCellValue($node);
case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($node);
case self::CELL_TYPE_TIME:
return $this->formatTimeCellValue($node);
case self::CELL_TYPE_CURRENCY:
return $this->formatCurrencyCellValue($node);
case self::CELL_TYPE_PERCENTAGE:
return $this->formatPercentageCellValue($node);
case self::CELL_TYPE_VOID:
default:
return '';
}
}
/**
* Returns the cell String value.
*
* @param \DOMNode $node
* @return string The value associated with the cell
*/
protected function formatStringCellValue($node)
{
$pNodeValues = [];
$pNodes = $node->getElementsByTagName(self::XML_NODE_P);
foreach ($pNodes as $pNode) {
$currentPValue = '';
foreach ($pNode->childNodes as $childNode) {
if ($childNode instanceof \DOMText) {
$currentPValue .= $childNode->nodeValue;
} elseif ($childNode->nodeName === self::XML_NODE_S) {
$spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C);
$numSpaces = (!empty($spaceAttribute)) ? (int) $spaceAttribute : 1;
$currentPValue .= str_repeat(' ', $numSpaces);
} elseif ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) {
$currentPValue .= $childNode->nodeValue;
}
}
$pNodeValues[] = $currentPValue;
}
$escapedCellValue = implode("\n", $pNodeValues);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell Numeric value from the given node.
*
* @param \DOMNode $node
* @return int|float The value associated with the cell
*/
protected function formatFloatCellValue($node)
{
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
$nodeIntValue = (int) $nodeValue;
$nodeFloatValue = (float) $nodeValue;
$cellValue = ((float) $nodeIntValue === $nodeFloatValue) ? $nodeIntValue : $nodeFloatValue;
return $cellValue;
}
/**
* Returns the cell Boolean value from the given node.
*
* @param \DOMNode $node
* @return bool The value associated with the cell
*/
protected function formatBooleanCellValue($node)
{
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE);
return (bool) $nodeValue;
}
/**
* Returns the cell Date value from the given node.
*
* @param \DOMNode $node
* @throws InvalidValueException If the value is not a valid date
* @return \DateTime|string The value associated with the cell
*/
protected function formatDateCellValue($node)
{
// The XML node looks like this:
//
// 05/19/16 04:39 PM
//
if ($this->shouldFormatDates) {
// The date is already formatted in the "p" tag
$nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
$cellValue = $nodeWithValueAlreadyFormatted->nodeValue;
} else {
// otherwise, get it from the "date-value" attribute
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
try {
$cellValue = new \DateTime($nodeValue);
} catch (\Exception $e) {
throw new InvalidValueException($nodeValue);
}
}
return $cellValue;
}
/**
* Returns the cell Time value from the given node.
*
* @param \DOMNode $node
* @throws InvalidValueException If the value is not a valid time
* @return \DateInterval|string The value associated with the cell
*/
protected function formatTimeCellValue($node)
{
// The XML node looks like this:
//
// 01:24:00 PM
//
if ($this->shouldFormatDates) {
// The date is already formatted in the "p" tag
$nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
$cellValue = $nodeWithValueAlreadyFormatted->nodeValue;
} else {
// otherwise, get it from the "time-value" attribute
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
try {
$cellValue = new \DateInterval($nodeValue);
} catch (\Exception $e) {
throw new InvalidValueException($nodeValue);
}
}
return $cellValue;
}
/**
* Returns the cell Currency value from the given node.
*
* @param \DOMNode $node
* @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR")
*/
protected function formatCurrencyCellValue($node)
{
$value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
$currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY);
return "$value $currency";
}
/**
* Returns the cell Percentage value from the given node.
*
* @param \DOMNode $node
* @return int|float The value associated with the cell
*/
protected function formatPercentageCellValue($node)
{
// percentages are formatted like floats
return $this->formatFloatCellValue($node);
}
}
src/Spout/Reader/ODS/Helper/SettingsHelper.php 0000666 00000003475 13476413670 0015214 0 ustar 00 entityFactory = $entityFactory;
}
/**
* @param string $filePath Path of the file to be read
* @return string|null Name of the sheet that was defined as active or NULL if none found
*/
public function getActiveSheetName($filePath)
{
$xmlReader = $this->entityFactory->createXMLReader();
if ($xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) {
return null;
}
$activeSheetName = null;
try {
while ($xmlReader->readUntilNodeFound(self::XML_NODE_CONFIG_ITEM)) {
if ($xmlReader->getAttribute(self::XML_ATTRIBUTE_CONFIG_NAME) === self::XML_ATTRIBUTE_VALUE_ACTIVE_TABLE) {
$activeSheetName = $xmlReader->readString();
break;
}
}
} catch (XMLProcessingException $exception) {
// do nothing
}
$xmlReader->close();
return $activeSheetName;
}
}
src/Spout/Reader/ODS/Manager/OptionsManager.php 0000666 00000001277 13476413670 0015333 0 ustar 00 setOption(Options::SHOULD_FORMAT_DATES, false);
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
}
}
src/Spout/Reader/ODS/Reader.php 0000666 00000004014 13476413670 0012225 0 ustar 00 entityFactory;
$this->zip = $entityFactory->createZipArchive();
if ($this->zip->open($filePath) === true) {
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->sheetIterator = $entityFactory->createSheetIterator($filePath, $this->optionsManager);
} else {
throw new IOException("Could not open $filePath for reading.");
}
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
protected function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->zip) {
$this->zip->close();
}
}
}
src/Spout/Reader/ODS/RowIterator.php 0000666 00000034751 13476413670 0013317 0 ustar 00 " element
* @param OptionsManagerInterface $optionsManager Reader's options manager
* @param CellValueFormatter $cellValueFormatter Helper to format cell values
* @param XMLProcessor $xmlProcessor Helper to process XML files
* @param RowManager $rowManager Manages rows
* @param InternalEntityFactory $entityFactory Factory to create entities
*/
public function __construct(
XMLReader $xmlReader,
OptionsManagerInterface $optionsManager,
CellValueFormatter $cellValueFormatter,
XMLProcessor $xmlProcessor,
RowManager $rowManager,
InternalEntityFactory $entityFactory
) {
$this->xmlReader = $xmlReader;
$this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
$this->cellValueFormatter = $cellValueFormatter;
$this->entityFactory = $entityFactory;
$this->rowManager = $rowManager;
// Register all callbacks to process different nodes when reading the XML file
$this->xmlProcessor = $xmlProcessor;
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']);
}
/**
* Rewind the Iterator to the first element.
* NOTE: It can only be done once, as it is not possible to read an XML file backwards.
* @see http://php.net/manual/en/iterator.rewind.php
*
* @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
* @return void
*/
public function rewind()
{
// Because sheet and row data is located in the file, we can't rewind both the
// sheet iterator and the row iterator, as XML file cannot be read backwards.
// Therefore, rewinding the row iterator has been disabled.
if ($this->hasAlreadyBeenRewound) {
throw new IteratorNotRewindableException();
}
$this->hasAlreadyBeenRewound = true;
$this->lastRowIndexProcessed = 0;
$this->nextRowIndexToBeProcessed = 1;
$this->rowBuffer = null;
$this->hasReachedEndOfFile = false;
$this->next();
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid()
{
return (!$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Empty rows will be skipped.
* @see http://php.net/manual/en/iterator.next.php
*
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
* @return void
*/
public function next()
{
if ($this->doesNeedDataForNextRowToBeProcessed()) {
$this->readDataForNextRow();
}
$this->lastRowIndexProcessed++;
}
/**
* Returns whether we need data for the next row to be processed.
* We DO need to read data if:
* - we have not read any rows yet
* OR
* - the next row to be processed immediately follows the last read row
*
* @return bool Whether we need data for the next row to be processed.
*/
protected function doesNeedDataForNextRowToBeProcessed()
{
$hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
return (
!$hasReadAtLeastOneRow ||
$this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1
);
}
/**
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
* @return void
*/
protected function readDataForNextRow()
{
$this->currentlyProcessedRow = $this->entityFactory->createRow();
try {
$this->xmlProcessor->readUntilStopped();
} catch (XMLProcessingException $exception) {
throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
}
$this->rowBuffer = $this->currentlyProcessedRow;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processRowStartingNode($xmlReader)
{
// Reset data from current row
$this->hasAlreadyReadOneCellInCurrentRow = false;
$this->lastProcessedCell = null;
$this->numColumnsRepeated = 1;
$this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader);
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processCellStartingNode($xmlReader)
{
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
// NOTE: expand() will automatically decode all XML entities of the child nodes
$node = $xmlReader->expand();
$currentCell = $this->getCell($node);
// process cell N only after having read cell N+1 (see below why)
if ($this->hasAlreadyReadOneCellInCurrentRow) {
for ($i = 0; $i < $this->numColumnsRepeated; $i++) {
$this->currentlyProcessedRow->addCell($this->lastProcessedCell);
}
}
$this->hasAlreadyReadOneCellInCurrentRow = true;
$this->lastProcessedCell = $currentCell;
$this->numColumnsRepeated = $currentNumColumnsRepeated;
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @return int A return code that indicates what action should the processor take next
*/
protected function processRowEndingNode()
{
$isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRow, $this->lastProcessedCell);
// if the fetched row is empty and we don't want to preserve it...
if (!$this->shouldPreserveEmptyRows && $isEmptyRow) {
// ... skip it
return XMLProcessor::PROCESSING_CONTINUE;
}
// if the row is empty, we don't want to return more than one cell
$actualNumColumnsRepeated = (!$isEmptyRow) ? $this->numColumnsRepeated : 1;
$numCellsInCurrentlyProcessedRow = $this->currentlyProcessedRow->getNumCells();
// Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
// The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData".
// This is to avoid creating a lot of empty cells, as Excel adds a last empty ""
// with a number-columns-repeated value equals to the number of (supported columns - used columns).
// In Excel, the number of supported columns is 16384, but we don't want to returns rows with
// always 16384 cells.
if (($numCellsInCurrentlyProcessedRow + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
for ($i = 0; $i < $actualNumColumnsRepeated; $i++) {
$this->currentlyProcessedRow->addCell($this->lastProcessedCell);
}
}
// If we are processing row N and the row is repeated M times,
// then the next row to be processed will be row (N+M).
$this->nextRowIndexToBeProcessed += $this->numRowsRepeated;
// at this point, we have all the data we need for the row
// so that we can populate the buffer
return XMLProcessor::PROCESSING_STOP;
}
/**
* @return int A return code that indicates what action should the processor take next
*/
protected function processTableEndingNode()
{
// The closing "" marks the end of the file
$this->hasReachedEndOfFile = true;
return XMLProcessor::PROCESSING_STOP;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing
*/
protected function getNumRowsRepeatedForCurrentNode($xmlReader)
{
$numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
return ($numRowsRepeated !== null) ? (int) $numRowsRepeated : 1;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
*/
protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
{
$numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
return ($numColumnsRepeated !== null) ? (int) $numColumnsRepeated : 1;
}
/**
* Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
*
* @param \DOMNode $node
* @return Cell The cell set with the associated with the cell
*/
protected function getCell($node)
{
try {
$cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node);
$cell = $this->entityFactory->createCell($cellValue);
} catch (InvalidValueException $exception) {
$cell = $this->entityFactory->createCell($exception->getInvalidValue());
$cell->setType(Cell::TYPE_ERROR);
}
return $cell;
}
/**
* After finishing processing each cell, a row is considered empty if it contains
* no cells or if the last read cell is empty.
* After finishing processing each cell, the last read cell is not part of the
* row data yet (as we still need to apply the "num-columns-repeated" attribute).
*
* @param Row $currentRow
* @param Cell $lastReadCell The last read cell
* @return bool Whether the row is empty
*/
protected function isEmptyRow($currentRow, $lastReadCell)
{
return (
$this->rowManager->isEmpty($currentRow) &&
(!isset($lastReadCell) || $lastReadCell->isEmpty())
);
}
/**
* Return the current element, from the buffer.
* @see http://php.net/manual/en/iterator.current.php
*
* @return Row
*/
public function current()
{
return $this->rowBuffer;
}
/**
* Return the key of the current element
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->lastRowIndexProcessed;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
$this->xmlReader->close();
}
}
src/Spout/Reader/ODS/Sheet.php 0000666 00000004071 13476413670 0012076 0 ustar 00 rowIterator = $rowIterator;
$this->index = $sheetIndex;
$this->name = $sheetName;
$this->isActive = $isSheetActive;
$this->isVisible = $isSheetVisible;
}
/**
* @return \Box\Spout\Reader\ODS\RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
/**
* @return int Index of the sheet, based on order in the workbook (zero-based)
*/
public function getIndex()
{
return $this->index;
}
/**
* @return string Name of the sheet
*/
public function getName()
{
return $this->name;
}
/**
* @return bool Whether the sheet was defined as active
*/
public function isActive()
{
return $this->isActive;
}
/**
* @return bool Whether the sheet is visible
*/
public function isVisible()
{
return $this->isVisible;
}
}
src/Spout/Reader/ODS/SheetIterator.php 0000666 00000017321 13476413670 0013612 0 ustar 00 [IS_SHEET_VISIBLE] */
protected $sheetsVisibility;
/**
* @param string $filePath Path of the file to be read
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager
* @param \Box\Spout\Common\Helper\Escaper\ODS $escaper Used to unescape XML data
* @param SettingsHelper $settingsHelper Helper to get data from "settings.xml"
* @param InternalEntityFactory $entityFactory Factory to create entities
*/
public function __construct($filePath, $optionsManager, $escaper, $settingsHelper, $entityFactory)
{
$this->filePath = $filePath;
$this->optionsManager = $optionsManager;
$this->entityFactory = $entityFactory;
$this->xmlReader = $entityFactory->createXMLReader();
$this->escaper = $escaper;
$this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
}
/**
* Rewind the Iterator to the first element
* @see http://php.net/manual/en/iterator.rewind.php
*
* @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data
* @return void
*/
public function rewind()
{
$this->xmlReader->close();
if ($this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH) === false) {
$contentXmlFilePath = $this->filePath . '#' . self::CONTENT_XML_FILE_PATH;
throw new IOException("Could not open \"{$contentXmlFilePath}\".");
}
try {
$this->sheetsVisibility = $this->readSheetsVisibility();
$this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
} catch (XMLProcessingException $exception) {
throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
}
$this->currentSheetIndex = 0;
}
/**
* Extracts the visibility of the sheets
*
* @return array Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE]
*/
private function readSheetsVisibility()
{
$sheetsVisibility = [];
$this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES);
$automaticStylesNode = $this->xmlReader->expand();
$tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES);
/** @var \DOMElement $tableStyleNode */
foreach ($tableStyleNodes as $tableStyleNode) {
$isSheetVisible = ($tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY) !== 'false');
$parentStyleNode = $tableStyleNode->parentNode;
$styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME);
$sheetsVisibility[$styleName] = $isSheetVisible;
}
return $sheetsVisibility;
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid()
{
return $this->hasFoundSheet;
}
/**
* Move forward to next element
* @see http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
$this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
if ($this->hasFoundSheet) {
$this->currentSheetIndex++;
}
}
/**
* Return the current element
* @see http://php.net/manual/en/iterator.current.php
*
* @return \Box\Spout\Reader\ODS\Sheet
*/
public function current()
{
$escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
$sheetName = $this->escaper->unescape($escapedSheetName);
$isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName);
$sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME);
$isSheetVisible = $this->isSheetVisible($sheetStyleName);
return $this->entityFactory->createSheet(
$this->xmlReader,
$this->currentSheetIndex,
$sheetName,
$isSheetActive,
$isSheetVisible,
$this->optionsManager
);
}
/**
* Returns whether the current sheet was defined as the active one
*
* @param string $sheetName Name of the current sheet
* @param int $sheetIndex Index of the current sheet
* @param string|null $activeSheetName Name of the sheet that was defined as active or NULL if none defined
* @return bool Whether the current sheet was defined as the active one
*/
private function isSheetActive($sheetName, $sheetIndex, $activeSheetName)
{
// The given sheet is active if its name matches the defined active sheet's name
// or if no information about the active sheet was found, it defaults to the first sheet.
return (
($activeSheetName === null && $sheetIndex === 0) ||
($activeSheetName === $sheetName)
);
}
/**
* Returns whether the current sheet is visible
*
* @param string $sheetStyleName Name of the sheet style
* @return bool Whether the current sheet is visible
*/
private function isSheetVisible($sheetStyleName)
{
return isset($this->sheetsVisibility[$sheetStyleName]) ?
$this->sheetsVisibility[$sheetStyleName] :
true;
}
/**
* Return the key of the current element
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->currentSheetIndex + 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
$this->xmlReader->close();
}
}
src/Spout/Reader/ReaderAbstract.php 0000666 00000017245 13476413670 0013276 0 ustar 00 optionsManager = $optionsManager;
$this->globalFunctionsHelper = $globalFunctionsHelper;
$this->entityFactory = $entityFactory;
}
/**
* Sets whether date/time values should be returned as PHP objects or be formatted as strings.
*
* @param bool $shouldFormatDates
* @return ReaderAbstract
*/
public function setShouldFormatDates($shouldFormatDates)
{
$this->optionsManager->setOption(Options::SHOULD_FORMAT_DATES, $shouldFormatDates);
return $this;
}
/**
* Sets whether empty rows should be returned or skipped.
*
* @param bool $shouldPreserveEmptyRows
* @return ReaderAbstract
*/
public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
{
$this->optionsManager->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, $shouldPreserveEmptyRows);
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
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
* @return void
*/
public function open($filePath)
{
if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) {
throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file.");
}
if (!$this->isPhpStream($filePath)) {
// we skip the checks if the provided file path points to a PHP stream
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
throw new IOException("Could not open $filePath for reading! File does not exist.");
}
if (!$this->globalFunctionsHelper->is_readable($filePath)) {
throw new IOException("Could not open $filePath for reading! File is not readable.");
}
}
try {
$fileRealPath = $this->getFileRealPath($filePath);
$this->openReader($fileRealPath);
$this->isStreamOpened = true;
} catch (\Exception $exception) {
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
}
}
/**
* Returns the real path of the given path.
* If the given path is a valid stream wrapper, returns the path unchanged.
*
* @param string $filePath
* @return string
*/
protected function getFileRealPath($filePath)
{
if ($this->isSupportedStreamWrapper($filePath)) {
return $filePath;
}
// Need to use realpath to fix "Can't open file" on some Windows setup
return realpath($filePath);
}
/**
* Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used.
* For example, php://temp => php, s3://path/to/file => s3...
*
* @param string $filePath Path of the file to be read
* @return string|null The stream wrapper scheme or NULL if not a stream wrapper
*/
protected function getStreamWrapperScheme($filePath)
{
$streamScheme = null;
if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
$streamScheme = $matches[1];
}
return $streamScheme;
}
/**
* Checks if the given path is an unsupported stream wrapper
* (like local path, php://temp, mystream://foo/bar...).
*
* @param string $filePath Path of the file to be read
* @return bool Whether the given path is an unsupported stream wrapper
*/
protected function isStreamWrapper($filePath)
{
return ($this->getStreamWrapperScheme($filePath) !== null);
}
/**
* Checks if the given path is an supported stream wrapper
* (like php://temp, mystream://foo/bar...).
* If the given path is a local path, returns true.
*
* @param string $filePath Path of the file to be read
* @return bool Whether the given path is an supported stream wrapper
*/
protected function isSupportedStreamWrapper($filePath)
{
$streamScheme = $this->getStreamWrapperScheme($filePath);
return ($streamScheme !== null) ?
in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) :
true;
}
/**
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
*
* @param string $filePath Path of the file to be read
* @return bool Whether the given path maps to a PHP stream
*/
protected function isPhpStream($filePath)
{
$streamScheme = $this->getStreamWrapperScheme($filePath);
return ($streamScheme === 'php');
}
/**
* Returns an iterator to iterate over sheets.
*
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
* @return \Iterator To iterate over sheets
*/
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;
}
}
}
src/Spout/Reader/ReaderInterface.php 0000666 00000001455 13476413670 0013427 0 ustar 00 initialUseInternalErrorsValue = libxml_use_internal_errors(true);
}
/**
* Throws an XMLProcessingException if an error occured.
* It also always resets the "libxml_use_internal_errors" setting back to its initial value.
*
* @throws \Box\Spout\Reader\Exception\XMLProcessingException
* @return void
*/
protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured()
{
if ($this->hasXMLErrorOccured()) {
$this->resetXMLInternalErrorsSetting();
throw new XMLProcessingException($this->getLastXMLErrorMessage());
}
$this->resetXMLInternalErrorsSetting();
}
/**
* Returns whether the a XML error has occured since the last time errors were cleared.
*
* @return bool TRUE if an error occured, FALSE otherwise
*/
private function hasXMLErrorOccured()
{
return (libxml_get_last_error() !== false);
}
/**
* Returns the error message for the last XML error that occured.
* @see libxml_get_last_error
*
* @return string|null Last XML error message or null if no error
*/
private function getLastXMLErrorMessage()
{
$errorMessage = null;
$error = libxml_get_last_error();
if ($error !== false) {
$errorMessage = trim($error->message);
}
return $errorMessage;
}
/**
* @return void
*/
protected function resetXMLInternalErrorsSetting()
{
libxml_use_internal_errors($this->initialUseInternalErrorsValue);
}
}
src/Spout/Reader/Wrapper/XMLReader.php 0000666 00000013412 13476413670 0013603 0 ustar 00 getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath);
// We need to check first that the file we are trying to read really exist because:
// - PHP emits a warning when trying to open a file that does not exist.
// - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779)
if ($this->fileExistsWithinZip($realPathURI)) {
$wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET);
}
return $wasOpenSuccessful;
}
/**
* Returns the real path for the given path components.
* This is useful to avoid issues on some Windows setup.
*
* @param string $zipFilePath Path to the ZIP file
* @param string $fileInsideZipPath Relative or absolute path of the file inside the zip
* @return string The real path URI
*/
public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath)
{
// The file path should not start with a '/', otherwise it won't be found
$fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/');
return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPathWithoutLeadingSlash);
}
/**
* Returns whether the file at the given location exists
*
* @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml"
* @return bool TRUE if the file exists, FALSE otherwise
*/
protected function fileExistsWithinZip($zipStreamURI)
{
$doesFileExists = false;
$pattern = '/zip:\/\/([^#]+)#(.*)/';
if (preg_match($pattern, $zipStreamURI, $matches)) {
$zipFilePath = $matches[1];
$innerFilePath = $matches[2];
$zip = new \ZipArchive();
if ($zip->open($zipFilePath) === true) {
$doesFileExists = ($zip->locateName($innerFilePath) !== false);
$zip->close();
}
}
return $doesFileExists;
}
/**
* Move to next node in document
* @see \XMLReader::read
*
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
* @return bool TRUE on success or FALSE on failure
*/
public function read()
{
$this->useXMLInternalErrors();
$wasReadSuccessful = parent::read();
$this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
return $wasReadSuccessful;
}
/**
* Read until the element with the given name is found, or the end of the file.
*
* @param string $nodeName Name of the node to find
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
* @return bool TRUE on success or FALSE on failure
*/
public function readUntilNodeFound($nodeName)
{
do {
$wasReadSuccessful = $this->read();
$isNotPositionedOnStartingNode = !$this->isPositionedOnStartingNode($nodeName);
} while ($wasReadSuccessful && $isNotPositionedOnStartingNode);
return $wasReadSuccessful;
}
/**
* Move cursor to next node skipping all subtrees
* @see \XMLReader::next
*
* @param string|null $localName The name of the next node to move to
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
* @return bool TRUE on success or FALSE on failure
*/
public function next($localName = null)
{
$this->useXMLInternalErrors();
$wasNextSuccessful = parent::next($localName);
$this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
return $wasNextSuccessful;
}
/**
* @param string $nodeName
* @return bool Whether the XML Reader is currently positioned on the starting node with given name
*/
public function isPositionedOnStartingNode($nodeName)
{
return $this->isPositionedOnNode($nodeName, self::ELEMENT);
}
/**
* @param string $nodeName
* @return bool Whether the XML Reader is currently positioned on the ending node with given name
*/
public function isPositionedOnEndingNode($nodeName)
{
return $this->isPositionedOnNode($nodeName, self::END_ELEMENT);
}
/**
* @param string $nodeName
* @param int $nodeType
* @return bool Whether the XML Reader is currently positioned on the node with given name and type
*/
private function isPositionedOnNode($nodeName, $nodeType)
{
// In some cases, the node has a prefix (for instance, "" can also be "").
// So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName").
// @see https://github.com/box/spout/issues/233
$hasPrefix = (strpos($nodeName, ':') !== false);
$currentNodeName = ($hasPrefix) ? $this->name : $this->localName;
return ($this->nodeType === $nodeType && $currentNodeName === $nodeName);
}
/**
* @return string The name of the current node, un-prefixed
*/
public function getCurrentNodeName()
{
return $this->localName;
}
}
src/Spout/Reader/XLSX/Creator/HelperFactory.php 0000666 00000002456 13476413670 0015352 0 ustar 00 createStringsEscaper();
return new CellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper);
}
/**
* @return Escaper\XLSX
*/
public function createStringsEscaper()
{
/* @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
return new Escaper\XLSX();
}
}
src/Spout/Reader/XLSX/Creator/InternalEntityFactory.php 0000666 00000011765 13476413670 0017107 0 ustar 00 managerFactory = $managerFactory;
$this->helperFactory = $helperFactory;
}
/**
* @param string $filePath Path of the file to be read
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @param SharedStringsManager $sharedStringsManager Manages shared strings
* @return SheetIterator
*/
public function createSheetIterator($filePath, $optionsManager, $sharedStringsManager)
{
$sheetManager = $this->managerFactory->createSheetManager(
$filePath,
$optionsManager,
$sharedStringsManager,
$this
);
return new SheetIterator($sheetManager);
}
/**
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
* @param string $sheetName Name of the sheet
* @param bool $isSheetActive Whether the sheet was defined as active
* @param bool $isSheetVisible Whether the sheet is visible
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @param SharedStringsManager $sharedStringsManager Manages shared strings
* @return Sheet
*/
public function createSheet(
$filePath,
$sheetDataXMLFilePath,
$sheetIndex,
$sheetName,
$isSheetActive,
$isSheetVisible,
$optionsManager,
$sharedStringsManager
) {
$rowIterator = $this->createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager);
return new Sheet($rowIterator, $sheetIndex, $sheetName, $isSheetActive, $isSheetVisible);
}
/**
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @param SharedStringsManager $sharedStringsManager Manages shared strings
* @return RowIterator
*/
private function createRowIterator($filePath, $sheetDataXMLFilePath, $optionsManager, $sharedStringsManager)
{
$xmlReader = $this->createXMLReader();
$xmlProcessor = $this->createXMLProcessor($xmlReader);
$styleManager = $this->managerFactory->createStyleManager($filePath, $this);
$rowManager = $this->managerFactory->createRowManager($this);
$shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES);
$shouldUse1904Dates = $optionsManager->getOption(Options::SHOULD_USE_1904_DATES);
$cellValueFormatter = $this->helperFactory->createCellValueFormatter(
$sharedStringsManager,
$styleManager,
$shouldFormatDates,
$shouldUse1904Dates
);
$shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
return new RowIterator(
$filePath,
$sheetDataXMLFilePath,
$shouldPreserveEmptyRows,
$xmlReader,
$xmlProcessor,
$cellValueFormatter,
$rowManager,
$this
);
}
/**
* @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);
}
/**
* @return \ZipArchive
*/
public function createZipArchive()
{
return new \ZipArchive();
}
/**
* @return XMLReader
*/
public function createXMLReader()
{
return new XMLReader();
}
/**
* @param $xmlReader
* @return XMLProcessor
*/
public function createXMLProcessor($xmlReader)
{
return new XMLProcessor($xmlReader);
}
}
src/Spout/Reader/XLSX/Creator/ManagerFactory.php 0000666 00000007420 13476413670 0015501 0 ustar 00 helperFactory = $helperFactory;
$this->cachingStrategyFactory = $cachingStrategyFactory;
}
/**
* @param string $filePath Path of the XLSX file being read
* @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
* @param InternalEntityFactory $entityFactory Factory to create entities
* @return SharedStringsManager
*/
public function createSharedStringsManager($filePath, $tempFolder, $entityFactory)
{
$workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory);
return new SharedStringsManager(
$filePath,
$tempFolder,
$workbookRelationshipsManager,
$entityFactory,
$this->helperFactory,
$this->cachingStrategyFactory
);
}
/**
* @param string $filePath Path of the XLSX file being read
* @param InternalEntityFactory $entityFactory Factory to create entities
* @return WorkbookRelationshipsManager
*/
private function createWorkbookRelationshipsManager($filePath, $entityFactory)
{
if (!isset($this->cachedWorkbookRelationshipsManager)) {
$this->cachedWorkbookRelationshipsManager = new WorkbookRelationshipsManager($filePath, $entityFactory);
}
return $this->cachedWorkbookRelationshipsManager;
}
/**
* @param string $filePath Path of the XLSX file being read
* @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager
* @param \Box\Spout\Reader\XLSX\Manager\SharedStringsManager $sharedStringsManager Manages shared strings
* @param InternalEntityFactory $entityFactory Factory to create entities
* @return SheetManager
*/
public function createSheetManager($filePath, $optionsManager, $sharedStringsManager, $entityFactory)
{
$escaper = $this->helperFactory->createStringsEscaper();
return new SheetManager($filePath, $optionsManager, $sharedStringsManager, $escaper, $entityFactory);
}
/**
* @param string $filePath Path of the XLSX file being read
* @param InternalEntityFactory $entityFactory Factory to create entities
* @return StyleManager
*/
public function createStyleManager($filePath, $entityFactory)
{
$workbookRelationshipsManager = $this->createWorkbookRelationshipsManager($filePath, $entityFactory);
return new StyleManager($filePath, $workbookRelationshipsManager, $entityFactory);
}
/**
* @param InternalEntityFactory $entityFactory Factory to create entities
* @return RowManager
*/
public function createRowManager($entityFactory)
{
return new RowManager($entityFactory);
}
}
src/Spout/Reader/XLSX/Helper/CellHelper.php 0000666 00000006534 13476413670 0014443 0 ustar 00 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6,
'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13,
'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20,
'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25,
];
/**
* Returns the base 10 column index associated to the cell index (base 26).
* Excel uses A to Z letters for column indexing, where A is the 1st column,
* Z is the 26th and AA is the 27th.
* The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26.
*
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
* @return int
*/
public static function getColumnIndexFromCellIndex($cellIndex)
{
if (!self::isValidCellIndex($cellIndex)) {
throw new InvalidArgumentException('Cannot get column index from an invalid cell index.');
}
$columnIndex = 0;
// Remove row information
$columnLetters = preg_replace('/\d/', '', $cellIndex);
// strlen() is super slow too... Using isset() is way faster and not too unreadable,
// since we checked before that there are between 1 and 3 letters.
$columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1;
// Looping over the different letters of the column is slower than this method.
// Also, not using the pow() function because it's slooooow...
switch ($columnLength) {
case 1:
$columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]);
break;
case 2:
$firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26;
$secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]];
$columnIndex = $firstLetterIndex + $secondLetterIndex;
break;
case 3:
$firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676;
$secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26;
$thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]];
$columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex;
break;
}
return $columnIndex;
}
/**
* Returns whether a cell index is valid, in an Excel world.
* To be valid, the cell index should start with capital letters and be followed by numbers.
* There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'.
*
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
* @return bool
*/
protected static function isValidCellIndex($cellIndex)
{
return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1);
}
}
src/Spout/Reader/XLSX/Helper/CellValueFormatter.php 0000666 00000026230 13476413670 0016157 0 ustar 00 sharedStringsManager = $sharedStringsManager;
$this->styleManager = $styleManager;
$this->shouldFormatDates = $shouldFormatDates;
$this->shouldUse1904Dates = $shouldUse1904Dates;
$this->escaper = $escaper;
}
/**
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
*
* @param \DOMNode $node
* @throws InvalidValueException If the value is not valid
* @return string|int|float|bool|\DateTime The value associated with the cell
*/
public function extractAndFormatNodeValue($node)
{
// Default cell type is "n"
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
$cellStyleId = (int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID);
$vNodeValue = $this->getVNodeValue($node);
if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
return $vNodeValue;
}
switch ($cellType) {
case self::CELL_TYPE_INLINE_STRING:
return $this->formatInlineStringCellValue($node);
case self::CELL_TYPE_SHARED_STRING:
return $this->formatSharedStringCellValue($vNodeValue);
case self::CELL_TYPE_STR:
return $this->formatStrCellValue($vNodeValue);
case self::CELL_TYPE_BOOLEAN:
return $this->formatBooleanCellValue($vNodeValue);
case self::CELL_TYPE_NUMERIC:
return $this->formatNumericCellValue($vNodeValue, $cellStyleId);
case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($vNodeValue);
default:
throw new InvalidValueException($vNodeValue);
}
}
/**
* Returns the cell's string value from a node's nested value node
*
* @param \DOMNode $node
* @return string The value associated with the cell
*/
protected function getVNodeValue($node)
{
// for cell types having a "v" tag containing the value.
// if not, the returned value should be empty string.
$vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0);
return ($vNode !== null) ? $vNode->nodeValue : '';
}
/**
* Returns the cell String value where string is inline.
*
* @param \DOMNode $node
* @return string The value associated with the cell
*/
protected function formatInlineStringCellValue($node)
{
// inline strings are formatted this way:
// [INLINE_STRING]
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
$cellValue = $this->escaper->unescape($tNode->nodeValue);
return $cellValue;
}
/**
* Returns the cell String value from shared-strings file using nodeValue index.
*
* @param string $nodeValue
* @return string The value associated with the cell
*/
protected function formatSharedStringCellValue($nodeValue)
{
// shared strings are formatted this way:
// [SHARED_STRING_INDEX]
$sharedStringIndex = (int) $nodeValue;
$escapedCellValue = $this->sharedStringsManager->getStringAtIndex($sharedStringIndex);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value, where string is stored in value node.
*
* @param string $nodeValue
* @return string The value associated with the cell
*/
protected function formatStrCellValue($nodeValue)
{
$escapedCellValue = trim($nodeValue);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell Numeric value from string of nodeValue.
* The value can also represent a timestamp and a DateTime will be returned.
*
* @param string $nodeValue
* @param int $cellStyleId 0 being the default style
* @return int|float|\DateTime The value associated with the cell
*/
protected function formatNumericCellValue($nodeValue, $cellStyleId)
{
// Numeric values can represent numbers as well as timestamps.
// We need to look at the style of the cell to determine whether it is one or the other.
$shouldFormatAsDate = $this->styleManager->shouldFormatNumericValueAsDate($cellStyleId);
if ($shouldFormatAsDate) {
$cellValue = $this->formatExcelTimestampValue((float) $nodeValue, $cellStyleId);
} else {
$nodeIntValue = (int) $nodeValue;
$nodeFloatValue = (float) $nodeValue;
$cellValue = ((float) $nodeIntValue === $nodeFloatValue) ? $nodeIntValue : $nodeFloatValue;
}
return $cellValue;
}
/**
* Returns a cell's PHP Date value, associated to the given timestamp.
* NOTE: The timestamp is a float representing the number of days since the base Excel date:
* Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting.
* NOTE: The timestamp can also represent a time, if it is a value between 0 and 1.
*
* @see ECMA-376 Part 1 - §18.17.4
*
* @param float $nodeValue
* @param int $cellStyleId 0 being the default style
* @throws InvalidValueException If the value is not a valid timestamp
* @return \DateTime The value associated with the cell
*/
protected function formatExcelTimestampValue($nodeValue, $cellStyleId)
{
if ($this->isValidTimestampValue($nodeValue)) {
$cellValue = $this->formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId);
} else {
throw new InvalidValueException($nodeValue);
}
return $cellValue;
}
/**
* Returns whether the given timestamp is supported by SpreadsheetML
* @see ECMA-376 Part 1 - §18.17.4 - this specifies the timestamp boundaries.
*
* @param float $timestampValue
* @return bool
*/
protected function isValidTimestampValue($timestampValue)
{
// @NOTE: some versions of Excel don't support negative dates (e.g. Excel for Mac 2011)
return (
$this->shouldUse1904Dates && $timestampValue >= -695055 && $timestampValue <= 2957003.9999884 ||
!$this->shouldUse1904Dates && $timestampValue >= -693593 && $timestampValue <= 2958465.9999884
);
}
/**
* Returns a cell's PHP DateTime value, associated to the given timestamp.
* Only the time value matters. The date part is set to the base Excel date:
* Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting.
*
* @param float $nodeValue
* @param int $cellStyleId 0 being the default style
* @return \DateTime|string The value associated with the cell
*/
protected function formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId)
{
$baseDate = $this->shouldUse1904Dates ? '1904-01-01' : '1899-12-30';
$daysSinceBaseDate = (int) $nodeValue;
$timeRemainder = fmod($nodeValue, 1);
$secondsRemainder = round($timeRemainder * self::NUM_SECONDS_IN_ONE_DAY, 0);
$dateObj = \DateTime::createFromFormat('|Y-m-d', $baseDate);
$dateObj->modify('+' . $daysSinceBaseDate . 'days');
$dateObj->modify('+' . $secondsRemainder . 'seconds');
if ($this->shouldFormatDates) {
$styleNumberFormatCode = $this->styleManager->getNumberFormatCode($cellStyleId);
$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
$cellValue = $dateObj->format($phpDateFormat);
} else {
$cellValue = $dateObj;
}
return $cellValue;
}
/**
* Returns the cell Boolean value from a specific node's Value.
*
* @param string $nodeValue
* @return bool The value associated with the cell
*/
protected function formatBooleanCellValue($nodeValue)
{
return (bool) $nodeValue;
}
/**
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
* @see ECMA-376 Part 1 - §18.17.4
*
* @param string $nodeValue ISO 8601 Date string
* @throws InvalidValueException If the value is not a valid date
* @return \DateTime|string The value associated with the cell
*/
protected function formatDateCellValue($nodeValue)
{
// Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
try {
$cellValue = ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue);
} catch (\Exception $e) {
throw new InvalidValueException($nodeValue);
}
return $cellValue;
}
}
src/Spout/Reader/XLSX/Helper/DateFormatHelper.php 0000666 00000013332 13476413670 0015604 0 ustar 00 [
// Time
'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem
':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month)
'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month)
'ss' => 's', // Seconds, with leading zeros
'.s' => '', // Ignore (fractional seconds format does not exist in PHP)
// Date
'e' => 'Y', // Full numeric representation of a year, 4 digits
'yyyy' => 'Y', // Full numeric representation of a year, 4 digits
'yy' => 'y', // Two digit representation of a year
'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...)
'mmmm' => 'F', // Full textual representation of a month
'mmm' => 'M', // Short textual representation of a month, three letters
'mm' => 'm', // Numeric representation of a month, with leading zeros
'm' => 'n', // Numeric representation of a month, without leading zeros
'dddd' => 'l', // Full textual representation of the day of the week
'ddd' => 'D', // Textual representation of a day, three letters
'dd' => 'd', // Day of the month, 2 digits with leading zeros
'd' => 'j', // Day of the month without leading zeros
],
self::KEY_HOUR_12 => [
'hh' => 'h', // 12-hour format of an hour without leading zeros
'h' => 'g', // 12-hour format of an hour without leading zeros
],
self::KEY_HOUR_24 => [
'hh' => 'H', // 24-hour hours with leading zero
'h' => 'G', // 24-hour format of an hour without leading zeros
],
];
/**
* Converts the given Excel date format to a format understandable by the PHP date function.
*
* @param string $excelDateFormat Excel date format
* @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php)
*/
public static function toPHPDateFormat($excelDateFormat)
{
// Remove brackets potentially present at the beginning of the format string
// and text portion of the format at the end of it (starting with ";")
// See §18.8.31 of ECMA-376 for more detail.
$dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat);
// Double quotes are used to escape characters that must not be interpreted.
// For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y"
// By exploding the format string using double quote as a delimiter, we can get all parts
// that must be transformed (even indexes) and all parts that must not be (odd indexes).
$dateFormatParts = explode('"', $dateFormat);
foreach ($dateFormatParts as $partIndex => $dateFormatPart) {
// do not look at odd indexes
if ($partIndex % 2 === 1) {
continue;
}
// Make sure all characters are lowercase, as the mapping table is using lowercase characters
$transformedPart = strtolower($dateFormatPart);
// Remove escapes related to non-format characters
$transformedPart = str_replace('\\', '', $transformedPart);
// Apply general transformation first...
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]);
// ... then apply hour transformation, for 12-hour or 24-hour format
if (self::has12HourFormatMarker($dateFormatPart)) {
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]);
} else {
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]);
}
// overwrite the parts array with the new transformed part
$dateFormatParts[$partIndex] = $transformedPart;
}
// Merge all transformed parts back together
$phpDateFormat = implode('"', $dateFormatParts);
// Finally, to have the date format compatible with the DateTime::format() function, we need to escape
// all characters that are inside double quotes (and double quotes must be removed).
// For instance, ["Day " dd] should become [\D\a\y\ dd]
$phpDateFormat = preg_replace_callback('/"(.+?)"/', function ($matches) {
$stringToEscape = $matches[1];
$letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY);
return '\\' . implode('\\', $letters);
}, $phpDateFormat);
return $phpDateFormat;
}
/**
* @param string $excelDateFormat Date format as defined by Excel
* @return bool Whether the given date format has the 12-hour format marker
*/
private static function has12HourFormatMarker($excelDateFormat)
{
return (stripos($excelDateFormat, 'am/pm') !== false);
}
}
src/Spout/Reader/XLSX/Manager/OptionsManager.php 0000666 00000001624 13476413670 0015500 0 ustar 00 setOption(Options::TEMP_FOLDER, sys_get_temp_dir());
$this->setOption(Options::SHOULD_FORMAT_DATES, false);
$this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false);
$this->setOption(Options::SHOULD_USE_1904_DATES, false);
}
}
src/Spout/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php 0000666 00000012542 13476413670 0023217 0 ustar 00 20 * 600 ≈ 12KB
*/
const AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB = 12;
/**
* To avoid running out of memory when extracting a huge number of shared strings, they can be saved to temporary files
* instead of in memory. Then, when accessing a string, the corresponding file contents will be loaded in memory
* and the string will be quickly retrieved.
* The performance bottleneck is not when creating these temporary files, but rather when loading their content.
* Because the contents of the last loaded file stays in memory until another file needs to be loaded, it works
* best when the indexes of the shared strings are sorted in the sheet data.
* 10,000 was chosen because it creates small files that are fast to be loaded in memory.
*/
const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
/**
* Returns the best caching strategy, given the number of unique shared strings
* and the amount of memory available.
*
* @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
* @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
* @param HelperFactory $helperFactory Factory to create helpers
* @return CachingStrategyInterface The best caching strategy
*/
public function createBestCachingStrategy($sharedStringsUniqueCount, $tempFolder, $helperFactory)
{
if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) {
return new InMemoryStrategy($sharedStringsUniqueCount);
}
return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE, $helperFactory);
}
/**
* Returns whether it is safe to use in-memory caching, given the number of unique shared strings
* and the amount of memory available.
*
* @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
* @return bool
*/
protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)
{
// if the number of shared strings in unknown, do not use "in memory" strategy
if ($sharedStringsUniqueCount === null) {
return false;
}
$memoryAvailable = $this->getMemoryLimitInKB();
if ($memoryAvailable === -1) {
// if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe
$isInMemoryStrategyUsageSafe = ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE);
} else {
$memoryNeeded = $sharedStringsUniqueCount * self::AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB;
$isInMemoryStrategyUsageSafe = ($memoryAvailable > $memoryNeeded);
}
return $isInMemoryStrategyUsageSafe;
}
/**
* Returns the PHP "memory_limit" in Kilobytes
*
* @return float
*/
protected function getMemoryLimitInKB()
{
$memoryLimitFormatted = $this->getMemoryLimitFromIni();
$memoryLimitFormatted = strtolower(trim($memoryLimitFormatted));
// No memory limit
if ($memoryLimitFormatted === '-1') {
return -1;
}
if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) {
$amount = (int) ($matches[1]);
$unit = $matches[2];
switch ($unit) {
case 'b': return ($amount / 1024);
case 'k': return $amount;
case 'm': return ($amount * 1024);
case 'g': return ($amount * 1024 * 1024);
case 't': return ($amount * 1024 * 1024 * 1024);
}
}
return -1;
}
/**
* Returns the formatted "memory_limit" value
*
* @return string
*/
protected function getMemoryLimitFromIni()
{
return ini_get('memory_limit');
}
}
src/Spout/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php 0000666 00000002372 13476413670 0023510 0 ustar 00 fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder);
$this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, uniqid('sharedstrings'));
$this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
$this->globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
$this->tempFilePointer = null;
}
/**
* Adds the given string to the cache.
*
* @param string $sharedString The string to be added to the cache
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
* @return void
*/
public function addStringForIndex($sharedString, $sharedStringIndex)
{
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
if ($this->tempFilePointer) {
$this->globalFunctionsHelper->fclose($this->tempFilePointer);
}
$this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w');
}
// The shared string retrieval logic expects each cell data to be on one line only
// Encoding the line feed character allows to preserve this assumption
$lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
$this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL);
}
/**
* Returns the path for the temp file that should contain the string for the given index
*
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
* @return string The temp file path for the given index
*/
protected function getSharedStringTempFilePath($sharedStringIndex)
{
$numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile);
return $this->tempFolder . '/sharedstrings' . $numTempFile;
}
/**
* Closes the cache after the last shared string was added.
* This prevents any additional string from being added to the cache.
*
* @return void
*/
public function closeCache()
{
// close pointer to the last temp file that was written
if ($this->tempFilePointer) {
$this->globalFunctionsHelper->fclose($this->tempFilePointer);
}
}
/**
* Returns the string located at the given index from the cache.
*
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
* @return string The shared string at the given index
*/
public function getStringAtIndex($sharedStringIndex)
{
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
$indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
}
if ($this->inMemoryTempFilePath !== $tempFilePath) {
// free memory
unset($this->inMemoryTempFileContents);
$this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath));
$this->inMemoryTempFilePath = $tempFilePath;
}
$sharedString = null;
// Using isset here because it is way faster than array_key_exists...
if (isset($this->inMemoryTempFileContents[$indexInFile])) {
$escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
$sharedString = $this->unescapeLineFeed($escapedSharedString);
}
if ($sharedString === null) {
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
}
return rtrim($sharedString, PHP_EOL);
}
/**
* Escapes the line feed characters (\n)
*
* @param string $unescapedString
* @return string
*/
private function escapeLineFeed($unescapedString)
{
return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
}
/**
* Unescapes the line feed characters (\n)
*
* @param string $escapedString
* @return string
*/
private function unescapeLineFeed($escapedString)
{
return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
}
/**
* Destroys the cache, freeing memory and removing any created artifacts
*
* @return void
*/
public function clearCache()
{
if ($this->tempFolder) {
$this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
}
}
}
src/Spout/Reader/XLSX/Manager/SharedStringsCaching/InMemoryStrategy.php 0000666 00000004736 13476413670 0022100 0 ustar 00 inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount);
$this->isCacheClosed = false;
}
/**
* Adds the given string to the cache.
*
* @param string $sharedString The string to be added to the cache
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
* @return void
*/
public function addStringForIndex($sharedString, $sharedStringIndex)
{
if (!$this->isCacheClosed) {
$this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString);
}
}
/**
* Closes the cache after the last shared string was added.
* This prevents any additional string from being added to the cache.
*
* @return void
*/
public function closeCache()
{
$this->isCacheClosed = true;
}
/**
* Returns the string located at the given index from the cache.
*
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
* @return string The shared string at the given index
*/
public function getStringAtIndex($sharedStringIndex)
{
try {
return $this->inMemoryCache->offsetGet($sharedStringIndex);
} catch (\RuntimeException $e) {
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
}
}
/**
* Destroys the cache, freeing memory and removing any created artifacts
*
* @return void
*/
public function clearCache()
{
unset($this->inMemoryCache);
$this->isCacheClosed = false;
}
}
src/Spout/Reader/XLSX/Manager/SharedStringsManager.php 0000666 00000023406 13476413670 0016627 0 ustar 00 filePath = $filePath;
$this->tempFolder = $tempFolder;
$this->workbookRelationshipsManager = $workbookRelationshipsManager;
$this->entityFactory = $entityFactory;
$this->helperFactory = $helperFactory;
$this->cachingStrategyFactory = $cachingStrategyFactory;
}
/**
* Returns whether the XLSX file contains a shared strings XML file
*
* @return bool
*/
public function hasSharedStrings()
{
return $this->workbookRelationshipsManager->hasSharedStringsXMLFile();
}
/**
* Builds an in-memory array containing all the shared strings of the sheet.
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
* It is then accessed by the sheet data, via the string index in the built table.
*
* More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx
*
* The XML file can be really big with sheets containing a lot of data. That is why
* we need to use a XML reader that provides streaming like the XMLReader library.
*
* @throws \Box\Spout\Common\Exception\IOException If shared strings XML file can't be read
* @return void
*/
public function extractSharedStrings()
{
$sharedStringsXMLFilePath = $this->workbookRelationshipsManager->getSharedStringsXMLFilePath();
$xmlReader = $this->entityFactory->createXMLReader();
$sharedStringIndex = 0;
if ($xmlReader->openFileInZip($this->filePath, $sharedStringsXMLFilePath) === false) {
throw new IOException('Could not open "' . $sharedStringsXMLFilePath . '".');
}
try {
$sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader);
$this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount);
$xmlReader->readUntilNodeFound(self::XML_NODE_SI);
while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SI) {
$this->processSharedStringsItem($xmlReader, $sharedStringIndex);
$sharedStringIndex++;
// jump to the next '' tag
$xmlReader->next(self::XML_NODE_SI);
}
$this->cachingStrategy->closeCache();
} catch (XMLProcessingException $exception) {
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
}
$xmlReader->close();
}
/**
* Returns the shared strings unique count, as specified in tag.
*
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read
* @return int|null Number of unique shared strings in the sharedStrings.xml file
*/
protected function getSharedStringsUniqueCount($xmlReader)
{
$xmlReader->next(self::XML_NODE_SST);
// Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE)
while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SST && $xmlReader->nodeType !== XMLReader::ELEMENT) {
$xmlReader->read();
}
$uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_UNIQUE_COUNT);
// some software do not add the "uniqueCount" attribute but only use the "count" one
// @see https://github.com/box/spout/issues/254
if ($uniqueCount === null) {
$uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_COUNT);
}
return ($uniqueCount !== null) ? (int) $uniqueCount : null;
}
/**
* Returns the best shared strings caching strategy.
*
* @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
* @return CachingStrategyInterface
*/
protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount)
{
return $this->cachingStrategyFactory
->createBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder, $this->helperFactory);
}
/**
* Processes the shared strings item XML node which the given XML reader is positioned on.
*
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on a "" node
* @param int $sharedStringIndex Index of the processed shared strings item
* @return void
*/
protected function processSharedStringsItem($xmlReader, $sharedStringIndex)
{
$sharedStringValue = '';
// NOTE: expand() will automatically decode all XML entities of the child nodes
$siNode = $xmlReader->expand();
$textNodes = $siNode->getElementsByTagName(self::XML_NODE_T);
foreach ($textNodes as $textNode) {
if ($this->shouldExtractTextNodeValue($textNode)) {
$textNodeValue = $textNode->nodeValue;
$shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode);
$sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : trim($textNodeValue);
}
}
$this->cachingStrategy->addStringForIndex($sharedStringValue, $sharedStringIndex);
}
/**
* Not all text nodes' values must be extracted.
* Some text nodes are part of a node describing the pronunciation for instance.
* We'll only consider the nodes whose parents are "" or "".
*
* @param \DOMElement $textNode Text node to check
* @return bool Whether the given text node's value must be extracted
*/
protected function shouldExtractTextNodeValue($textNode)
{
$parentTagName = $textNode->parentNode->localName;
return ($parentTagName === self::XML_NODE_SI || $parentTagName === self::XML_NODE_R);
}
/**
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
*
* @param \DOMElement $textNode The text node element () whose whitespace may be preserved
* @return bool Whether whitespace should be preserved
*/
protected function shouldPreserveWhitespace($textNode)
{
$spaceValue = $textNode->getAttribute(self::XML_ATTRIBUTE_XML_SPACE);
return ($spaceValue === self::XML_ATTRIBUTE_VALUE_PRESERVE);
}
/**
* Returns the shared string at the given index, using the previously chosen caching strategy.
*
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
* @return string The shared string at the given index
*/
public function getStringAtIndex($sharedStringIndex)
{
return $this->cachingStrategy->getStringAtIndex($sharedStringIndex);
}
/**
* Destroys the cache, freeing memory and removing any created artifacts
*
* @return void
*/
public function cleanup()
{
if ($this->cachingStrategy) {
$this->cachingStrategy->clearCache();
}
}
}
src/Spout/Reader/XLSX/Manager/SheetManager.php 0000666 00000023042 13476413670 0015113 0 ustar 00 filePath = $filePath;
$this->optionsManager = $optionsManager;
$this->sharedStringsManager = $sharedStringsManager;
$this->escaper = $escaper;
$this->entityFactory = $entityFactory;
}
/**
* Returns the sheets metadata of the file located at the previously given file path.
* The paths to the sheets' data are read from the [Content_Types].xml file.
*
* @return Sheet[] Sheets within the XLSX file
*/
public function getSheets()
{
$this->sheets = [];
$this->currentSheetIndex = 0;
$this->activeSheetIndex = 0; // By default, the first sheet is active
$xmlReader = $this->entityFactory->createXMLReader();
$xmlProcessor = $this->entityFactory->createXMLProcessor($xmlReader);
$xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_PROPERTIES, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookPropertiesStartingNode']);
$xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_VIEW, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookViewStartingNode']);
$xmlProcessor->registerCallback(self::XML_NODE_SHEET, XMLProcessor::NODE_TYPE_START, [$this, 'processSheetStartingNode']);
$xmlProcessor->registerCallback(self::XML_NODE_SHEETS, XMLProcessor::NODE_TYPE_END, [$this, 'processSheetsEndingNode']);
if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
$xmlProcessor->readUntilStopped();
$xmlReader->close();
}
return $this->sheets;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processWorkbookPropertiesStartingNode($xmlReader)
{
// Using "filter_var($x, FILTER_VALIDATE_BOOLEAN)" here because the value of the "date1904" attribute
// may be the string "false", that is not mapped to the boolean "false" by default...
$shouldUse1904Dates = filter_var($xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904), FILTER_VALIDATE_BOOLEAN);
$this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates);
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processWorkbookViewStartingNode($xmlReader)
{
// The "workbookView" node is located before "sheet" nodes, ensuring that
// the active sheet is known before parsing sheets data.
$this->activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processSheetStartingNode($xmlReader)
{
$isSheetActive = ($this->currentSheetIndex === $this->activeSheetIndex);
$this->sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $this->currentSheetIndex, $isSheetActive);
$this->currentSheetIndex++;
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @return int A return code that indicates what action should the processor take next
*/
protected function processSheetsEndingNode()
{
return XMLProcessor::PROCESSING_STOP;
}
/**
* Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
* We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
* ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
*
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
* @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
* @param bool $isSheetActive Whether this sheet was defined as active
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
*/
protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
{
$sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
$sheetState = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_STATE);
$isSheetVisible = ($sheetState !== self::SHEET_STATE_HIDDEN);
$escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
$sheetName = $this->escaper->unescape($escapedSheetName);
$sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
return $this->entityFactory->createSheet(
$this->filePath,
$sheetDataXMLFilePath,
$sheetIndexZeroBased,
$sheetName,
$isSheetActive,
$isSheetVisible,
$this->optionsManager,
$this->sharedStringsManager
);
}
/**
* @param string $sheetId The sheet ID, as defined in "workbook.xml"
* @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
*/
protected function getSheetDataXMLFilePathForSheetId($sheetId)
{
$sheetDataXMLFilePath = '';
// find the file path of the sheet, by looking at the "workbook.xml.res" file
$xmlReader = $this->entityFactory->createXMLReader();
if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
while ($xmlReader->read()) {
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
$relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
if ($relationshipSheetId === $sheetId) {
// In workbook.xml.rels, it is only "worksheets/sheet1.xml"
// In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
$sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
// sometimes, the sheet data file path already contains "/xl/"...
if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
$sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
break;
}
}
}
}
$xmlReader->close();
}
return $sheetDataXMLFilePath;
}
}
src/Spout/Reader/XLSX/Manager/StyleManager.php 0000666 00000030705 13476413670 0015147 0 ustar 00 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy'
15 => 'd-mmm-yy',
16 => 'd-mmm',
17 => 'mmm-yy',
18 => 'h:mm AM/PM',
19 => 'h:mm:ss AM/PM',
20 => 'h:mm',
21 => 'h:mm:ss',
22 => 'm/d/yyyy h:mm', // @NOTE: ECMA spec is 'm/d/yy h:mm',
45 => 'mm:ss',
46 => '[h]:mm:ss',
47 => 'mm:ss.0', // @NOTE: ECMA spec is 'mmss.0',
];
/** @var string Path of the XLSX file being read */
protected $filePath;
/** @var string Path of the styles XML file */
protected $stylesXMLFilePath;
/** @var InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var array Array containing the IDs of built-in number formats indicating a date */
protected $builtinNumFmtIdIndicatingDates;
/** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
protected $customNumberFormats;
/** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
protected $stylesAttributes;
/** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */
protected $numFmtIdToIsDateFormatCache = [];
/**
* @param string $filePath Path of the XLSX file being read
* @param WorkbookRelationshipsManager $workbookRelationshipsManager Helps retrieving workbook relationships
* @param InternalEntityFactory $entityFactory Factory to create entities
*/
public function __construct($filePath, $workbookRelationshipsManager, $entityFactory)
{
$this->filePath = $filePath;
$this->entityFactory = $entityFactory;
$this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping);
$this->stylesXMLFilePath = $workbookRelationshipsManager->getStylesXMLFilePath();
}
/**
* Returns whether the style with the given ID should consider
* numeric values as timestamps and format the cell as a date.
*
* @param int $styleId Zero-based style ID
* @return bool Whether the cell with the given cell should display a date instead of a numeric value
*/
public function shouldFormatNumericValueAsDate($styleId)
{
$stylesAttributes = $this->getStylesAttributes();
// Default style (0) does not format numeric values as timestamps. Only custom styles do.
// Also if the style ID does not exist in the styles.xml file, format as numeric value.
// Using isset here because it is way faster than array_key_exists...
if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) {
return false;
}
$styleAttributes = $stylesAttributes[$styleId];
return $this->doesStyleIndicateDate($styleAttributes);
}
/**
* Reads the styles.xml file and extract the relevant information from the file.
*
* @return void
*/
protected function extractRelevantInfo()
{
$this->customNumberFormats = [];
$this->stylesAttributes = [];
$xmlReader = $this->entityFactory->createXMLReader();
if ($xmlReader->openFileInZip($this->filePath, $this->stylesXMLFilePath)) {
while ($xmlReader->read()) {
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
$this->extractNumberFormats($xmlReader);
} elseif ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
$this->extractStyleAttributes($xmlReader);
}
}
$xmlReader->close();
}
}
/**
* Extracts number formats from the "numFmt" nodes.
* For simplicity, the styles attributes are kept in memory. This is possible thanks
* to the reuse of formats. So 1 million cells should not use 1 million formats.
*
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node
* @return void
*/
protected function extractNumberFormats($xmlReader)
{
while ($xmlReader->read()) {
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) {
$numFmtId = (int) ($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
$formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
$this->customNumberFormats[$numFmtId] = $formatCode;
} elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) {
// Once done reading "numFmts" node's children
break;
}
}
}
/**
* Extracts style attributes from the "xf" nodes, inside the "cellXfs" section.
* For simplicity, the styles attributes are kept in memory. This is possible thanks
* to the reuse of styles. So 1 million cells should not use 1 million styles.
*
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node
* @return void
*/
protected function extractStyleAttributes($xmlReader)
{
while ($xmlReader->read()) {
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) {
$numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID);
$normalizedNumFmtId = ($numFmtId !== null) ? (int) $numFmtId : null;
$applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT);
$normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? (bool) $applyNumberFormat : null;
$this->stylesAttributes[] = [
self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId,
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat,
];
} elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
// Once done reading "cellXfs" node's children
break;
}
}
}
/**
* @return array The custom number formats
*/
protected function getCustomNumberFormats()
{
if (!isset($this->customNumberFormats)) {
$this->extractRelevantInfo();
}
return $this->customNumberFormats;
}
/**
* @return array The styles attributes
*/
protected function getStylesAttributes()
{
if (!isset($this->stylesAttributes)) {
$this->extractRelevantInfo();
}
return $this->stylesAttributes;
}
/**
* @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId")
* @return bool Whether the style with the given attributes indicates that the number is a date
*/
protected function doesStyleIndicateDate($styleAttributes)
{
$applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
// A style may apply a date format if it has:
// - "applyNumberFormat" attribute not set to "false"
// - "numFmtId" attribute set
// This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format,
// but this is not necessarily a date.
if ($applyNumberFormat === false || $numFmtId === null) {
return false;
}
return $this->doesNumFmtIdIndicateDate($numFmtId);
}
/**
* Returns whether the number format ID indicates that the number is a date.
* The result is cached to avoid recomputing the same thing over and over, as
* "numFmtId" attributes can be shared between multiple styles.
*
* @param int $numFmtId
* @return bool Whether the number format ID indicates that the number is a date
*/
protected function doesNumFmtIdIndicateDate($numFmtId)
{
if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) {
$formatCode = $this->getFormatCodeForNumFmtId($numFmtId);
$this->numFmtIdToIsDateFormatCache[$numFmtId] = (
$this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
$this->isFormatCodeCustomDateFormat($formatCode)
);
}
return $this->numFmtIdToIsDateFormatCache[$numFmtId];
}
/**
* @param int $numFmtId
* @return string|null The custom number format or NULL if none defined for the given numFmtId
*/
protected function getFormatCodeForNumFmtId($numFmtId)
{
$customNumberFormats = $this->getCustomNumberFormats();
// Using isset here because it is way faster than array_key_exists...
return (isset($customNumberFormats[$numFmtId])) ? $customNumberFormats[$numFmtId] : null;
}
/**
* @param int $numFmtId
* @return bool Whether the number format ID indicates that the number is a date
*/
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
{
return in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates);
}
/**
* @param string|null $formatCode
* @return bool Whether the given format code indicates that the number is a date
*/
protected function isFormatCodeCustomDateFormat($formatCode)
{
// if no associated format code or if using the default "General" format
if ($formatCode === null || strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) {
return false;
}
return $this->isFormatCodeMatchingDateFormatPattern($formatCode);
}
/**
* @param string $formatCode
* @return bool Whether the given format code matches a date format pattern
*/
protected function isFormatCodeMatchingDateFormatPattern($formatCode)
{
// Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
$pattern = '((?getStylesAttributes();
$styleAttributes = $stylesAttributes[$styleId];
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) {
$numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId];
} else {
$customNumberFormats = $this->getCustomNumberFormats();
$numberFormatCode = $customNumberFormats[$numFmtId];
}
return $numberFormatCode;
}
}
src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php 0000666 00000011740 13476413670 0020407 0 ustar 00 [FILE_NAME] */
private $cachedWorkbookRelationships;
/**
* @param string $filePath Path of the XLSX file being read
* @param InternalEntityFactory $entityFactory Factory to create entities
*/
public function __construct($filePath, $entityFactory)
{
$this->filePath = $filePath;
$this->entityFactory = $entityFactory;
}
/**
* @return string The path of the shared string XML file
*/
public function getSharedStringsXMLFilePath()
{
$workbookRelationships = $this->getWorkbookRelationships();
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false);
if (!$doesContainBasePath) {
// make sure we return an absolute file path
$sharedStringsXMLFilePath = self::BASE_PATH . $sharedStringsXMLFilePath;
}
return $sharedStringsXMLFilePath;
}
/**
* @return bool Whether the XLSX file contains a shared string XML file
*/
public function hasSharedStringsXMLFile()
{
$workbookRelationships = $this->getWorkbookRelationships();
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]);
}
/**
* @return string|null The path of the styles XML file
*/
public function getStylesXMLFilePath()
{
$workbookRelationships = $this->getWorkbookRelationships();
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (strpos($stylesXMLFilePath, self::BASE_PATH) !== false);
if (!$doesContainBasePath) {
// make sure we return a full path
$stylesXMLFilePath = self::BASE_PATH . $stylesXMLFilePath;
}
return $stylesXMLFilePath;
}
/**
* Reads the workbook.xml.rels and extracts the filename associated to the different types.
* It caches the result so that the file is read only once.
*
* @throws \Box\Spout\Common\Exception\IOException If workbook.xml.rels can't be read
* @return array
*/
private function getWorkbookRelationships()
{
if (!isset($this->cachedWorkbookRelationships)) {
$xmlReader = $this->entityFactory->createXMLReader();
if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_RELS_XML_FILE_PATH) === false) {
throw new IOException('Could not open "' . self::WORKBOOK_RELS_XML_FILE_PATH . '".');
}
$this->cachedWorkbookRelationships = [];
while ($xmlReader->readUntilNodeFound(self::XML_NODE_RELATIONSHIP)) {
$this->processWorkbookRelationship($xmlReader);
}
}
return $this->cachedWorkbookRelationships;
}
/**
* Extracts and store the data of the current workbook relationship.
*
* @param XMLReader $xmlReader
* @return void
*/
private function processWorkbookRelationship($xmlReader)
{
$type = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TYPE);
$target = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
// @NOTE: if a type is defined more than once, we overwrite the previous value
// To be changed if we want to get the file paths of sheet XML files for instance.
$this->cachedWorkbookRelationships[$type] = $target;
}
}
src/Spout/Reader/XLSX/Reader.php 0000666 00000010021 13476413670 0012371 0 ustar 00 managerFactory = $managerFactory;
}
/**
* @param string $tempFolder Temporary folder where the temporary files will be created
* @return Reader
*/
public function setTempFolder($tempFolder)
{
$this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder);
return $this;
}
/**
* Returns whether stream wrappers are supported
*
* @return bool
*/
protected function doesSupportStreamWrapper()
{
return false;
}
/**
* Opens the file at the given file path to make it ready to be read.
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
* and fetches all the available sheets.
*
* @param string $filePath Path of the file to be read
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
* @return void
*/
protected function openReader($filePath)
{
/** @var InternalEntityFactory $entityFactory */
$entityFactory = $this->entityFactory;
$this->zip = $entityFactory->createZipArchive();
if ($this->zip->open($filePath) === true) {
$tempFolder = $this->optionsManager->getOption(Options::TEMP_FOLDER);
$this->sharedStringsManager = $this->managerFactory->createSharedStringsManager($filePath, $tempFolder, $entityFactory);
if ($this->sharedStringsManager->hasSharedStrings()) {
// Extracts all the strings from the sheets for easy access in the future
$this->sharedStringsManager->extractSharedStrings();
}
$this->sheetIterator = $entityFactory->createSheetIterator(
$filePath,
$this->optionsManager,
$this->sharedStringsManager
);
} else {
throw new IOException("Could not open $filePath for reading.");
}
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
protected function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->zip) {
$this->zip->close();
}
if ($this->sharedStringsManager) {
$this->sharedStringsManager->cleanup();
}
}
}
src/Spout/Reader/XLSX/RowIterator.php 0000666 00000037074 13476413670 0013471 0 ustar 00 filePath = $filePath;
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
$this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows;
$this->xmlReader = $xmlReader;
$this->cellValueFormatter = $cellValueFormatter;
$this->rowManager = $rowManager;
$this->entityFactory = $entityFactory;
// Register all callbacks to process different nodes when reading the XML file
$this->xmlProcessor = $xmlProcessor;
$this->xmlProcessor->registerCallback(self::XML_NODE_DIMENSION, XMLProcessor::NODE_TYPE_START, [$this, 'processDimensionStartingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
$this->xmlProcessor->registerCallback(self::XML_NODE_WORKSHEET, XMLProcessor::NODE_TYPE_END, [$this, 'processWorksheetEndingNode']);
}
/**
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @return string Path of the XML file containing the sheet data,
* without the leading slash.
*/
protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
{
return ltrim($sheetDataXMLFilePath, '/');
}
/**
* Rewind the Iterator to the first element.
* Initializes the XMLReader object that reads the associated sheet data.
* The XMLReader is configured to be safe from billion laughs attack.
* @see http://php.net/manual/en/iterator.rewind.php
*
* @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
* @return void
*/
public function rewind()
{
$this->xmlReader->close();
if ($this->xmlReader->openFileInZip($this->filePath, $this->sheetDataXMLFilePath) === false) {
throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
}
$this->numReadRows = 0;
$this->lastRowIndexProcessed = 0;
$this->nextRowIndexToBeProcessed = 0;
$this->rowBuffer = null;
$this->hasReachedEndOfFile = false;
$this->numColumns = 0;
$this->next();
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid()
{
return (!$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Reads data describing the next unprocessed row.
* @see http://php.net/manual/en/iterator.next.php
*
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
* @return void
*/
public function next()
{
$this->nextRowIndexToBeProcessed++;
if ($this->doesNeedDataForNextRowToBeProcessed()) {
$this->readDataForNextRow();
}
}
/**
* Returns whether we need data for the next row to be processed.
* We don't need to read data if:
* we have already read at least one row
* AND
* we need to preserve empty rows
* AND
* the last row that was read is not the row that need to be processed
* (i.e. if we need to return empty rows)
*
* @return bool Whether we need data for the next row to be processed.
*/
protected function doesNeedDataForNextRowToBeProcessed()
{
$hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
return (
!$hasReadAtLeastOneRow ||
!$this->shouldPreserveEmptyRows ||
$this->lastRowIndexProcessed < $this->nextRowIndexToBeProcessed
);
}
/**
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
* @return void
*/
protected function readDataForNextRow()
{
$this->currentlyProcessedRow = $this->entityFactory->createRow();
try {
$this->xmlProcessor->readUntilStopped();
} catch (XMLProcessingException $exception) {
throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
}
$this->rowBuffer = $this->currentlyProcessedRow;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processDimensionStartingNode($xmlReader)
{
// Read dimensions of the sheet
$dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
if (preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) {
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1;
}
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processRowStartingNode($xmlReader)
{
// Reset index of the last processed column
$this->lastColumnIndexProcessed = -1;
// Mark the last processed row as the one currently being read
$this->lastRowIndexProcessed = $this->getRowIndex($xmlReader);
// Read spans info if present
$numberOfColumnsForRow = $this->numColumns;
$spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
if ($spans) {
list(, $numberOfColumnsForRow) = explode(':', $spans);
$numberOfColumnsForRow = (int) $numberOfColumnsForRow;
}
$cells = array_fill(0, $numberOfColumnsForRow, $this->entityFactory->createCell(''));
$this->currentlyProcessedRow->setCells($cells);
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
* @return int A return code that indicates what action should the processor take next
*/
protected function processCellStartingNode($xmlReader)
{
$currentColumnIndex = $this->getColumnIndex($xmlReader);
// NOTE: expand() will automatically decode all XML entities of the child nodes
$node = $xmlReader->expand();
$cell = $this->getCell($node);
$this->currentlyProcessedRow->setCellAtIndex($cell, $currentColumnIndex);
$this->lastColumnIndexProcessed = $currentColumnIndex;
return XMLProcessor::PROCESSING_CONTINUE;
}
/**
* @return int A return code that indicates what action should the processor take next
*/
protected function processRowEndingNode()
{
// if the fetched row is empty and we don't want to preserve it..,
if (!$this->shouldPreserveEmptyRows && $this->rowManager->isEmpty($this->currentlyProcessedRow)) {
// ... skip it
return XMLProcessor::PROCESSING_CONTINUE;
}
$this->numReadRows++;
// If needed, we fill the empty cells
if ($this->numColumns === 0) {
$this->currentlyProcessedRow = $this->rowManager->fillMissingIndexesWithEmptyCells($this->currentlyProcessedRow);
}
// at this point, we have all the data we need for the row
// so that we can populate the buffer
return XMLProcessor::PROCESSING_STOP;
}
/**
* @return int A return code that indicates what action should the processor take next
*/
protected function processWorksheetEndingNode()
{
// The closing "" marks the end of the file
$this->hasReachedEndOfFile = true;
return XMLProcessor::PROCESSING_STOP;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
* @return int Row index
*/
protected function getRowIndex($xmlReader)
{
// Get "r" attribute if present (from something like
$currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX);
return ($currentRowIndex !== null) ?
(int) $currentRowIndex :
$this->lastRowIndexProcessed + 1;
}
/**
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
* @return int Column index
*/
protected function getColumnIndex($xmlReader)
{
// Get "r" attribute if present (from something like
$currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
return ($currentCellIndex !== null) ?
CellHelper::getColumnIndexFromCellIndex($currentCellIndex) :
$this->lastColumnIndexProcessed + 1;
}
/**
* Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
*
* @param \DOMNode $node
* @return Cell The cell set with the associated with the cell
*/
protected function getCell($node)
{
try {
$cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node);
$cell = $this->entityFactory->createCell($cellValue);
} catch (InvalidValueException $exception) {
$cell = $this->entityFactory->createCell($exception->getInvalidValue());
$cell->setType(Cell::TYPE_ERROR);
}
return $cell;
}
/**
* Return the current element, either an empty row or from the buffer.
* @see http://php.net/manual/en/iterator.current.php
*
* @return Row|null
*/
public function current()
{
$rowToBeProcessed = $this->rowBuffer;
if ($this->shouldPreserveEmptyRows) {
// when we need to preserve empty rows, we will either return
// an empty row or the last row read. This depends whether the
// index of last row that was read matches the index of the last
// row whose value should be returned.
if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) {
// return empty row if mismatch between last processed row
// and the row that needs to be returned
$rowToBeProcessed = $this->entityFactory->createRow();
}
}
return $rowToBeProcessed;
}
/**
* Return the key of the current element. Here, the row index.
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
// TODO: This should return $this->nextRowIndexToBeProcessed
// but to avoid a breaking change, the return value for
// this function has been kept as the number of rows read.
return $this->shouldPreserveEmptyRows ?
$this->nextRowIndexToBeProcessed :
$this->numReadRows;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
$this->xmlReader->close();
}
}
src/Spout/Reader/XLSX/Sheet.php 0000666 00000004005 13476413670 0012244 0 ustar 00 rowIterator = $rowIterator;
$this->index = $sheetIndex;
$this->name = $sheetName;
$this->isActive = $isSheetActive;
$this->isVisible = $isSheetVisible;
}
/**
* @return \Box\Spout\Reader\XLSX\RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
/**
* @return int Index of the sheet, based on order in the workbook (zero-based)
*/
public function getIndex()
{
return $this->index;
}
/**
* @return string Name of the sheet
*/
public function getName()
{
return $this->name;
}
/**
* @return bool Whether the sheet was defined as active
*/
public function isActive()
{
return $this->isActive;
}
/**
* @return bool Whether the sheet is visible
*/
public function isVisible()
{
return $this->isVisible;
}
}
src/Spout/Reader/XLSX/SheetIterator.php 0000666 00000005303 13476413670 0013760 0 ustar 00 sheets = $sheetManager->getSheets();
if (count($this->sheets) === 0) {
throw new NoSheetsFoundException('The file must contain at least one sheet.');
}
}
/**
* Rewind the Iterator to the first element
* @see http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->currentSheetIndex = 0;
}
/**
* Checks if current position is valid
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool
*/
public function valid()
{
return ($this->currentSheetIndex < count($this->sheets));
}
/**
* Move forward to next element
* @see http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
// Using isset here because it is way faster than array_key_exists...
if (isset($this->sheets[$this->currentSheetIndex])) {
$currentSheet = $this->sheets[$this->currentSheetIndex];
$currentSheet->getRowIterator()->end();
$this->currentSheetIndex++;
}
}
/**
* Return the current element
* @see http://php.net/manual/en/iterator.current.php
*
* @return \Box\Spout\Reader\XLSX\Sheet
*/
public function current()
{
return $this->sheets[$this->currentSheetIndex];
}
/**
* Return the key of the current element
* @see http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->currentSheetIndex + 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// make sure we are not leaking memory in case the iteration stopped before the end
foreach ($this->sheets as $sheet) {
$sheet->getRowIterator()->end();
}
}
}
src/Spout/Writer/CSV/Manager/OptionsManager.php 0000666 00000001373 13476413670 0015410 0 ustar 00 setOption(Options::FIELD_DELIMITER, ',');
$this->setOption(Options::FIELD_ENCLOSURE, '"');
$this->setOption(Options::SHOULD_ADD_BOM, true);
}
}
src/Spout/Writer/CSV/Writer.php 0000666 00000006113 13476413670 0012361 0 ustar 00 optionsManager->setOption(Options::FIELD_DELIMITER, $fieldDelimiter);
return $this;
}
/**
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return Writer
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->optionsManager->setOption(Options::FIELD_ENCLOSURE, $fieldEnclosure);
return $this;
}
/**
* Set if a BOM has to be added to the file
*
* @param bool $shouldAddBOM
* @return Writer
*/
public function setShouldAddBOM($shouldAddBOM)
{
$this->optionsManager->setOption(Options::SHOULD_ADD_BOM, (bool) $shouldAddBOM);
return $this;
}
/**
* Opens the CSV streamer and makes it ready to accept data.
*
* @return void
*/
protected function openWriter()
{
if ($this->optionsManager->getOption(Options::SHOULD_ADD_BOM)) {
// Adds UTF-8 BOM for Unicode compatibility
$this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8);
}
}
/**
* Adds a row to the currently opened writer.
*
* @param Row $row The row containing cells and styles
* @throws IOException If unable to write data
* @return void
*/
protected function addRowToWriter(Row $row)
{
$fieldDelimiter = $this->optionsManager->getOption(Options::FIELD_DELIMITER);
$fieldEnclosure = $this->optionsManager->getOption(Options::FIELD_ENCLOSURE);
$wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $row->getCells(), $fieldDelimiter, $fieldEnclosure);
if ($wasWriteSuccessful === false) {
throw new IOException('Unable to write data');
}
$this->lastWrittenRowIndex++;
if ($this->lastWrittenRowIndex % self::FLUSH_THRESHOLD === 0) {
$this->globalFunctionsHelper->fflush($this->filePointer);
}
}
/**
* Closes the CSV streamer, preventing any additional writing.
* If set, sets the headers and redirects output to the browser.
*
* @return void
*/
protected function closeWriter()
{
$this->lastWrittenRowIndex = 0;
}
}
src/Spout/Writer/Common/Creator/InternalEntityFactory.php 0000666 00000002436 13476413670 0017606 0 ustar 00 border = new Border();
}
/**
* @param string $color Border A RGB color code
* @param string $width Border width @see BorderPart::allowedWidths
* @param string $style Border style @see BorderPart::allowedStyles
* @return BorderBuilder
*/
public function setBorderTop($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
{
$this->border->addPart(new BorderPart(Border::TOP, $color, $width, $style));
return $this;
}
/**
* @param string $color Border A RGB color code
* @param string $width Border width @see BorderPart::allowedWidths
* @param string $style Border style @see BorderPart::allowedStyles
* @return BorderBuilder
*/
public function setBorderRight($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
{
$this->border->addPart(new BorderPart(Border::RIGHT, $color, $width, $style));
return $this;
}
/**
* @param string $color Border A RGB color code
* @param string $width Border width @see BorderPart::allowedWidths
* @param string $style Border style @see BorderPart::allowedStyles
* @return BorderBuilder
*/
public function setBorderBottom($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
{
$this->border->addPart(new BorderPart(Border::BOTTOM, $color, $width, $style));
return $this;
}
/**
* @param string $color Border A RGB color code
* @param string $width Border width @see BorderPart::allowedWidths
* @param string $style Border style @see BorderPart::allowedStyles
* @return BorderBuilder
*/
public function setBorderLeft($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
{
$this->border->addPart(new BorderPart(Border::LEFT, $color, $width, $style));
return $this;
}
/**
* @return Border
*/
public function build()
{
return $this->border;
}
}
src/Spout/Writer/Common/Creator/Style/StyleBuilder.php 0000666 00000005604 13476413670 0017014 0 ustar 00 style = new Style();
}
/**
* Makes the font bold.
*
* @return StyleBuilder
*/
public function setFontBold()
{
$this->style->setFontBold();
return $this;
}
/**
* Makes the font italic.
*
* @return StyleBuilder
*/
public function setFontItalic()
{
$this->style->setFontItalic();
return $this;
}
/**
* Makes the font underlined.
*
* @return StyleBuilder
*/
public function setFontUnderline()
{
$this->style->setFontUnderline();
return $this;
}
/**
* Makes the font struck through.
*
* @return StyleBuilder
*/
public function setFontStrikethrough()
{
$this->style->setFontStrikethrough();
return $this;
}
/**
* Sets the font size.
*
* @param int $fontSize Font size, in pixels
* @return StyleBuilder
*/
public function setFontSize($fontSize)
{
$this->style->setFontSize($fontSize);
return $this;
}
/**
* Sets the font color.
*
* @param string $fontColor ARGB color (@see Color)
* @return StyleBuilder
*/
public function setFontColor($fontColor)
{
$this->style->setFontColor($fontColor);
return $this;
}
/**
* Sets the font name.
*
* @param string $fontName Name of the font to use
* @return StyleBuilder
*/
public function setFontName($fontName)
{
$this->style->setFontName($fontName);
return $this;
}
/**
* Makes the text wrap in the cell if requested
*
* @param bool $shouldWrap Should the text be wrapped
* @return StyleBuilder
*/
public function setShouldWrapText($shouldWrap = true)
{
$this->style->setShouldWrapText($shouldWrap);
return $this;
}
/**
* Set a border
*
* @param Border $border
* @return $this
*/
public function setBorder(Border $border)
{
$this->style->setBorder($border);
return $this;
}
/**
* Sets a background color
*
* @param string $color ARGB color (@see Color)
* @return StyleBuilder
*/
public function setBackgroundColor($color)
{
$this->style->setBackgroundColor($color);
return $this;
}
/**
* Returns the configured style. The style is cached and can be reused.
*
* @return Style
*/
public function build()
{
return $this->style;
}
}
src/Spout/Writer/Common/Creator/WriterEntityFactory.php 0000666 00000006077 13476413670 0017313 0 ustar 00 index = $sheetIndex;
$this->associatedWorkbookId = $associatedWorkbookId;
$this->sheetManager = $sheetManager;
$this->sheetManager->markWorkbookIdAsUsed($associatedWorkbookId);
$this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1));
$this->setIsVisible(true);
}
/**
* @return int Index of the sheet, based on order in the workbook (zero-based)
*/
public function getIndex()
{
return $this->index;
}
/**
* @return string
*/
public function getAssociatedWorkbookId()
{
return $this->associatedWorkbookId;
}
/**
* @return string Name of the sheet
*/
public function getName()
{
return $this->name;
}
/**
* Sets the name of the sheet. Note that Excel has some restrictions on the name:
* - it should not be blank
* - it should not exceed 31 characters
* - it should not contain these characters: \ / ? * : [ or ]
* - it should be unique
*
* @param string $name Name of the sheet
* @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
* @return Sheet
*/
public function setName($name)
{
$this->sheetManager->throwIfNameIsInvalid($name, $this);
$this->name = $name;
$this->sheetManager->markSheetNameAsUsed($this);
return $this;
}
/**
* @return bool isVisible Visibility of the sheet
*/
public function isVisible()
{
return $this->isVisible;
}
/**
* @param bool $isVisible Visibility of the sheet
* @return Sheet
*/
public function setIsVisible($isVisible)
{
$this->isVisible = $isVisible;
return $this;
}
}
src/Spout/Writer/Common/Entity/Workbook.php 0000666 00000001517 13476413670 0014756 0 ustar 00 internalId = uniqid();
}
/**
* @return Worksheet[]
*/
public function getWorksheets()
{
return $this->worksheets;
}
/**
* @param Worksheet[] $worksheets
*/
public function setWorksheets($worksheets)
{
$this->worksheets = $worksheets;
}
/**
* @return string
*/
public function getInternalId()
{
return $this->internalId;
}
}
src/Spout/Writer/Common/Entity/Worksheet.php 0000666 00000004503 13476413670 0015132 0 ustar 00 filePath = $worksheetFilePath;
$this->filePointer = null;
$this->externalSheet = $externalSheet;
$this->maxNumColumns = 0;
$this->lastWrittenRowIndex = 0;
}
/**
* @return string
*/
public function getFilePath()
{
return $this->filePath;
}
/**
* @return resource
*/
public function getFilePointer()
{
return $this->filePointer;
}
/**
* @param resource $filePointer
*/
public function setFilePointer($filePointer)
{
$this->filePointer = $filePointer;
}
/**
* @return Sheet
*/
public function getExternalSheet()
{
return $this->externalSheet;
}
/**
* @return int
*/
public function getMaxNumColumns()
{
return $this->maxNumColumns;
}
/**
* @param int $maxNumColumns
*/
public function setMaxNumColumns($maxNumColumns)
{
$this->maxNumColumns = $maxNumColumns;
}
/**
* @return int
*/
public function getLastWrittenRowIndex()
{
return $this->lastWrittenRowIndex;
}
/**
* @param int $lastWrittenRowIndex
*/
public function setLastWrittenRowIndex($lastWrittenRowIndex)
{
$this->lastWrittenRowIndex = $lastWrittenRowIndex;
}
/**
* @return int The ID of the worksheet
*/
public function getId()
{
// sheet index is zero-based, while ID is 1-based
return $this->externalSheet->getIndex() + 1;
}
}
src/Spout/Writer/Common/Helper/CellHelper.php 0000666 00000003046 13476413670 0015142 0 ustar 00 cell index */
private static $columnIndexToCellIndexCache = [];
/**
* Returns the cell index (base 26) associated to the base 10 column index.
* Excel uses A to Z letters for column indexing, where A is the 1st column,
* Z is the 26th and AA is the 27th.
* The mapping is zero based, so that 0 maps to A, B maps to 1, Z to 25 and AA to 26.
*
* @param int $columnIndex The Excel column index (0, 42, ...)
* @return string The associated cell index ('A', 'BC', ...)
*/
public static function getCellIndexFromColumnIndex($columnIndex)
{
$originalColumnIndex = $columnIndex;
// Using isset here because it is way faster than array_key_exists...
if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) {
$cellIndex = '';
$capitalAAsciiValue = ord('A');
do {
$modulus = $columnIndex % 26;
$cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex;
// substracting 1 because it's zero-based
$columnIndex = (int) ($columnIndex / 26) - 1;
} while ($columnIndex >= 0);
self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex;
}
return self::$columnIndexToCellIndexCache[$originalColumnIndex];
}
}
src/Spout/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php 0000666 00000001347 13476413670 0023026 0 ustar 00 entityFactory = $entityFactory;
}
/**
* Returns a new ZipArchive instance pointing at the given path.
*
* @param string $tmpFolderPath Path of the temp folder where the zip file will be created
* @return \ZipArchive
*/
public function createZip($tmpFolderPath)
{
$zip = $this->entityFactory->createZipArchive();
$zipFilePath = $tmpFolderPath . self::ZIP_EXTENSION;
$zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
return $zip;
}
/**
* @param \ZipArchive $zip An opened zip archive object
* @return string Path where the zip file of the given folder will be created
*/
public function getZipFilePath(\ZipArchive $zip)
{
return $zip->filename;
}
/**
* Adds the given file, located under the given root folder to the archive.
* The file will be compressed.
*
* Example of use:
* addFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml');
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
*
* @param \ZipArchive $zip An opened zip archive object
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
* @param string $localFilePath Path of the file to be added, under the root folder
* @param string $existingFileMode Controls what to do when trying to add an existing file
* @return void
*/
public function addFileToArchive($zip, $rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
{
$this->addFileToArchiveWithCompressionMethod(
$zip,
$rootFolderPath,
$localFilePath,
$existingFileMode,
\ZipArchive::CM_DEFAULT
);
}
/**
* Adds the given file, located under the given root folder to the archive.
* The file will NOT be compressed.
*
* Example of use:
* addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml');
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
*
* @param \ZipArchive $zip An opened zip archive object
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
* @param string $localFilePath Path of the file to be added, under the root folder
* @param string $existingFileMode Controls what to do when trying to add an existing file
* @return void
*/
public function addUncompressedFileToArchive($zip, $rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
{
$this->addFileToArchiveWithCompressionMethod(
$zip,
$rootFolderPath,
$localFilePath,
$existingFileMode,
\ZipArchive::CM_STORE
);
}
/**
* Adds the given file, located under the given root folder to the archive.
* The file will NOT be compressed.
*
* Example of use:
* addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml');
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
*
* @param \ZipArchive $zip An opened zip archive object
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
* @param string $localFilePath Path of the file to be added, under the root folder
* @param string $existingFileMode Controls what to do when trying to add an existing file
* @param int $compressionMethod The compression method
* @return void
*/
protected function addFileToArchiveWithCompressionMethod($zip, $rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod)
{
if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) {
$normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath);
$zip->addFile($normalizedFullFilePath, $localFilePath);
if (self::canChooseCompressionMethod()) {
$zip->setCompressionName($localFilePath, $compressionMethod);
}
}
}
/**
* @return bool Whether it is possible to choose the desired compression method to be used
*/
public static function canChooseCompressionMethod()
{
// setCompressionName() is a PHP7+ method...
return (method_exists(new \ZipArchive(), 'setCompressionName'));
}
/**
* @param \ZipArchive $zip An opened zip archive object
* @param string $folderPath Path to the folder to be zipped
* @param string $existingFileMode Controls what to do when trying to add an existing file
* @return void
*/
public function addFolderToArchive($zip, $folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
{
$folderRealPath = $this->getNormalizedRealPath($folderPath) . '/';
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
foreach ($itemIterator as $itemInfo) {
$itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) {
$zip->addFile($itemRealPath, $itemLocalPath);
}
}
}
/**
* @param \ZipArchive $zip
* @param string $itemLocalPath
* @param string $existingFileMode
* @return bool Whether the file should be added to the archive or skipped
*/
protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode)
{
// Skip files if:
// - EXISTING_FILES_SKIP mode chosen
// - File already exists in the archive
return ($existingFileMode === self::EXISTING_FILES_SKIP && $zip->locateName($itemLocalPath) !== false);
}
/**
* Returns canonicalized absolute pathname, containing only forward slashes.
*
* @param string $path Path to normalize
* @return string Normalized and canonicalized path
*/
protected function getNormalizedRealPath($path)
{
$realPath = realpath($path);
return str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
}
/**
* Closes the archive and copies it into the given stream
*
* @param \ZipArchive $zip An opened zip archive object
* @param resource $streamPointer Pointer to the stream to copy the zip
* @return void
*/
public function closeArchiveAndCopyToStream($zip, $streamPointer)
{
$zipFilePath = $zip->filename;
$zip->close();
$this->copyZipToStream($zipFilePath, $streamPointer);
}
/**
* Streams the contents of the zip file into the given stream
*
* @param string $zipFilePath Path of the zip file
* @param resource $pointer Pointer to the stream to copy the zip
* @return void
*/
protected function copyZipToStream($zipFilePath, $pointer)
{
$zipFilePointer = fopen($zipFilePath, 'r');
stream_copy_to_stream($zipFilePointer, $pointer);
fclose($zipFilePointer);
}
}
src/Spout/Writer/Common/Manager/CellManager.php 0000666 00000001375 13476413670 0015433 0 ustar 00 styleMerger = $styleMerger;
}
/**
* Merges a Style into a cell's Style.
*
* @param Cell $cell
* @param Style $style
* @return void
*/
public function applyStyle(Cell $cell, Style $style)
{
$mergedStyle = $this->styleMerger->merge($cell->getStyle(), $style);
$cell->setStyle($mergedStyle);
}
}
src/Spout/Writer/Common/Manager/RowManager.php 0000666 00000000736 13476413670 0015323 0 ustar 00 getCells() as $cell) {
if (!$cell->isEmpty()) {
return false;
}
}
return true;
}
}
src/Spout/Writer/Common/Manager/SheetManager.php 0000666 00000011656 13476413670 0015627 0 ustar 00 [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */
private static $SHEETS_NAME_USED = [];
/** @var StringHelper */
private $stringHelper;
/**
* SheetManager constructor.
*
* @param StringHelper $stringHelper
*/
public function __construct(StringHelper $stringHelper)
{
$this->stringHelper = $stringHelper;
}
/**
* Throws an exception if the given sheet's name is not valid.
* @see Sheet::setName for validity rules.
*
* @param string $name
* @param Sheet $sheet The sheet whose future name is checked
* @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
* @return void
*/
public function throwIfNameIsInvalid($name, Sheet $sheet)
{
if (!is_string($name)) {
$actualType = gettype($name);
$errorMessage = "The sheet's name is invalid. It must be a string ($actualType given).";
throw new InvalidSheetNameException($errorMessage);
}
$failedRequirements = [];
$nameLength = $this->stringHelper->getStringLength($name);
if (!$this->isNameUnique($name, $sheet)) {
$failedRequirements[] = 'It should be unique';
} else {
if ($nameLength === 0) {
$failedRequirements[] = 'It should not be blank';
} else {
if ($nameLength > self::MAX_LENGTH_SHEET_NAME) {
$failedRequirements[] = 'It should not exceed 31 characters';
}
if ($this->doesContainInvalidCharacters($name)) {
$failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]';
}
if ($this->doesStartOrEndWithSingleQuote($name)) {
$failedRequirements[] = 'It should not start or end with a single quote';
}
}
}
if (count($failedRequirements) !== 0) {
$errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - ";
$errorMessage .= implode("\n - ", $failedRequirements);
throw new InvalidSheetNameException($errorMessage);
}
}
/**
* Returns whether the given name contains at least one invalid character.
* @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list.
*
* @param string $name
* @return bool TRUE if the name contains invalid characters, FALSE otherwise.
*/
private function doesContainInvalidCharacters($name)
{
return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name);
}
/**
* Returns whether the given name starts or ends with a single quote
*
* @param string $name
* @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise.
*/
private function doesStartOrEndWithSingleQuote($name)
{
$startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0);
$endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1));
return ($startsWithSingleQuote || $endsWithSingleQuote);
}
/**
* Returns whether the given name is unique.
*
* @param string $name
* @param Sheet $sheet The sheet whose future name is checked
* @return bool TRUE if the name is unique, FALSE otherwise.
*/
private function isNameUnique($name, Sheet $sheet)
{
foreach (self::$SHEETS_NAME_USED[$sheet->getAssociatedWorkbookId()] as $sheetIndex => $sheetName) {
if ($sheetIndex !== $sheet->getIndex() && $sheetName === $name) {
return false;
}
}
return true;
}
/**
* @param int $workbookId Workbook ID associated to a Sheet
* @return void
*/
public function markWorkbookIdAsUsed($workbookId)
{
if (!isset(self::$SHEETS_NAME_USED[$workbookId])) {
self::$SHEETS_NAME_USED[$workbookId] = [];
}
}
/**
* @param Sheet $sheet
* @return void
*/
public function markSheetNameAsUsed(Sheet $sheet)
{
self::$SHEETS_NAME_USED[$sheet->getAssociatedWorkbookId()][$sheet->getIndex()] = $sheet->getName();
}
}
src/Spout/Writer/Common/Manager/Style/StyleManager.php 0000666 00000005044 13476413670 0016751 0 ustar 00 styleRegistry = $styleRegistry;
}
/**
* Returns the default style
*
* @return Style Default style
*/
protected function getDefaultStyle()
{
// By construction, the default style has ID 0
return $this->styleRegistry->getRegisteredStyles()[0];
}
/**
* Registers the given style as a used style.
* Duplicate styles won't be registered more than once.
*
* @param Style $style The style to be registered
* @return Style The registered style, updated with an internal ID.
*/
public function registerStyle($style)
{
return $this->styleRegistry->registerStyle($style);
}
/**
* Apply additional styles if the given row needs it.
* Typically, set "wrap text" if a cell contains a new line.
*
* @param Cell $cell
* @return Style
*/
public function applyExtraStylesIfNeeded(Cell $cell)
{
$updatedStyle = $this->applyWrapTextIfCellContainsNewLine($cell);
return $updatedStyle;
}
/**
* Set the "wrap text" option if a cell of the given row contains a new line.
*
* @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines
* are ignored even when the "wrap text" option is set. This only occurs with
* inline strings (shared strings do work fine).
* A workaround would be to encode "\n" as "_x000D_" but it does not work
* on the Windows version of Excel...
*
* @param Cell $cell The cell the style should be applied to
* @return \Box\Spout\Common\Entity\Style\Style The eventually updated style
*/
protected function applyWrapTextIfCellContainsNewLine(Cell $cell)
{
$cellStyle = $cell->getStyle();
// if the "wrap text" option is already set, no-op
if ($cellStyle->hasSetWrapText()) {
return $cellStyle;
}
if ($cell->isString() && strpos($cell->getValue(), "\n") !== false) {
$cellStyle->setShouldWrapText();
}
return $cellStyle;
}
}
src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php 0000666 00000001417 13476413670 0020572 0 ustar 00 mergeFontStyles($mergedStyle, $style, $baseStyle);
$this->mergeOtherFontProperties($mergedStyle, $style, $baseStyle);
$this->mergeCellProperties($mergedStyle, $style, $baseStyle);
return $mergedStyle;
}
/**
* @param Style $styleToUpdate (passed as reference)
* @param Style $style
* @param Style $baseStyle
* @return void
*/
private function mergeFontStyles(Style $styleToUpdate, Style $style, Style $baseStyle)
{
if (!$style->hasSetFontBold() && $baseStyle->isFontBold()) {
$styleToUpdate->setFontBold();
}
if (!$style->hasSetFontItalic() && $baseStyle->isFontItalic()) {
$styleToUpdate->setFontItalic();
}
if (!$style->hasSetFontUnderline() && $baseStyle->isFontUnderline()) {
$styleToUpdate->setFontUnderline();
}
if (!$style->hasSetFontStrikethrough() && $baseStyle->isFontStrikethrough()) {
$styleToUpdate->setFontStrikethrough();
}
}
/**
* @param Style $styleToUpdate Style to update (passed as reference)
* @param Style $style
* @param Style $baseStyle
* @return void
*/
private function mergeOtherFontProperties(Style $styleToUpdate, Style $style, Style $baseStyle)
{
if (!$style->hasSetFontSize() && $baseStyle->getFontSize() !== Style::DEFAULT_FONT_SIZE) {
$styleToUpdate->setFontSize($baseStyle->getFontSize());
}
if (!$style->hasSetFontColor() && $baseStyle->getFontColor() !== Style::DEFAULT_FONT_COLOR) {
$styleToUpdate->setFontColor($baseStyle->getFontColor());
}
if (!$style->hasSetFontName() && $baseStyle->getFontName() !== Style::DEFAULT_FONT_NAME) {
$styleToUpdate->setFontName($baseStyle->getFontName());
}
}
/**
* @param Style $styleToUpdate Style to update (passed as reference)
* @param Style $style
* @param Style $baseStyle
* @return void
*/
private function mergeCellProperties(Style $styleToUpdate, Style $style, Style $baseStyle)
{
if (!$style->hasSetWrapText() && $baseStyle->shouldWrapText()) {
$styleToUpdate->setShouldWrapText();
}
if (!$style->getBorder() && $baseStyle->shouldApplyBorder()) {
$styleToUpdate->setBorder($baseStyle->getBorder());
}
if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) {
$styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor());
}
}
}
src/Spout/Writer/Common/Manager/Style/StyleRegistry.php 0000666 00000006461 13476413670 0017213 0 ustar 00 [STYLE_ID] mapping table, keeping track of the registered styles */
protected $serializedStyleToStyleIdMappingTable = [];
/** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */
protected $styleIdToStyleMappingTable = [];
/**
* @param Style $defaultStyle
*/
public function __construct(Style $defaultStyle)
{
// This ensures that the default style is the first one to be registered
$this->registerStyle($defaultStyle);
}
/**
* Registers the given style as a used style.
* Duplicate styles won't be registered more than once.
*
* @param Style $style The style to be registered
* @return Style The registered style, updated with an internal ID.
*/
public function registerStyle(Style $style)
{
$serializedStyle = $this->serialize($style);
if (!$this->hasStyleAlreadyBeenRegistered($style)) {
$nextStyleId = count($this->serializedStyleToStyleIdMappingTable);
$style->setId($nextStyleId);
$this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
$this->styleIdToStyleMappingTable[$nextStyleId] = $style;
}
return $this->getStyleFromSerializedStyle($serializedStyle);
}
/**
* Returns whether the given style has already been registered.
*
* @param Style $style
* @return bool
*/
protected function hasStyleAlreadyBeenRegistered(Style $style)
{
$serializedStyle = $this->serialize($style);
// Using isset here because it is way faster than array_key_exists...
return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]);
}
/**
* Returns the registered style associated to the given serialization.
*
* @param string $serializedStyle The serialized style from which the actual style should be fetched from
* @return Style
*/
protected function getStyleFromSerializedStyle($serializedStyle)
{
$styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle];
return $this->styleIdToStyleMappingTable[$styleId];
}
/**
* @return Style[] List of registered styles
*/
public function getRegisteredStyles()
{
return array_values($this->styleIdToStyleMappingTable);
}
/**
* @param int $styleId
* @return Style
*/
public function getStyleFromStyleId($styleId)
{
return $this->styleIdToStyleMappingTable[$styleId];
}
/**
* Serializes the style for future comparison with other styles.
* The ID is excluded from the comparison, as we only care about
* actual style properties.
*
* @param Style $style
* @return string The serialized style
*/
public function serialize(Style $style)
{
// In order to be able to properly compare style, set static ID value
$currentId = $style->getId();
$style->setId(0);
$serializedStyle = serialize($style);
$style->setId($currentId);
return $serializedStyle;
}
}
src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php 0000666 00000025455 13476413670 0020042 0 ustar 00 workbook = $workbook;
$this->optionsManager = $optionsManager;
$this->worksheetManager = $worksheetManager;
$this->styleManager = $styleManager;
$this->styleMerger = $styleMerger;
$this->fileSystemHelper = $fileSystemHelper;
$this->entityFactory = $entityFactory;
$this->managerFactory = $managerFactory;
}
/**
* @return int Maximum number of rows/columns a sheet can contain
*/
abstract protected function getMaxRowsPerWorksheet();
/**
* @param Sheet $sheet
* @return string The file path where the data for the given sheet will be stored
*/
abstract protected function getWorksheetFilePath(Sheet $sheet);
/**
* @return Workbook
*/
public function getWorkbook()
{
return $this->workbook;
}
/**
* Creates a new sheet in the workbook and make it the current sheet.
* The writing will resume where it stopped (i.e. data won't be truncated).
*
* @throws IOException If unable to open the sheet for writing
* @return Worksheet The created sheet
*/
public function addNewSheetAndMakeItCurrent()
{
$worksheet = $this->addNewSheet();
$this->setCurrentWorksheet($worksheet);
return $worksheet;
}
/**
* Creates a new sheet in the workbook. The current sheet remains unchanged.
*
* @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
* @return Worksheet The created sheet
*/
private function addNewSheet()
{
$worksheets = $this->getWorksheets();
$newSheetIndex = count($worksheets);
$sheetManager = $this->managerFactory->createSheetManager();
$sheet = $this->entityFactory->createSheet($newSheetIndex, $this->workbook->getInternalId(), $sheetManager);
$worksheetFilePath = $this->getWorksheetFilePath($sheet);
$worksheet = $this->entityFactory->createWorksheet($worksheetFilePath, $sheet);
$this->worksheetManager->startSheet($worksheet);
$worksheets[] = $worksheet;
$this->workbook->setWorksheets($worksheets);
return $worksheet;
}
/**
* @return Worksheet[] All the workbook's sheets
*/
public function getWorksheets()
{
return $this->workbook->getWorksheets();
}
/**
* Returns the current sheet
*
* @return Worksheet The current sheet
*/
public function getCurrentWorksheet()
{
return $this->currentWorksheet;
}
/**
* Sets the given sheet as the current one. New data will be written to this sheet.
* The writing will resume where it stopped (i.e. data won't be truncated).
*
* @param Sheet $sheet The "external" sheet to set as current
* @throws SheetNotFoundException If the given sheet does not exist in the workbook
* @return void
*/
public function setCurrentSheet(Sheet $sheet)
{
$worksheet = $this->getWorksheetFromExternalSheet($sheet);
if ($worksheet !== null) {
$this->currentWorksheet = $worksheet;
} else {
throw new SheetNotFoundException('The given sheet does not exist in the workbook.');
}
}
/**
* @param Worksheet $worksheet
* @return void
*/
private function setCurrentWorksheet($worksheet)
{
$this->currentWorksheet = $worksheet;
}
/**
* Returns the worksheet associated to the given external sheet.
*
* @param Sheet $sheet
* @return Worksheet|null The worksheet associated to the given external sheet or null if not found.
*/
private function getWorksheetFromExternalSheet($sheet)
{
$worksheetFound = null;
foreach ($this->getWorksheets() as $worksheet) {
if ($worksheet->getExternalSheet() === $sheet) {
$worksheetFound = $worksheet;
break;
}
}
return $worksheetFound;
}
/**
* Adds a row to the current sheet.
* If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
* with the creation of new worksheets if one worksheet has reached its maximum capicity.
*
* @param Row $row The row to be added
* @throws IOException If trying to create a new sheet and unable to open the sheet for writing
* @throws WriterException If unable to write data
* @return void
*/
public function addRowToCurrentWorksheet(Row $row)
{
$currentWorksheet = $this->getCurrentWorksheet();
$hasReachedMaxRows = $this->hasCurrentWorksheetReachedMaxRows();
// if we reached the maximum number of rows for the current sheet...
if ($hasReachedMaxRows) {
// ... continue writing in a new sheet if option set
if ($this->optionsManager->getOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY)) {
$currentWorksheet = $this->addNewSheetAndMakeItCurrent();
$this->addRowToWorksheet($currentWorksheet, $row);
} else {
// otherwise, do nothing as the data won't be written anyways
}
} else {
$this->addRowToWorksheet($currentWorksheet, $row);
}
}
/**
* @return bool Whether the current worksheet has reached the maximum number of rows per sheet.
*/
private function hasCurrentWorksheetReachedMaxRows()
{
$currentWorksheet = $this->getCurrentWorksheet();
return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet());
}
/**
* Adds a row to the given sheet.
*
* @param Worksheet $worksheet Worksheet to write the row to
* @param Row $row The row to be added
* @throws WriterException If unable to write data
* @return void
*/
private function addRowToWorksheet(Worksheet $worksheet, Row $row)
{
$this->applyDefaultRowStyle($row);
$this->worksheetManager->addRow($worksheet, $row);
// update max num columns for the worksheet
$currentMaxNumColumns = $worksheet->getMaxNumColumns();
$cellsCount = $row->getNumCells();
$worksheet->setMaxNumColumns(max($currentMaxNumColumns, $cellsCount));
}
/**
* @param Row $row
*/
private function applyDefaultRowStyle(Row $row)
{
$defaultRowStyle = $this->optionsManager->getOption(Options::DEFAULT_ROW_STYLE);
if ($defaultRowStyle !== null) {
$mergedStyle = $this->styleMerger->merge($row->getStyle(), $defaultRowStyle);
$row->setStyle($mergedStyle);
}
}
/**
* Closes the workbook and all its associated sheets.
* All the necessary files are written to disk and zipped together to create the final file.
* All the temporary files are then deleted.
*
* @param resource $finalFilePointer Pointer to the spreadsheet that will be created
* @return void
*/
public function close($finalFilePointer)
{
$this->closeAllWorksheets();
$this->closeRemainingObjects();
$this->writeAllFilesToDiskAndZipThem($finalFilePointer);
$this->cleanupTempFolder();
}
/**
* Closes custom objects that are still opened
*
* @return void
*/
protected function closeRemainingObjects()
{
// do nothing by default
}
/**
* Writes all the necessary files to disk and zip them together to create the final file.
*
* @param resource $finalFilePointer Pointer to the spreadsheet that will be created
* @return void
*/
abstract protected function writeAllFilesToDiskAndZipThem($finalFilePointer);
/**
* Closes all workbook's associated sheets.
*
* @return void
*/
private function closeAllWorksheets()
{
$worksheets = $this->getWorksheets();
foreach ($worksheets as $worksheet) {
$this->worksheetManager->close($worksheet);
}
}
/**
* Deletes the root folder created in the temp folder and all its contents.
*
* @return void
*/
protected function cleanupTempFolder()
{
$rootFolder = $this->fileSystemHelper->getRootFolder();
$this->fileSystemHelper->deleteFolderRecursively($rootFolder);
}
}
src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php 0000666 00000004737 13476413670 0020177 0 ustar 00 getOption(Options::TEMP_FOLDER);
$zipHelper = $this->createZipHelper($entityFactory);
return new FileSystemHelper($tempFolder, $zipHelper);
}
/**
* @param $entityFactory
* @return ZipHelper
*/
private function createZipHelper($entityFactory)
{
return new ZipHelper($entityFactory);
}
/**
* @return Escaper\ODS
*/
public function createStringsEscaper()
{
return new Escaper\ODS();
}
/**
* @return StringHelper
*/
public function createStringHelper()
{
return new StringHelper();
}
}
src/Spout/Writer/ODS/Creator/ManagerFactory.php 0000666 00000006750 13476413670 0015407 0 ustar 00 entityFactory = $entityFactory;
$this->helperFactory = $helperFactory;
}
/**
* @param OptionsManagerInterface $optionsManager
* @return WorkbookManager
*/
public function createWorkbookManager(OptionsManagerInterface $optionsManager)
{
$workbook = $this->entityFactory->createWorkbook();
$fileSystemHelper = $this->helperFactory->createSpecificFileSystemHelper($optionsManager, $this->entityFactory);
$fileSystemHelper->createBaseFilesAndFolders();
$styleMerger = $this->createStyleMerger();
$styleManager = $this->createStyleManager($optionsManager);
$worksheetManager = $this->createWorksheetManager($styleManager, $styleMerger);
return new WorkbookManager(
$workbook,
$optionsManager,
$worksheetManager,
$styleManager,
$styleMerger,
$fileSystemHelper,
$this->entityFactory,
$this
);
}
/**
* @param StyleManager $styleManager
* @param StyleMerger $styleMerger
* @return WorksheetManager
*/
private function createWorksheetManager(StyleManager $styleManager, StyleMerger $styleMerger)
{
$stringsEscaper = $this->helperFactory->createStringsEscaper();
$stringsHelper = $this->helperFactory->createStringHelper();
return new WorksheetManager($styleManager, $styleMerger, $stringsEscaper, $stringsHelper);
}
/**
* @return SheetManager
*/
public function createSheetManager()
{
$stringHelper = $this->helperFactory->createStringHelper();
return new SheetManager($stringHelper);
}
/**
* @param OptionsManagerInterface $optionsManager
* @return StyleManager
*/
private function createStyleManager(OptionsManagerInterface $optionsManager)
{
$styleRegistry = $this->createStyleRegistry($optionsManager);
return new StyleManager($styleRegistry);
}
/**
* @param OptionsManagerInterface $optionsManager
* @return StyleRegistry
*/
private function createStyleRegistry(OptionsManagerInterface $optionsManager)
{
$defaultRowStyle = $optionsManager->getOption(Options::DEFAULT_ROW_STYLE);
return new StyleRegistry($defaultRowStyle);
}
/**
* @return StyleMerger
*/
private function createStyleMerger()
{
return new StyleMerger();
}
}
src/Spout/Writer/ODS/Helper/BorderHelper.php 0000666 00000003621 13476413670 0014674 0 ustar 00
*/
class BorderHelper
{
/**
* Width mappings
*
* @var array
*/
protected static $widthMap = [
Border::WIDTH_THIN => '0.75pt',
Border::WIDTH_MEDIUM => '1.75pt',
Border::WIDTH_THICK => '2.5pt',
];
/**
* Style mapping
*
* @var array
*/
protected static $styleMap = [
Border::STYLE_SOLID => 'solid',
Border::STYLE_DASHED => 'dashed',
Border::STYLE_DOTTED => 'dotted',
Border::STYLE_DOUBLE => 'double',
];
/**
* @param BorderPart $borderPart
* @return string
*/
public static function serializeBorderPart(BorderPart $borderPart)
{
$definition = 'fo:border-%s="%s"';
if ($borderPart->getStyle() === Border::STYLE_NONE) {
$borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none');
} else {
$attributes = [
self::$widthMap[$borderPart->getWidth()],
self::$styleMap[$borderPart->getStyle()],
'#' . $borderPart->getColor(),
];
$borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes));
}
return $borderPartDefinition;
}
}
src/Spout/Writer/ODS/Helper/FileSystemHelper.php 0000666 00000026404 13476413670 0015547 0 ustar 00 zipHelper = $zipHelper;
}
/**
* @return string
*/
public function getRootFolder()
{
return $this->rootFolder;
}
/**
* @return string
*/
public function getSheetsContentTempFolder()
{
return $this->sheetsContentTempFolder;
}
/**
* Creates all the folders needed to create a ODS file, as well as the files that won't change.
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
* @return void
*/
public function createBaseFilesAndFolders()
{
$this
->createRootFolder()
->createMetaInfoFolderAndFile()
->createSheetsContentTempFolder()
->createMetaFile()
->createMimetypeFile();
}
/**
* Creates the folder that will be used as root
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
* @return FileSystemHelper
*/
protected function createRootFolder()
{
$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));
return $this;
}
/**
* Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file
* @return FileSystemHelper
*/
protected function createMetaInfoFolderAndFile()
{
$this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);
$this->createManifestFile();
return $this;
}
/**
* Creates the "manifest.xml" file under the "META-INF" folder (under root)
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
* @return FileSystemHelper
*/
protected function createManifestFile()
{
$manifestXmlFileContents = <<<'EOD'
EOD;
$this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);
return $this;
}
/**
* Creates the temp folder where specific sheets content will be written to.
* This folder is not part of the final ODS file and is only used to be able to jump between sheets.
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
* @return FileSystemHelper
*/
protected function createSheetsContentTempFolder()
{
$this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME);
return $this;
}
/**
* Creates the "meta.xml" file under the root folder
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
* @return FileSystemHelper
*/
protected function createMetaFile()
{
$appName = self::APP_NAME;
$createdDate = (new \DateTime())->format(\DateTime::W3C);
$metaXmlFileContents = <<
$appName
$createdDate
$createdDate
EOD;
$this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);
return $this;
}
/**
* Creates the "mimetype" file under the root folder
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
* @return FileSystemHelper
*/
protected function createMimetypeFile()
{
$this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);
return $this;
}
/**
* Creates the "content.xml" file under the root folder
*
* @param WorksheetManager $worksheetManager
* @param StyleManager $styleManager
* @param Worksheet[] $worksheets
* @return FileSystemHelper
*/
public function createContentFile($worksheetManager, $styleManager, $worksheets)
{
$contentXmlFileContents = <<<'EOD'
EOD;
$contentXmlFileContents .= $styleManager->getContentXmlFontFaceSectionContent();
$contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheets);
$contentXmlFileContents .= '';
$this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents);
// Append sheets content to "content.xml"
$contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME;
$contentXmlHandle = fopen($contentXmlFilePath, 'a');
foreach ($worksheets as $worksheet) {
// write the "" node, with the final sheet's name
fwrite($contentXmlHandle, $worksheetManager->getTableElementStartAsString($worksheet));
$worksheetFilePath = $worksheet->getFilePath();
$this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);
fwrite($contentXmlHandle, '');
}
$contentXmlFileContents = '';
fwrite($contentXmlHandle, $contentXmlFileContents);
fclose($contentXmlHandle);
return $this;
}
/**
* Streams the content of the file at the given path into the target resource.
* Depending on which mode the target resource was created with, it will truncate then copy
* or append the content to the target file.
*
* @param string $sourceFilePath Path of the file whose content will be copied
* @param resource $targetResource Target resource that will receive the content
* @return void
*/
protected function copyFileContentsToTarget($sourceFilePath, $targetResource)
{
$sourceHandle = fopen($sourceFilePath, 'r');
stream_copy_to_stream($sourceHandle, $targetResource);
fclose($sourceHandle);
}
/**
* Deletes the temporary folder where sheets content was stored.
*
* @return FileSystemHelper
*/
public function deleteWorksheetTempFolder()
{
$this->deleteFolderRecursively($this->sheetsContentTempFolder);
return $this;
}
/**
* Creates the "styles.xml" file under the root folder
*
* @param StyleManager $styleManager
* @param int $numWorksheets Number of created worksheets
* @return FileSystemHelper
*/
public function createStylesFile($styleManager, $numWorksheets)
{
$stylesXmlFileContents = $styleManager->getStylesXMLFileContent($numWorksheets);
$this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
return $this;
}
/**
* Zips the root folder and streams the contents of the zip into the given stream
*
* @param resource $streamPointer Pointer to the stream to copy the zip
* @return void
*/
public function zipRootFolderAndCopyToStream($streamPointer)
{
$zip = $this->zipHelper->createZip($this->rootFolder);
$zipFilePath = $this->zipHelper->getZipFilePath($zip);
// In order to have the file's mime type detected properly, files need to be added
// to the zip file in a particular order.
// @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
$this->zipHelper->addUncompressedFileToArchive($zip, $this->rootFolder, self::MIMETYPE_FILE_NAME);
$this->zipHelper->addFolderToArchive($zip, $this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
$this->zipHelper->closeArchiveAndCopyToStream($zip, $streamPointer);
// once the zip is copied, remove it
$this->deleteFile($zipFilePath);
}
}
src/Spout/Writer/ODS/Manager/OptionsManager.php 0000666 00000002301 13476413670 0015372 0 ustar 00 styleBuilder = $styleBuilder;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function getSupportedOptions()
{
return [
Options::TEMP_FOLDER,
Options::DEFAULT_ROW_STYLE,
Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY,
];
}
/**
* {@inheritdoc}
*/
protected function setDefaultOptions()
{
$this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir());
$this->setOption(Options::DEFAULT_ROW_STYLE, $this->styleBuilder->build());
$this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true);
}
}
src/Spout/Writer/ODS/Manager/Style/StyleManager.php 0000666 00000026620 13476413670 0016151 0 ustar 00
EOD;
$content .= $this->getFontFaceSectionContent();
$content .= $this->getStylesSectionContent();
$content .= $this->getAutomaticStylesSectionContent($numWorksheets);
$content .= $this->getMasterStylesSectionContent($numWorksheets);
$content .= <<<'EOD'
EOD;
return $content;
}
/**
* Returns the content of the "" section, inside "styles.xml" file.
*
* @return string
*/
protected function getFontFaceSectionContent()
{
$content = '';
foreach ($this->styleRegistry->getUsedFonts() as $fontName) {
$content .= '';
}
$content .= '';
return $content;
}
/**
* Returns the content of the "" section, inside "styles.xml" file.
*
* @return string
*/
protected function getStylesSectionContent()
{
$defaultStyle = $this->getDefaultStyle();
return <<
EOD;
}
/**
* Returns the content of the "" section, inside "styles.xml" file.
*
* @param int $numWorksheets Number of worksheets created
* @return string
*/
protected function getAutomaticStylesSectionContent($numWorksheets)
{
$content = '';
for ($i = 1; $i <= $numWorksheets; $i++) {
$content .= <<
EOD;
}
$content .= '';
return $content;
}
/**
* Returns the content of the "" section, inside "styles.xml" file.
*
* @param int $numWorksheets Number of worksheets created
* @return string
*/
protected function getMasterStylesSectionContent($numWorksheets)
{
$content = '';
for ($i = 1; $i <= $numWorksheets; $i++) {
$content .= <<
EOD;
}
$content .= '';
return $content;
}
/**
* Returns the contents of the "" section, inside "content.xml" file.
*
* @return string
*/
public function getContentXmlFontFaceSectionContent()
{
$content = '';
foreach ($this->styleRegistry->getUsedFonts() as $fontName) {
$content .= '';
}
$content .= '';
return $content;
}
/**
* Returns the contents of the "" section, inside "content.xml" file.
*
* @param Worksheet[] $worksheets
* @return string
*/
public function getContentXmlAutomaticStylesSectionContent($worksheets)
{
$content = '';
foreach ($this->styleRegistry->getRegisteredStyles() as $style) {
$content .= $this->getStyleSectionContent($style);
}
$content .= <<<'EOD'
EOD;
foreach ($worksheets as $worksheet) {
$worksheetId = $worksheet->getId();
$isSheetVisible = $worksheet->getExternalSheet()->isVisible() ? 'true' : 'false';
$content .= <<
EOD;
}
$content .= '';
return $content;
}
/**
* Returns the contents of the "" section, inside "" section
*
* @param \Box\Spout\Common\Entity\Style\Style $style
* @return string
*/
protected function getStyleSectionContent($style)
{
$styleIndex = $style->getId() + 1; // 1-based
$content = '';
$content .= $this->getTextPropertiesSectionContent($style);
$content .= $this->getTableCellPropertiesSectionContent($style);
$content .= '';
return $content;
}
/**
* Returns the contents of the "" section, inside "" section
*
* @param \Box\Spout\Common\Entity\Style\Style $style
* @return string
*/
private function getTextPropertiesSectionContent($style)
{
$content = '';
if ($style->shouldApplyFont()) {
$content .= $this->getFontSectionContent($style);
}
return $content;
}
/**
* Returns the contents of the "" section, inside "" section
*
* @param \Box\Spout\Common\Entity\Style\Style $style
* @return string
*/
private function getFontSectionContent($style)
{
$defaultStyle = $this->getDefaultStyle();
$content = 'getFontColor();
if ($fontColor !== $defaultStyle->getFontColor()) {
$content .= ' fo:color="#' . $fontColor . '"';
}
$fontName = $style->getFontName();
if ($fontName !== $defaultStyle->getFontName()) {
$content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"';
}
$fontSize = $style->getFontSize();
if ($fontSize !== $defaultStyle->getFontSize()) {
$content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"';
}
if ($style->isFontBold()) {
$content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"';
}
if ($style->isFontItalic()) {
$content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"';
}
if ($style->isFontUnderline()) {
$content .= ' style:text-underline-style="solid" style:text-underline-type="single"';
}
if ($style->isFontStrikethrough()) {
$content .= ' style:text-line-through-style="solid"';
}
$content .= '/>';
return $content;
}
/**
* Returns the contents of the "" section, inside "" section
*
* @param \Box\Spout\Common\Entity\Style\Style $style
* @return string
*/
private function getTableCellPropertiesSectionContent($style)
{
$content = '';
if ($style->shouldWrapText()) {
$content .= $this->getWrapTextXMLContent();
}
if ($style->shouldApplyBorder()) {
$content .= $this->getBorderXMLContent($style);
}
if ($style->shouldApplyBackgroundColor()) {
$content .= $this->getBackgroundColorXMLContent($style);
}
return $content;
}
/**
* Returns the contents of the wrap text definition for the "" section
*
* @return string
*/
private function getWrapTextXMLContent()
{
return '';
}
/**
* Returns the contents of the borders definition for the "" section
*
* @param \Box\Spout\Common\Entity\Style\Style $style
* @return string
*/
private function getBorderXMLContent($style)
{
$borderProperty = '';
$borders = array_map(function (BorderPart $borderPart) {
return BorderHelper::serializeBorderPart($borderPart);
}, $style->getBorder()->getParts());
return sprintf($borderProperty, implode(' ', $borders));
}
/**
* Returns the contents of the background color definition for the "" section
*
* @param \Box\Spout\Common\Entity\Style\Style $style
* @return string
*/
private function getBackgroundColorXMLContent($style)
{
return sprintf(
'',
$style->getBackgroundColor()
);
}
}
src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php 0000666 00000001737 13476413670 0016411 0 ustar 00 [] Map whose keys contain all the fonts used */
protected $usedFontsSet = [];
/**
* Registers the given style as a used style.
* Duplicate styles won't be registered more than once.
*
* @param Style $style The style to be registered
* @return Style The registered style, updated with an internal ID.
*/
public function registerStyle(Style $style)
{
$registeredStyle = parent::registerStyle($style);
$this->usedFontsSet[$style->getFontName()] = true;
return $registeredStyle;
}
/**
* @return string[] List of used fonts name
*/
public function getUsedFonts()
{
return array_keys($this->usedFontsSet);
}
}
src/Spout/Writer/ODS/Manager/WorkbookManager.php 0000666 00000004203 13476413670 0015537 0 ustar 00 fileSystemHelper->getSheetsContentTempFolder();
return $sheetsContentTempFolder . '/sheet' . $sheet->getIndex() . '.xml';
}
/**
* Writes all the necessary files to disk and zip them together to create the final file.
*
* @param resource $finalFilePointer Pointer to the spreadsheet that will be created
* @return void
*/
protected function writeAllFilesToDiskAndZipThem($finalFilePointer)
{
$worksheets = $this->getWorksheets();
$numWorksheets = count($worksheets);
$this->fileSystemHelper
->createContentFile($this->worksheetManager, $this->styleManager, $worksheets)
->deleteWorksheetTempFolder()
->createStylesFile($this->styleManager, $numWorksheets)
->zipRootFolderAndCopyToStream($finalFilePointer);
}
}
src/Spout/Writer/ODS/Manager/WorksheetManager.php 0000666 00000020360 13476413670 0015717 0 ustar 00 styleManager = $styleManager;
$this->styleMerger = $styleMerger;
$this->stringsEscaper = $stringsEscaper;
$this->stringHelper = $stringHelper;
}
/**
* Prepares the worksheet to accept data
*
* @param Worksheet $worksheet The worksheet to start
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
* @return void
*/
public function startSheet(Worksheet $worksheet)
{
$sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
$this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
$worksheet->setFilePointer($sheetFilePointer);
}
/**
* Checks if the sheet has been sucessfully created. Throws an exception if not.
*
* @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
* @throws IOException If the sheet data file cannot be opened for writing
* @return void
*/
private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
{
if (!$sheetFilePointer) {
throw new IOException('Unable to open sheet for writing.');
}
}
/**
* Returns the table XML root node as string.
*
* @param Worksheet $worksheet
* @return string node as string
*/
public function getTableElementStartAsString(Worksheet $worksheet)
{
$externalSheet = $worksheet->getExternalSheet();
$escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
$tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
$tableElement = '';
$tableElement .= '';
return $tableElement;
}
/**
* Adds a row to the given worksheet.
*
* @param Worksheet $worksheet The worksheet to add the row to
* @param Row $row The row to be added
* @throws IOException If the data cannot be written
* @throws InvalidArgumentException If a cell value's type is not supported
* @return void
*/
public function addRow(Worksheet $worksheet, Row $row)
{
$cells = $row->getCells();
$rowStyle = $row->getStyle();
$data = '';
$currentCellIndex = 0;
$nextCellIndex = 1;
for ($i = 0; $i < $row->getNumCells(); $i++) {
/** @var Cell $cell */
$cell = $cells[$currentCellIndex];
/** @var Cell|null $nextCell */
$nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) {
$data .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $currentCellIndex, $nextCellIndex);
$currentCellIndex = $nextCellIndex;
}
$nextCellIndex++;
}
$data .= '';
$wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $data);
if ($wasWriteSuccessful === false) {
throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
}
// only update the count if the write worked
$lastWrittenRowIndex = $worksheet->getLastWrittenRowIndex();
$worksheet->setLastWrittenRowIndex($lastWrittenRowIndex + 1);
}
/**
* Applies styles to the given style, merging the cell's style with its row's style
* Then builds and returns xml for the cell.
*
* @param Cell $cell
* @param Style $rowStyle
* @param int $currentCellIndex
* @param int $nextCellIndex
* @throws InvalidArgumentException If a cell value's type is not supported
* @return string
*/
private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $currentCellIndex, $nextCellIndex)
{
// Apply row and extra styles
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
$styleIndex = $registeredStyle->getId() + 1; // 1-based
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
return $this->getCellXML($cell, $styleIndex, $numTimesValueRepeated);
}
/**
* Returns the cell XML content, given its value.
*
* @param Cell $cell The cell to be written
* @param int $styleIndex Index of the used style
* @param int $numTimesValueRepeated Number of times the value is consecutively repeated
* @throws InvalidArgumentException If a cell value's type is not supported
* @return string The cell XML content
*/
private function getCellXML(Cell $cell, $styleIndex, $numTimesValueRepeated)
{
$data = 'isString()) {
$data .= ' office:value-type="string" calcext:value-type="string">';
$cellValueLines = explode("\n", $cell->getValue());
foreach ($cellValueLines as $cellValueLine) {
$data .= '' . $this->stringsEscaper->escape($cellValueLine) . '';
}
$data .= '';
} elseif ($cell->isBoolean()) {
$data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cell->getValue() . '">';
$data .= '' . $cell->getValue() . '';
$data .= '';
} elseif ($cell->isNumeric()) {
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">';
$data .= '' . $cell->getValue() . '';
$data .= '';
} elseif ($cell->isEmpty()) {
$data .= '/>';
} else {
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
}
return $data;
}
/**
* Closes the worksheet
*
* @param Worksheet $worksheet
* @return void
*/
public function close(Worksheet $worksheet)
{
$worksheetFilePointer = $worksheet->getFilePointer();
if (!is_resource($worksheetFilePointer)) {
return;
}
fclose($worksheetFilePointer);
}
}
src/Spout/Writer/ODS/Writer.php 0000666 00000002033 13476413670 0012350 0 ustar 00 throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
$this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder);
return $this;
}
}
src/Spout/Writer/WriterAbstract.php 0000666 00000016650 13476413670 0013421 0 ustar 00 optionsManager = $optionsManager;
$this->globalFunctionsHelper = $globalFunctionsHelper;
$this->helperFactory = $helperFactory;
}
/**
* Opens the streamer and makes it ready to accept data.
*
* @throws IOException If the writer cannot be opened
* @return void
*/
abstract protected function openWriter();
/**
* Adds a row to the currently opened writer.
*
* @param Row $row The row containing cells and styles
* @throws WriterNotOpenedException If the workbook is not created yet
* @throws IOException If unable to write data
* @return void
*/
abstract protected function addRowToWriter(Row $row);
/**
* Closes the streamer, preventing any additional writing.
*
* @return void
*/
abstract protected function closeWriter();
/**
* {@inheritdoc}
*/
public function setDefaultRowStyle(Style $defaultStyle)
{
$this->optionsManager->setOption(Options::DEFAULT_ROW_STYLE, $defaultStyle);
return $this;
}
/**
* {@inheritdoc}
*/
public function openToFile($outputFilePath)
{
$this->outputFilePath = $outputFilePath;
$this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+');
$this->throwIfFilePointerIsNotAvailable();
$this->openWriter();
$this->isWriterOpened = true;
return $this;
}
/**
* @codeCoverageIgnore
* {@inheritdoc}
*/
public function openToBrowser($outputFileName)
{
$this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName);
$this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w');
$this->throwIfFilePointerIsNotAvailable();
// Clear any previous output (otherwise the generated file will be corrupted)
// @see https://github.com/box/spout/issues/241
$this->globalFunctionsHelper->ob_end_clean();
// Set headers
$this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType);
$this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"');
/*
* When forcing the download of a file over SSL,IE8 and lower browsers fail
* if the Cache-Control and Pragma headers are not set.
*
* @see http://support.microsoft.com/KB/323308
* @see https://github.com/liuggio/ExcelBundle/issues/45
*/
$this->globalFunctionsHelper->header('Cache-Control: max-age=0');
$this->globalFunctionsHelper->header('Pragma: public');
$this->openWriter();
$this->isWriterOpened = true;
return $this;
}
/**
* Checks if the pointer to the file/stream to write to is available.
* Will throw an exception if not available.
*
* @throws IOException If the pointer is not available
* @return void
*/
protected function throwIfFilePointerIsNotAvailable()
{
if (!$this->filePointer) {
throw new IOException('File pointer has not be opened');
}
}
/**
* Checks if the writer has already been opened, since some actions must be done before it gets opened.
* Throws an exception if already opened.
*
* @param string $message Error message
* @throws WriterAlreadyOpenedException If the writer was already opened and must not be.
* @return void
*/
protected function throwIfWriterAlreadyOpened($message)
{
if ($this->isWriterOpened) {
throw new WriterAlreadyOpenedException($message);
}
}
/**
* {@inheritdoc}
*/
public function addRow(Row $row)
{
if ($this->isWriterOpened) {
try {
$this->addRowToWriter($row);
} catch (SpoutException $e) {
// if an exception occurs while writing data,
// close the writer and remove all files created so far.
$this->closeAndAttemptToCleanupAllFiles();
// re-throw the exception to alert developers of the error
throw $e;
}
} else {
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
}
return $this;
}
/**
* {@inheritdoc}
*/
public function addRows(array $rows)
{
foreach ($rows as $row) {
if (!$row instanceof Row) {
$this->closeAndAttemptToCleanupAllFiles();
throw new InvalidArgumentException('The input should be an array of Row');
}
$this->addRow($row);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function close()
{
if (!$this->isWriterOpened) {
return;
}
$this->closeWriter();
if (is_resource($this->filePointer)) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
$this->isWriterOpened = false;
}
/**
* Closes the writer and attempts to cleanup all files that were
* created during the writing process (temp files & final file).
*
* @return void
*/
private function closeAndAttemptToCleanupAllFiles()
{
// close the writer, which should remove all temp files
$this->close();
// remove output file if it was created
if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) {
$outputFolderPath = dirname($this->outputFilePath);
$fileSystemHelper = $this->helperFactory->createFileSystemHelper($outputFolderPath);
$fileSystemHelper->deleteFile($this->outputFilePath);
}
}
}
src/Spout/Writer/WriterInterface.php 0000666 00000004747 13476413670 0013562 0 ustar 00 managerFactory = $managerFactory;
}
/**
* Sets whether new sheets should be automatically created when the max rows limit per sheet is reached.
* This must be set before opening the writer.
*
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
* @throws WriterAlreadyOpenedException If the writer was already opened
* @return WriterMultiSheetsAbstract
*/
public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically)
{
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
$this->optionsManager->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, $shouldCreateNewSheetsAutomatically);
return $this;
}
/**
* {@inheritdoc}
*/
protected function openWriter()
{
if (!$this->workbookManager) {
$this->workbookManager = $this->managerFactory->createWorkbookManager($this->optionsManager);
$this->workbookManager->addNewSheetAndMakeItCurrent();
}
}
/**
* Returns all the workbook's sheets
*
* @throws WriterNotOpenedException If the writer has not been opened yet
* @return Sheet[] All the workbook's sheets
*/
public function getSheets()
{
$this->throwIfWorkbookIsNotAvailable();
$externalSheets = [];
$worksheets = $this->workbookManager->getWorksheets();
/** @var Worksheet $worksheet */
foreach ($worksheets as $worksheet) {
$externalSheets[] = $worksheet->getExternalSheet();
}
return $externalSheets;
}
/**
* Creates a new sheet and make it the current sheet. The data will now be written to this sheet.
*
* @throws WriterNotOpenedException If the writer has not been opened yet
* @return Sheet The created sheet
*/
public function addNewSheetAndMakeItCurrent()
{
$this->throwIfWorkbookIsNotAvailable();
$worksheet = $this->workbookManager->addNewSheetAndMakeItCurrent();
return $worksheet->getExternalSheet();
}
/**
* Returns the current sheet
*
* @throws WriterNotOpenedException If the writer has not been opened yet
* @return Sheet The current sheet
*/
public function getCurrentSheet()
{
$this->throwIfWorkbookIsNotAvailable();
return $this->workbookManager->getCurrentWorksheet()->getExternalSheet();
}
/**
* Sets the given sheet as the current one. New data will be written to this sheet.
* The writing will resume where it stopped (i.e. data won't be truncated).
*
* @param Sheet $sheet The sheet to set as current
* @throws WriterNotOpenedException If the writer has not been opened yet
* @throws SheetNotFoundException If the given sheet does not exist in the workbook
* @return void
*/
public function setCurrentSheet($sheet)
{
$this->throwIfWorkbookIsNotAvailable();
$this->workbookManager->setCurrentSheet($sheet);
}
/**
* Checks if the workbook has been created. Throws an exception if not created yet.
*
* @throws WriterNotOpenedException If the workbook is not created yet
* @return void
*/
protected function throwIfWorkbookIsNotAvailable()
{
if (!$this->workbookManager->getWorkbook()) {
throw new WriterNotOpenedException('The writer must be opened before performing this action.');
}
}
/**
* {@inheritdoc}
*/
protected function addRowToWriter(Row $row)
{
$this->throwIfWorkbookIsNotAvailable();
$this->workbookManager->addRowToCurrentWorksheet($row);
}
/**
* {@inheritdoc}
*/
protected function closeWriter()
{
if ($this->workbookManager) {
$this->workbookManager->close($this->filePointer);
}
}
}
src/Spout/Writer/XLSX/Creator/HelperFactory.php 0000666 00000003054 13476413670 0015417 0 ustar 00 getOption(Options::TEMP_FOLDER);
$zipHelper = $this->createZipHelper($entityFactory);
$escaper = $this->createStringsEscaper();
return new FileSystemHelper($tempFolder, $zipHelper, $escaper);
}
/**
* @param InternalEntityFactory $entityFactory
* @return ZipHelper
*/
private function createZipHelper(InternalEntityFactory $entityFactory)
{
return new ZipHelper($entityFactory);
}
/**
* @return Escaper\XLSX
*/
public function createStringsEscaper()
{
return new Escaper\XLSX();
}
/**
* @return StringHelper
*/
public function createStringHelper()
{
return new StringHelper();
}
}
src/Spout/Writer/XLSX/Creator/ManagerFactory.php 0000666 00000011215 13476413670 0015550 0 ustar 00 entityFactory = $entityFactory;
$this->helperFactory = $helperFactory;
}
/**
* @param OptionsManagerInterface $optionsManager
* @return WorkbookManager
*/
public function createWorkbookManager(OptionsManagerInterface $optionsManager)
{
$workbook = $this->entityFactory->createWorkbook();
$fileSystemHelper = $this->helperFactory->createSpecificFileSystemHelper($optionsManager, $this->entityFactory);
$fileSystemHelper->createBaseFilesAndFolders();
$xlFolder = $fileSystemHelper->getXlFolder();
$sharedStringsManager = $this->createSharedStringsManager($xlFolder);
$styleMerger = $this->createStyleMerger();
$styleManager = $this->createStyleManager($optionsManager);
$worksheetManager = $this->createWorksheetManager($optionsManager, $styleManager, $styleMerger, $sharedStringsManager);
return new WorkbookManager(
$workbook,
$optionsManager,
$worksheetManager,
$styleManager,
$styleMerger,
$fileSystemHelper,
$this->entityFactory,
$this
);
}
/**
* @param OptionsManagerInterface $optionsManager
* @param StyleManager $styleManager
* @param StyleMerger $styleMerger
* @param SharedStringsManager $sharedStringsManager
* @return WorksheetManager
*/
private function createWorksheetManager(
OptionsManagerInterface $optionsManager,
StyleManager $styleManager,
StyleMerger $styleMerger,
SharedStringsManager $sharedStringsManager
) {
$rowManager = $this->createRowManager();
$stringsEscaper = $this->helperFactory->createStringsEscaper();
$stringsHelper = $this->helperFactory->createStringHelper();
return new WorksheetManager(
$optionsManager,
$rowManager,
$styleManager,
$styleMerger,
$sharedStringsManager,
$stringsEscaper,
$stringsHelper,
$this->entityFactory
);
}
/**
* @return SheetManager
*/
public function createSheetManager()
{
$stringHelper = $this->helperFactory->createStringHelper();
return new SheetManager($stringHelper);
}
/**
* @return RowManager
*/
public function createRowManager()
{
return new RowManager();
}
/**
* @param OptionsManagerInterface $optionsManager
* @return StyleManager
*/
private function createStyleManager(OptionsManagerInterface $optionsManager)
{
$styleRegistry = $this->createStyleRegistry($optionsManager);
return new StyleManager($styleRegistry);
}
/**
* @param OptionsManagerInterface $optionsManager
* @return StyleRegistry
*/
private function createStyleRegistry(OptionsManagerInterface $optionsManager)
{
$defaultRowStyle = $optionsManager->getOption(Options::DEFAULT_ROW_STYLE);
return new StyleRegistry($defaultRowStyle);
}
/**
* @return StyleMerger
*/
private function createStyleMerger()
{
return new StyleMerger();
}
/**
* @param string $xlFolder Path to the "xl" folder
* @return SharedStringsManager
*/
private function createSharedStringsManager($xlFolder)
{
$stringEscaper = $this->helperFactory->createStringsEscaper();
return new SharedStringsManager($xlFolder, $stringEscaper);
}
}
src/Spout/Writer/XLSX/Helper/BorderHelper.php 0000666 00000003664 13476413670 0015054 0 ustar 00 [
Border::WIDTH_THIN => 'thin',
Border::WIDTH_MEDIUM => 'medium',
Border::WIDTH_THICK => 'thick',
],
Border::STYLE_DOTTED => [
Border::WIDTH_THIN => 'dotted',
Border::WIDTH_MEDIUM => 'dotted',
Border::WIDTH_THICK => 'dotted',
],
Border::STYLE_DASHED => [
Border::WIDTH_THIN => 'dashed',
Border::WIDTH_MEDIUM => 'mediumDashed',
Border::WIDTH_THICK => 'mediumDashed',
],
Border::STYLE_DOUBLE => [
Border::WIDTH_THIN => 'double',
Border::WIDTH_MEDIUM => 'double',
Border::WIDTH_THICK => 'double',
],
Border::STYLE_NONE => [
Border::WIDTH_THIN => 'none',
Border::WIDTH_MEDIUM => 'none',
Border::WIDTH_THICK => 'none',
],
];
/**
* @param BorderPart $borderPart
* @return string
*/
public static function serializeBorderPart(BorderPart $borderPart)
{
$borderStyle = self::getBorderStyle($borderPart);
$colorEl = $borderPart->getColor() ? sprintf('', $borderPart->getColor()) : '';
$partEl = sprintf(
'<%s style="%s">%s%s>',
$borderPart->getName(),
$borderStyle,
$colorEl,
$borderPart->getName()
);
return $partEl . PHP_EOL;
}
/**
* Get the style definition from the style map
*
* @param BorderPart $borderPart
* @return string
*/
protected static function getBorderStyle(BorderPart $borderPart)
{
return self::$xlsxStyleMap[$borderPart->getStyle()][$borderPart->getWidth()];
}
}
src/Spout/Writer/XLSX/Helper/FileSystemHelper.php 0000666 00000034523 13476413670 0015721 0 ustar 00 zipHelper = $zipHelper;
$this->escaper = $escaper;
}
/**
* @return string
*/
public function getRootFolder()
{
return $this->rootFolder;
}
/**
* @return string
*/
public function getXlFolder()
{
return $this->xlFolder;
}
/**
* @return string
*/
public function getXlWorksheetsFolder()
{
return $this->xlWorksheetsFolder;
}
/**
* Creates all the folders needed to create a XLSX file, as well as the files that won't change.
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
* @return void
*/
public function createBaseFilesAndFolders()
{
$this
->createRootFolder()
->createRelsFolderAndFile()
->createDocPropsFolderAndFiles()
->createXlFolderAndSubFolders();
}
/**
* Creates the folder that will be used as root
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
* @return FileSystemHelper
*/
private function createRootFolder()
{
$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('xlsx', true));
return $this;
}
/**
* Creates the "_rels" folder under the root folder as well as the ".rels" file in it
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the ".rels" file
* @return FileSystemHelper
*/
private function createRelsFolderAndFile()
{
$this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME);
$this->createRelsFile();
return $this;
}
/**
* Creates the ".rels" file under the "_rels" folder (under root)
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
* @return FileSystemHelper
*/
private function createRelsFile()
{
$relsFileContents = <<<'EOD'
EOD;
$this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents);
return $this;
}
/**
* Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or one of the files
* @return FileSystemHelper
*/
private function createDocPropsFolderAndFiles()
{
$this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME);
$this->createAppXmlFile();
$this->createCoreXmlFile();
return $this;
}
/**
* Creates the "app.xml" file under the "docProps" folder
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
* @return FileSystemHelper
*/
private function createAppXmlFile()
{
$appName = self::APP_NAME;
$appXmlFileContents = <<
$appName
0
EOD;
$this->createFileWithContents($this->docPropsFolder, self::APP_XML_FILE_NAME, $appXmlFileContents);
return $this;
}
/**
* Creates the "core.xml" file under the "docProps" folder
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
* @return FileSystemHelper
*/
private function createCoreXmlFile()
{
$createdDate = (new \DateTime())->format(\DateTime::W3C);
$coreXmlFileContents = <<
$createdDate
$createdDate
0
EOD;
$this->createFileWithContents($this->docPropsFolder, self::CORE_XML_FILE_NAME, $coreXmlFileContents);
return $this;
}
/**
* Creates the "xl" folder under the root folder as well as its subfolders
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the folders
* @return FileSystemHelper
*/
private function createXlFolderAndSubFolders()
{
$this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME);
$this->createXlRelsFolder();
$this->createXlWorksheetsFolder();
return $this;
}
/**
* Creates the "_rels" folder under the "xl" folder
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
* @return FileSystemHelper
*/
private function createXlRelsFolder()
{
$this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME);
return $this;
}
/**
* Creates the "worksheets" folder under the "xl" folder
*
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
* @return FileSystemHelper
*/
private function createXlWorksheetsFolder()
{
$this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME);
return $this;
}
/**
* Creates the "[Content_Types].xml" file under the root folder
*
* @param Worksheet[] $worksheets
* @return FileSystemHelper
*/
public function createContentTypesFile($worksheets)
{
$contentTypesXmlFileContents = <<<'EOD'
EOD;
/** @var Worksheet $worksheet */
foreach ($worksheets as $worksheet) {
$contentTypesXmlFileContents .= '';
}
$contentTypesXmlFileContents .= <<<'EOD'
EOD;
$this->createFileWithContents($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME, $contentTypesXmlFileContents);
return $this;
}
/**
* Creates the "workbook.xml" file under the "xl" folder
*
* @param Worksheet[] $worksheets
* @return FileSystemHelper
*/
public function createWorkbookFile($worksheets)
{
$workbookXmlFileContents = <<<'EOD'
EOD;
/** @var Worksheet $worksheet */
foreach ($worksheets as $worksheet) {
$worksheetName = $worksheet->getExternalSheet()->getName();
$worksheetVisibility = $worksheet->getExternalSheet()->isVisible() ? 'visible' : 'hidden';
$worksheetId = $worksheet->getId();
$workbookXmlFileContents .= '';
}
$workbookXmlFileContents .= <<<'EOD'
EOD;
$this->createFileWithContents($this->xlFolder, self::WORKBOOK_XML_FILE_NAME, $workbookXmlFileContents);
return $this;
}
/**
* Creates the "workbook.xml.res" file under the "xl/_res" folder
*
* @param Worksheet[] $worksheets
* @return FileSystemHelper
*/
public function createWorkbookRelsFile($worksheets)
{
$workbookRelsXmlFileContents = <<<'EOD'
EOD;
/** @var Worksheet $worksheet */
foreach ($worksheets as $worksheet) {
$worksheetId = $worksheet->getId();
$workbookRelsXmlFileContents .= '';
}
$workbookRelsXmlFileContents .= '';
$this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents);
return $this;
}
/**
* Creates the "styles.xml" file under the "xl" folder
*
* @param StyleManager $styleManager
* @return FileSystemHelper
*/
public function createStylesFile($styleManager)
{
$stylesXmlFileContents = $styleManager->getStylesXMLFileContent();
$this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
return $this;
}
/**
* Zips the root folder and streams the contents of the zip into the given stream
*
* @param resource $streamPointer Pointer to the stream to copy the zip
* @return void
*/
public function zipRootFolderAndCopyToStream($streamPointer)
{
$zip = $this->zipHelper->createZip($this->rootFolder);
$zipFilePath = $this->zipHelper->getZipFilePath($zip);
// In order to have the file's mime type detected properly, files need to be added
// to the zip file in a particular order.
// "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first.
$this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME);
$this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME . '/' . self::WORKBOOK_XML_FILE_NAME);
$this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME . '/' . self::STYLES_XML_FILE_NAME);
$this->zipHelper->addFolderToArchive($zip, $this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
$this->zipHelper->closeArchiveAndCopyToStream($zip, $streamPointer);
// once the zip is copied, remove it
$this->deleteFile($zipFilePath);
}
}
src/Spout/Writer/XLSX/Manager/OptionsManager.php 0000666 00000003111 13476413670 0015543 0 ustar 00 styleBuilder = $styleBuilder;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function getSupportedOptions()
{
return [
Options::TEMP_FOLDER,
Options::DEFAULT_ROW_STYLE,
Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY,
Options::SHOULD_USE_INLINE_STRINGS,
];
}
/**
* {@inheritdoc}
*/
protected function setDefaultOptions()
{
$defaultRowStyle = $this->styleBuilder
->setFontSize(self::DEFAULT_FONT_SIZE)
->setFontName(self::DEFAULT_FONT_NAME)
->build();
$this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir());
$this->setOption(Options::DEFAULT_ROW_STYLE, $defaultRowStyle);
$this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true);
$this->setOption(Options::SHOULD_USE_INLINE_STRINGS, true);
}
}
src/Spout/Writer/XLSX/Manager/SharedStringsManager.php 0000666 00000007474 13476413670 0016710 0 ustar 00
sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w');
$this->throwIfSharedStringsFilePointerIsNotAvailable();
// the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later
$header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>';
fwrite($this->sharedStringsFilePointer, $header);
$this->stringsEscaper = $stringsEscaper;
}
/**
* Checks if the book has been created. Throws an exception if not created yet.
*
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
* @return void
*/
protected function throwIfSharedStringsFilePointerIsNotAvailable()
{
if (!$this->sharedStringsFilePointer) {
throw new IOException('Unable to open shared strings file for writing.');
}
}
/**
* Writes the given string into the sharedStrings.xml file.
* Starting and ending whitespaces are preserved.
*
* @param string $string
* @return int ID of the written shared string
*/
public function writeString($string)
{
fwrite($this->sharedStringsFilePointer, '' . $this->stringsEscaper->escape($string) . '');
$this->numSharedStrings++;
// Shared string ID is zero-based
return ($this->numSharedStrings - 1);
}
/**
* Finishes writing the data in the sharedStrings.xml file and closes the file.
*
* @return void
*/
public function close()
{
if (!is_resource($this->sharedStringsFilePointer)) {
return;
}
fwrite($this->sharedStringsFilePointer, '');
// Replace the default strings count with the actual number of shared strings in the file header
$firstPartHeaderLength = strlen(self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER);
$defaultStringsCountPartLength = strlen(self::DEFAULT_STRINGS_COUNT_PART);
// Adding 1 to take into account the space between the last xml attribute and "count"
fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1);
fwrite($this->sharedStringsFilePointer, sprintf("%-{$defaultStringsCountPartLength}s", 'count="' . $this->numSharedStrings . '" uniqueCount="' . $this->numSharedStrings . '"'));
fclose($this->sharedStringsFilePointer);
}
}
src/Spout/Writer/XLSX/Manager/Style/StyleManager.php 0000666 00000020577 13476413670 0016327 0 ustar 00 styleRegistry->getFillIdForStyleId($styleId);
$hasStyleCustomFill = ($associatedFillId !== null && $associatedFillId !== 0);
$associatedBorderId = $this->styleRegistry->getBorderIdForStyleId($styleId);
$hasStyleCustomBorders = ($associatedBorderId !== null && $associatedBorderId !== 0);
return ($hasStyleCustomFill || $hasStyleCustomBorders);
}
/**
* Returns the content of the "styles.xml" file, given a list of styles.
*
* @return string
*/
public function getStylesXMLFileContent()
{
$content = <<<'EOD'
EOD;
$content .= $this->getFontsSectionContent();
$content .= $this->getFillsSectionContent();
$content .= $this->getBordersSectionContent();
$content .= $this->getCellStyleXfsSectionContent();
$content .= $this->getCellXfsSectionContent();
$content .= $this->getCellStylesSectionContent();
$content .= <<<'EOD'
EOD;
return $content;
}
/**
* Returns the content of the "" section.
*
* @return string
*/
protected function getFontsSectionContent()
{
$registeredStyles = $this->styleRegistry->getRegisteredStyles();
$content = '';
/** @var Style $style */
foreach ($registeredStyles as $style) {
$content .= '';
$content .= '';
$content .= '';
$content .= '';
if ($style->isFontBold()) {
$content .= '';
}
if ($style->isFontItalic()) {
$content .= '';
}
if ($style->isFontUnderline()) {
$content .= '';
}
if ($style->isFontStrikethrough()) {
$content .= '';
}
$content .= '';
}
$content .= '';
return $content;
}
/**
* Returns the content of the "" section.
*
* @return string
*/
protected function getFillsSectionContent()
{
$registeredFills = $this->styleRegistry->getRegisteredFills();
// Excel reserves two default fills
$fillsCount = count($registeredFills) + 2;
$content = sprintf('', $fillsCount);
$content .= '';
$content .= '';
// The other fills are actually registered by setting a background color
foreach ($registeredFills as $styleId) {
/** @var Style $style */
$style = $this->styleRegistry->getStyleFromStyleId($styleId);
$backgroundColor = $style->getBackgroundColor();
$content .= sprintf(
'',
$backgroundColor
);
}
$content .= '';
return $content;
}
/**
* Returns the content of the "" section.
*
* @return string
*/
protected function getBordersSectionContent()
{
$registeredBorders = $this->styleRegistry->getRegisteredBorders();
// There is one default border with index 0
$borderCount = count($registeredBorders) + 1;
$content = '';
// Default border starting at index 0
$content .= '';
foreach ($registeredBorders as $styleId) {
/** @var \Box\Spout\Common\Entity\Style\Style $style */
$style = $this->styleRegistry->getStyleFromStyleId($styleId);
$border = $style->getBorder();
$content .= '';
// @link https://github.com/box/spout/issues/271
$sortOrder = ['left', 'right', 'top', 'bottom'];
foreach ($sortOrder as $partName) {
if ($border->hasPart($partName)) {
/** @var $part \Box\Spout\Common\Entity\Style\BorderPart */
$part = $border->getPart($partName);
$content .= BorderHelper::serializeBorderPart($part);
}
}
$content .= '';
}
$content .= '';
return $content;
}
/**
* Returns the content of the "" section.
*
* @return string
*/
protected function getCellStyleXfsSectionContent()
{
return <<<'EOD'
EOD;
}
/**
* Returns the content of the "" section.
*
* @return string
*/
protected function getCellXfsSectionContent()
{
$registeredStyles = $this->styleRegistry->getRegisteredStyles();
$content = '';
foreach ($registeredStyles as $style) {
$styleId = $style->getId();
$fillId = $this->getFillIdForStyleId($styleId);
$borderId = $this->getBorderIdForStyleId($styleId);
$content .= 'shouldApplyFont()) {
$content .= ' applyFont="1"';
}
$content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0);
if ($style->shouldWrapText()) {
$content .= ' applyAlignment="1">';
$content .= '';
$content .= '';
} else {
$content .= '/>';
}
}
$content .= '';
return $content;
}
/**
* Returns the fill ID associated to the given style ID.
* For the default style, we don't a fill.
*
* @param int $styleId
* @return int
*/
private function getFillIdForStyleId($styleId)
{
// For the default style (ID = 0), we don't want to override the fill.
// Otherwise all cells of the spreadsheet will have a background color.
$isDefaultStyle = ($styleId === 0);
return $isDefaultStyle ? 0 : ($this->styleRegistry->getFillIdForStyleId($styleId) ?: 0);
}
/**
* Returns the fill ID associated to the given style ID.
* For the default style, we don't a border.
*
* @param int $styleId
* @return int
*/
private function getBorderIdForStyleId($styleId)
{
// For the default style (ID = 0), we don't want to override the border.
// Otherwise all cells of the spreadsheet will have a border.
$isDefaultStyle = ($styleId === 0);
return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?: 0);
}
/**
* Returns the content of the "" section.
*
* @return string
*/
protected function getCellStylesSectionContent()
{
return <<<'EOD'
EOD;
}
}
src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php 0000666 00000010723 13476413670 0016555 0 ustar 00 [FILL_ID] maps a style to a fill declaration
*/
protected $styleIdToFillMappingTable = [];
/**
* Excel preserves two default fills with index 0 and 1
* Since Excel is the dominant vendor - we play along here
*
* @var int The fill index counter for custom fills.
*/
protected $fillIndex = 2;
/**
* @var array
*/
protected $registeredBorders = [];
/**
* @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration
*/
protected $styleIdToBorderMappingTable = [];
/**
* XLSX specific operations on the registered styles
*
* @param Style $style
* @return Style
*/
public function registerStyle(Style $style)
{
$registeredStyle = parent::registerStyle($style);
$this->registerFill($registeredStyle);
$this->registerBorder($registeredStyle);
return $registeredStyle;
}
/**
* Register a fill definition
*
* @param Style $style
*/
private function registerFill(Style $style)
{
$styleId = $style->getId();
// Currently - only solid backgrounds are supported
// so $backgroundColor is a scalar value (RGB Color)
$backgroundColor = $style->getBackgroundColor();
if ($backgroundColor) {
$isBackgroundColorRegistered = isset($this->registeredFills[$backgroundColor]);
// We need to track the already registered background definitions
if ($isBackgroundColorRegistered) {
$registeredStyleId = $this->registeredFills[$backgroundColor];
$registeredFillId = $this->styleIdToFillMappingTable[$registeredStyleId];
$this->styleIdToFillMappingTable[$styleId] = $registeredFillId;
} else {
$this->registeredFills[$backgroundColor] = $styleId;
$this->styleIdToFillMappingTable[$styleId] = $this->fillIndex++;
}
} else {
// The fillId maps a style to a fill declaration
// When there is no background color definition - we default to 0
$this->styleIdToFillMappingTable[$styleId] = 0;
}
}
/**
* @param int $styleId
* @return int|null Fill ID associated to the given style ID
*/
public function getFillIdForStyleId($styleId)
{
return (isset($this->styleIdToFillMappingTable[$styleId])) ?
$this->styleIdToFillMappingTable[$styleId] :
null;
}
/**
* Register a border definition
*
* @param Style $style
*/
private function registerBorder(Style $style)
{
$styleId = $style->getId();
if ($style->shouldApplyBorder()) {
$border = $style->getBorder();
$serializedBorder = serialize($border);
$isBorderAlreadyRegistered = isset($this->registeredBorders[$serializedBorder]);
if ($isBorderAlreadyRegistered) {
$registeredStyleId = $this->registeredBorders[$serializedBorder];
$registeredBorderId = $this->styleIdToBorderMappingTable[$registeredStyleId];
$this->styleIdToBorderMappingTable[$styleId] = $registeredBorderId;
} else {
$this->registeredBorders[$serializedBorder] = $styleId;
$this->styleIdToBorderMappingTable[$styleId] = count($this->registeredBorders);
}
} else {
// If no border should be applied - the mapping is the default border: 0
$this->styleIdToBorderMappingTable[$styleId] = 0;
}
}
/**
* @param int $styleId
* @return int|null Fill ID associated to the given style ID
*/
public function getBorderIdForStyleId($styleId)
{
return (isset($this->styleIdToBorderMappingTable[$styleId])) ?
$this->styleIdToBorderMappingTable[$styleId] :
null;
}
/**
* @return array
*/
public function getRegisteredFills()
{
return $this->registeredFills;
}
/**
* @return array
*/
public function getRegisteredBorders()
{
return $this->registeredBorders;
}
}
src/Spout/Writer/XLSX/Manager/WorkbookManager.php 0000666 00000004474 13476413670 0015722 0 ustar 00 fileSystemHelper->getXlWorksheetsFolder();
return $worksheetFilesFolder . '/' . strtolower($sheet->getName()) . '.xml';
}
/**
* Closes custom objects that are still opened
*
* @return void
*/
protected function closeRemainingObjects()
{
$this->worksheetManager->getSharedStringsManager()->close();
}
/**
* Writes all the necessary files to disk and zip them together to create the final file.
*
* @param resource $finalFilePointer Pointer to the spreadsheet that will be created
* @return void
*/
protected function writeAllFilesToDiskAndZipThem($finalFilePointer)
{
$worksheets = $this->getWorksheets();
$this->fileSystemHelper
->createContentTypesFile($worksheets)
->createWorkbookFile($worksheets)
->createWorkbookRelsFile($worksheets)
->createStylesFile($this->styleManager)
->zipRootFolderAndCopyToStream($finalFilePointer);
}
}
src/Spout/Writer/XLSX/Manager/WorksheetManager.php 0000666 00000023364 13476413670 0016077 0 ustar 00
EOD;
/** @var bool Whether inline or shared strings should be used */
protected $shouldUseInlineStrings;
/** @var RowManager Manages rows */
private $rowManager;
/** @var StyleManager Manages styles */
private $styleManager;
/** @var StyleMerger Helper to merge styles together */
private $styleMerger;
/** @var SharedStringsManager Helper to write shared strings */
private $sharedStringsManager;
/** @var XLSXEscaper Strings escaper */
private $stringsEscaper;
/** @var StringHelper String helper */
private $stringHelper;
/** @var InternalEntityFactory Factory to create entities */
private $entityFactory;
/**
* WorksheetManager constructor.
*
* @param OptionsManagerInterface $optionsManager
* @param RowManager $rowManager
* @param StyleManager $styleManager
* @param StyleMerger $styleMerger
* @param SharedStringsManager $sharedStringsManager
* @param XLSXEscaper $stringsEscaper
* @param StringHelper $stringHelper
* @param InternalEntityFactory $entityFactory
*/
public function __construct(
OptionsManagerInterface $optionsManager,
RowManager $rowManager,
StyleManager $styleManager,
StyleMerger $styleMerger,
SharedStringsManager $sharedStringsManager,
XLSXEscaper $stringsEscaper,
StringHelper $stringHelper,
InternalEntityFactory $entityFactory
) {
$this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
$this->rowManager = $rowManager;
$this->styleManager = $styleManager;
$this->styleMerger = $styleMerger;
$this->sharedStringsManager = $sharedStringsManager;
$this->stringsEscaper = $stringsEscaper;
$this->stringHelper = $stringHelper;
$this->entityFactory = $entityFactory;
}
/**
* @return SharedStringsManager
*/
public function getSharedStringsManager()
{
return $this->sharedStringsManager;
}
/**
* {@inheritdoc}
*/
public function startSheet(Worksheet $worksheet)
{
$sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
$this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
$worksheet->setFilePointer($sheetFilePointer);
fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
fwrite($sheetFilePointer, '');
}
/**
* Checks if the sheet has been sucessfully created. Throws an exception if not.
*
* @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
* @throws IOException If the sheet data file cannot be opened for writing
* @return void
*/
private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
{
if (!$sheetFilePointer) {
throw new IOException('Unable to open sheet for writing.');
}
}
/**
* {@inheritdoc}
*/
public function addRow(Worksheet $worksheet, Row $row)
{
if (!$this->rowManager->isEmpty($row)) {
$this->addNonEmptyRow($worksheet, $row);
}
$worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
}
/**
* Adds non empty row to the worksheet.
*
* @param Worksheet $worksheet The worksheet to add the row to
* @param Row $row The row to be written
* @throws IOException If the data cannot be written
* @throws InvalidArgumentException If a cell value's type is not supported
* @return void
*/
private function addNonEmptyRow(Worksheet $worksheet, Row $row)
{
$cellIndex = 0;
$rowStyle = $row->getStyle();
$rowIndex = $worksheet->getLastWrittenRowIndex() + 1;
$numCells = $row->getNumCells();
$rowXML = '';
foreach ($row->getCells() as $cell) {
$rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndex, $cellIndex);
$cellIndex++;
}
$rowXML .= ' ';
$wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
if ($wasWriteSuccessful === false) {
throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
}
}
/**
* Applies styles to the given style, merging the cell's style with its row's style
* Then builds and returns xml for the cell.
*
* @param Cell $cell
* @param Style $rowStyle
* @param int $rowIndex
* @param int $cellIndex
* @throws InvalidArgumentException If the given value cannot be processed
* @return string
*/
private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $rowIndex, $cellIndex)
{
// Apply row and extra styles
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
return $this->getCellXML($rowIndex, $cellIndex, $cell, $registeredStyle->getId());
}
/**
* Builds and returns xml for a single cell.
*
* @param int $rowIndex
* @param int $cellNumber
* @param Cell $cell
* @param int $styleId
* @throws InvalidArgumentException If the given value cannot be processed
* @return string
*/
private function getCellXML($rowIndex, $cellNumber, Cell $cell, $styleId)
{
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
$cellXML = 'isString()) {
$cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
} elseif ($cell->isBoolean()) {
$cellXML .= ' t="b">' . (int) ($cell->getValue()) . '';
} elseif ($cell->isNumeric()) {
$cellXML .= '>' . $cell->getValue() . '';
} elseif ($cell->isEmpty()) {
if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
$cellXML .= '/>';
} else {
// don't write empty cells that do no need styling
// NOTE: not appending to $cellXML is the right behavior!!
$cellXML = '';
}
} else {
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
}
return $cellXML;
}
/**
* Returns the XML fragment for a cell containing a non empty string
*
* @param string $cellValue The cell value
* @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
* @return string The XML fragment representing the cell
*/
private function getCellXMLFragmentForNonEmptyString($cellValue)
{
if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
}
if ($this->shouldUseInlineStrings) {
$cellXMLFragment = ' t="inlineStr">' . $this->stringsEscaper->escape($cellValue) . '';
} else {
$sharedStringId = $this->sharedStringsManager->writeString($cellValue);
$cellXMLFragment = ' t="s">' . $sharedStringId . '';
}
return $cellXMLFragment;
}
/**
* {@inheritdoc}
*/
public function close(Worksheet $worksheet)
{
$worksheetFilePointer = $worksheet->getFilePointer();
if (!is_resource($worksheetFilePointer)) {
return;
}
fwrite($worksheetFilePointer, '');
fwrite($worksheetFilePointer, '');
fclose($worksheetFilePointer);
}
}
src/Spout/Writer/XLSX/Writer.php 0000666 00000003346 13476413670 0012531 0 ustar 00 throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
$this->optionsManager->setOption(Options::TEMP_FOLDER, $tempFolder);
return $this;
}
/**
* Use inline string to be more memory efficient. If set to false, it will use shared strings.
* This must be set before opening the writer.
*
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
* @return Writer
*/
public function setShouldUseInlineStrings($shouldUseInlineStrings)
{
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
$this->optionsManager->setOption(Options::SHOULD_USE_INLINE_STRINGS, $shouldUseInlineStrings);
return $this;
}
}
|