Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -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:
|
||||||
@@ -282,10 +277,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
* Prepares the reader to read the given file. It also makes sure
|
* Prepares the reader to read the given file. It also makes sure
|
||||||
* that the file exists and is readable.
|
* that the file exists and is readable.
|
||||||
*
|
*
|
||||||
|
* @api
|
||||||
* @param string $filePath Path of the file to be read
|
* @param string $filePath Path of the file to be read
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
||||||
@@ -71,7 +72,9 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->openReader($filePath);
|
// Need to use realpath to fix "Can't open file" on some Windows setup
|
||||||
|
$fileRealPath = realpath($filePath);
|
||||||
|
$this->openReader($fileRealPath);
|
||||||
$this->isStreamOpened = true;
|
$this->isStreamOpened = true;
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
|
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
|
||||||
@@ -92,6 +95,7 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
/**
|
/**
|
||||||
* 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 +111,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()
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ use Box\Spout\Common\Helper\EncodingHelper;
|
|||||||
*/
|
*/
|
||||||
class RowIterator implements IteratorInterface
|
class RowIterator implements IteratorInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* If no value is given to stream_get_line(), it defaults to 8192 (which may be too low).
|
||||||
|
* Alignement with other functions like fgets() is discussed here: https://bugs.php.net/bug.php?id=48421
|
||||||
|
*/
|
||||||
|
const MAX_READ_BYTES_PER_LINE = 32768;
|
||||||
|
|
||||||
/** @var resource Pointer to the CSV file to read */
|
/** @var resource Pointer to the CSV file to read */
|
||||||
protected $filePointer;
|
protected $filePointer;
|
||||||
|
|
||||||
@@ -111,26 +117,28 @@ 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;
|
$lineData = false;
|
||||||
$this->numReadRows++;
|
$utf8EncodedLineData = $this->getNextUTF8EncodedLine();
|
||||||
} else {
|
if ($utf8EncodedLineData !== false) {
|
||||||
// If we reach this point, it means end of file was reached.
|
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||||
// This happens when the last lines are empty lines.
|
|
||||||
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile;
|
|
||||||
}
|
}
|
||||||
|
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||||
|
} while (($lineData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($lineData));
|
||||||
|
|
||||||
|
if ($lineData !== false) {
|
||||||
|
$this->rowDataBuffer = $lineData;
|
||||||
|
$this->numReadRows++;
|
||||||
|
} else {
|
||||||
|
// If we reach this point, it means end of file was reached.
|
||||||
|
// This happens when the last lines are empty lines.
|
||||||
|
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +153,7 @@ class RowIterator implements IteratorInterface
|
|||||||
{
|
{
|
||||||
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
|
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
|
||||||
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
|
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
|
||||||
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, 0, $encodedEOLDelimiter);
|
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $encodedEOLDelimiter);
|
||||||
|
|
||||||
// If the line could have been read, it can be converted to UTF-8
|
// If the line could have been read, it can be converted to UTF-8
|
||||||
$utf8EncodedLineData = ($encodedLineData !== false) ?
|
$utf8EncodedLineData = ($encodedLineData !== false) ?
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +27,8 @@ class Sheet implements SheetInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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 */
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -138,14 +154,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,6 +313,7 @@ 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()
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>' . $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}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,126 @@
|
|||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,6 @@ 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');
|
||||||
|
|
||||||
|
// 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.
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user