Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9cd7a397e | ||
|
|
8bb42ebc23 | ||
|
|
71a6f6a937 | ||
|
|
616925148e | ||
|
|
6f0f7c9690 | ||
|
|
b69e28050b | ||
|
|
d6e8fe4b54 | ||
|
|
e321f30c3b | ||
|
|
c31373fb1a | ||
|
|
6c57125c0c | ||
|
|
30837f869d | ||
|
|
e60054f3c4 | ||
|
|
3ee7099c95 | ||
|
|
2b1160bb33 | ||
|
|
049fd990b9 | ||
|
|
d2ac54c578 | ||
|
|
0c90d102ef | ||
|
|
e39dcb3847 | ||
|
|
10d1140a95 | ||
|
|
48debbcbca | ||
|
|
f4d6fb87ee | ||
|
|
771afcb5f1 | ||
|
|
86e26632f6 | ||
|
|
8614f79da3 | ||
|
|
4827e56cac | ||
|
|
03e85ffc21 | ||
|
|
e4cc8b4eaa | ||
|
|
73341d06a9 | ||
|
|
209372462b | ||
|
|
4a5da2ad74 | ||
|
|
c48c07db99 | ||
|
|
a804be4844 | ||
|
|
4bfbb41c95 | ||
|
|
61b71bf379 | ||
|
|
3c3294061a | ||
|
|
1fde6d836a | ||
|
|
05489cda88 | ||
|
|
128db45f22 | ||
|
|
9a85d84a2e | ||
|
|
4407cffeff | ||
|
|
8d27b3097d | ||
|
|
2c6cb1ffe5 | ||
|
|
22daea5f9a | ||
|
|
ed0e8f79cc | ||
|
|
44d72d8245 | ||
|
|
728dd3b399 | ||
|
|
05a9a1b60a | ||
|
|
a76624a721 | ||
|
|
90cbb7b5a6 | ||
|
|
f55520661e | ||
|
|
cb5dae22e4 | ||
|
|
2f6193ce20 | ||
|
|
582da8403d | ||
|
|
8b666fc6cd | ||
|
|
f8aab6eefd | ||
|
|
c4c6dddb20 | ||
|
|
6c97141679 | ||
|
|
9c8cc05364 | ||
|
|
2a9400dfca | ||
|
|
8ef6bdac62 | ||
|
|
46c246e6a4 | ||
|
|
8fd606ae4f | ||
|
|
6767386daf | ||
|
|
3395d3abb3 | ||
|
|
d1c4d563c1 | ||
|
|
01cc8b3da0 | ||
|
|
45980195cd | ||
|
|
a1a1077677 | ||
|
|
16d0290a17 | ||
|
|
f8c39287ad | ||
|
|
118810de22 | ||
|
|
3720c9ea1c |
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
# Ignore all test and documentation for archive
|
||||||
|
/tests export-ignore
|
||||||
|
/.editorconfig export-ignore
|
||||||
|
/.gitattributes export-ignore
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/.scrutinizer.yml export-ignore
|
||||||
|
/.travis.yml export-ignore
|
||||||
|
/composer.lock export-ignore
|
||||||
|
/CONTRIBUTING.md export-ignore
|
||||||
|
/logo.png export-ignore
|
||||||
|
/phpunit.xml export-ignore
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://packagist.org/packages/box/spout)
|
[](https://packagist.org/packages/box/spout)
|
||||||
[](http://opensource.box.com/badges)
|
[](http://opensource.box.com/badges)
|
||||||
[](http://travis-ci.org/box/spout)
|
[](https://travis-ci.org/box/spout)
|
||||||
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
||||||
[](https://packagist.org/packages/box/spout)
|
[](https://packagist.org/packages/box/spout)
|
||||||
[](https://packagist.org/packages/box/spout)
|
[](https://packagist.org/packages/box/spout)
|
||||||
@@ -18,16 +18,9 @@ Join the community and come discuss about Spout: [.
|
Spout can be installed directly from [Composer](https://getcomposer.org/).
|
||||||
|
|
||||||
Add "box/spout" as a dependency in your project's composer.json file:
|
Run the following command:
|
||||||
```json
|
|
||||||
"require": {
|
|
||||||
"box/spout": "~2.0"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
$ composer require box/spout
|
||||||
Then run the install command from Composer:
|
|
||||||
```
|
|
||||||
php composer.phar install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual installation
|
### Manual installation
|
||||||
@@ -62,8 +55,8 @@ Regardless of the file type, the interface to read a file is always the same:
|
|||||||
use Box\Spout\Reader\ReaderFactory;
|
use Box\Spout\Reader\ReaderFactory;
|
||||||
use Box\Spout\Common\Type;
|
use Box\Spout\Common\Type;
|
||||||
|
|
||||||
$reader = ReaderFactory::create(Type::CSV); // for CSV files
|
$reader = ReaderFactory::create(Type::XLSX); // for XLSX files
|
||||||
//$reader = ReaderFactory::create(Type::XLSX); // for XLSX files
|
//$reader = ReaderFactory::create(Type::CSV); // for CSV files
|
||||||
//$reader = ReaderFactory::create(Type::ODS); // for ODS files
|
//$reader = ReaderFactory::create(Type::ODS); // for ODS files
|
||||||
|
|
||||||
$reader->open($filePath);
|
$reader->open($filePath);
|
||||||
@@ -87,8 +80,8 @@ As with the reader, there is one common interface to write data to a file:
|
|||||||
use Box\Spout\Writer\WriterFactory;
|
use Box\Spout\Writer\WriterFactory;
|
||||||
use Box\Spout\Common\Type;
|
use Box\Spout\Common\Type;
|
||||||
|
|
||||||
$writer = WriterFactory::create(Type::CSV); // for CSV files
|
$writer = WriterFactory::create(Type::XLSX); // for XLSX files
|
||||||
//$writer = WriterFactory::create(Type::XLSX); // for XLSX files
|
//$writer = WriterFactory::create(Type::CSV); // for CSV files
|
||||||
//$writer = WriterFactory::create(Type::ODS); // for ODS files
|
//$writer = WriterFactory::create(Type::ODS); // for ODS files
|
||||||
|
|
||||||
$writer->openToFile($filePath); // write data to a file or to a PHP stream
|
$writer->openToFile($filePath); // write data to a file or to a PHP stream
|
||||||
@@ -105,6 +98,8 @@ For XLSX and ODS files, the number of rows per sheet is limited to 1,048,576. By
|
|||||||
|
|
||||||
## Advanced usage
|
## Advanced usage
|
||||||
|
|
||||||
|
If you are looking for how to perform some common, more advanced tasks with Spout, please take a look at the [Wiki](https://github.com/box/spout/wiki). It contains code snippets, ready to be used.
|
||||||
|
|
||||||
### Configuring the CSV reader and writer
|
### Configuring the CSV reader and writer
|
||||||
|
|
||||||
It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
|
It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
|
||||||
@@ -115,6 +110,7 @@ use Box\Spout\Common\Type;
|
|||||||
$reader = ReaderFactory::create(Type::CSV);
|
$reader = ReaderFactory::create(Type::CSV);
|
||||||
$reader->setFieldDelimiter('|');
|
$reader->setFieldDelimiter('|');
|
||||||
$reader->setFieldEnclosure('@');
|
$reader->setFieldEnclosure('@');
|
||||||
|
$reader->setEndOfLineCharacter("\r");
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
|
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
|
||||||
@@ -164,7 +160,7 @@ Font | Bold | `StyleBuilder::setFontBold()`
|
|||||||
| Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
| Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
||||||
| Font name | `StyleBuilder::setFontName('Arial')`
|
| Font name | `StyleBuilder::setFontName('Arial')`
|
||||||
| Font size | `StyleBuilder::setFontSize(14)`
|
| Font size | `StyleBuilder::setFontSize(14)`
|
||||||
| Font color | `StyleBuilder::setFontSize(Color::BLUE)`<br>`StyleBuilder::setFontSize(Color::rgb(0, 128, 255))`
|
| Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
|
||||||
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
|
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
|
||||||
|
|
||||||
|
|
||||||
@@ -282,10 +278,12 @@ If you just want to check that everything is working as expected, executing the
|
|||||||
If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output:
|
If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output:
|
||||||
|
|
||||||
* `phpunit` - runs the whole test suite (unit + functional + performance tests)
|
* `phpunit` - runs the whole test suite (unit + functional + performance tests)
|
||||||
* `phpunit --testuite no-perf-tests` - only runs the unit and functional tests
|
* `phpunit --exclude-group perf-tests` - only runs the unit and functional tests
|
||||||
* `phpunit --testuite perf-tests` - only runs the performance tests
|
* `phpunit --group perf-tests` - only runs the performance tests
|
||||||
|
|
||||||
For information, the performance tests take about one hour to run (processing 2 million rows files is not a quick thing).
|
For information, the performance tests take about 30 minutes to run (processing 1 million rows files is not a quick thing).
|
||||||
|
|
||||||
|
> Performance tests status: [](https://travis-ci.org/box/spout)
|
||||||
|
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|||||||
@@ -29,5 +29,11 @@
|
|||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Box\\Spout\\": "src/Spout"
|
"Box\\Spout\\": "src/Spout"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.5.x-dev"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Box\Spout\Common\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class BadUsageException
|
|
||||||
*
|
|
||||||
* @package Box\Spout\Common\Exception
|
|
||||||
*/
|
|
||||||
class BadUsageException extends SpoutException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class EncodingConversionException
|
* Class EncodingConversionException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
*/
|
*/
|
||||||
class EncodingConversionException extends SpoutException
|
class EncodingConversionException extends SpoutException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class IOException
|
* Class IOException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
*/
|
*/
|
||||||
class IOException extends SpoutException
|
class IOException extends SpoutException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class InvalidArgumentException
|
* Class InvalidArgumentException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
*/
|
*/
|
||||||
class InvalidArgumentException extends SpoutException
|
class InvalidArgumentException extends SpoutException
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
* Class SpoutException
|
* Class SpoutException
|
||||||
*
|
*
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
|
* @abstract
|
||||||
*/
|
*/
|
||||||
abstract class SpoutException extends \Exception
|
abstract class SpoutException extends \Exception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Common\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class UnsupportedTypeException
|
* Class UnsupportedTypeException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Common\Exception
|
* @package Box\Spout\Common\Exception
|
||||||
*/
|
*/
|
||||||
class UnsupportedTypeException extends SpoutException
|
class UnsupportedTypeException extends SpoutException
|
||||||
|
|||||||
@@ -164,7 +164,43 @@ class GlobalFunctionsHelper
|
|||||||
*/
|
*/
|
||||||
public function file_get_contents($filePath)
|
public function file_get_contents($filePath)
|
||||||
{
|
{
|
||||||
return file_get_contents($filePath);
|
$realFilePath = $this->convertToUseRealPath($filePath);
|
||||||
|
return file_get_contents($realFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the given file path to use a real path.
|
||||||
|
* This is to avoid issues on some Windows setup.
|
||||||
|
*
|
||||||
|
* @param string $filePath File path
|
||||||
|
* @return string The file path using a real path
|
||||||
|
*/
|
||||||
|
protected function convertToUseRealPath($filePath)
|
||||||
|
{
|
||||||
|
$realFilePath = $filePath;
|
||||||
|
|
||||||
|
if ($this->isZipStream($filePath)) {
|
||||||
|
if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
|
||||||
|
$documentPath = $matches[1];
|
||||||
|
$documentInsideZipPath = $matches[2];
|
||||||
|
$realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$realFilePath = realpath($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $realFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given path is a zip stream.
|
||||||
|
*
|
||||||
|
* @param string $path Path pointing to a document
|
||||||
|
* @return bool TRUE if path is a zip stream, FALSE otherwise
|
||||||
|
*/
|
||||||
|
protected function isZipStream($path)
|
||||||
|
{
|
||||||
|
return (strpos($path, 'zip://') === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -245,32 +281,14 @@ class GlobalFunctionsHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around global function stream_get_line()
|
* Wrapper around global function stream_get_wrappers()
|
||||||
* @see stream_get_line()
|
* @see stream_get_wrappers()
|
||||||
*
|
*
|
||||||
* @param resource $handle
|
|
||||||
* @param int $length
|
|
||||||
* @param string|void $ending
|
|
||||||
* @return string|bool
|
|
||||||
*/
|
|
||||||
public function stream_get_line($handle, $length, $ending = null)
|
|
||||||
{
|
|
||||||
return stream_get_line($handle, $length, $ending);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around global function str_getcsv()
|
|
||||||
* @see str_getcsv()
|
|
||||||
*
|
|
||||||
* @param string $input
|
|
||||||
* @param string|void $delimiter
|
|
||||||
* @param string|void $enclosure
|
|
||||||
* @param string|void $escape
|
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function str_getcsv($input, $delimiter = null, $enclosure = null, $escape = null)
|
public function stream_get_wrappers()
|
||||||
{
|
{
|
||||||
return str_getcsv($input, $delimiter, $enclosure, $escape);
|
return stream_get_wrappers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ namespace Box\Spout\Common;
|
|||||||
/**
|
/**
|
||||||
* Class Type
|
* Class Type
|
||||||
* This class references the supported types
|
* This class references the supported types
|
||||||
|
*
|
||||||
|
* @api
|
||||||
*/
|
*/
|
||||||
abstract class Type
|
abstract class Type
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||||
protected $globalFunctionsHelper;
|
protected $globalFunctionsHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether stream wrappers are supported
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
abstract protected function doesSupportStreamWrapper();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the file at the given file path to make it ready to be read
|
* Opens the file at the given file path to make it ready to be read
|
||||||
*
|
*
|
||||||
@@ -55,12 +62,17 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
* Prepares the reader to read the given file. It also makes sure
|
* Prepares the reader to read the given file. It also makes sure
|
||||||
* that the file exists and is readable.
|
* that the file exists and is readable.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $filePath Path of the file to be read
|
* @param string $filePath Path of the file to be read
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
||||||
*/
|
*/
|
||||||
public function open($filePath)
|
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)) {
|
if (!$this->isPhpStream($filePath)) {
|
||||||
// we skip the checks if the provided file path points to a PHP stream
|
// we skip the checks if the provided file path points to a PHP stream
|
||||||
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
||||||
@@ -71,13 +83,75 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->openReader($filePath);
|
$fileRealPath = $this->getFileRealPath($filePath);
|
||||||
|
$this->openReader($fileRealPath);
|
||||||
$this->isStreamOpened = true;
|
$this->isStreamOpened = true;
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
|
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, ...)
|
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
|
||||||
*
|
*
|
||||||
@@ -86,12 +160,14 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
*/
|
*/
|
||||||
protected function isPhpStream($filePath)
|
protected function isPhpStream($filePath)
|
||||||
{
|
{
|
||||||
return (strpos($filePath, 'php://') === 0);
|
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||||
|
return ($streamScheme === 'php');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an iterator to iterate over sheets.
|
* Returns an iterator to iterate over sheets.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return \Iterator To iterate over sheets
|
* @return \Iterator To iterate over sheets
|
||||||
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
|
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
|
||||||
*/
|
*/
|
||||||
@@ -107,6 +183,7 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
/**
|
/**
|
||||||
* Closes the reader, preventing any additional reading
|
* Closes the reader, preventing any additional reading
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ class Reader extends AbstractReader
|
|||||||
/** @var string Encoding of the CSV file to be read */
|
/** @var string Encoding of the CSV file to be read */
|
||||||
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
||||||
|
|
||||||
|
/** @var string Defines the End of line */
|
||||||
|
protected $endOfLineCharacter = "\n";
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $autoDetectLineEndings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the field delimiter for the CSV.
|
* Sets the field delimiter for the CSV.
|
||||||
* Needs to be called before opening the reader.
|
* Needs to be called before opening the reader.
|
||||||
@@ -68,6 +74,29 @@ class Reader extends AbstractReader
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the EOL for the CSV.
|
||||||
|
* Needs to be called before opening the reader.
|
||||||
|
*
|
||||||
|
* @param string $endOfLineCharacter used to properly get lines from the CSV file.
|
||||||
|
* @return Reader
|
||||||
|
*/
|
||||||
|
public function setEndOfLineCharacter($endOfLineCharacter)
|
||||||
|
{
|
||||||
|
$this->endOfLineCharacter = $endOfLineCharacter;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether stream wrappers are supported
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function doesSupportStreamWrapper()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the file at the given path to make it ready to be read.
|
* 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.
|
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
|
||||||
@@ -78,6 +107,9 @@ class Reader extends AbstractReader
|
|||||||
*/
|
*/
|
||||||
protected function openReader($filePath)
|
protected function openReader($filePath)
|
||||||
{
|
{
|
||||||
|
$this->autoDetectLineEndings = ini_get('auto_detect_line_endings');
|
||||||
|
ini_set('auto_detect_line_endings', '1');
|
||||||
|
|
||||||
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
|
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
|
||||||
if (!$this->filePointer) {
|
if (!$this->filePointer) {
|
||||||
throw new IOException("Could not open file $filePath for reading.");
|
throw new IOException("Could not open file $filePath for reading.");
|
||||||
@@ -88,6 +120,7 @@ class Reader extends AbstractReader
|
|||||||
$this->fieldDelimiter,
|
$this->fieldDelimiter,
|
||||||
$this->fieldEnclosure,
|
$this->fieldEnclosure,
|
||||||
$this->encoding,
|
$this->encoding,
|
||||||
|
$this->endOfLineCharacter,
|
||||||
$this->globalFunctionsHelper
|
$this->globalFunctionsHelper
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,5 +146,7 @@ class Reader extends AbstractReader
|
|||||||
if ($this->filePointer) {
|
if ($this->filePointer) {
|
||||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ini_set('auto_detect_line_endings', $this->autoDetectLineEndings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,12 @@ use Box\Spout\Common\Helper\EncodingHelper;
|
|||||||
*/
|
*/
|
||||||
class RowIterator implements IteratorInterface
|
class RowIterator implements IteratorInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* If no value is given to fgetcsv(), it defaults to 8192 (which may be too low).
|
||||||
|
* Alignement with other functions like fgets() is discussed here: https://bugs.php.net/bug.php?id=48421
|
||||||
|
*/
|
||||||
|
const MAX_READ_BYTES_PER_LINE = 32768;
|
||||||
|
|
||||||
/** @var resource Pointer to the CSV file to read */
|
/** @var resource Pointer to the CSV file to read */
|
||||||
protected $filePointer;
|
protected $filePointer;
|
||||||
|
|
||||||
@@ -43,6 +49,9 @@ class RowIterator implements IteratorInterface
|
|||||||
/** @var string End of line delimiter, encoded using the same encoding as the CSV */
|
/** @var string End of line delimiter, encoded using the same encoding as the CSV */
|
||||||
protected $encodedEOLDelimiter;
|
protected $encodedEOLDelimiter;
|
||||||
|
|
||||||
|
/** @var string End of line delimiter, given by the user as input. */
|
||||||
|
protected $inputEOLDelimiter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param resource $filePointer Pointer to the CSV file to read
|
* @param resource $filePointer Pointer to the CSV file to read
|
||||||
* @param string $fieldDelimiter Character that delimits fields
|
* @param string $fieldDelimiter Character that delimits fields
|
||||||
@@ -50,12 +59,13 @@ class RowIterator implements IteratorInterface
|
|||||||
* @param string $encoding Encoding of the CSV file to be read
|
* @param string $encoding Encoding of the CSV file to be read
|
||||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
*/
|
*/
|
||||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineDelimiter, $globalFunctionsHelper)
|
||||||
{
|
{
|
||||||
$this->filePointer = $filePointer;
|
$this->filePointer = $filePointer;
|
||||||
$this->fieldDelimiter = $fieldDelimiter;
|
$this->fieldDelimiter = $fieldDelimiter;
|
||||||
$this->fieldEnclosure = $fieldEnclosure;
|
$this->fieldEnclosure = $fieldEnclosure;
|
||||||
$this->encoding = $encoding;
|
$this->encoding = $encoding;
|
||||||
|
$this->inputEOLDelimiter = $endOfLineDelimiter;
|
||||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||||
|
|
||||||
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
|
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
|
||||||
@@ -111,48 +121,61 @@ class RowIterator implements IteratorInterface
|
|||||||
*/
|
*/
|
||||||
public function next()
|
public function next()
|
||||||
{
|
{
|
||||||
$lineData = false;
|
|
||||||
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||||
|
|
||||||
if (!$this->hasReachedEndOfFile) {
|
if ($this->hasReachedEndOfFile) {
|
||||||
do {
|
return;
|
||||||
$utf8EncodedLineData = $this->getNextUTF8EncodedLine();
|
}
|
||||||
if ($utf8EncodedLineData !== false) {
|
|
||||||
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
|
|
||||||
}
|
|
||||||
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
|
||||||
} while (($lineData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($lineData));
|
|
||||||
|
|
||||||
if ($lineData !== false) {
|
do {
|
||||||
$this->rowDataBuffer = $lineData;
|
$rowData = $this->getNextUTF8EncodedRow();
|
||||||
$this->numReadRows++;
|
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||||
} else {
|
} while (($rowData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($rowData));
|
||||||
// If we reach this point, it means end of file was reached.
|
|
||||||
// This happens when the last lines are empty lines.
|
if ($rowData !== false) {
|
||||||
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile;
|
$this->rowDataBuffer = $rowData;
|
||||||
}
|
$this->numReadRows++;
|
||||||
|
} else {
|
||||||
|
// If we reach this point, it means end of file was reached.
|
||||||
|
// This happens when the last lines are empty lines.
|
||||||
|
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the next line, converted if necessary to UTF-8.
|
* Returns the next row, converted if necessary to UTF-8.
|
||||||
* Neither fgets nor fgetcsv don't work with non UTF-8 data... so we need to do some things manually.
|
* As fgetcsv() does not manage correctly encoding for non UTF-8 data,
|
||||||
|
* we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes)
|
||||||
*
|
*
|
||||||
* @return string|false The next line for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
|
* @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
|
||||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||||
*/
|
*/
|
||||||
protected function getNextUTF8EncodedLine()
|
protected function getNextUTF8EncodedRow()
|
||||||
{
|
{
|
||||||
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
|
$encodedRowData = fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||||
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
|
if (false === $encodedRowData) {
|
||||||
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, 0, $encodedEOLDelimiter);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If the line could have been read, it can be converted to UTF-8
|
foreach ($encodedRowData as $cellIndex => $cellValue) {
|
||||||
$utf8EncodedLineData = ($encodedLineData !== false) ?
|
switch($this->encoding) {
|
||||||
$this->encodingHelper->attemptConversionToUTF8($encodedLineData, $this->encoding) :
|
case EncodingHelper::ENCODING_UTF16_LE:
|
||||||
false;
|
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;
|
||||||
|
|
||||||
return $utf8EncodedLineData;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +187,7 @@ class RowIterator implements IteratorInterface
|
|||||||
protected function getEncodedEOLDelimiter()
|
protected function getEncodedEOLDelimiter()
|
||||||
{
|
{
|
||||||
if (!isset($this->encodedEOLDelimiter)) {
|
if (!isset($this->encodedEOLDelimiter)) {
|
||||||
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8("\n", $this->encoding);
|
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->encodedEOLDelimiter;
|
return $this->encodedEOLDelimiter;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use Box\Spout\Reader\SheetInterface;
|
|||||||
*/
|
*/
|
||||||
class Sheet implements SheetInterface
|
class Sheet implements SheetInterface
|
||||||
{
|
{
|
||||||
/** @var RowIterator To iterate over the CSV's rows */
|
/** @var \Box\Spout\Reader\CSV\RowIterator To iterate over the CSV's rows */
|
||||||
protected $rowIterator;
|
protected $rowIterator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,13 +21,14 @@ class Sheet implements SheetInterface
|
|||||||
* @param string $encoding Encoding of the CSV file to be read
|
* @param string $encoding Encoding of the CSV file to be read
|
||||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
*/
|
*/
|
||||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
|
||||||
{
|
{
|
||||||
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
|
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return RowIterator
|
* @api
|
||||||
|
* @return \Box\Spout\Reader\CSV\RowIterator
|
||||||
*/
|
*/
|
||||||
public function getRowIterator()
|
public function getRowIterator()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use Box\Spout\Reader\IteratorInterface;
|
|||||||
*/
|
*/
|
||||||
class SheetIterator implements IteratorInterface
|
class SheetIterator implements IteratorInterface
|
||||||
{
|
{
|
||||||
/** @var Sheet The CSV unique "sheet" */
|
/** @var \Box\Spout\Reader\CSV\Sheet The CSV unique "sheet" */
|
||||||
protected $sheet;
|
protected $sheet;
|
||||||
|
|
||||||
/** @var bool Whether the unique "sheet" has already been read */
|
/** @var bool Whether the unique "sheet" has already been read */
|
||||||
@@ -25,9 +25,9 @@ class SheetIterator implements IteratorInterface
|
|||||||
* @param string $encoding Encoding of the CSV file to be read
|
* @param string $encoding Encoding of the CSV file to be read
|
||||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
*/
|
*/
|
||||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
|
||||||
{
|
{
|
||||||
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
|
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +67,7 @@ class SheetIterator implements IteratorInterface
|
|||||||
* Return the current element
|
* Return the current element
|
||||||
* @link http://php.net/manual/en/iterator.current.php
|
* @link http://php.net/manual/en/iterator.current.php
|
||||||
*
|
*
|
||||||
* @return Sheet
|
* @return \Box\Spout\Reader\CSV\Sheet
|
||||||
*/
|
*/
|
||||||
public function current()
|
public function current()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Reader\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class IteratorNotRewindableException
|
* Class IteratorNotRewindableException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class IteratorNotRewindableException extends ReaderException
|
class IteratorNotRewindableException extends ReaderException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Reader\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class NoSheetsFoundException
|
* Class NoSheetsFoundException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class NoSheetsFoundException extends ReaderException
|
class NoSheetsFoundException extends ReaderException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Reader\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class ReaderNotOpenedException
|
* Class ReaderNotOpenedException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class ReaderNotOpenedException extends ReaderException
|
class ReaderNotOpenedException extends ReaderException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Reader\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class SharedStringNotFoundException
|
* Class SharedStringNotFoundException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Reader\Exception
|
* @package Box\Spout\Reader\Exception
|
||||||
*/
|
*/
|
||||||
class SharedStringNotFoundException extends ReaderException
|
class SharedStringNotFoundException extends ReaderException
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class CellValueFormatter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
|
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
|
||||||
* @TODO Add other types !!
|
|
||||||
* @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
|
* @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
|
||||||
*
|
*
|
||||||
* @param \DOMNode $node
|
* @param \DOMNode $node
|
||||||
@@ -119,7 +118,8 @@ class CellValueFormatter
|
|||||||
protected function formatFloatCellValue($node)
|
protected function formatFloatCellValue($node)
|
||||||
{
|
{
|
||||||
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
|
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
|
||||||
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
|
$nodeIntValue = intval($nodeValue);
|
||||||
|
$cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
|
||||||
return $cellValue;
|
return $cellValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ class Reader extends AbstractReader
|
|||||||
/** @var SheetIterator To iterator over the ODS sheets */
|
/** @var SheetIterator To iterator over the ODS sheets */
|
||||||
protected $sheetIterator;
|
protected $sheetIterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Opens the file at the given file path to make it ready to be read.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class RowIterator implements IteratorInterface
|
|||||||
const XML_NODE_TABLE = 'table:table';
|
const XML_NODE_TABLE = 'table:table';
|
||||||
const XML_NODE_ROW = 'table:table-row';
|
const XML_NODE_ROW = 'table:table-row';
|
||||||
const XML_NODE_CELL = 'table:table-cell';
|
const XML_NODE_CELL = 'table:table-cell';
|
||||||
|
const MAX_COLUMNS_EXCEL = 16384;
|
||||||
|
|
||||||
/** Definition of XML attribute used to parse data */
|
/** Definition of XML attribute used to parse data */
|
||||||
const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
|
const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
|
||||||
@@ -127,22 +128,23 @@ class RowIterator implements IteratorInterface
|
|||||||
|
|
||||||
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
|
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
|
||||||
// End of the row description
|
// End of the row description
|
||||||
$isEmptyRow = ($numCellsRead <= 1 && empty($cellValue));
|
$isEmptyRow = ($numCellsRead <= 1 && $this->isEmptyCellValue($cellValue));
|
||||||
if ($isEmptyRow) {
|
if ($isEmptyRow) {
|
||||||
// skip empty rows
|
// skip empty rows
|
||||||
$this->next();
|
$this->next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add value if the last read cell is not empty or does not need to repeat cell values.
|
// 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 $rowData.
|
||||||
// This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
|
// This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
|
||||||
// with a number-columns-repeated value equals to the number of (supported columns - used columns).
|
// 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.
|
// In Excel, the number of supported columns is 16384, but we don't want to returns rows with
|
||||||
if (!empty($cellValue) || $numColumnsRepeated === 1) {
|
// always 16384 cells.
|
||||||
|
if ((count($rowData) + $numColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
|
||||||
for ($i = 0; $i < $numColumnsRepeated; $i++) {
|
for ($i = 0; $i < $numColumnsRepeated; $i++) {
|
||||||
$rowData[] = $cellValue;
|
$rowData[] = $cellValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->numReadRows++;
|
$this->numReadRows++;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -181,6 +183,17 @@ class RowIterator implements IteratorInterface
|
|||||||
return $this->cellValueFormatter->extractAndFormatNodeValue($node);
|
return $this->cellValueFormatter->extractAndFormatNodeValue($node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* empty() replacement that honours 0 as a valid value
|
||||||
|
*
|
||||||
|
* @param $value The cell value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isEmptyCellValue($value)
|
||||||
|
{
|
||||||
|
return (!isset($value) || trim($value) === '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current element, from the buffer.
|
* Return the current element, from the buffer.
|
||||||
* @link http://php.net/manual/en/iterator.current.php
|
* @link http://php.net/manual/en/iterator.current.php
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ use Box\Spout\Reader\Wrapper\XMLReader;
|
|||||||
*/
|
*/
|
||||||
class Sheet implements SheetInterface
|
class Sheet implements SheetInterface
|
||||||
{
|
{
|
||||||
/** @var RowIterator To iterate over sheet's rows */
|
/** @var \Box\Spout\Reader\ODS\RowIterator To iterate over sheet's rows */
|
||||||
protected $rowIterator;
|
protected $rowIterator;
|
||||||
|
|
||||||
/** @var int ID of the sheet */
|
/** @var int ID of the sheet */
|
||||||
protected $id;
|
protected $id;
|
||||||
|
|
||||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||||
protected $index;
|
protected $index;
|
||||||
|
|
||||||
/** @var string Name of the sheet */
|
/** @var string Name of the sheet */
|
||||||
@@ -27,7 +27,7 @@ class Sheet implements SheetInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||||
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||||
* @param string $sheetName Name of the sheet
|
* @param string $sheetName Name of the sheet
|
||||||
*/
|
*/
|
||||||
public function __construct($xmlReader, $sheetIndex, $sheetName)
|
public function __construct($xmlReader, $sheetIndex, $sheetName)
|
||||||
@@ -38,7 +38,8 @@ class Sheet implements SheetInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return RowIterator
|
* @api
|
||||||
|
* @return \Box\Spout\Reader\ODS\RowIterator
|
||||||
*/
|
*/
|
||||||
public function getRowIterator()
|
public function getRowIterator()
|
||||||
{
|
{
|
||||||
@@ -46,7 +47,8 @@ class Sheet implements SheetInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
* @api
|
||||||
|
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||||
*/
|
*/
|
||||||
public function getIndex()
|
public function getIndex()
|
||||||
{
|
{
|
||||||
@@ -54,6 +56,7 @@ class Sheet implements SheetInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @api
|
||||||
* @return string Name of the sheet
|
* @return string Name of the sheet
|
||||||
*/
|
*/
|
||||||
public function getName()
|
public function getName()
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class SheetIterator implements IteratorInterface
|
|||||||
* Return the current element
|
* Return the current element
|
||||||
* @link http://php.net/manual/en/iterator.current.php
|
* @link http://php.net/manual/en/iterator.current.php
|
||||||
*
|
*
|
||||||
* @return Sheet
|
* @return \Box\Spout\Reader\ODS\Sheet
|
||||||
*/
|
*/
|
||||||
public function current()
|
public function current()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class ReaderFactory
|
|||||||
/**
|
/**
|
||||||
* This creates an instance of the appropriate reader, given the type of the file to be read
|
* This creates an instance of the appropriate reader, given the type of the file to be read
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $readerType Type of the reader to instantiate
|
* @param string $readerType Type of the reader to instantiate
|
||||||
* @return ReaderInterface
|
* @return ReaderInterface
|
||||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||||
|
|||||||
@@ -151,6 +151,22 @@ class SimpleXMLElement
|
|||||||
return $doesElementExist ? $this->wrapSimpleXMLElement($realElement) : null;
|
return $doesElementExist ? $this->wrapSimpleXMLElement($realElement) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the immediate children.
|
||||||
|
*
|
||||||
|
* @return array The children
|
||||||
|
*/
|
||||||
|
public function children()
|
||||||
|
{
|
||||||
|
$children = [];
|
||||||
|
|
||||||
|
foreach ($this->simpleXMLElement->children() as $child) {
|
||||||
|
$children[] = $this->wrapSimpleXMLElement($child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $children;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,20 +26,45 @@ class XMLReader extends \XMLReader
|
|||||||
public function open($URI, $encoding = null, $options = 0)
|
public function open($URI, $encoding = null, $options = 0)
|
||||||
{
|
{
|
||||||
$wasOpenSuccessful = false;
|
$wasOpenSuccessful = false;
|
||||||
|
$realPathURI = $this->convertURIToUseRealPath($URI);
|
||||||
|
|
||||||
// HHVM does not check if file exists within zip file
|
// HHVM does not check if file exists within zip file
|
||||||
// @link https://github.com/facebook/hhvm/issues/5779
|
// @link https://github.com/facebook/hhvm/issues/5779
|
||||||
if ($this->isRunningHHVM() && $this->isZipStream($URI)) {
|
if ($this->isRunningHHVM() && $this->isZipStream($realPathURI)) {
|
||||||
if ($this->fileExistsWithinZip($URI)) {
|
if ($this->fileExistsWithinZip($realPathURI)) {
|
||||||
$wasOpenSuccessful = parent::open($URI, $encoding, $options|LIBXML_NONET);
|
$wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$wasOpenSuccessful = parent::open($URI, $encoding, $options|LIBXML_NONET);
|
$wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $wasOpenSuccessful;
|
return $wasOpenSuccessful;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the given URI to use a real path.
|
||||||
|
* This is to avoid issues on some Windows setup.
|
||||||
|
*
|
||||||
|
* @param string $URI URI
|
||||||
|
* @return string The URI using a real path
|
||||||
|
*/
|
||||||
|
protected function convertURIToUseRealPath($URI)
|
||||||
|
{
|
||||||
|
$realPathURI = $URI;
|
||||||
|
|
||||||
|
if ($this->isZipStream($URI)) {
|
||||||
|
if (preg_match('/zip:\/\/(.*)#(.*)/', $URI, $matches)) {
|
||||||
|
$documentPath = $matches[1];
|
||||||
|
$documentInsideZipPath = $matches[2];
|
||||||
|
$realPathURI = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$realPathURI = realpath($URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $realPathURI;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the given URI is a zip stream.
|
* Returns whether the given URI is a zip stream.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ use Box\Spout\Common\Exception\InvalidArgumentException;
|
|||||||
*/
|
*/
|
||||||
class CellHelper
|
class CellHelper
|
||||||
{
|
{
|
||||||
|
// Using ord() is super slow... Using a pre-computed hash table instead.
|
||||||
|
private static $columnLetterToIndexMapping = [
|
||||||
|
'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6,
|
||||||
|
'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13,
|
||||||
|
'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20,
|
||||||
|
'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills the missing indexes of an array with a given value.
|
* Fills the missing indexes of an array with a given value.
|
||||||
* For instance, $dataArray = []; $a[1] = 1; $a[3] = 3;
|
* For instance, $dataArray = []; $a[1] = 1; $a[3] = 3;
|
||||||
@@ -50,34 +58,31 @@ class CellHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
$columnIndex = 0;
|
$columnIndex = 0;
|
||||||
$capitalAAsciiValue = ord('A');
|
|
||||||
$capitalZAsciiValue = ord('Z');
|
|
||||||
$step = $capitalZAsciiValue - $capitalAAsciiValue + 1;
|
|
||||||
|
|
||||||
// Remove row information
|
// Remove row information
|
||||||
$column = preg_replace('/\d/', '', $cellIndex);
|
$columnLetters = preg_replace('/\d/', '', $cellIndex);
|
||||||
$columnLength = strlen($column);
|
|
||||||
|
|
||||||
/*
|
// strlen() is super slow too... Using isset() is way faster and not too unreadable,
|
||||||
* This is how the following loop will process the data:
|
// since we checked before that there are between 1 and 3 letters.
|
||||||
* A => 0
|
$columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1;
|
||||||
* Z => 25
|
|
||||||
* AA => 26 : (26^(2-1) * (0+1)) + 0
|
|
||||||
* AB => 27 : (26^(2-1) * (0+1)) + 1
|
|
||||||
* BC => 54 : (26^(2-1) * (1+1)) + 2
|
|
||||||
* BCZ => 1455 : (26^(3-1) * (1+1)) + (26^(2-1) * (2+1)) + 25
|
|
||||||
*/
|
|
||||||
foreach (str_split($column) as $single_cell_index)
|
|
||||||
{
|
|
||||||
$currentColumnIndex = ord($single_cell_index) - $capitalAAsciiValue;
|
|
||||||
|
|
||||||
if ($columnLength === 1) {
|
// Looping over the different letters of the column is slower than this method.
|
||||||
$columnIndex += $currentColumnIndex;
|
// Also, not using the pow() function because it's slooooow...
|
||||||
} else {
|
switch ($columnLength) {
|
||||||
$columnIndex += pow($step, ($columnLength - 1)) * ($currentColumnIndex + 1);
|
case 1:
|
||||||
}
|
$columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]);
|
||||||
|
break;
|
||||||
$columnLength--;
|
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;
|
return $columnIndex;
|
||||||
@@ -86,12 +91,13 @@ class CellHelper
|
|||||||
/**
|
/**
|
||||||
* Returns whether a cell index is valid, in an Excel world.
|
* 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.
|
* 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', ...)
|
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
protected static function isValidCellIndex($cellIndex)
|
protected static function isValidCellIndex($cellIndex)
|
||||||
{
|
{
|
||||||
return (preg_match('/^[A-Z]+\d+$/', $cellIndex) === 1);
|
return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,19 +25,34 @@ class CellValueFormatter
|
|||||||
|
|
||||||
/** Definition of XML attributes used to parse data */
|
/** Definition of XML attributes used to parse data */
|
||||||
const XML_ATTRIBUTE_TYPE = 't';
|
const XML_ATTRIBUTE_TYPE = 't';
|
||||||
|
const XML_ATTRIBUTE_STYLE_ID = 's';
|
||||||
|
|
||||||
|
/** Constants used for date formatting */
|
||||||
|
const NUM_SECONDS_IN_ONE_DAY = 86400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* February 29th, 1900 is NOT a leap year but Excel thinks it is...
|
||||||
|
* @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
|
||||||
|
*/
|
||||||
|
const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
|
||||||
|
|
||||||
/** @var SharedStringsHelper Helper to work with shared strings */
|
/** @var SharedStringsHelper Helper to work with shared strings */
|
||||||
protected $sharedStringsHelper;
|
protected $sharedStringsHelper;
|
||||||
|
|
||||||
|
/** @var StyleHelper Helper to work with styles */
|
||||||
|
protected $styleHelper;
|
||||||
|
|
||||||
/** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
|
/** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
|
||||||
protected $escaper;
|
protected $escaper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
|
* @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
|
||||||
|
* @param StyleHelper $styleHelper Helper to work with styles
|
||||||
*/
|
*/
|
||||||
public function __construct($sharedStringsHelper)
|
public function __construct($sharedStringsHelper, $styleHelper)
|
||||||
{
|
{
|
||||||
$this->sharedStringsHelper = $sharedStringsHelper;
|
$this->sharedStringsHelper = $sharedStringsHelper;
|
||||||
|
$this->styleHelper = $styleHelper;
|
||||||
|
|
||||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
$this->escaper = new \Box\Spout\Common\Escaper\XLSX();
|
$this->escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
@@ -53,6 +68,7 @@ class CellValueFormatter
|
|||||||
{
|
{
|
||||||
// Default cell type is "n"
|
// Default cell type is "n"
|
||||||
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
|
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
|
||||||
|
$cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID));
|
||||||
$vNodeValue = $this->getVNodeValue($node);
|
$vNodeValue = $this->getVNodeValue($node);
|
||||||
|
|
||||||
if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
|
if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
|
||||||
@@ -69,7 +85,7 @@ class CellValueFormatter
|
|||||||
case self::CELL_TYPE_BOOLEAN:
|
case self::CELL_TYPE_BOOLEAN:
|
||||||
return $this->formatBooleanCellValue($vNodeValue);
|
return $this->formatBooleanCellValue($vNodeValue);
|
||||||
case self::CELL_TYPE_NUMERIC:
|
case self::CELL_TYPE_NUMERIC:
|
||||||
return $this->formatNumericCellValue($vNodeValue);
|
return $this->formatNumericCellValue($vNodeValue, $cellStyleId);
|
||||||
case self::CELL_TYPE_DATE:
|
case self::CELL_TYPE_DATE:
|
||||||
return $this->formatDateCellValue($vNodeValue);
|
return $this->formatDateCellValue($vNodeValue);
|
||||||
default:
|
default:
|
||||||
@@ -102,8 +118,7 @@ class CellValueFormatter
|
|||||||
// inline strings are formatted this way:
|
// inline strings are formatted this way:
|
||||||
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
|
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
|
||||||
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
|
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
|
||||||
$escapedCellValue = trim($tNode->nodeValue);
|
$cellValue = $this->escaper->unescape($tNode->nodeValue);
|
||||||
$cellValue = $this->escaper->unescape($escapedCellValue);
|
|
||||||
return $cellValue;
|
return $cellValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,14 +153,59 @@ class CellValueFormatter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cell Numeric value from string of nodeValue.
|
* 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 string $nodeValue
|
||||||
* @return int|float The value associated with the cell
|
* @param int $cellStyleId 0 being the default style
|
||||||
|
* @return int|float|\DateTime|null The value associated with the cell
|
||||||
*/
|
*/
|
||||||
protected function formatNumericCellValue($nodeValue)
|
protected function formatNumericCellValue($nodeValue, $cellStyleId)
|
||||||
{
|
{
|
||||||
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
|
// Numeric values can represent numbers as well as timestamps.
|
||||||
return $cellValue;
|
// We need to look at the style of the cell to determine whether it is one or the other.
|
||||||
|
$shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId);
|
||||||
|
|
||||||
|
if ($shouldFormatAsDate) {
|
||||||
|
return $this->formatExcelTimestampValue(floatval($nodeValue));
|
||||||
|
} else {
|
||||||
|
$nodeIntValue = intval($nodeValue);
|
||||||
|
return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cell's PHP Date value, associated to the given timestamp.
|
||||||
|
* NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
|
||||||
|
*
|
||||||
|
* @param float $nodeValue
|
||||||
|
* @return \DateTime|null The value associated with the cell or NULL if invalid date value
|
||||||
|
*/
|
||||||
|
protected function formatExcelTimestampValue($nodeValue)
|
||||||
|
{
|
||||||
|
// Fix for the erroneous leap year in Excel
|
||||||
|
if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
|
||||||
|
--$nodeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The value 1.0 represents 1900-01-01. Numbers below 1.0 are not valid Excel dates.
|
||||||
|
if ($nodeValue < 1.0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not use any unix timestamps for calculation to prevent
|
||||||
|
// issues with numbers exceeding 2^31.
|
||||||
|
$secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY;
|
||||||
|
$secondsRemainder = round($secondsRemainder, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cellValue = \DateTime::createFromFormat('|Y-m-d', '1899-12-31');
|
||||||
|
$cellValue->modify('+' . intval($nodeValue) . 'days');
|
||||||
|
$cellValue->modify('+' . $secondsRemainder . 'seconds');
|
||||||
|
|
||||||
|
return $cellValue;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -143,7 +143,9 @@ class FileBasedStrategy implements CachingStrategyInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sharedString = null;
|
$sharedString = null;
|
||||||
if (array_key_exists($indexInFile, $this->inMemoryTempFileContents)) {
|
|
||||||
|
// Using isset here because it is way faster than array_key_exists...
|
||||||
|
if (isset($this->inMemoryTempFileContents[$indexInFile])) {
|
||||||
$escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
|
$escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
|
||||||
$sharedString = $this->unescapeLineFeed($escapedSharedString);
|
$sharedString = $this->unescapeLineFeed($escapedSharedString);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ use Box\Spout\Reader\XLSX\Sheet;
|
|||||||
*/
|
*/
|
||||||
class SheetHelper
|
class SheetHelper
|
||||||
{
|
{
|
||||||
/** Extension for XML files */
|
|
||||||
const XML_EXTENSION = '.xml';
|
|
||||||
|
|
||||||
/** Paths of XML files relative to the XLSX file root */
|
/** Paths of XML files relative to the XLSX file root */
|
||||||
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
||||||
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
|
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
|
||||||
@@ -79,9 +76,15 @@ class SheetHelper
|
|||||||
$sheetNode = $sheetNodes[$i];
|
$sheetNode = $sheetNodes[$i];
|
||||||
$sheetDataXMLFilePath = $sheetNode->getAttribute('PartName');
|
$sheetDataXMLFilePath = $sheetNode->getAttribute('PartName');
|
||||||
|
|
||||||
$sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath, $i);
|
$sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure the sheets are sorted by index
|
||||||
|
// (as the sheets are not necessarily in this order in the XML file)
|
||||||
|
usort($sheets, function ($sheet1, $sheet2) {
|
||||||
|
return ($sheet1->getIndex() - $sheet2->getIndex());
|
||||||
|
});
|
||||||
|
|
||||||
return $sheets;
|
return $sheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,60 +94,37 @@ class SheetHelper
|
|||||||
* Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID.
|
* Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID.
|
||||||
* The entry contains the ID and name of the sheet.
|
* The entry contains the ID and name of the sheet.
|
||||||
*
|
*
|
||||||
* If this piece of data can't be found by parsing the different XML files, the ID will default
|
|
||||||
* to the sheet index, based on order in [Content_Types].xml. Similarly, the sheet's name will
|
|
||||||
* default to the data sheet XML file name ("xl/worksheets/sheet2.xml" => "sheet2").
|
|
||||||
*
|
|
||||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||||
* @param int $sheetIndexZeroBased Index of the sheet, based on order in [Content_Types].xml (zero-based)
|
|
||||||
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
|
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
|
||||||
*/
|
*/
|
||||||
protected function getSheetFromXML($sheetDataXMLFilePath, $sheetIndexZeroBased)
|
protected function getSheetFromXML($sheetDataXMLFilePath)
|
||||||
{
|
{
|
||||||
$sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath);
|
// In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
|
||||||
|
// In workbook.xml.rels, it is only "worksheets/sheet1.xml"
|
||||||
/*
|
|
||||||
* In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
|
|
||||||
* In workbook.xml.rels, it is only "worksheets/sheet1.xml"
|
|
||||||
*/
|
|
||||||
$sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/');
|
$sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/');
|
||||||
|
|
||||||
// find the node associated to the given file path
|
// find the node associated to the given file path
|
||||||
$workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement();
|
$workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement();
|
||||||
$relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]');
|
$relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]');
|
||||||
|
$relationshipNode = $relationshipNodes[0];
|
||||||
|
|
||||||
if (count($relationshipNodes) === 1) {
|
$relationshipSheetId = $relationshipNode->getAttribute('Id');
|
||||||
$relationshipNode = $relationshipNodes[0];
|
|
||||||
$sheetId = $relationshipNode->getAttribute('Id');
|
|
||||||
|
|
||||||
$workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
|
$workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
|
||||||
$sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $sheetId . '"]');
|
$sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $relationshipSheetId . '"]');
|
||||||
|
$sheetNode = $sheetNodes[0];
|
||||||
|
|
||||||
if (count($sheetNodes) === 1) {
|
$escapedSheetName = $sheetNode->getAttribute('name');
|
||||||
$sheetNode = $sheetNodes[0];
|
$sheetIdOneBased = $sheetNode->getAttribute('sheetId');
|
||||||
$escapedSheetName = $sheetNode->getAttribute('name');
|
$sheetIndexZeroBased = $sheetIdOneBased - 1;
|
||||||
|
|
||||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
$sheetName = $escaper->unescape($escapedSheetName);
|
$sheetName = $escaper->unescape($escapedSheetName);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName);
|
return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the default name of the sheet whose data is located
|
|
||||||
* at the given path.
|
|
||||||
*
|
|
||||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file
|
|
||||||
* @return string The default sheet name
|
|
||||||
*/
|
|
||||||
protected function getDefaultSheetName($sheetDataXMLFilePath)
|
|
||||||
{
|
|
||||||
return $this->globalFunctionsHelper->basename($sheetDataXMLFilePath, self::XML_EXTENSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a representation of the workbook.xml.rels file, ready to be parsed.
|
* Returns a representation of the workbook.xml.rels file, ready to be parsed.
|
||||||
* The returned value is cached.
|
* The returned value is cached.
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
|
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
|
||||||
|
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class StyleHelper
|
||||||
|
* This class provides helper functions related to XLSX styles
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\XLSX\Helper
|
||||||
|
*/
|
||||||
|
class StyleHelper
|
||||||
|
{
|
||||||
|
/** Paths of XML files relative to the XLSX file root */
|
||||||
|
const STYLES_XML_FILE_PATH = 'xl/styles.xml';
|
||||||
|
|
||||||
|
/** Nodes used to find relevant information in the styles XML file */
|
||||||
|
const XML_NODE_NUM_FMTS = 'numFmts';
|
||||||
|
const XML_NODE_NUM_FMT = 'numFmt';
|
||||||
|
const XML_NODE_CELL_XFS = 'cellXfs';
|
||||||
|
const XML_NODE_XF = 'xf';
|
||||||
|
|
||||||
|
/** Attributes used to find relevant information in the styles XML file */
|
||||||
|
const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId';
|
||||||
|
const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode';
|
||||||
|
const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat';
|
||||||
|
|
||||||
|
/** By convention, default style ID is 0 */
|
||||||
|
const DEFAULT_STYLE_ID = 0;
|
||||||
|
|
||||||
|
/** @var string Path of the XLSX file being read */
|
||||||
|
protected $filePath;
|
||||||
|
|
||||||
|
/** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
|
||||||
|
protected $customNumberFormats;
|
||||||
|
|
||||||
|
/** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
|
||||||
|
protected $stylesAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $filePath Path of the XLSX file being read
|
||||||
|
*/
|
||||||
|
public function __construct($filePath)
|
||||||
|
{
|
||||||
|
$this->filePath = $filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the styles.xml file and extract the relevant information from the file.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function extractRelevantInfo()
|
||||||
|
{
|
||||||
|
$this->customNumberFormats = [];
|
||||||
|
$this->stylesAttributes = [];
|
||||||
|
|
||||||
|
$stylesXmlFilePath = $this->filePath .'#' . self::STYLES_XML_FILE_PATH;
|
||||||
|
$xmlReader = new XMLReader();
|
||||||
|
|
||||||
|
if ($xmlReader->open('zip://' . $stylesXmlFilePath)) {
|
||||||
|
while ($xmlReader->read()) {
|
||||||
|
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
|
||||||
|
$numFmtsNode = new SimpleXMLElement($xmlReader->readOuterXml());
|
||||||
|
$this->extractNumberFormats($numFmtsNode);
|
||||||
|
|
||||||
|
} else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
|
||||||
|
$cellXfsNode = new SimpleXMLElement($xmlReader->readOuterXml());
|
||||||
|
$this->extractStyleAttributes($cellXfsNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$xmlReader->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts number formats from the "numFmt" nodes.
|
||||||
|
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
||||||
|
* to the reuse of formats. So 1 million cells should not use 1 million formats.
|
||||||
|
*
|
||||||
|
* @param SimpleXMLElement $numFmtsNode The "numFmts" node
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function extractNumberFormats($numFmtsNode)
|
||||||
|
{
|
||||||
|
foreach ($numFmtsNode->children() as $numFmtNode) {
|
||||||
|
$numFmtId = intval($numFmtNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
|
||||||
|
$formatCode = $numFmtNode->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
|
||||||
|
$this->customNumberFormats[$numFmtId] = $formatCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts style attributes from the "xf" nodes, inside the "cellXfs" section.
|
||||||
|
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
||||||
|
* to the reuse of styles. So 1 million cells should not use 1 million styles.
|
||||||
|
*
|
||||||
|
* @param SimpleXMLElement $cellXfsNode The "cellXfs" node
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function extractStyleAttributes($cellXfsNode)
|
||||||
|
{
|
||||||
|
foreach ($cellXfsNode->children() as $xfNode) {
|
||||||
|
$this->stylesAttributes[] = [
|
||||||
|
self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xfNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)),
|
||||||
|
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xfNode->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array The custom number formats
|
||||||
|
*/
|
||||||
|
protected function getCustomNumberFormats()
|
||||||
|
{
|
||||||
|
if (!isset($this->customNumberFormats)) {
|
||||||
|
$this->extractRelevantInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->customNumberFormats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array The styles attributes
|
||||||
|
*/
|
||||||
|
protected function getStylesAttributes()
|
||||||
|
{
|
||||||
|
if (!isset($this->stylesAttributes)) {
|
||||||
|
$this->extractRelevantInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->stylesAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the style with the given ID should consider
|
||||||
|
* numeric values as timestamps and format the cell as a date.
|
||||||
|
*
|
||||||
|
* @param int $styleId Zero-based style ID
|
||||||
|
* @return bool Whether the cell with the given cell should display a date instead of a numeric value
|
||||||
|
*/
|
||||||
|
public function shouldFormatNumericValueAsDate($styleId)
|
||||||
|
{
|
||||||
|
$stylesAttributes = $this->getStylesAttributes();
|
||||||
|
|
||||||
|
// Default style (0) does not format numeric values as timestamps. Only custom styles do.
|
||||||
|
// Also if the style ID does not exist in the styles.xml file, format as numeric value.
|
||||||
|
// 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];
|
||||||
|
|
||||||
|
$applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
|
||||||
|
if (!$applyNumberFormat) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||||
|
return $this->doesNumFmtIdIndicateDate($numFmtId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $numFmtId
|
||||||
|
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||||
|
*/
|
||||||
|
protected function doesNumFmtIdIndicateDate($numFmtId)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
$this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
|
||||||
|
$this->isNumFmtIdCustomDateFormat($numFmtId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $numFmtId
|
||||||
|
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||||
|
*/
|
||||||
|
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
|
||||||
|
{
|
||||||
|
$builtInDateFormatIds = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47];
|
||||||
|
return in_array($numFmtId, $builtInDateFormatIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $numFmtId
|
||||||
|
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||||
|
*/
|
||||||
|
protected function isNumFmtIdCustomDateFormat($numFmtId)
|
||||||
|
{
|
||||||
|
$customNumberFormats = $this->getCustomNumberFormats();
|
||||||
|
|
||||||
|
// Using isset here because it is way faster than array_key_exists...
|
||||||
|
if (!isset($customNumberFormats[$numFmtId])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customNumberFormat = $customNumberFormats[$numFmtId];
|
||||||
|
|
||||||
|
// Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
|
||||||
|
$pattern = '((?<!\\\)\[.+?(?<!\\\)\])';
|
||||||
|
$customNumberFormat = preg_replace($pattern, '', $customNumberFormat);
|
||||||
|
|
||||||
|
// custom date formats contain specific characters to represent the date:
|
||||||
|
// e - yy - m - d - h - s
|
||||||
|
// and all of their variants (yyyy - mm - dd...)
|
||||||
|
$dateFormatCharacters = ['e', 'yy', 'm', 'd', 'h', 's'];
|
||||||
|
|
||||||
|
$hasFoundDateFormatCharacter = false;
|
||||||
|
foreach ($dateFormatCharacters as $dateFormatCharacter) {
|
||||||
|
// character not preceded by "\"
|
||||||
|
$pattern = '/(?<!\\\)' . $dateFormatCharacter . '/';
|
||||||
|
|
||||||
|
if (preg_match($pattern, $customNumberFormat)) {
|
||||||
|
$hasFoundDateFormatCharacter = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasFoundDateFormatCharacter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,16 @@ class Reader extends AbstractReader
|
|||||||
return $this;
|
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.
|
* 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
|
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Box\Spout\Reader\IteratorInterface;
|
|||||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||||
use Box\Spout\Reader\XLSX\Helper\CellHelper;
|
use Box\Spout\Reader\XLSX\Helper\CellHelper;
|
||||||
use Box\Spout\Reader\XLSX\Helper\CellValueFormatter;
|
use Box\Spout\Reader\XLSX\Helper\CellValueFormatter;
|
||||||
|
use Box\Spout\Reader\XLSX\Helper\StyleHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class RowIterator
|
* Class RowIterator
|
||||||
@@ -39,6 +40,9 @@ class RowIterator implements IteratorInterface
|
|||||||
/** @var Helper\CellValueFormatter Helper to format cell values */
|
/** @var Helper\CellValueFormatter Helper to format cell values */
|
||||||
protected $cellValueFormatter;
|
protected $cellValueFormatter;
|
||||||
|
|
||||||
|
/** @var Helper\StyleHelper $styleHelper Helper to work with styles */
|
||||||
|
protected $styleHelper;
|
||||||
|
|
||||||
/** @var int Number of read rows */
|
/** @var int Number of read rows */
|
||||||
protected $numReadRows = 0;
|
protected $numReadRows = 0;
|
||||||
|
|
||||||
@@ -62,7 +66,9 @@ class RowIterator implements IteratorInterface
|
|||||||
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
|
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
|
||||||
|
|
||||||
$this->xmlReader = new XMLReader();
|
$this->xmlReader = new XMLReader();
|
||||||
$this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper);
|
|
||||||
|
$this->styleHelper = new StyleHelper($filePath);
|
||||||
|
$this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ use Box\Spout\Reader\SheetInterface;
|
|||||||
*/
|
*/
|
||||||
class Sheet implements SheetInterface
|
class Sheet implements SheetInterface
|
||||||
{
|
{
|
||||||
/** @var RowIterator To iterate over sheet's rows */
|
/** @var \Box\Spout\Reader\XLSX\RowIterator To iterate over sheet's rows */
|
||||||
protected $rowIterator;
|
protected $rowIterator;
|
||||||
|
|
||||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||||
protected $index;
|
protected $index;
|
||||||
|
|
||||||
/** @var string Name of the sheet */
|
/** @var string Name of the sheet */
|
||||||
@@ -25,7 +25,7 @@ class Sheet implements SheetInterface
|
|||||||
* @param string $filePath Path of the XLSX file being read
|
* @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 string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||||
* @param Helper\SharedStringsHelper Helper to work with shared strings
|
* @param Helper\SharedStringsHelper Helper to work with shared strings
|
||||||
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||||
* @param string $sheetName Name of the sheet
|
* @param string $sheetName Name of the sheet
|
||||||
*/
|
*/
|
||||||
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetIndex, $sheetName)
|
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetIndex, $sheetName)
|
||||||
@@ -36,7 +36,8 @@ class Sheet implements SheetInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return RowIterator
|
* @api
|
||||||
|
* @return \Box\Spout\Reader\XLSX\RowIterator
|
||||||
*/
|
*/
|
||||||
public function getRowIterator()
|
public function getRowIterator()
|
||||||
{
|
{
|
||||||
@@ -44,7 +45,8 @@ class Sheet implements SheetInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
* @api
|
||||||
|
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||||
*/
|
*/
|
||||||
public function getIndex()
|
public function getIndex()
|
||||||
{
|
{
|
||||||
@@ -52,6 +54,7 @@ class Sheet implements SheetInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @api
|
||||||
* @return string Name of the sheet
|
* @return string Name of the sheet
|
||||||
*/
|
*/
|
||||||
public function getName()
|
public function getName()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use Box\Spout\Reader\Exception\NoSheetsFoundException;
|
|||||||
*/
|
*/
|
||||||
class SheetIterator implements IteratorInterface
|
class SheetIterator implements IteratorInterface
|
||||||
{
|
{
|
||||||
/** @var Sheet[] The list of sheet present in the file */
|
/** @var \Box\Spout\Reader\XLSX\Sheet[] The list of sheet present in the file */
|
||||||
protected $sheets;
|
protected $sheets;
|
||||||
|
|
||||||
/** @var int The index of the sheet being read (zero-based) */
|
/** @var int The index of the sheet being read (zero-based) */
|
||||||
@@ -67,7 +67,8 @@ class SheetIterator implements IteratorInterface
|
|||||||
*/
|
*/
|
||||||
public function next()
|
public function next()
|
||||||
{
|
{
|
||||||
if (array_key_exists($this->currentSheetIndex, $this->sheets)) {
|
// Using isset here because it is way faster than array_key_exists...
|
||||||
|
if (isset($this->sheets[$this->currentSheetIndex])) {
|
||||||
$currentSheet = $this->sheets[$this->currentSheetIndex];
|
$currentSheet = $this->sheets[$this->currentSheetIndex];
|
||||||
$currentSheet->getRowIterator()->end();
|
$currentSheet->getRowIterator()->end();
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ class SheetIterator implements IteratorInterface
|
|||||||
* Return the current element
|
* Return the current element
|
||||||
* @link http://php.net/manual/en/iterator.current.php
|
* @link http://php.net/manual/en/iterator.current.php
|
||||||
*
|
*
|
||||||
* @return Sheet
|
* @return \Box\Spout\Reader\XLSX\Sheet
|
||||||
*/
|
*/
|
||||||
public function current()
|
public function current()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ abstract class AbstractMultiSheetsWriter extends AbstractWriter
|
|||||||
* Sets whether new sheets should be automatically created when the max rows limit per sheet is reached.
|
* 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.
|
* This must be set before opening the writer.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
|
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
|
||||||
* @return AbstractMultiSheetsWriter
|
* @return AbstractMultiSheetsWriter
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
||||||
@@ -39,6 +40,7 @@ abstract class AbstractMultiSheetsWriter extends AbstractWriter
|
|||||||
/**
|
/**
|
||||||
* Returns all the workbook's sheets
|
* Returns all the workbook's sheets
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return Common\Sheet[] All the workbook's sheets
|
* @return Common\Sheet[] All the workbook's sheets
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +62,7 @@ abstract class AbstractMultiSheetsWriter extends AbstractWriter
|
|||||||
/**
|
/**
|
||||||
* Creates a new sheet and make it the current sheet. The data will now be written to this sheet.
|
* Creates a new sheet and make it the current sheet. The data will now be written to this sheet.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return Common\Sheet The created sheet
|
* @return Common\Sheet The created sheet
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||||
*/
|
*/
|
||||||
@@ -74,6 +77,7 @@ abstract class AbstractMultiSheetsWriter extends AbstractWriter
|
|||||||
/**
|
/**
|
||||||
* Returns the current sheet
|
* Returns the current sheet
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return Common\Sheet The current sheet
|
* @return Common\Sheet The current sheet
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||||
*/
|
*/
|
||||||
@@ -87,6 +91,7 @@ abstract class AbstractMultiSheetsWriter extends AbstractWriter
|
|||||||
* Sets the given sheet as the current one. New data will be written to this sheet.
|
* Sets the given sheet as the current one. New data will be written to this sheet.
|
||||||
* The writing will resume where it stopped (i.e. data won't be truncated).
|
* The writing will resume where it stopped (i.e. data won't be truncated).
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param Common\Sheet $sheet The sheet to set as current
|
* @param Common\Sheet $sheet The sheet to set as current
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* Inits the writer and opens it to accept data.
|
* Inits the writer and opens it to accept data.
|
||||||
* By using this method, the data will be written to a file.
|
* By using this method, the data will be written to a file.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $outputFilePath Path of the output file that will contain the data
|
* @param string $outputFilePath Path of the output file that will contain the data
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
|
||||||
@@ -108,6 +109,7 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
*
|
*
|
||||||
* @codeCoverageIgnore
|
* @codeCoverageIgnore
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
|
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
||||||
@@ -174,7 +176,7 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* @param array $dataRow Array containing data to be streamed.
|
* @param array $dataRow Array containing data to be streamed.
|
||||||
* If empty, no data is added (i.e. not even as a blank row)
|
* If empty, no data is added (i.e. not even as a blank row)
|
||||||
* Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
|
* Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
|
||||||
*
|
* @api
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
|
||||||
@@ -197,6 +199,7 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* Write given data to the output and apply the given style.
|
* Write given data to the output and apply the given style.
|
||||||
* @see addRow
|
* @see addRow
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param array $dataRow Array of array containing data to be streamed.
|
* @param array $dataRow Array of array containing data to be streamed.
|
||||||
* @param Style\Style $style Style to be applied to the row.
|
* @param Style\Style $style Style to be applied to the row.
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
@@ -220,13 +223,13 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
/**
|
/**
|
||||||
* Write given data to the output. New data will be appended to end of stream.
|
* Write given data to the output. New data will be appended to end of stream.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param array $dataRows Array of array containing data to be streamed.
|
* @param array $dataRows Array of array containing data to be streamed.
|
||||||
* If a row is empty, it won't be added (i.e. not even as a blank row)
|
* If a row is empty, it won't be added (i.e. not even as a blank row)
|
||||||
* Example: $dataRows = [
|
* Example: $dataRows = [
|
||||||
* ['data11', 12, , '', 'data13'],
|
* ['data11', 12, , '', 'data13'],
|
||||||
* ['data21', 'data22', null, false],
|
* ['data21', 'data22', null, false],
|
||||||
* ];
|
* ];
|
||||||
*
|
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
||||||
@@ -251,6 +254,7 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* Write given data to the output and apply the given style.
|
* Write given data to the output and apply the given style.
|
||||||
* @see addRows
|
* @see addRows
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param array $dataRows Array of array containing data to be streamed.
|
* @param array $dataRows Array of array containing data to be streamed.
|
||||||
* @param Style\Style $style Style to be applied to the rows.
|
* @param Style\Style $style Style to be applied to the rows.
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
@@ -309,11 +313,17 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* Closes the writer. This will close the streamer as well, preventing new data
|
* Closes the writer. This will close the streamer as well, preventing new data
|
||||||
* to be written to the file.
|
* to be written to the file.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
$this->closeWriter();
|
$this->closeWriter();
|
||||||
|
|
||||||
|
if (is_resource($this->filePointer)) {
|
||||||
|
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||||
|
}
|
||||||
|
|
||||||
$this->isWriterOpened = false;
|
$this->isWriterOpened = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Writer extends AbstractWriter
|
|||||||
/**
|
/**
|
||||||
* Sets the field delimiter for the CSV
|
* Sets the field delimiter for the CSV
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $fieldDelimiter Character that delimits fields
|
* @param string $fieldDelimiter Character that delimits fields
|
||||||
* @return Writer
|
* @return Writer
|
||||||
*/
|
*/
|
||||||
@@ -44,6 +45,7 @@ class Writer extends AbstractWriter
|
|||||||
/**
|
/**
|
||||||
* Sets the field enclosure for the CSV
|
* Sets the field enclosure for the CSV
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $fieldEnclosure Character that enclose fields
|
* @param string $fieldEnclosure Character that enclose fields
|
||||||
* @return Writer
|
* @return Writer
|
||||||
*/
|
*/
|
||||||
@@ -94,10 +96,6 @@ class Writer extends AbstractWriter
|
|||||||
*/
|
*/
|
||||||
protected function closeWriter()
|
protected function closeWriter()
|
||||||
{
|
{
|
||||||
if ($this->filePointer) {
|
|
||||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->lastWrittenRowIndex = 0;
|
$this->lastWrittenRowIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,9 @@ abstract class AbstractStyleHelper
|
|||||||
protected function hasStyleAlreadyBeenRegistered($style)
|
protected function hasStyleAlreadyBeenRegistered($style)
|
||||||
{
|
{
|
||||||
$serializedStyle = $style->serialize();
|
$serializedStyle = $style->serialize();
|
||||||
return array_key_exists($serializedStyle, $this->serializedStyleToStyleIdMappingTable);
|
|
||||||
|
// Using isset here because it is way faster than array_key_exists...
|
||||||
|
return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ namespace Box\Spout\Writer\Common\Helper;
|
|||||||
*/
|
*/
|
||||||
class CellHelper
|
class CellHelper
|
||||||
{
|
{
|
||||||
|
/** @var array Cache containing the mapping column index => cell index */
|
||||||
|
private static $columnIndexToCellIndexCache = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cell index (base 26) associated to the base 10 column index.
|
* 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,
|
* Excel uses A to Z letters for column indexing, where A is the 1st column,
|
||||||
@@ -21,18 +24,26 @@ class CellHelper
|
|||||||
*/
|
*/
|
||||||
public static function getCellIndexFromColumnIndex($columnIndex)
|
public static function getCellIndexFromColumnIndex($columnIndex)
|
||||||
{
|
{
|
||||||
$cellIndex = '';
|
$originalColumnIndex = $columnIndex;
|
||||||
$capitalAAsciiValue = ord('A');
|
|
||||||
|
|
||||||
do {
|
// Using isset here because it is way faster than array_key_exists...
|
||||||
$modulus = $columnIndex % 26;
|
if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) {
|
||||||
$cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex;
|
$cellIndex = '';
|
||||||
|
$capitalAAsciiValue = ord('A');
|
||||||
|
|
||||||
// substracting 1 because it's zero-based
|
do {
|
||||||
$columnIndex = intval($columnIndex / 26) - 1;
|
$modulus = $columnIndex % 26;
|
||||||
} while ($columnIndex >= 0);
|
$cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex;
|
||||||
|
|
||||||
return $cellIndex;
|
// substracting 1 because it's zero-based
|
||||||
|
$columnIndex = intval($columnIndex / 26) - 1;
|
||||||
|
|
||||||
|
} while ($columnIndex >= 0);
|
||||||
|
|
||||||
|
self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$columnIndexToCellIndexCache[$originalColumnIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,52 +12,142 @@ class ZipHelper
|
|||||||
{
|
{
|
||||||
const ZIP_EXTENSION = '.zip';
|
const ZIP_EXTENSION = '.zip';
|
||||||
|
|
||||||
|
/** Controls what to do when trying to add an existing file */
|
||||||
|
const EXISTING_FILES_SKIP = 'skip';
|
||||||
|
const EXISTING_FILES_OVERWRITE = 'overwrite';
|
||||||
|
|
||||||
|
/** @var string Path of the folder where the zip file will be created */
|
||||||
|
protected $tmpFolderPath;
|
||||||
|
|
||||||
|
/** @var \ZipArchive The ZipArchive instance */
|
||||||
|
protected $zip;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zips the root folder and streams the contents of the zip into the given stream
|
* @param string $tmpFolderPath Path of the temp folder where the zip file will be created
|
||||||
*
|
|
||||||
* @param string $folderPath Path to the folder to be zipped
|
|
||||||
* @param resource $streamPointer Pointer to the stream to copy the zip
|
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
public function zipFolderAndCopyToStream($folderPath, $streamPointer)
|
public function __construct($tmpFolderPath)
|
||||||
{
|
{
|
||||||
$zipFilePath = $this->getZipFilePath($folderPath);
|
$this->tmpFolderPath = $tmpFolderPath;
|
||||||
$this->zipFolder($folderPath, $zipFilePath);
|
}
|
||||||
$this->copyZipToStream($zipFilePath, $streamPointer);
|
|
||||||
|
/**
|
||||||
|
* Returns the already created ZipArchive instance or
|
||||||
|
* creates one if none exists.
|
||||||
|
*
|
||||||
|
* @return \ZipArchive
|
||||||
|
*/
|
||||||
|
protected function createOrGetZip()
|
||||||
|
{
|
||||||
|
if (!isset($this->zip)) {
|
||||||
|
$this->zip = new \ZipArchive();
|
||||||
|
$zipFilePath = $this->getZipFilePath();
|
||||||
|
|
||||||
|
$this->zip->open($zipFilePath, \ZipArchive::CREATE|\ZipArchive::OVERWRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->zip;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $folderPathToZip Path to the folder to be zipped
|
|
||||||
* @return string Path where the zip file of the given folder will be created
|
* @return string Path where the zip file of the given folder will be created
|
||||||
*/
|
*/
|
||||||
public function getZipFilePath($folderPathToZip)
|
public function getZipFilePath()
|
||||||
{
|
{
|
||||||
return $folderPathToZip . self::ZIP_EXTENSION;
|
return $this->tmpFolderPath . self::ZIP_EXTENSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zips the given folder
|
* Adds the given file, located under the given root folder to the archive.
|
||||||
|
* The file will be compressed.
|
||||||
*
|
*
|
||||||
* @param string $folderPath Path of the folder to be zipped
|
* Example of use:
|
||||||
* @param string $destinationPath Path where the zip file will be created
|
* addFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
|
||||||
|
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
|
||||||
|
*
|
||||||
|
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
|
||||||
|
* @param string $localFilePath Path of the file to be added, under the root folder
|
||||||
|
* @param string|void $existingFileMode Controls what to do when trying to add an existing file
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function zipFolder($folderPath, $destinationPath)
|
public function addFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
|
||||||
{
|
{
|
||||||
$zip = new \ZipArchive();
|
$this->addFileToArchiveWithCompressionMethod(
|
||||||
if ($zip->open($destinationPath, \ZipArchive::CREATE)) {
|
$rootFolderPath,
|
||||||
$this->addFolderToZip($zip, $folderPath);
|
$localFilePath,
|
||||||
$zip->close();
|
$existingFileMode,
|
||||||
|
\ZipArchive::CM_DEFAULT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given file, located under the given root folder to the archive.
|
||||||
|
* The file will NOT be compressed.
|
||||||
|
*
|
||||||
|
* Example of use:
|
||||||
|
* addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
|
||||||
|
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
|
||||||
|
*
|
||||||
|
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
|
||||||
|
* @param string $localFilePath Path of the file to be added, under the root folder
|
||||||
|
* @param string|void $existingFileMode Controls what to do when trying to add an existing file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addUncompressedFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
|
||||||
|
{
|
||||||
|
$this->addFileToArchiveWithCompressionMethod(
|
||||||
|
$rootFolderPath,
|
||||||
|
$localFilePath,
|
||||||
|
$existingFileMode,
|
||||||
|
\ZipArchive::CM_STORE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given file, located under the given root folder to the archive.
|
||||||
|
* The file will NOT be compressed.
|
||||||
|
*
|
||||||
|
* Example of use:
|
||||||
|
* addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
|
||||||
|
* => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
|
||||||
|
*
|
||||||
|
* @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
|
||||||
|
* @param string $localFilePath Path of the file to be added, under the root folder
|
||||||
|
* @param string $existingFileMode Controls what to do when trying to add an existing file
|
||||||
|
* @param int $compressionMethod The compression method
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addFileToArchiveWithCompressionMethod($rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod)
|
||||||
|
{
|
||||||
|
$zip = $this->createOrGetZip();
|
||||||
|
|
||||||
|
if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) {
|
||||||
|
$normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath);
|
||||||
|
$zip->addFile($normalizedFullFilePath, $localFilePath);
|
||||||
|
|
||||||
|
if (self::canChooseCompressionMethod()) {
|
||||||
|
$zip->setCompressionName($localFilePath, $compressionMethod);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \ZipArchive $zip
|
* @return bool Whether it is possible to choose the desired compression method to be used
|
||||||
* @param string $folderPath Path of the folder to add to the zip
|
*/
|
||||||
|
public static function canChooseCompressionMethod()
|
||||||
|
{
|
||||||
|
// setCompressionName() is a PHP7+ method...
|
||||||
|
return (method_exists(new \ZipArchive(), 'setCompressionName'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $folderPath Path to the folder to be zipped
|
||||||
|
* @param string|void $existingFileMode Controls what to do when trying to add an existing file
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function addFolderToZip($zip, $folderPath)
|
public function addFolderToArchive($folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
|
||||||
{
|
{
|
||||||
|
$zip = $this->createOrGetZip();
|
||||||
|
|
||||||
$folderRealPath = $this->getNormalizedRealPath($folderPath) . '/';
|
$folderRealPath = $this->getNormalizedRealPath($folderPath) . '/';
|
||||||
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
|
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
|
||||||
@@ -65,14 +155,26 @@ class ZipHelper
|
|||||||
$itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
|
$itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
|
||||||
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
|
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
|
||||||
|
|
||||||
if ($itemInfo->isFile()) {
|
if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) {
|
||||||
$zip->addFile($itemRealPath, $itemLocalPath);
|
$zip->addFile($itemRealPath, $itemLocalPath);
|
||||||
} else if ($itemInfo->isDir()) {
|
|
||||||
$zip->addEmptyDir($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.
|
* Returns canonicalized absolute pathname, containing only forward slashes.
|
||||||
*
|
*
|
||||||
@@ -85,16 +187,30 @@ class ZipHelper
|
|||||||
return str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
|
return str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the archive and copies it into the given stream
|
||||||
|
*
|
||||||
|
* @param resource $streamPointer Pointer to the stream to copy the zip
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function closeArchiveAndCopyToStream($streamPointer)
|
||||||
|
{
|
||||||
|
$zip = $this->createOrGetZip();
|
||||||
|
$zip->close();
|
||||||
|
unset($this->zip);
|
||||||
|
|
||||||
|
$this->copyZipToStream($streamPointer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the contents of the zip file into the given stream
|
* Streams the contents of the zip file into the given stream
|
||||||
*
|
*
|
||||||
* @param string $zipFilePath Path to the zip file
|
|
||||||
* @param resource $pointer Pointer to the stream to copy the zip
|
* @param resource $pointer Pointer to the stream to copy the zip
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function copyZipToStream($zipFilePath, $pointer)
|
protected function copyZipToStream($pointer)
|
||||||
{
|
{
|
||||||
$zipFilePointer = fopen($zipFilePath, 'r');
|
$zipFilePointer = fopen($this->getZipFilePath(), 'r');
|
||||||
stream_copy_to_stream($zipFilePointer, $pointer);
|
stream_copy_to_stream($zipFilePointer, $pointer);
|
||||||
fclose($zipFilePointer);
|
fclose($zipFilePointer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class Sheet
|
|||||||
/** @var array Associative array [SHEET_INDEX] => [SHEET_NAME] keeping track of sheets' name to enforce uniqueness */
|
/** @var array Associative array [SHEET_INDEX] => [SHEET_NAME] keeping track of sheets' name to enforce uniqueness */
|
||||||
protected static $SHEETS_NAME_USED = [];
|
protected static $SHEETS_NAME_USED = [];
|
||||||
|
|
||||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||||
protected $index;
|
protected $index;
|
||||||
|
|
||||||
/** @var string Name of the sheet */
|
/** @var string Name of the sheet */
|
||||||
@@ -34,7 +34,7 @@ class Sheet
|
|||||||
protected $stringHelper;
|
protected $stringHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||||
*/
|
*/
|
||||||
public function __construct($sheetIndex)
|
public function __construct($sheetIndex)
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,8 @@ class Sheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
* @api
|
||||||
|
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||||
*/
|
*/
|
||||||
public function getIndex()
|
public function getIndex()
|
||||||
{
|
{
|
||||||
@@ -52,6 +53,7 @@ class Sheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @api
|
||||||
* @return string Name of the sheet
|
* @return string Name of the sheet
|
||||||
*/
|
*/
|
||||||
public function getName()
|
public function getName()
|
||||||
@@ -66,6 +68,7 @@ class Sheet
|
|||||||
* - it should not contain these characters: \ / ? * : [ or ]
|
* - it should not contain these characters: \ / ? * : [ or ]
|
||||||
* - it should be unique
|
* - it should be unique
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $name Name of the sheet
|
* @param string $name Name of the sheet
|
||||||
* @return Sheet
|
* @return Sheet
|
||||||
* @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
|
* @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class InvalidColorException
|
* Class InvalidColorException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Writer\Exception
|
* @package Box\Spout\Writer\Exception
|
||||||
*/
|
*/
|
||||||
class InvalidColorException extends WriterException
|
class InvalidColorException extends WriterException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class InvalidSheetNameException
|
* Class InvalidSheetNameException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Writer\Exception
|
* @package Box\Spout\Writer\Exception
|
||||||
*/
|
*/
|
||||||
class InvalidSheetNameException extends WriterException
|
class InvalidSheetNameException extends WriterException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class SheetNotFoundException
|
* Class SheetNotFoundException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Writer\Exception
|
* @package Box\Spout\Writer\Exception
|
||||||
*/
|
*/
|
||||||
class SheetNotFoundException extends WriterException
|
class SheetNotFoundException extends WriterException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class WriterAlreadyOpenedException
|
* Class WriterAlreadyOpenedException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Writer\Exception
|
* @package Box\Spout\Writer\Exception
|
||||||
*/
|
*/
|
||||||
class WriterAlreadyOpenedException extends WriterException
|
class WriterAlreadyOpenedException extends WriterException
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\Exception;
|
|||||||
/**
|
/**
|
||||||
* Class WriterNotOpenedException
|
* Class WriterNotOpenedException
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @package Box\Spout\Writer\Exception
|
* @package Box\Spout\Writer\Exception
|
||||||
*/
|
*/
|
||||||
class WriterNotOpenedException extends WriterException
|
class WriterNotOpenedException extends WriterException
|
||||||
|
|||||||
@@ -103,14 +103,12 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
|||||||
protected function createManifestFile()
|
protected function createManifestFile()
|
||||||
{
|
{
|
||||||
$manifestXmlFileContents = <<<EOD
|
$manifestXmlFileContents = <<<EOD
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
|
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
|
||||||
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
|
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
|
||||||
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
|
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
|
||||||
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
||||||
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
|
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
|
||||||
<manifest:file-entry manifest:full-path="mimetype" manifest:media-type="text/plain"/>
|
|
||||||
<manifest:file-entry manifest:full-path="META-INF/manifest.xml" manifest:media-type="text/xml"/>
|
|
||||||
</manifest:manifest>
|
</manifest:manifest>
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
@@ -265,10 +263,17 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function zipRootFolderAndCopyToStream($streamPointer)
|
public function zipRootFolderAndCopyToStream($streamPointer)
|
||||||
{
|
{
|
||||||
$zipHelper = new ZipHelper();
|
$zipHelper = new ZipHelper($this->rootFolder);
|
||||||
$zipHelper->zipFolderAndCopyToStream($this->rootFolder, $streamPointer);
|
|
||||||
|
// In order to have the file's mime type detected properly, files need to be added
|
||||||
|
// to the zip file in a particular order.
|
||||||
|
// @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
|
||||||
|
$zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME);
|
||||||
|
|
||||||
|
$zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
|
||||||
|
$zipHelper->closeArchiveAndCopyToStream($streamPointer);
|
||||||
|
|
||||||
// once the zip is copied, remove it
|
// once the zip is copied, remove it
|
||||||
$this->deleteFile($zipHelper->getZipFilePath($this->rootFolder));
|
$this->deleteFile($zipHelper->getZipFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,8 @@ class Worksheet implements WorksheetInterface
|
|||||||
for ($i = 0; $i < $cellsCount; $i++) {
|
for ($i = 0; $i < $cellsCount; $i++) {
|
||||||
$currentCellValue = $dataRow[$currentCellIndex];
|
$currentCellValue = $dataRow[$currentCellIndex];
|
||||||
|
|
||||||
if (!array_key_exists($nextCellIndex, $dataRow) || $currentCellValue !== $dataRow[$nextCellIndex]) {
|
// Using isset here because it is way faster than array_key_exists...
|
||||||
|
if (!isset($dataRow[$nextCellIndex]) || $currentCellValue !== $dataRow[$nextCellIndex]) {
|
||||||
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
|
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
|
||||||
$data .= $this->getCellContent($currentCellValue, $styleIndex, $numTimesValueRepeated);
|
$data .= $this->getCellContent($currentCellValue, $styleIndex, $numTimesValueRepeated);
|
||||||
|
|
||||||
@@ -217,6 +218,10 @@ class Worksheet implements WorksheetInterface
|
|||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
|
if (!is_resource($this->sheetFilePointer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fclose($this->sheetFilePointer);
|
fclose($this->sheetFilePointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ class Writer extends AbstractMultiSheetsWriter
|
|||||||
* Sets a custom temporary folder for creating intermediate files/folders.
|
* Sets a custom temporary folder for creating intermediate files/folders.
|
||||||
* This must be set before opening the writer.
|
* This must be set before opening the writer.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $tempFolder Temporary folder where the files to create the ODS will be stored
|
* @param string $tempFolder Temporary folder where the files to create the ODS will be stored
|
||||||
* @return Writer
|
* @return Writer
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class Color
|
|||||||
/**
|
/**
|
||||||
* Returns an RGB color from R, G and B values
|
* Returns an RGB color from R, G and B values
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param int $red Red component, 0 - 255
|
* @param int $red Red component, 0 - 255
|
||||||
* @param int $green Green component, 0 - 255
|
* @param int $green Green component, 0 - 255
|
||||||
* @param int $blue Blue component, 0 - 255
|
* @param int $blue Blue component, 0 - 255
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ class Style
|
|||||||
if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
|
if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
|
||||||
$mergedStyle->setFontSize($baseStyle->getFontSize());
|
$mergedStyle->setFontSize($baseStyle->getFontSize());
|
||||||
}
|
}
|
||||||
if (!$this->hasSetFontSize && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) {
|
if (!$this->hasSetFontColor && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) {
|
||||||
$mergedStyle->setFontColor($baseStyle->getFontColor());
|
$mergedStyle->setFontColor($baseStyle->getFontColor());
|
||||||
}
|
}
|
||||||
if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
|
if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Makes the font bold.
|
* Makes the font bold.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
public function setFontBold()
|
public function setFontBold()
|
||||||
@@ -35,6 +36,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Makes the font italic.
|
* Makes the font italic.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
public function setFontItalic()
|
public function setFontItalic()
|
||||||
@@ -46,6 +48,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Makes the font underlined.
|
* Makes the font underlined.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
public function setFontUnderline()
|
public function setFontUnderline()
|
||||||
@@ -57,6 +60,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Makes the font struck through.
|
* Makes the font struck through.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
public function setFontStrikethrough()
|
public function setFontStrikethrough()
|
||||||
@@ -68,6 +72,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Sets the font size.
|
* Sets the font size.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param int $fontSize Font size, in pixels
|
* @param int $fontSize Font size, in pixels
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
@@ -80,6 +85,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Sets the font color.
|
* Sets the font color.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $fontColor ARGB color (@see Color)
|
* @param string $fontColor ARGB color (@see Color)
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
@@ -92,6 +98,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Sets the font name.
|
* Sets the font name.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $fontName Name of the font to use
|
* @param string $fontName Name of the font to use
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
@@ -105,6 +112,7 @@ class StyleBuilder
|
|||||||
* Makes the text wrap in the cell if it's too long or
|
* Makes the text wrap in the cell if it's too long or
|
||||||
* on multiple lines.
|
* on multiple lines.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return StyleBuilder
|
* @return StyleBuilder
|
||||||
*/
|
*/
|
||||||
public function setShouldWrapText()
|
public function setShouldWrapText()
|
||||||
@@ -116,6 +124,7 @@ class StyleBuilder
|
|||||||
/**
|
/**
|
||||||
* Returns the configured style. The style is cached and can be reused.
|
* Returns the configured style. The style is cached and can be reused.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @return Style
|
* @return Style
|
||||||
*/
|
*/
|
||||||
public function build()
|
public function build()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class WriterFactory
|
|||||||
/**
|
/**
|
||||||
* This creates an instance of the appropriate writer, given the type of the file to be read
|
* This creates an instance of the appropriate writer, given the type of the file to be read
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $writerType Type of the writer to instantiate
|
* @param string $writerType Type of the writer to instantiate
|
||||||
* @return WriterInterface
|
* @return WriterInterface
|
||||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
|||||||
*/
|
*/
|
||||||
protected function createRootFolder()
|
protected function createRootFolder()
|
||||||
{
|
{
|
||||||
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx'));
|
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx', true));
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,10 +353,19 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function zipRootFolderAndCopyToStream($streamPointer)
|
public function zipRootFolderAndCopyToStream($streamPointer)
|
||||||
{
|
{
|
||||||
$zipHelper = new ZipHelper();
|
$zipHelper = new ZipHelper($this->rootFolder);
|
||||||
$zipHelper->zipFolderAndCopyToStream($this->rootFolder, $streamPointer);
|
|
||||||
|
// 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.
|
||||||
|
$zipHelper->addFileToArchive($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME);
|
||||||
|
$zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::WORKBOOK_XML_FILE_NAME);
|
||||||
|
$zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::STYLES_XML_FILE_NAME);
|
||||||
|
|
||||||
|
$zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
|
||||||
|
$zipHelper->closeArchiveAndCopyToStream($streamPointer);
|
||||||
|
|
||||||
// once the zip is copied, remove it
|
// once the zip is copied, remove it
|
||||||
$this->deleteFile($zipHelper->getZipFilePath($this->rootFolder));
|
$this->deleteFile($zipHelper->getZipFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +88,10 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
|
if (!is_resource($this->sharedStringsFilePointer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fwrite($this->sharedStringsFilePointer, '</sst>');
|
fwrite($this->sharedStringsFilePointer, '</sst>');
|
||||||
|
|
||||||
// Replace the default strings count with the actual number of shared strings in the file header
|
// Replace the default strings count with the actual number of shared strings in the file header
|
||||||
|
|||||||
@@ -131,36 +131,38 @@ EOD;
|
|||||||
$rowIndex = $this->lastWrittenRowIndex + 1;
|
$rowIndex = $this->lastWrittenRowIndex + 1;
|
||||||
$numCells = count($dataRow);
|
$numCells = count($dataRow);
|
||||||
|
|
||||||
$data = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
|
$rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
|
||||||
|
|
||||||
foreach($dataRow as $cellValue) {
|
foreach($dataRow as $cellValue) {
|
||||||
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
||||||
$data .= '<c r="' . $columnIndex . $rowIndex . '"';
|
$cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
|
||||||
$data .= ' s="' . $style->getId() . '"';
|
$cellXML .= ' s="' . $style->getId() . '"';
|
||||||
|
|
||||||
if (CellHelper::isNonEmptyString($cellValue)) {
|
if (CellHelper::isNonEmptyString($cellValue)) {
|
||||||
if ($this->shouldUseInlineStrings) {
|
if ($this->shouldUseInlineStrings) {
|
||||||
$data .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
|
$cellXML .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
|
||||||
} else {
|
} else {
|
||||||
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
||||||
$data .= ' t="s"><v>' . $sharedStringId . '</v></c>';
|
$cellXML .= ' t="s"><v>' . $sharedStringId . '</v></c>';
|
||||||
}
|
}
|
||||||
} else if (CellHelper::isBoolean($cellValue)) {
|
} else if (CellHelper::isBoolean($cellValue)) {
|
||||||
$data .= ' t="b"><v>' . $cellValue . '</v></c>';
|
$cellXML .= ' t="b"><v>' . intval($cellValue) . '</v></c>';
|
||||||
} else if (CellHelper::isNumeric($cellValue)) {
|
} else if (CellHelper::isNumeric($cellValue)) {
|
||||||
$data .= '><v>' . $cellValue . '</v></c>';
|
$cellXML .= '><v>' . $cellValue . '</v></c>';
|
||||||
} else if (empty($cellValue)) {
|
} else if (empty($cellValue)) {
|
||||||
$data .= '/>';
|
// don't write empty cells (not appending to $cellXML is the right behavior!)
|
||||||
|
$cellXML = '';
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
|
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$rowXML .= $cellXML;
|
||||||
$cellNumber++;
|
$cellNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data .= '</row>';
|
$rowXML .= '</row>';
|
||||||
|
|
||||||
$wasWriteSuccessful = fwrite($this->sheetFilePointer, $data);
|
$wasWriteSuccessful = fwrite($this->sheetFilePointer, $rowXML);
|
||||||
if ($wasWriteSuccessful === false) {
|
if ($wasWriteSuccessful === false) {
|
||||||
throw new IOException("Unable to write data in {$this->worksheetFilePath}");
|
throw new IOException("Unable to write data in {$this->worksheetFilePath}");
|
||||||
}
|
}
|
||||||
@@ -176,6 +178,10 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
|
if (!is_resource($this->sheetFilePointer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fwrite($this->sheetFilePointer, '</sheetData>');
|
fwrite($this->sheetFilePointer, '</sheetData>');
|
||||||
fwrite($this->sheetFilePointer, '</worksheet>');
|
fwrite($this->sheetFilePointer, '</worksheet>');
|
||||||
fclose($this->sheetFilePointer);
|
fclose($this->sheetFilePointer);
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ class Writer extends AbstractMultiSheetsWriter
|
|||||||
/** @var bool Whether inline or shared strings should be used - inline string is more memory efficient */
|
/** @var bool Whether inline or shared strings should be used - inline string is more memory efficient */
|
||||||
protected $shouldUseInlineStrings = true;
|
protected $shouldUseInlineStrings = true;
|
||||||
|
|
||||||
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
|
|
||||||
protected $shouldCreateNewSheetsAutomatically = true;
|
|
||||||
|
|
||||||
/** @var Internal\Workbook The workbook for the XLSX file */
|
/** @var Internal\Workbook The workbook for the XLSX file */
|
||||||
protected $book;
|
protected $book;
|
||||||
|
|
||||||
@@ -37,6 +34,7 @@ class Writer extends AbstractMultiSheetsWriter
|
|||||||
* Sets a custom temporary folder for creating intermediate files/folders.
|
* Sets a custom temporary folder for creating intermediate files/folders.
|
||||||
* This must be set before opening the writer.
|
* This must be set before opening the writer.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
|
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
|
||||||
* @return Writer
|
* @return Writer
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
||||||
@@ -53,6 +51,7 @@ class Writer extends AbstractMultiSheetsWriter
|
|||||||
* Use inline string to be more memory efficient. If set to false, it will use shared strings.
|
* 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.
|
* This must be set before opening the writer.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
||||||
* @return Writer
|
* @return Writer
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
||||||
@@ -65,22 +64,6 @@ class Writer extends AbstractMultiSheetsWriter
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @return Writer
|
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
|
|
||||||
*/
|
|
||||||
public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically)
|
|
||||||
{
|
|
||||||
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
|
|
||||||
|
|
||||||
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the write and sets the current sheet pointer to a new sheet.
|
* Configures the write and sets the current sheet pointer to a new sheet.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -142,6 +142,29 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedRows, $allRows);
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForTestReadShouldReadEmptyFile()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['csv_empty.csv'],
|
||||||
|
['csv_all_lines_empty.csv'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForTestReadShouldReadEmptyFile
|
||||||
|
*
|
||||||
|
* @param string $fileName
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldReadEmptyFile($fileName)
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile($fileName);
|
||||||
|
$this->assertEquals([], $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -181,6 +204,15 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals('This is, a comma', $allRows[0][0]);
|
$this->assertEquals('This is, a comma', $allRows[0][0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldNotTruncateLineBreak()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('csv_with_line_breaks.csv', ',');
|
||||||
|
$this->assertEquals("This is,\na comma", $allRows[0][0]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
@@ -320,6 +352,55 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedRows, $allRows);
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/184
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldInludeRowsWithZerosOnly()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.csv');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A', 'B', 'C'],
|
||||||
|
['1', '2', '3'],
|
||||||
|
['0', '0', '0']
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/184
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldCreateOutputEmptyCellPreserved()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.csv');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A', 'B', 'C'],
|
||||||
|
['0', '', ''],
|
||||||
|
['1', '1', '']
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/195
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReaderShouldNotTrimCellValues()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_untrimmed_strings.csv');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A'],
|
||||||
|
[' A '],
|
||||||
|
["\n\tA\n\t"],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @param string|void $fieldDelimiter
|
* @param string|void $fieldDelimiter
|
||||||
@@ -354,4 +435,96 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
return $allRows;
|
return $allRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForTestReadCustomEOL()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['csv_with_CR_EOL.csv', "\r"],
|
||||||
|
['csv_standard.csv', "\n"],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForTestReadCustomEOL
|
||||||
|
*
|
||||||
|
* @param string $fileName
|
||||||
|
* @param string $customEOL
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadCustomEOLs($fileName, $customEOL)
|
||||||
|
{
|
||||||
|
$allRows = [];
|
||||||
|
$resourcePath = $this->getResourcePath($fileName);
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||||
|
$reader = ReaderFactory::create(Type::CSV);
|
||||||
|
$reader
|
||||||
|
->setEndOfLineCharacter($customEOL)
|
||||||
|
->open($resourcePath);
|
||||||
|
|
||||||
|
foreach ($reader->getSheetIterator() as $sheet) {
|
||||||
|
foreach ($sheet->getRowIterator() as $row) {
|
||||||
|
$allRows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader->close();
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['csv--11', 'csv--12', 'csv--13'],
|
||||||
|
['csv--21', 'csv--22', 'csv--23'],
|
||||||
|
['csv--31', 'csv--32', 'csv--33'],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadCustomStreamWrapper()
|
||||||
|
{
|
||||||
|
$allRows = [];
|
||||||
|
$resourcePath = 'spout://csv_standard';
|
||||||
|
|
||||||
|
// register stream wrapper
|
||||||
|
stream_wrapper_register('spout', SpoutTestStream::CLASS_NAME);
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||||
|
$reader = ReaderFactory::create(Type::CSV);
|
||||||
|
$reader->open($resourcePath);
|
||||||
|
|
||||||
|
foreach ($reader->getSheetIterator() as $sheet) {
|
||||||
|
foreach ($sheet->getRowIterator() as $row) {
|
||||||
|
$allRows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader->close();
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['csv--11', 'csv--12', 'csv--13'],
|
||||||
|
['csv--21', 'csv--22', 'csv--23'],
|
||||||
|
['csv--31', 'csv--32', 'csv--33'],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
stream_wrapper_unregister('spout');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||||
|
{
|
||||||
|
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||||
|
$reader = ReaderFactory::create(Type::CSV);
|
||||||
|
$reader->open('unsupported://foobar');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\CSV;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SpoutTestStream
|
||||||
|
* Custom stream that reads CSV files located in the tests/resources/csv folder.
|
||||||
|
* For example: spout://foobar will point to tests/resources/csv/foobar.csv
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\CSV
|
||||||
|
*/
|
||||||
|
class SpoutTestStream
|
||||||
|
{
|
||||||
|
const CLASS_NAME = __CLASS__;
|
||||||
|
|
||||||
|
const PATH_TO_CSV_RESOURCES = 'tests/resources/csv/';
|
||||||
|
const CSV_EXTENSION = '.csv';
|
||||||
|
|
||||||
|
/** @var int $position */
|
||||||
|
private $position;
|
||||||
|
|
||||||
|
/** @var resource $fileHandle */
|
||||||
|
private $fileHandle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
* @param int $flag
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function url_stat($path, $flag)
|
||||||
|
{
|
||||||
|
$filePath = $this->getFilePathFromStreamPath($path);
|
||||||
|
return stat($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $streamPath
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getFilePathFromStreamPath($streamPath)
|
||||||
|
{
|
||||||
|
$fileName = parse_url($streamPath, PHP_URL_HOST);
|
||||||
|
return self::PATH_TO_CSV_RESOURCES . $fileName . self::CSV_EXTENSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
* @param string $mode
|
||||||
|
* @param int $options
|
||||||
|
* @param string $opened_path
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
// the path is like "spout://csv_name" so the actual file name correspond the name of the host.
|
||||||
|
$filePath = $this->getFilePathFromStreamPath($path);
|
||||||
|
$this->fileHandle = fopen($filePath, $mode);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $numBytes
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function stream_read($numBytes)
|
||||||
|
{
|
||||||
|
$this->position += $numBytes;
|
||||||
|
return fread($this->fileHandle, $numBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $offset
|
||||||
|
* @param int|void $whence
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function stream_seek($offset, $whence = SEEK_SET)
|
||||||
|
{
|
||||||
|
$result = fseek($this->fileHandle, $offset, $whence);
|
||||||
|
if ($result === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($whence === SEEK_SET) {
|
||||||
|
$this->position = $offset;
|
||||||
|
} else if ($whence === SEEK_CUR) {
|
||||||
|
$this->position += $offset;
|
||||||
|
} else {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
return fclose($this->fileHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->fileHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -363,6 +363,79 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedRows, $allRows);
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||||
|
{
|
||||||
|
/** @var \Box\Spout\Reader\ODS\Reader $reader */
|
||||||
|
$reader = ReaderFactory::create(Type::ODS);
|
||||||
|
$reader->open('unsupported://foobar');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadWithSupportedCustomStreamWrapper()
|
||||||
|
{
|
||||||
|
/** @var \Box\Spout\Reader\ODS\Reader $reader */
|
||||||
|
$reader = ReaderFactory::create(Type::ODS);
|
||||||
|
$reader->open('php://memory');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/184
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldInludeRowsWithZerosOnly()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.ods');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A', 'B', 'C'],
|
||||||
|
['1', '2', '3'],
|
||||||
|
['0', '0', '0']
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/184
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldCreateOutputEmptyCellPreserved()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.ods');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A', 'B', 'C'],
|
||||||
|
['0', '', ''],
|
||||||
|
['1', '1', '']
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/195
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReaderShouldNotTrimCellValues()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_untrimmed_strings.ods');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A'],
|
||||||
|
[' A '],
|
||||||
|
["\n\tA\n\t"],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @return array All the read rows the given file
|
* @return array All the read rows the given file
|
||||||
|
|||||||
@@ -163,4 +163,39 @@ class XMLReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$this->assertEquals($expectedResult, $isZipStream);
|
$this->assertEquals($expectedResult, $isZipStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForTestConvertURIToUseRealPath()
|
||||||
|
{
|
||||||
|
$tempFolder = realpath(sys_get_temp_dir());
|
||||||
|
|
||||||
|
return [
|
||||||
|
['/../../../' . $tempFolder . '/test.xlsx', $tempFolder . '/test.xlsx'],
|
||||||
|
[$tempFolder . '/test.xlsx', $tempFolder . '/test.xlsx'],
|
||||||
|
['zip://' . $tempFolder . '/test.xlsx#test.xml', 'zip://' . $tempFolder . '/test.xlsx#test.xml'],
|
||||||
|
['zip:///../../../' . $tempFolder . '/test.xlsx#test.xml', 'zip://' . $tempFolder . '/test.xlsx#test.xml'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForTestConvertURIToUseRealPath
|
||||||
|
*
|
||||||
|
* @param string $URI
|
||||||
|
* @param string $expectedConvertedURI
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testConvertURIToUseRealPath($URI, $expectedConvertedURI)
|
||||||
|
{
|
||||||
|
$tempFolder = sys_get_temp_dir();
|
||||||
|
touch($tempFolder . '/test.xlsx');
|
||||||
|
|
||||||
|
$xmlReader = new XMLReader();
|
||||||
|
$convertedURI = \ReflectionHelper::callMethodOnObject($xmlReader, 'convertURIToUseRealPath', $URI);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedConvertedURI, $convertedURI);
|
||||||
|
|
||||||
|
unlink($tempFolder . '/test.xlsx');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CellValueFormatterTest
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\XLSX\Helper
|
||||||
|
*/
|
||||||
|
class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForTestExcelDate()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, 42429, '2016-02-29 00:00:00'],
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, '146098', '2299-12-31 00:00:00'],
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, -700, null],
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, 0, null],
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, 0.5, null],
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, 1, '1900-01-01 00:00:00'],
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, 59.999988425926, '1900-02-28 23:59:59'],
|
||||||
|
[CellValueFormatter::CELL_TYPE_NUMERIC, 60.458333333333, '1900-02-28 11:00:00'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForTestExcelDate
|
||||||
|
*
|
||||||
|
* @param string $cellType
|
||||||
|
* @param int|float|string $nodeValue
|
||||||
|
* @param string|null $expectedDateAsString
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testExcelDate($cellType, $nodeValue, $expectedDateAsString)
|
||||||
|
{
|
||||||
|
$nodeListMock = $this->getMockBuilder('DOMNodeList')->disableOriginalConstructor()->getMock();
|
||||||
|
|
||||||
|
$nodeListMock
|
||||||
|
->expects($this->atLeastOnce())
|
||||||
|
->method('item')
|
||||||
|
->with(0)
|
||||||
|
->will($this->returnValue((object)['nodeValue' => $nodeValue]));
|
||||||
|
|
||||||
|
$nodeMock = $this->getMockBuilder('DOMElement')->disableOriginalConstructor()->getMock();
|
||||||
|
|
||||||
|
$nodeMock
|
||||||
|
->expects($this->atLeastOnce())
|
||||||
|
->method('getAttribute')
|
||||||
|
->will($this->returnValueMap([
|
||||||
|
[CellValueFormatter::XML_ATTRIBUTE_TYPE, $cellType],
|
||||||
|
[CellValueFormatter::XML_ATTRIBUTE_STYLE_ID, 123],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$nodeMock
|
||||||
|
->expects($this->atLeastOnce())
|
||||||
|
->method('getElementsByTagName')
|
||||||
|
->with(CellValueFormatter::XML_NODE_VALUE)
|
||||||
|
->will($this->returnValue($nodeListMock));
|
||||||
|
|
||||||
|
$styleHelperMock = $this->getMockBuilder('Box\Spout\Reader\XLSX\Helper\StyleHelper')->disableOriginalConstructor()->getMock();
|
||||||
|
|
||||||
|
$styleHelperMock
|
||||||
|
->expects($this->once())
|
||||||
|
->method('shouldFormatNumericValueAsDate')
|
||||||
|
->with(123)
|
||||||
|
->will($this->returnValue(true));
|
||||||
|
|
||||||
|
$formatter = new CellValueFormatter(null, $styleHelperMock);
|
||||||
|
$result = $formatter->extractAndFormatNodeValue($nodeMock);
|
||||||
|
|
||||||
|
if ($expectedDateAsString === null) {
|
||||||
|
$this->assertNull($result);
|
||||||
|
} else {
|
||||||
|
$this->assertInstanceOf('DateTime', $result);
|
||||||
|
$this->assertSame($expectedDateAsString, $result->format('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForTestFormatNumericCellValueWithNumbers()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[42, 42, 'integer'],
|
||||||
|
[42.5, 42.5, 'double'],
|
||||||
|
[-42, -42, 'integer'],
|
||||||
|
[-42.5, -42.5, 'double'],
|
||||||
|
['42', 42, 'integer'],
|
||||||
|
['42.5', 42.5, 'double'],
|
||||||
|
[865640023012945, 865640023012945, 'integer'],
|
||||||
|
['865640023012945', 865640023012945, 'integer'],
|
||||||
|
[865640023012945.5, 865640023012945.5, 'double'],
|
||||||
|
['865640023012945.5', 865640023012945.5, 'double'],
|
||||||
|
[PHP_INT_MAX, PHP_INT_MAX, 'integer'],
|
||||||
|
[~PHP_INT_MAX + 1, ~PHP_INT_MAX + 1, 'integer'], // ~PHP_INT_MAX === PHP_INT_MIN, PHP_INT_MIN being PHP7+
|
||||||
|
[PHP_INT_MAX + 1, PHP_INT_MAX + 1, 'double'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForTestFormatNumericCellValueWithNumbers
|
||||||
|
*
|
||||||
|
* @param int|float|string $value
|
||||||
|
* @param int|float $expectedFormattedValue
|
||||||
|
* @param string $expectedType
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testFormatNumericCellValueWithNumbers($value, $expectedFormattedValue, $expectedType)
|
||||||
|
{
|
||||||
|
$styleHelperMock = $this->getMockBuilder('Box\Spout\Reader\XLSX\Helper\StyleHelper')->disableOriginalConstructor()->getMock();
|
||||||
|
$styleHelperMock
|
||||||
|
->expects($this->once())
|
||||||
|
->method('shouldFormatNumericValueAsDate')
|
||||||
|
->will($this->returnValue(false));
|
||||||
|
|
||||||
|
$formatter = new CellValueFormatter(null, $styleHelperMock);
|
||||||
|
$formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatNumericCellValue', $value, 0);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedFormattedValue, $formattedValue);
|
||||||
|
$this->assertEquals($expectedType, gettype($formattedValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForTestFormatStringCellValue()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['A', 'A'],
|
||||||
|
[' A ', ' A '],
|
||||||
|
["\n\tA\n\t", "\n\tA\n\t"],
|
||||||
|
[' ', ' '],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForTestFormatStringCellValue
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @param string $expectedFormattedValue
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testFormatInlineStringCellValue($value, $expectedFormattedValue)
|
||||||
|
{
|
||||||
|
$nodeListMock = $this->getMockBuilder('DOMNodeList')->disableOriginalConstructor()->getMock();
|
||||||
|
$nodeListMock
|
||||||
|
->expects($this->atLeastOnce())
|
||||||
|
->method('item')
|
||||||
|
->with(0)
|
||||||
|
->will($this->returnValue((object)['nodeValue' => $value]));
|
||||||
|
|
||||||
|
$nodeMock = $this->getMockBuilder('DOMElement')->disableOriginalConstructor()->getMock();
|
||||||
|
$nodeMock
|
||||||
|
->expects($this->atLeastOnce())
|
||||||
|
->method('getElementsByTagName')
|
||||||
|
->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE)
|
||||||
|
->will($this->returnValue($nodeListMock));
|
||||||
|
|
||||||
|
$formatter = new CellValueFormatter(null, null);
|
||||||
|
$formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedFormattedValue, $formattedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class StyleHelperTest
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\XLSX\Helper
|
||||||
|
*/
|
||||||
|
class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $styleAttributes
|
||||||
|
* @param array|void $customNumberFormats
|
||||||
|
* @return StyleHelper
|
||||||
|
*/
|
||||||
|
private function getStyleHelperMock($styleAttributes, $customNumberFormats = [])
|
||||||
|
{
|
||||||
|
/** @var StyleHelper $styleHelper */
|
||||||
|
$styleHelper = $this->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\StyleHelper')
|
||||||
|
->setMethods(['getCustomNumberFormats', 'getStylesAttributes'])
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$styleHelper->method('getStylesAttributes')->willReturn($styleAttributes);
|
||||||
|
$styleHelper->method('getCustomNumberFormats')->willReturn($customNumberFormats);
|
||||||
|
|
||||||
|
return $styleHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testShouldFormatNumericValueAsDateWithDefaultStyle()
|
||||||
|
{
|
||||||
|
$styleHelper = $this->getStyleHelperMock([]);
|
||||||
|
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(0);
|
||||||
|
$this->assertFalse($shouldFormatAsDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testShouldFormatNumericValueAsDateWhenStyleIdNotListed()
|
||||||
|
{
|
||||||
|
$styleHelper = $this->getStyleHelperMock([['applyNumberFormat' => true]]);
|
||||||
|
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||||
|
$this->assertFalse($shouldFormatAsDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testShouldFormatNumericValueAsDateWhenShouldNotApplyNumberFormat()
|
||||||
|
{
|
||||||
|
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => false]]);
|
||||||
|
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||||
|
$this->assertFalse($shouldFormatAsDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testShouldFormatNumericValueAsDateWithBuiltinDateFormats()
|
||||||
|
{
|
||||||
|
$builtinNumFmtIdsForDate = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47];
|
||||||
|
|
||||||
|
foreach ($builtinNumFmtIdsForDate as $builtinNumFmtIdForDate) {
|
||||||
|
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => $builtinNumFmtIdForDate]]);
|
||||||
|
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||||
|
|
||||||
|
$this->assertTrue($shouldFormatAsDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testShouldFormatNumericValueAsDateWhenCustomNumberFormatNotFound()
|
||||||
|
{
|
||||||
|
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 165]], [166 => []]);
|
||||||
|
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||||
|
|
||||||
|
$this->assertFalse($shouldFormatAsDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForCustomDateFormats()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// number format, expectedResult
|
||||||
|
['[$-409]dddd\,\ mmmm\ d\,\ yy', true],
|
||||||
|
['[$-409]d\-mmm\-yy;@', true],
|
||||||
|
['[$-409]d\-mmm\-yyyy;@', true],
|
||||||
|
['mm/dd/yy;@', true],
|
||||||
|
['[$-F800]dddd\,\ mmmm\ dd\,\ yyyy', true],
|
||||||
|
['m/d;@', true],
|
||||||
|
['m/d/yy;@', true],
|
||||||
|
['[$-409]d\-mmm;@', true],
|
||||||
|
['[$-409]dd\-mmm\-yy;@', true],
|
||||||
|
['[$-409]mmm\-yy;@', true],
|
||||||
|
['[$-409]mmmm\-yy;@', true],
|
||||||
|
['[$-409]mmmm\ d\,\ yyyy;@', true],
|
||||||
|
['[$-409]m/d/yy\ h:mm\ AM/PM;@', true],
|
||||||
|
['m/d/yy\ h:mm;@', true],
|
||||||
|
['[$-409]mmmmm;@', true],
|
||||||
|
['[$-409]mmmmm\-yy;@', true],
|
||||||
|
['m/d/yyyy;@', true],
|
||||||
|
['[$-409]m/d/yy\--h:mm;@', true],
|
||||||
|
['GENERAL', false],
|
||||||
|
['\ma\yb\e', false],
|
||||||
|
['[Red]foo;', false],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForCustomDateFormats
|
||||||
|
*
|
||||||
|
* @param string $numberFormat
|
||||||
|
* @param bool $expectedResult
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testShouldFormatNumericValueAsDateWithCustomDateFormats($numberFormat, $expectedResult)
|
||||||
|
{
|
||||||
|
$numFmtId = 165;
|
||||||
|
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => $numFmtId]], [$numFmtId => $numberFormat]);
|
||||||
|
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedResult, $shouldFormatAsDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,28 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldSupportSheetsDefinitionInRandomOrder()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('two_sheets_with_sheets_definition_in_reverse_order.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
|
||||||
|
['s1 - A2', 's1 - B2', 's1 - C2', 's1 - D2', 's1 - E2'],
|
||||||
|
['s1 - A3', 's1 - B3', 's1 - C3', 's1 - D3', 's1 - E3'],
|
||||||
|
['s1 - A4', 's1 - B4', 's1 - C4', 's1 - D4', 's1 - E4'],
|
||||||
|
['s1 - A5', 's1 - B5', 's1 - C5', 's1 - D5', 's1 - E5'],
|
||||||
|
['s2 - A1', 's2 - B1', 's2 - C1', 's2 - D1', 's2 - E1'],
|
||||||
|
['s2 - A2', 's2 - B2', 's2 - C2', 's2 - D2', 's2 - E2'],
|
||||||
|
['s2 - A3', 's2 - B3', 's2 - C3', 's2 - D3', 's2 - E3'],
|
||||||
|
['s2 - A4', 's2 - B4', 's2 - C4', 's2 - D4', 's2 - E4'],
|
||||||
|
['s2 - A5', 's2 - B5', 's2 - C5', 's2 - D5', 's2 - E5'],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -113,6 +135,52 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedRows, $allRows);
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldSupportNumericTimestampFormattedDifferentlyAsDate()
|
||||||
|
{
|
||||||
|
// make sure dates are always created with the same timezone
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
|
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_same_numeric_value_date_formatted_differently.xlsx');
|
||||||
|
|
||||||
|
$expectedDate = \DateTime::createFromFormat('Y-m-d H:i:s', '2015-01-01 00:00:00');
|
||||||
|
$expectedRows = [
|
||||||
|
array_fill(0, 10, $expectedDate),
|
||||||
|
array_fill(0, 10, $expectedDate),
|
||||||
|
array_fill(0, 10, $expectedDate),
|
||||||
|
array_merge(array_fill(0, 7, $expectedDate), ['', '', '']),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldSupportDifferentDatesAsNumericTimestamp()
|
||||||
|
{
|
||||||
|
// make sure dates are always created with the same timezone
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
|
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_different_numeric_value_dates.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
[
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-01 00:00:00'),
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-02 00:00:00'),
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-01 22:23:00'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 23:59:59'),
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '1900-03-01 00:00:00'),
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 11:00:00'), // 1900-02-29 should be converted to 1900-02-28
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -336,6 +404,81 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedRows, $allRows);
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||||
|
{
|
||||||
|
/** @var \Box\Spout\Reader\XLSX\Reader $reader */
|
||||||
|
$reader = ReaderFactory::create(Type::XLSX);
|
||||||
|
$reader->open('unsupported://foobar');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadWithSupportedCustomStreamWrapper()
|
||||||
|
{
|
||||||
|
/** @var \Box\Spout\Reader\XLSX\Reader $reader */
|
||||||
|
$reader = ReaderFactory::create(Type::XLSX);
|
||||||
|
$reader->open('php://memory');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/184
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldInludeRowsWithZerosOnly()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A', 'B', 'C'],
|
||||||
|
['1', '2', '3'],
|
||||||
|
['0', '0', '0']
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/184
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldCreateOutputEmptyCellPreserved()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A', 'B', 'C'],
|
||||||
|
['0', '', ''],
|
||||||
|
['1', '1', '']
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/box/spout/issues/195
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReaderShouldNotTrimCellValues()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_untrimmed_inline_strings.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['A'],
|
||||||
|
[' A '],
|
||||||
|
["\n\tA\n\t"],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @return array All the read rows the given file
|
* @return array All the read rows the given file
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ namespace Box\Spout;
|
|||||||
trait TestUsingResource {
|
trait TestUsingResource {
|
||||||
|
|
||||||
/** @var string Path to the test resources folder */
|
/** @var string Path to the test resources folder */
|
||||||
private $resourcesPath = 'tests/resources/';
|
private $resourcesPath = 'tests/resources';
|
||||||
|
|
||||||
/** @var string Path to the test generated resources folder */
|
/** @var string Path to the test generated resources folder */
|
||||||
private $generatedResourcesPath = 'tests/resources/generated/';
|
private $generatedResourcesPath = 'tests/resources/generated';
|
||||||
|
|
||||||
/** @var string Path to the test resources folder, that does not have writing permissions */
|
/** @var string Path to the test resources folder, that does not have writing permissions */
|
||||||
private $generatedUnwritableResourcesPath = 'tests/resources/generated/unwritable/';
|
private $generatedUnwritableResourcesPath = 'tests/resources/generated/unwritable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $resourceName
|
* @param string $resourceName
|
||||||
@@ -25,7 +25,7 @@ trait TestUsingResource {
|
|||||||
protected function getResourcePath($resourceName)
|
protected function getResourcePath($resourceName)
|
||||||
{
|
{
|
||||||
$resourceType = pathinfo($resourceName, PATHINFO_EXTENSION);
|
$resourceType = pathinfo($resourceName, PATHINFO_EXTENSION);
|
||||||
$resourcePath = $this->resourcesPath . strtolower($resourceType) . '/' . $resourceName;
|
$resourcePath = realpath($this->resourcesPath) . '/' . strtolower($resourceType) . '/' . $resourceName;
|
||||||
|
|
||||||
return (file_exists($resourcePath) ? $resourcePath : null);
|
return (file_exists($resourcePath) ? $resourcePath : null);
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ trait TestUsingResource {
|
|||||||
protected function getGeneratedResourcePath($resourceName)
|
protected function getGeneratedResourcePath($resourceName)
|
||||||
{
|
{
|
||||||
$resourceType = pathinfo($resourceName, PATHINFO_EXTENSION);
|
$resourceType = pathinfo($resourceName, PATHINFO_EXTENSION);
|
||||||
$generatedResourcePath = $this->generatedResourcesPath . strtolower($resourceType) . '/' . $resourceName;
|
$generatedResourcePath = realpath($this->generatedResourcesPath) . '/' . strtolower($resourceType) . '/' . $resourceName;
|
||||||
|
|
||||||
return $generatedResourcePath;
|
return $generatedResourcePath;
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ trait TestUsingResource {
|
|||||||
protected function createGeneratedFolderIfNeeded($resourceName)
|
protected function createGeneratedFolderIfNeeded($resourceName)
|
||||||
{
|
{
|
||||||
$resourceType = pathinfo($resourceName, PATHINFO_EXTENSION);
|
$resourceType = pathinfo($resourceName, PATHINFO_EXTENSION);
|
||||||
$generatedResourcePathForType = $this->generatedResourcesPath . strtolower($resourceType);
|
$generatedResourcePathForType = $this->generatedResourcesPath . '/' . strtolower($resourceType);
|
||||||
|
|
||||||
if (!file_exists($generatedResourcePathForType)) {
|
if (!file_exists($generatedResourcePathForType)) {
|
||||||
mkdir($generatedResourcePathForType, 0777, true);
|
mkdir($generatedResourcePathForType, 0777, true);
|
||||||
@@ -62,7 +62,7 @@ trait TestUsingResource {
|
|||||||
*/
|
*/
|
||||||
protected function getGeneratedUnwritableResourcePath($resourceName)
|
protected function getGeneratedUnwritableResourcePath($resourceName)
|
||||||
{
|
{
|
||||||
return $this->generatedUnwritableResourcesPath . $resourceName;
|
return realpath($this->generatedUnwritableResourcesPath) . '/' . $resourceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\ODS;
|
|||||||
use Box\Spout\Common\Type;
|
use Box\Spout\Common\Type;
|
||||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||||
use Box\Spout\TestUsingResource;
|
use Box\Spout\TestUsingResource;
|
||||||
|
use Box\Spout\Writer\Common\Helper\ZipHelper;
|
||||||
use Box\Spout\Writer\WriterFactory;
|
use Box\Spout\Writer\WriterFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -368,6 +369,30 @@ class WriterTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertValueWasWrittenToSheet($fileName, 1, 'a dream');
|
$this->assertValueWasWrittenToSheet($fileName, 1, 'a dream');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGeneratedFileShouldHaveTheCorrectMimeType()
|
||||||
|
{
|
||||||
|
// Only PHP7+ gives the correct mime type since it requires adding
|
||||||
|
// uncompressed files to the final archive (which support was added in PHP7)
|
||||||
|
if (!ZipHelper::canChooseCompressionMethod()) {
|
||||||
|
$this->markTestSkipped(
|
||||||
|
'The PHP version used does not support setting the compression method of archived files,
|
||||||
|
resulting in the mime type to be detected incorrectly.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = 'test_mime_type.ods';
|
||||||
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
|
$dataRow = ['foo'];
|
||||||
|
|
||||||
|
$this->writeToODSFile([$dataRow], $fileName);
|
||||||
|
|
||||||
|
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$this->assertEquals('application/vnd.oasis.opendocument.spreadsheet', $finfo->file($resourcePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $allRows
|
* @param array $allRows
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
|
|||||||
@@ -385,6 +385,21 @@ class WriterTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'control's _x0015_ "character"');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'control's _x0015_ "character"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGeneratedFileShouldHaveTheCorrectMimeType()
|
||||||
|
{
|
||||||
|
$fileName = 'test_mime_type.xlsx';
|
||||||
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
|
$dataRows = [['foo']];
|
||||||
|
|
||||||
|
$this->writeToXLSXFile($dataRows, $fileName);
|
||||||
|
|
||||||
|
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $finfo->file($resourcePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $allRows
|
* @param array $allRows
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ require_once(dirname(__DIR__) . '/vendor/autoload.php');
|
|||||||
|
|
||||||
require_once(dirname(__DIR__) . '/tests/Spout/TestUsingResource.php');
|
require_once(dirname(__DIR__) . '/tests/Spout/TestUsingResource.php');
|
||||||
require_once(dirname(__DIR__) . '/tests/Spout/ReflectionHelper.php');
|
require_once(dirname(__DIR__) . '/tests/Spout/ReflectionHelper.php');
|
||||||
|
require_once(dirname(__DIR__) . '/tests/Spout/Reader/CSV/SpoutTestStream.php');
|
||||||
|
|
||||||
|
// Make sure a timezone is set to be able to work with dates
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
Whitespace-only changes.
|
|
@@ -0,0 +1 @@
|
|||||||
|
csv--11,csv--12,csv--13
csv--21,csv--22,csv--23
csv--31,csv--32,csv--33
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
"This is,
|
||||||
|
a comma",csv--12
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
"A","B","C"
|
||||||
|
0,,
|
||||||
|
1,1,
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
A
|
||||||
|
" A "
|
||||||
|
"
|
||||||
|
A
|
||||||
|
"
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
"A","B","C"
|
||||||
|
1,2,3
|
||||||
|
0,0,0
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user