Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a330debb3 | ||
|
|
a19231fb68 | ||
|
|
73d5d0ea17 | ||
|
|
687c321363 | ||
|
|
2fa01cd838 | ||
|
|
752f4bf64e | ||
|
|
b61323d7d2 | ||
|
|
179ab483d6 | ||
|
|
2fafb63115 | ||
|
|
5ef5647558 | ||
|
|
0978d340f0 | ||
|
|
77178122c3 | ||
|
|
23f8cc4f05 | ||
|
|
442a9837f1 | ||
|
|
cc07072cbb | ||
|
|
30aa1b87e2 | ||
|
|
ddb7365a79 | ||
|
|
a07a96f523 |
No files matched your search
@@ -5,7 +5,6 @@
|
||||
[](https://travis-ci.org/box/spout)
|
||||
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
|
||||
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
|
||||
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
|
||||
@@ -209,7 +208,7 @@ Font | Bold | `StyleBuilder::setFontBold()`
|
||||
| Font name | `StyleBuilder::setFontName('Arial')`
|
||||
| Font size | `StyleBuilder::setFontSize(14)`
|
||||
| Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
|
||||
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
|
||||
Alignment | Wrap text | `StyleBuilder::setShouldWrapText(true|false)`
|
||||
|
||||
#### New sheet creation
|
||||
|
||||
@@ -259,7 +258,7 @@ $writer->setShouldUseInlineStrings(false); // will use shared strings
|
||||
|
||||
When reading a spreadsheet containing dates or times, Spout returns the values by default as DateTime objects.
|
||||
It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet.
|
||||
|
||||
|
||||
```php
|
||||
use Box\Spout\Reader\ReaderFactory;
|
||||
use Box\Spout\Common\Type;
|
||||
@@ -302,7 +301,7 @@ $sheetName = $sheet->getName();
|
||||
// Customizing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheet->setName('My custom name');
|
||||
```
|
||||
```
|
||||
|
||||
> Please note that Excel has some restrictions on the sheet's name:
|
||||
> * it must not be blank
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.6.x-dev"
|
||||
"dev-master": "2.7.x-dev"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ use Box\Spout\Common\Exception\IOException;
|
||||
*/
|
||||
class FileSystemHelper
|
||||
{
|
||||
/** @var string Path of the base folder where all the I/O can occur */
|
||||
protected $baseFolderPath;
|
||||
/** @var string Real path of the base folder where all the I/O can occur */
|
||||
protected $baseFolderRealPath;
|
||||
|
||||
/**
|
||||
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
|
||||
*/
|
||||
public function __construct($baseFolderPath)
|
||||
{
|
||||
$this->baseFolderPath = $baseFolderPath;
|
||||
$this->baseFolderRealPath = realpath($baseFolderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,9 +124,10 @@ class FileSystemHelper
|
||||
*/
|
||||
protected function throwIfOperationNotInBaseFolder($operationFolderPath)
|
||||
{
|
||||
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0);
|
||||
$operationFolderRealPath = realpath($operationFolderPath);
|
||||
$isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
|
||||
if (!$isInBaseFolder) {
|
||||
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderPath}");
|
||||
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,15 @@ abstract class AbstractReader implements ReaderInterface
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
protected $shouldFormatDates = false;
|
||||
/** @var \Box\Spout\Reader\Common\ReaderOptions Reader's customized options */
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return \Box\Spout\Reader\Common\ReaderOptions
|
||||
*/
|
||||
abstract protected function getOptions();
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
@@ -42,7 +49,7 @@ abstract class AbstractReader implements ReaderInterface
|
||||
*
|
||||
* @return \Iterator To iterate over sheets
|
||||
*/
|
||||
abstract public function getConcreteSheetIterator();
|
||||
abstract protected function getConcreteSheetIterator();
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
@@ -64,12 +71,26 @@ abstract class AbstractReader implements ReaderInterface
|
||||
/**
|
||||
* Sets whether date/time values should be returned as PHP objects or be formatted as strings.
|
||||
*
|
||||
* @api
|
||||
* @param bool $shouldFormatDates
|
||||
* @return AbstractReader
|
||||
*/
|
||||
public function setShouldFormatDates($shouldFormatDates)
|
||||
{
|
||||
$this->shouldFormatDates = $shouldFormatDates;
|
||||
$this->getOptions()->setShouldFormatDates($shouldFormatDates);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether empty rows should be returned or skipped.
|
||||
*
|
||||
* @api
|
||||
* @param bool $shouldPreserveEmptyRows
|
||||
* @return AbstractReader
|
||||
*/
|
||||
public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
|
||||
{
|
||||
$this->getOptions()->setShouldPreserveEmptyRows($shouldPreserveEmptyRows);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Reader\AbstractReader;
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
|
||||
/**
|
||||
* Class Reader
|
||||
@@ -20,20 +19,21 @@ class Reader extends AbstractReader
|
||||
/** @var SheetIterator To iterator over the CSV unique "sheet" */
|
||||
protected $sheetIterator;
|
||||
|
||||
/** @var string Defines the character used to delimit fields (one character only) */
|
||||
protected $fieldDelimiter = ',';
|
||||
/** @var string Original value for the "auto_detect_line_endings" INI value */
|
||||
protected $originalAutoDetectLineEndings;
|
||||
|
||||
/** @var string Defines the character used to enclose fields (one character only) */
|
||||
protected $fieldEnclosure = '"';
|
||||
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
||||
|
||||
/** @var string Defines the End of line */
|
||||
protected $endOfLineCharacter = "\n";
|
||||
|
||||
/** @var string */
|
||||
protected $autoDetectLineEndings;
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
if (!isset($this->options)) {
|
||||
$this->options = new ReaderOptions();
|
||||
}
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field delimiter for the CSV.
|
||||
@@ -44,7 +44,7 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
public function setFieldDelimiter($fieldDelimiter)
|
||||
{
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
$this->getOptions()->setFieldDelimiter($fieldDelimiter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
public function setFieldEnclosure($fieldEnclosure)
|
||||
{
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
$this->getOptions()->setFieldEnclosure($fieldEnclosure);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
$this->encoding = $encoding;
|
||||
$this->getOptions()->setEncoding($encoding);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
public function setEndOfLineCharacter($endOfLineCharacter)
|
||||
{
|
||||
$this->endOfLineCharacter = $endOfLineCharacter;
|
||||
$this->getOptions()->setEndOfLineCharacter($endOfLineCharacter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
protected function openReader($filePath)
|
||||
{
|
||||
$this->autoDetectLineEndings = ini_get('auto_detect_line_endings');
|
||||
$this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings');
|
||||
ini_set('auto_detect_line_endings', '1');
|
||||
|
||||
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
|
||||
@@ -117,10 +117,7 @@ class Reader extends AbstractReader
|
||||
|
||||
$this->sheetIterator = new SheetIterator(
|
||||
$this->filePointer,
|
||||
$this->fieldDelimiter,
|
||||
$this->fieldEnclosure,
|
||||
$this->encoding,
|
||||
$this->endOfLineCharacter,
|
||||
$this->getOptions(),
|
||||
$this->globalFunctionsHelper
|
||||
);
|
||||
}
|
||||
@@ -130,7 +127,7 @@ class Reader extends AbstractReader
|
||||
*
|
||||
* @return SheetIterator To iterate over sheets
|
||||
*/
|
||||
public function getConcreteSheetIterator()
|
||||
protected function getConcreteSheetIterator()
|
||||
{
|
||||
return $this->sheetIterator;
|
||||
}
|
||||
@@ -147,6 +144,6 @@ class Reader extends AbstractReader
|
||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||
}
|
||||
|
||||
ini_set('auto_detect_line_endings', $this->autoDetectLineEndings);
|
||||
ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
use Box\Spout\Common\Helper\EncodingHelper;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* This class is used to customize the reader's behavior
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
|
||||
{
|
||||
/** @var string Defines the character used to delimit fields (one character only) */
|
||||
protected $fieldDelimiter = ',';
|
||||
|
||||
/** @var string Defines the character used to enclose fields (one character only) */
|
||||
protected $fieldEnclosure = '"';
|
||||
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
||||
|
||||
/** @var string Defines the End of line */
|
||||
protected $endOfLineCharacter = "\n";
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFieldDelimiter()
|
||||
{
|
||||
return $this->fieldDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field delimiter for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setFieldDelimiter($fieldDelimiter)
|
||||
{
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFieldEnclosure()
|
||||
{
|
||||
return $this->fieldEnclosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field enclosure for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setFieldEnclosure($fieldEnclosure)
|
||||
{
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encoding of the CSV file to be read.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
$this->encoding = $encoding;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string EOL for the CSV
|
||||
*/
|
||||
public function getEndOfLineCharacter()
|
||||
{
|
||||
return $this->endOfLineCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the EOL for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $endOfLineCharacter used to properly get lines from the CSV file.
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setEndOfLineCharacter($endOfLineCharacter)
|
||||
{
|
||||
$this->endOfLineCharacter = $endOfLineCharacter;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,12 @@ class RowIterator implements IteratorInterface
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding;
|
||||
|
||||
/** @var string End of line delimiter, given by the user as input. */
|
||||
protected $inputEOLDelimiter;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
@@ -49,24 +55,19 @@ class RowIterator implements IteratorInterface
|
||||
/** @var string End of line delimiter, encoded using the same encoding as the CSV */
|
||||
protected $encodedEOLDelimiter;
|
||||
|
||||
/** @var string End of line delimiter, given by the user as input. */
|
||||
protected $inputEOLDelimiter;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param string $endOfLineDelimiter End of line delimiter
|
||||
* @param \Box\Spout\Reader\CSV\ReaderOptions $options
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineDelimiter, $globalFunctionsHelper)
|
||||
public function __construct($filePointer, $options, $globalFunctionsHelper)
|
||||
{
|
||||
$this->filePointer = $filePointer;
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
$this->encoding = $encoding;
|
||||
$this->inputEOLDelimiter = $endOfLineDelimiter;
|
||||
$this->fieldDelimiter = $options->getFieldDelimiter();
|
||||
$this->fieldEnclosure = $options->getFieldEnclosure();
|
||||
$this->encoding = $options->getEncoding();
|
||||
$this->inputEOLDelimiter = $options->getEndOfLineCharacter();
|
||||
$this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
|
||||
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
|
||||
@@ -106,7 +107,7 @@ class RowIterator implements IteratorInterface
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
@@ -114,7 +115,7 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element. Empty rows are skipped.
|
||||
* Move forward to next element. Reads data for the next unprocessed row.
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
@@ -124,25 +125,48 @@ class RowIterator implements IteratorInterface
|
||||
{
|
||||
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
|
||||
if ($this->hasReachedEndOfFile) {
|
||||
return;
|
||||
if (!$this->hasReachedEndOfFile) {
|
||||
$this->readDataForNextRow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
*/
|
||||
protected function readDataForNextRow()
|
||||
{
|
||||
do {
|
||||
$rowData = $this->getNextUTF8EncodedRow();
|
||||
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
} while (($rowData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($rowData));
|
||||
} while ($this->shouldReadNextRow($rowData));
|
||||
|
||||
if ($rowData !== false) {
|
||||
$this->rowDataBuffer = $rowData;
|
||||
// str_replace will replace NULL values by empty strings
|
||||
$this->rowDataBuffer = str_replace(null, null, $rowData);
|
||||
$this->numReadRows++;
|
||||
} else {
|
||||
// If we reach this point, it means end of file was reached.
|
||||
// This happens when the last lines are empty lines.
|
||||
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile;
|
||||
$this->hasReachedEndOfFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|bool $currentRowData
|
||||
* @return bool Whether the data for the current row can be returned or if we need to keep reading
|
||||
*/
|
||||
protected function shouldReadNextRow($currentRowData)
|
||||
{
|
||||
$hasSuccessfullyFetchedRowData = ($currentRowData !== false);
|
||||
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
$isEmptyLine = $this->isEmptyLine($currentRowData);
|
||||
|
||||
return (
|
||||
(!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) ||
|
||||
(!$this->shouldPreserveEmptyRows && $isEmptyLine)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next row, converted if necessary to UTF-8.
|
||||
* As fgetcsv() does not manage correctly encoding for non UTF-8 data,
|
||||
@@ -154,7 +178,7 @@ class RowIterator implements IteratorInterface
|
||||
protected function getNextUTF8EncodedRow()
|
||||
{
|
||||
$encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
if (false === $encodedRowData) {
|
||||
if ($encodedRowData === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -195,7 +219,7 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $lineData Array containing the cells value for the line
|
||||
* @param array|bool $lineData Array containing the cells value for the line
|
||||
* @return bool Whether the given line is empty
|
||||
*/
|
||||
protected function isEmptyLine($lineData)
|
||||
|
||||
@@ -16,14 +16,12 @@ class Sheet implements SheetInterface
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Reader\CSV\ReaderOptions $options
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
|
||||
public function __construct($filePointer, $options, $globalFunctionsHelper)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
|
||||
$this->rowIterator = new RowIterator($filePointer, $options, $globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,14 +20,12 @@ class SheetIterator implements IteratorInterface
|
||||
|
||||
/**
|
||||
* @param resource $filePointer
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
* @param string $fieldEnclosure Character that enclose fields
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Reader\CSV\ReaderOptions $options
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
|
||||
public function __construct($filePointer, $options, $globalFunctionsHelper)
|
||||
{
|
||||
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
|
||||
$this->sheet = new Sheet($filePointer, $options, $globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +43,7 @@ class SheetIterator implements IteratorInterface
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Common;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* Readers' common options
|
||||
*
|
||||
* @package Box\Spout\Reader\Common
|
||||
*/
|
||||
class ReaderOptions
|
||||
{
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
protected $shouldFormatDates = false;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows = false;
|
||||
|
||||
/**
|
||||
* @return bool Whether date/time values should be returned as PHP objects or be formatted as strings.
|
||||
*/
|
||||
public function shouldFormatDates()
|
||||
{
|
||||
return $this->shouldFormatDates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether date/time values should be returned as PHP objects or be formatted as strings.
|
||||
*
|
||||
* @param bool $shouldFormatDates
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setShouldFormatDates($shouldFormatDates)
|
||||
{
|
||||
$this->shouldFormatDates = $shouldFormatDates;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether empty rows should be returned or skipped.
|
||||
*/
|
||||
public function shouldPreserveEmptyRows()
|
||||
{
|
||||
return $this->shouldPreserveEmptyRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether empty rows should be returned or skipped.
|
||||
*
|
||||
* @param bool $shouldPreserveEmptyRows
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
|
||||
{
|
||||
$this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Common;
|
||||
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
|
||||
/**
|
||||
* Class XMLProcessor
|
||||
* Helps process XML files
|
||||
*
|
||||
* @package Box\Spout\Reader\Common
|
||||
*/
|
||||
class XMLProcessor
|
||||
{
|
||||
/* Node types */
|
||||
const NODE_TYPE_START = XMLReader::ELEMENT;
|
||||
const NODE_TYPE_END = XMLReader::END_ELEMENT;
|
||||
|
||||
/* Keys associated to reflection attributes to invoke a callback */
|
||||
const CALLBACK_REFLECTION_METHOD = 'reflectionMethod';
|
||||
const CALLBACK_REFLECTION_OBJECT = 'reflectionObject';
|
||||
|
||||
/* Values returned by the callbacks to indicate what the processor should do next */
|
||||
const PROCESSING_CONTINUE = 1;
|
||||
const PROCESSING_STOP = 2;
|
||||
|
||||
|
||||
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
|
||||
protected $xmlReader;
|
||||
|
||||
/** @var array Registered callbacks */
|
||||
private $callbacks = [];
|
||||
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object
|
||||
*/
|
||||
public function __construct($xmlReader)
|
||||
{
|
||||
$this->xmlReader = $xmlReader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nodeName A callback may be triggered when a node with this name is read
|
||||
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
|
||||
* @param callable $callback Callback to execute when the read node has the given name and type
|
||||
* @return XMLProcessor
|
||||
*/
|
||||
public function registerCallback($nodeName, $nodeType, $callback)
|
||||
{
|
||||
$callbackKey = $this->getCallbackKey($nodeName, $nodeType);
|
||||
$this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nodeName Name of the node
|
||||
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
|
||||
* @return string Key used to store the associated callback
|
||||
*/
|
||||
private function getCallbackKey($nodeName, $nodeType)
|
||||
{
|
||||
return "$nodeName$nodeType";
|
||||
}
|
||||
|
||||
/**
|
||||
* Because the callback can be a "protected" function, we don't want to use call_user_func() directly
|
||||
* but instead invoke the callback using Reflection. This allows the invocation of "protected" functions.
|
||||
* Since some functions can be called a lot, we pre-process the callback to only return the elements that
|
||||
* will be needed to invoke the callback later.
|
||||
*
|
||||
* @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME]
|
||||
* @return array Associative array containing the elements needed to invoke the callback using Reflection
|
||||
*/
|
||||
private function getInvokableCallbackData($callback)
|
||||
{
|
||||
$callbackObject = $callback[0];
|
||||
$callbackMethodName = $callback[1];
|
||||
$reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName);
|
||||
$reflectionMethod->setAccessible(true);
|
||||
|
||||
return [
|
||||
self::CALLBACK_REFLECTION_METHOD => $reflectionMethod,
|
||||
self::CALLBACK_REFLECTION_OBJECT => $callbackObject,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the reading of the XML file where it was left off.
|
||||
* Stops whenever a callback indicates that reading should stop or at the end of the file.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\XMLProcessingException
|
||||
*/
|
||||
public function readUntilStopped()
|
||||
{
|
||||
while ($this->xmlReader->read()) {
|
||||
$nodeType = $this->xmlReader->nodeType;
|
||||
$nodeNamePossiblyWithPrefix = $this->xmlReader->name;
|
||||
$nodeNameWithoutPrefix = $this->xmlReader->localName;
|
||||
|
||||
$callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType);
|
||||
|
||||
if ($callbackData !== null) {
|
||||
$callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]);
|
||||
|
||||
if ($callbackResponse === self::PROCESSING_STOP) {
|
||||
// stop reading
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nodeNamePossiblyWithPrefix Name of the node, possibly prefixed
|
||||
* @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed
|
||||
* @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
|
||||
* @return array|null Callback data to be used for execution when a node of the given name/type is read or NULL if none found
|
||||
*/
|
||||
private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType)
|
||||
{
|
||||
// With prefixed nodes, we should match if (by order of preference):
|
||||
// 1. the callback was registered with the prefixed node name (e.g. "x:worksheet")
|
||||
// 2. the callback was registered with the un-prefixed node name (e.g. "worksheet")
|
||||
$callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType);
|
||||
$callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType);
|
||||
$hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix);
|
||||
|
||||
$callbackKeyToUse = $callbackKeyForUnPrefixedName;
|
||||
if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) {
|
||||
$callbackKeyToUse = $callbackKeyForPossiblyPrefixedName;
|
||||
}
|
||||
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
return isset($this->callbacks[$callbackKeyToUse]) ? $this->callbacks[$callbackKeyToUse] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $callbackData Associative array containing data to invoke the callback using Reflection
|
||||
* @param array $args Arguments to pass to the callback
|
||||
* @return int Callback response
|
||||
*/
|
||||
private function invokeCallback($callbackData, $args)
|
||||
{
|
||||
$reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
|
||||
$callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT];
|
||||
|
||||
return $reflectionMethod->invokeArgs($callbackObject, $args);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,19 @@ class Reader extends AbstractReader
|
||||
/** @var SheetIterator To iterator over the ODS sheets */
|
||||
protected $sheetIterator;
|
||||
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
if (!isset($this->options)) {
|
||||
$this->options = new ReaderOptions();
|
||||
}
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
@@ -42,7 +55,7 @@ class Reader extends AbstractReader
|
||||
$this->zip = new \ZipArchive();
|
||||
|
||||
if ($this->zip->open($filePath) === true) {
|
||||
$this->sheetIterator = new SheetIterator($filePath, $this->shouldFormatDates);
|
||||
$this->sheetIterator = new SheetIterator($filePath, $this->getOptions());
|
||||
} else {
|
||||
throw new IOException("Could not open $filePath for reading.");
|
||||
}
|
||||
@@ -53,7 +66,7 @@ class Reader extends AbstractReader
|
||||
*
|
||||
* @return SheetIterator To iterate over sheets
|
||||
*/
|
||||
public function getConcreteSheetIterator()
|
||||
protected function getConcreteSheetIterator()
|
||||
{
|
||||
return $this->sheetIterator;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\ODS;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* This class is used to customize the reader's behavior
|
||||
*
|
||||
* @package Box\Spout\Reader\ODS
|
||||
*/
|
||||
class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
|
||||
{
|
||||
// No extra options
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use Box\Spout\Reader\Exception\XMLProcessingException;
|
||||
use Box\Spout\Reader\IteratorInterface;
|
||||
use Box\Spout\Reader\ODS\Helper\CellValueFormatter;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
use Box\Spout\Reader\Common\XMLProcessor;
|
||||
|
||||
/**
|
||||
* Class RowIterator
|
||||
@@ -23,19 +24,26 @@ class RowIterator implements IteratorInterface
|
||||
const MAX_COLUMNS_EXCEL = 16384;
|
||||
|
||||
/** Definition of XML attribute used to parse data */
|
||||
const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated';
|
||||
const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
|
||||
|
||||
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
|
||||
protected $xmlReader;
|
||||
|
||||
/** @var \Box\Spout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
|
||||
protected $xmlProcessor;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows;
|
||||
|
||||
/** @var Helper\CellValueFormatter Helper to format cell values */
|
||||
protected $cellValueFormatter;
|
||||
|
||||
/** @var bool Whether the iterator has already been rewound once */
|
||||
protected $hasAlreadyBeenRewound = false;
|
||||
|
||||
/** @var int Number of read rows */
|
||||
protected $numReadRows = 0;
|
||||
/** @var array Contains the data for the currently processed row (key = cell index, value = cell value) */
|
||||
protected $currentlyProcessedRowData = [];
|
||||
|
||||
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
|
||||
protected $rowDataBuffer = null;
|
||||
@@ -43,14 +51,41 @@ class RowIterator implements IteratorInterface
|
||||
/** @var bool Indicates whether all rows have been read */
|
||||
protected $hasReachedEndOfFile = false;
|
||||
|
||||
/** @var int Last row index processed (one-based) */
|
||||
protected $lastRowIndexProcessed = 0;
|
||||
|
||||
/** @var int Row index to be processed next (one-based) */
|
||||
protected $nextRowIndexToBeProcessed = 1;
|
||||
|
||||
/** @var mixed|null Value of the last processed cell (because when reading cell at column N+1, cell N is processed) */
|
||||
protected $lastProcessedCellValue = null;
|
||||
|
||||
/** @var int Number of times the last processed row should be repeated */
|
||||
protected $numRowsRepeated = 1;
|
||||
|
||||
/** @var int Number of times the last cell value should be copied to the cells on its right */
|
||||
protected $numColumnsRepeated = 1;
|
||||
|
||||
/** @var bool Whether at least one cell has been read for the row currently being processed */
|
||||
protected $hasAlreadyReadOneCellInCurrentRow = false;
|
||||
|
||||
|
||||
/**
|
||||
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
* @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
|
||||
*/
|
||||
public function __construct($xmlReader, $shouldFormatDates)
|
||||
public function __construct($xmlReader, $options)
|
||||
{
|
||||
$this->xmlReader = $xmlReader;
|
||||
$this->cellValueFormatter = new CellValueFormatter($shouldFormatDates);
|
||||
$this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
|
||||
$this->cellValueFormatter = new CellValueFormatter($options->shouldFormatDates());
|
||||
|
||||
// Register all callbacks to process different nodes when reading the XML file
|
||||
$this->xmlProcessor = new XMLProcessor($this->xmlReader);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +106,8 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
$this->hasAlreadyBeenRewound = true;
|
||||
$this->numReadRows = 0;
|
||||
$this->lastRowIndexProcessed = 0;
|
||||
$this->nextRowIndexToBeProcessed = 1;
|
||||
$this->rowDataBuffer = null;
|
||||
$this->hasReachedEndOfFile = false;
|
||||
|
||||
@@ -82,7 +118,7 @@ class RowIterator implements IteratorInterface
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
@@ -99,77 +135,155 @@ class RowIterator implements IteratorInterface
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$rowData = [];
|
||||
$cellValue = null;
|
||||
$numColumnsRepeated = 1;
|
||||
$numCellsRead = 0;
|
||||
$hasAlreadyReadOneCell = false;
|
||||
if ($this->doesNeedDataForNextRowToBeProcessed()) {
|
||||
$this->readDataForNextRow();
|
||||
}
|
||||
|
||||
$this->lastRowIndexProcessed++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we need data for the next row to be processed.
|
||||
* We DO need to read data if:
|
||||
* - we have not read any rows yet
|
||||
* OR
|
||||
* - the next row to be processed immediately follows the last read row
|
||||
*
|
||||
* @return bool Whether we need data for the next row to be processed.
|
||||
*/
|
||||
protected function doesNeedDataForNextRowToBeProcessed()
|
||||
{
|
||||
$hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
|
||||
|
||||
return (
|
||||
!$hasReadAtLeastOneRow ||
|
||||
$this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
|
||||
*/
|
||||
protected function readDataForNextRow()
|
||||
{
|
||||
$this->currentlyProcessedRowData = [];
|
||||
|
||||
try {
|
||||
while ($this->xmlReader->read()) {
|
||||
if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
|
||||
// Start of a cell description
|
||||
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode();
|
||||
|
||||
$node = $this->xmlReader->expand();
|
||||
$currentCellValue = $this->getCellValue($node);
|
||||
|
||||
// process cell N only after having read cell N+1 (see below why)
|
||||
if ($hasAlreadyReadOneCell) {
|
||||
for ($i = 0; $i < $numColumnsRepeated; $i++) {
|
||||
$rowData[] = $cellValue;
|
||||
}
|
||||
}
|
||||
|
||||
$cellValue = $currentCellValue;
|
||||
$numColumnsRepeated = $currentNumColumnsRepeated;
|
||||
|
||||
$numCellsRead++;
|
||||
$hasAlreadyReadOneCell = true;
|
||||
|
||||
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
|
||||
// End of the row description
|
||||
$isEmptyRow = ($numCellsRead <= 1 && $this->isEmptyCellValue($cellValue));
|
||||
if ($isEmptyRow) {
|
||||
// skip empty rows
|
||||
$this->next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
|
||||
// The current count of read columns is determined by counting the values in $rowData.
|
||||
// This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
|
||||
// with a number-columns-repeated value equals to the number of (supported columns - used columns).
|
||||
// In Excel, the number of supported columns is 16384, but we don't want to returns rows with
|
||||
// always 16384 cells.
|
||||
if ((count($rowData) + $numColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
|
||||
for ($i = 0; $i < $numColumnsRepeated; $i++) {
|
||||
$rowData[] = $cellValue;
|
||||
}
|
||||
$this->numReadRows++;
|
||||
}
|
||||
break;
|
||||
|
||||
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_TABLE)) {
|
||||
// The closing "</table:table>" marks the end of the file
|
||||
$this->hasReachedEndOfFile = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->xmlProcessor->readUntilStopped();
|
||||
} catch (XMLProcessingException $exception) {
|
||||
throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
|
||||
}
|
||||
|
||||
$this->rowDataBuffer = $rowData;
|
||||
$this->rowDataBuffer = $this->currentlyProcessedRowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowStartingNode($xmlReader)
|
||||
{
|
||||
// Reset data from current row
|
||||
$this->hasAlreadyReadOneCellInCurrentRow = false;
|
||||
$this->lastProcessedCellValue = null;
|
||||
$this->numColumnsRepeated = 1;
|
||||
$this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader);
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processCellStartingNode($xmlReader)
|
||||
{
|
||||
$currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
|
||||
|
||||
$node = $xmlReader->expand();
|
||||
$currentCellValue = $this->getCellValue($node);
|
||||
|
||||
// process cell N only after having read cell N+1 (see below why)
|
||||
if ($this->hasAlreadyReadOneCellInCurrentRow) {
|
||||
for ($i = 0; $i < $this->numColumnsRepeated; $i++) {
|
||||
$this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->hasAlreadyReadOneCellInCurrentRow = true;
|
||||
$this->lastProcessedCellValue = $currentCellValue;
|
||||
$this->numColumnsRepeated = $currentNumColumnsRepeated;
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowEndingNode()
|
||||
{
|
||||
$isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRowData, $this->lastProcessedCellValue);
|
||||
|
||||
// if the fetched row is empty and we don't want to preserve it...
|
||||
if (!$this->shouldPreserveEmptyRows && $isEmptyRow) {
|
||||
// ... skip it
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
// if the row is empty, we don't want to return more than one cell
|
||||
$actualNumColumnsRepeated = (!$isEmptyRow) ? $this->numColumnsRepeated : 1;
|
||||
|
||||
// Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
|
||||
// The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData".
|
||||
// This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
|
||||
// with a number-columns-repeated value equals to the number of (supported columns - used columns).
|
||||
// In Excel, the number of supported columns is 16384, but we don't want to returns rows with
|
||||
// always 16384 cells.
|
||||
if ((count($this->currentlyProcessedRowData) + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
|
||||
for ($i = 0; $i < $actualNumColumnsRepeated; $i++) {
|
||||
$this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are processing row N and the row is repeated M times,
|
||||
// then the next row to be processed will be row (N+M).
|
||||
$this->nextRowIndexToBeProcessed += $this->numRowsRepeated;
|
||||
|
||||
// at this point, we have all the data we need for the row
|
||||
// so that we can populate the buffer
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processTableEndingNode()
|
||||
{
|
||||
// The closing "</table:table>" marks the end of the file
|
||||
$this->hasReachedEndOfFile = true;
|
||||
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
|
||||
* @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing
|
||||
*/
|
||||
protected function getNumRowsRepeatedForCurrentNode($xmlReader)
|
||||
{
|
||||
$numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
|
||||
return ($numRowsRepeated !== null) ? intval($numRowsRepeated) : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
|
||||
* @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
|
||||
*/
|
||||
protected function getNumColumnsRepeatedForCurrentNode()
|
||||
protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
|
||||
{
|
||||
$numColumnsRepeated = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
|
||||
$numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
|
||||
return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1;
|
||||
}
|
||||
|
||||
@@ -185,14 +299,21 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* empty() replacement that honours 0 as a valid value
|
||||
* After finishing processing each cell, a row is considered empty if it contains
|
||||
* no cells or if the value of the last read cell is an empty string.
|
||||
* After finishing processing each cell, the last read cell is not part of the
|
||||
* row data yet (as we still need to apply the "num-columns-repeated" attribute).
|
||||
*
|
||||
* @param string|int|float|bool|\DateTime|\DateInterval|null $value The cell value
|
||||
* @return bool
|
||||
* @param array $rowData
|
||||
* @param string|int|float|bool|\DateTime|\DateInterval|null The value of the last read cell
|
||||
* @return bool Whether the row is empty
|
||||
*/
|
||||
protected function isEmptyCellValue($value)
|
||||
protected function isEmptyRow($rowData, $lastReadCellValue)
|
||||
{
|
||||
return (!isset($value) || trim($value) === '');
|
||||
return (
|
||||
count($rowData) === 0 &&
|
||||
(!isset($lastReadCellValue) || trim($lastReadCellValue) === '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +335,7 @@ class RowIterator implements IteratorInterface
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->numReadRows;
|
||||
return $this->lastRowIndexProcessed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ class Sheet implements SheetInterface
|
||||
|
||||
/**
|
||||
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
|
||||
* @param string $sheetName Name of the sheet
|
||||
*/
|
||||
public function __construct($xmlReader, $shouldFormatDates, $sheetIndex, $sheetName)
|
||||
public function __construct($xmlReader, $sheetIndex, $sheetName, $options)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($xmlReader, $shouldFormatDates);
|
||||
$this->rowIterator = new RowIterator($xmlReader, $options);
|
||||
$this->index = $sheetIndex;
|
||||
$this->name = $sheetName;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ class SheetIterator implements IteratorInterface
|
||||
/** @var string $filePath Path of the file to be read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
protected $shouldFormatDates;
|
||||
/** @var \Box\Spout\Reader\ODS\ReaderOptions Reader's current options */
|
||||
protected $options;
|
||||
|
||||
/** @var XMLReader The XMLReader object that will help read sheet's XML data */
|
||||
protected $xmlReader;
|
||||
@@ -41,13 +41,13 @@ class SheetIterator implements IteratorInterface
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
* @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
|
||||
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
|
||||
*/
|
||||
public function __construct($filePath, $shouldFormatDates)
|
||||
public function __construct($filePath, $options)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->shouldFormatDates = $shouldFormatDates;
|
||||
$this->options = $options;
|
||||
$this->xmlReader = new XMLReader();
|
||||
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
@@ -83,7 +83,7 @@ class SheetIterator implements IteratorInterface
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
@@ -116,7 +116,7 @@ class SheetIterator implements IteratorInterface
|
||||
$escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
|
||||
$sheetName = $this->escaper->unescape($escapedSheetName);
|
||||
|
||||
return new Sheet($this->xmlReader, $this->shouldFormatDates, $sheetName, $this->currentSheetIndex);
|
||||
return new Sheet($this->xmlReader, $sheetName, $this->currentSheetIndex, $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -149,22 +149,6 @@ class SimpleXMLElement
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -228,8 +228,8 @@ class CellValueFormatter
|
||||
$dateObj->setTime($hours, $minutes, $seconds);
|
||||
|
||||
if ($this->shouldFormatDates) {
|
||||
$styleNumberFormat = $this->styleHelper->getNumberFormat($cellStyleId);
|
||||
$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormat);
|
||||
$styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
|
||||
$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
|
||||
return $dateObj->format($phpDateFormat);
|
||||
} else {
|
||||
return $dateObj;
|
||||
@@ -257,8 +257,8 @@ class CellValueFormatter
|
||||
$dateObj->modify('+' . $secondsRemainder . 'seconds');
|
||||
|
||||
if ($this->shouldFormatDates) {
|
||||
$styleNumberFormat = $this->styleHelper->getNumberFormat($cellStyleId);
|
||||
$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormat);
|
||||
$styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
|
||||
$phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
|
||||
return $dateObj->format($phpDateFormat);
|
||||
} else {
|
||||
return $dateObj;
|
||||
|
||||
@@ -62,7 +62,9 @@ class DateFormatHelper
|
||||
public static function toPHPDateFormat($excelDateFormat)
|
||||
{
|
||||
// Remove brackets potentially present at the beginning of the format string
|
||||
$dateFormat = preg_replace('/^(\[\$[^\]]+?\])/i', '', $excelDateFormat);
|
||||
// and text portion of the format at the end of it (starting with ";")
|
||||
// See §18.8.31 of ECMA-376 for more detail.
|
||||
$dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat);
|
||||
|
||||
// Double quotes are used to escape characters that must not be interpreted.
|
||||
// For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y"
|
||||
|
||||
@@ -34,7 +34,7 @@ class SharedStringsHelper
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
|
||||
* @param string|null|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
|
||||
*/
|
||||
public function __construct($filePath, $tempFolder = null)
|
||||
{
|
||||
@@ -94,44 +94,19 @@ class SharedStringsHelper
|
||||
$xmlReader->readUntilNodeFound('si');
|
||||
|
||||
while ($xmlReader->name === 'si') {
|
||||
$node = $this->getSimpleXmlElementNodeFromXMLReader($xmlReader);
|
||||
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
|
||||
|
||||
// removes nodes that should not be read, like the pronunciation of the Kanji characters
|
||||
$cleanNode = $this->removeSuperfluousTextNodes($node);
|
||||
|
||||
// find all text nodes "t"; there can be multiple if the cell contains formatting
|
||||
$textNodes = $cleanNode->xpath('//ns:t');
|
||||
|
||||
$textValue = '';
|
||||
foreach ($textNodes as $nodeIndex => $textNode) {
|
||||
if ($nodeIndex !== 0) {
|
||||
// add a space between each "t" node
|
||||
$textValue .= ' ';
|
||||
}
|
||||
|
||||
if ($this->shouldPreserveWhitespace($textNode)) {
|
||||
$textValue .= $textNode->__toString();
|
||||
} else {
|
||||
$textValue .= trim($textNode->__toString());
|
||||
}
|
||||
}
|
||||
|
||||
$unescapedTextValue = $escaper->unescape($textValue);
|
||||
$this->cachingStrategy->addStringForIndex($unescapedTextValue, $sharedStringIndex);
|
||||
|
||||
$this->processSharedStringsItem($xmlReader, $sharedStringIndex, $escaper);
|
||||
$sharedStringIndex++;
|
||||
|
||||
// jump to the next 'si' tag
|
||||
$xmlReader->next('si');
|
||||
}
|
||||
|
||||
$this->cachingStrategy->closeCache();
|
||||
|
||||
} catch (XMLProcessingException $exception) {
|
||||
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
|
||||
}
|
||||
|
||||
$this->cachingStrategy->closeCache();
|
||||
|
||||
$xmlReader->close();
|
||||
}
|
||||
|
||||
@@ -182,6 +157,31 @@ class SharedStringsHelper
|
||||
->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the shared strings item XML node which the given XML reader is positioned on.
|
||||
*
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader
|
||||
* @param int $sharedStringIndex Index of the processed shared strings item
|
||||
* @param \Box\Spout\Common\Escaper\XLSX $escaper Helper to escape values
|
||||
* @return void
|
||||
*/
|
||||
protected function processSharedStringsItem($xmlReader, $sharedStringIndex, $escaper)
|
||||
{
|
||||
$node = $this->getSimpleXmlElementNodeFromXMLReader($xmlReader);
|
||||
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
|
||||
|
||||
// removes nodes that should not be read, like the pronunciation of the Kanji characters
|
||||
$cleanNode = $this->removeSuperfluousTextNodes($node);
|
||||
|
||||
// find all text nodes "t"; there can be multiple if the cell contains formatting
|
||||
$textNodes = $cleanNode->xpath('//ns:t');
|
||||
|
||||
$textValue = $this->extractTextValueForNodes($textNodes);
|
||||
$unescapedTextValue = $escaper->unescape($textValue);
|
||||
|
||||
$this->cachingStrategy->addStringForIndex($unescapedTextValue, $sharedStringIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SimpleXMLElement node from the current node in the given XMLReader instance.
|
||||
* This is to simplify the parsing of the subtree.
|
||||
@@ -225,6 +225,29 @@ class SharedStringsHelper
|
||||
return $parentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $textNodes Text XML nodes ("<t>")
|
||||
* @return string The value associated with the given text node(s)
|
||||
*/
|
||||
protected function extractTextValueForNodes($textNodes)
|
||||
{
|
||||
$textValue = '';
|
||||
|
||||
foreach ($textNodes as $nodeIndex => $textNode) {
|
||||
if ($nodeIndex !== 0) {
|
||||
// add a space between each "t" node
|
||||
$textValue .= ' ';
|
||||
}
|
||||
|
||||
$textNodeAsString = $textNode->__toString();
|
||||
$shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode);
|
||||
|
||||
$textValue .= ($shouldPreserveWhitespace) ? $textNodeAsString : trim($textNodeAsString);
|
||||
}
|
||||
|
||||
return $textValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
|
||||
*
|
||||
|
||||
@@ -20,27 +20,27 @@ class SheetHelper
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var \Box\Spout\Reader\XLSX\ReaderOptions Reader's current options */
|
||||
protected $options;
|
||||
|
||||
/** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
|
||||
protected $sharedStringsHelper;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
protected $shouldFormatDates;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
*/
|
||||
public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates)
|
||||
public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->options = $options;
|
||||
$this->sharedStringsHelper = $sharedStringsHelper;
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
$this->shouldFormatDates = $shouldFormatDates;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ class SheetHelper
|
||||
|
||||
$sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
|
||||
|
||||
return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $this->shouldFormatDates, $sheetIndexZeroBased, $sheetName);
|
||||
return new Sheet($this->filePath, $sheetDataXMLFilePath, $sheetIndexZeroBased, $sheetName, $this->options, $this->sharedStringsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,8 @@ class StyleHelper
|
||||
/** By convention, default style ID is 0 */
|
||||
const DEFAULT_STYLE_ID = 0;
|
||||
|
||||
const NUMBER_FORMAT_GENERAL = 'General';
|
||||
|
||||
/**
|
||||
* @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx
|
||||
* @var array Mapping between built-in numFmtId and the associated format - for dates only
|
||||
@@ -51,18 +53,48 @@ class StyleHelper
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var array Array containing the IDs of built-in number formats indicating a date */
|
||||
protected $builtinNumFmtIdIndicatingDates;
|
||||
|
||||
/** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
|
||||
protected $customNumberFormats;
|
||||
|
||||
/** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
|
||||
protected $stylesAttributes;
|
||||
|
||||
/** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */
|
||||
protected $numFmtIdToIsDateFormatCache = [];
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
*/
|
||||
public function __construct($filePath)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the style with the given ID should consider
|
||||
* numeric values as timestamps and format the cell as a date.
|
||||
*
|
||||
* @param int $styleId Zero-based style ID
|
||||
* @return bool Whether the cell with the given cell should display a date instead of a numeric value
|
||||
*/
|
||||
public function shouldFormatNumericValueAsDate($styleId)
|
||||
{
|
||||
$stylesAttributes = $this->getStylesAttributes();
|
||||
|
||||
// Default style (0) does not format numeric values as timestamps. Only custom styles do.
|
||||
// Also if the style ID does not exist in the styles.xml file, format as numeric value.
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$styleAttributes = $stylesAttributes[$styleId];
|
||||
|
||||
return $this->doesStyleIndicateDate($styleAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,9 +157,15 @@ class StyleHelper
|
||||
{
|
||||
while ($xmlReader->read()) {
|
||||
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) {
|
||||
$numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID);
|
||||
$normalizedNumFmtId = ($numFmtId !== null) ? intval($numFmtId) : null;
|
||||
|
||||
$applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT);
|
||||
$normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? !!$applyNumberFormat : null;
|
||||
|
||||
$this->stylesAttributes[] = [
|
||||
self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)),
|
||||
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)),
|
||||
self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId,
|
||||
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat,
|
||||
];
|
||||
} else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
|
||||
// Once done reading "cellXfs" node's children
|
||||
@@ -161,86 +199,92 @@ class StyleHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId")
|
||||
* @return bool Whether the style with the given attributes indicates that the number is a date
|
||||
*/
|
||||
public function shouldFormatNumericValueAsDate($styleId)
|
||||
protected function doesStyleIndicateDate($styleAttributes)
|
||||
{
|
||||
$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) {
|
||||
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||
|
||||
// A style may apply a date format if it has:
|
||||
// - "applyNumberFormat" attribute not set to "false"
|
||||
// - "numFmtId" attribute set
|
||||
// This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format,
|
||||
// but this is not necessarily a date.
|
||||
if ($applyNumberFormat === false || $numFmtId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||
return $this->doesNumFmtIdIndicateDate($numFmtId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the number format ID indicates that the number is a date.
|
||||
* The result is cached to avoid recomputing the same thing over and over, as
|
||||
* "numFmtId" attributes can be shared between multiple styles.
|
||||
*
|
||||
* @param int $numFmtId
|
||||
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||
* @return bool Whether the number format ID indicates that the number is a date
|
||||
*/
|
||||
protected function doesNumFmtIdIndicateDate($numFmtId)
|
||||
{
|
||||
return (
|
||||
!$this->doesNumFmtIdIndicateGeneralFormat($numFmtId) &&
|
||||
(
|
||||
if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) {
|
||||
$formatCode = $this->getFormatCodeForNumFmtId($numFmtId);
|
||||
|
||||
$this->numFmtIdToIsDateFormatCache[$numFmtId] = (
|
||||
$this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
|
||||
$this->isNumFmtIdCustomDateFormat($numFmtId)
|
||||
)
|
||||
);
|
||||
$this->isFormatCodeCustomDateFormat($formatCode)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->numFmtIdToIsDateFormatCache[$numFmtId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numFmtId
|
||||
* @return bool Whether the number format ID indicates the "General" format (0 by convention)
|
||||
* @return string|null The custom number format or NULL if none defined for the given numFmtId
|
||||
*/
|
||||
protected function doesNumFmtIdIndicateGeneralFormat($numFmtId)
|
||||
{
|
||||
return ($numFmtId === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numFmtId
|
||||
* @return bool Whether the number format ID indicates that the number is a timestamp
|
||||
*/
|
||||
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
|
||||
{
|
||||
$builtInDateFormatIds = array_keys(self::$builtinNumFmtIdToNumFormatMapping);
|
||||
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)
|
||||
protected function getFormatCodeForNumFmtId($numFmtId)
|
||||
{
|
||||
$customNumberFormats = $this->getCustomNumberFormats();
|
||||
|
||||
// Using isset here because it is way faster than array_key_exists...
|
||||
if (!isset($customNumberFormats[$numFmtId])) {
|
||||
return (isset($customNumberFormats[$numFmtId])) ? $customNumberFormats[$numFmtId] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numFmtId
|
||||
* @return bool Whether the number format ID indicates that the number is a date
|
||||
*/
|
||||
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
|
||||
{
|
||||
return in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $formatCode
|
||||
* @return bool Whether the given format code indicates that the number is a date
|
||||
*/
|
||||
protected function isFormatCodeCustomDateFormat($formatCode)
|
||||
{
|
||||
// if no associated format code or if using the default "General" format
|
||||
if ($formatCode === null || strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customNumberFormat = $customNumberFormats[$numFmtId];
|
||||
return $this->isFormatCodeMatchingDateFormatPattern($formatCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $formatCode
|
||||
* @return bool Whether the given format code matches a date format pattern
|
||||
*/
|
||||
protected function isFormatCodeMatchingDateFormatPattern($formatCode)
|
||||
{
|
||||
// Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
|
||||
$pattern = '((?<!\\\)\[.+?(?<!\\\)\])';
|
||||
$customNumberFormat = preg_replace($pattern, '', $customNumberFormat);
|
||||
$formatCode = preg_replace($pattern, '', $formatCode);
|
||||
|
||||
// custom date formats contain specific characters to represent the date:
|
||||
// e - yy - m - d - h - s
|
||||
@@ -249,10 +293,10 @@ class StyleHelper
|
||||
|
||||
$hasFoundDateFormatCharacter = false;
|
||||
foreach ($dateFormatCharacters as $dateFormatCharacter) {
|
||||
// character not preceded by "\"
|
||||
$pattern = '/(?<!\\\)' . $dateFormatCharacter . '/';
|
||||
// character not preceded by "\" (case insensitive)
|
||||
$pattern = '/(?<!\\\)' . $dateFormatCharacter . '/i';
|
||||
|
||||
if (preg_match($pattern, $customNumberFormat)) {
|
||||
if (preg_match($pattern, $formatCode)) {
|
||||
$hasFoundDateFormatCharacter = true;
|
||||
break;
|
||||
}
|
||||
@@ -266,21 +310,21 @@ class StyleHelper
|
||||
* NOTE: It is assumed that the style DOES have a number format associated to it.
|
||||
*
|
||||
* @param int $styleId Zero-based style ID
|
||||
* @return string The number format associated with the given style
|
||||
* @return string The number format code associated with the given style
|
||||
*/
|
||||
public function getNumberFormat($styleId)
|
||||
public function getNumberFormatCode($styleId)
|
||||
{
|
||||
$stylesAttributes = $this->getStylesAttributes();
|
||||
$styleAttributes = $stylesAttributes[$styleId];
|
||||
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||
|
||||
if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) {
|
||||
$numberFormat = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId];
|
||||
$numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId];
|
||||
} else {
|
||||
$customNumberFormats = $this->getCustomNumberFormats();
|
||||
$numberFormat = $customNumberFormats[$numFmtId];
|
||||
$numberFormatCode = $customNumberFormats[$numFmtId];
|
||||
}
|
||||
|
||||
return $numberFormat;
|
||||
return $numberFormatCode;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,6 @@ use Box\Spout\Reader\XLSX\Helper\SharedStringsHelper;
|
||||
*/
|
||||
class Reader extends AbstractReader
|
||||
{
|
||||
/** @var string Temporary folder where the temporary files will be created */
|
||||
protected $tempFolder;
|
||||
|
||||
/** @var \ZipArchive */
|
||||
protected $zip;
|
||||
|
||||
@@ -27,13 +24,26 @@ class Reader extends AbstractReader
|
||||
protected $sheetIterator;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the reader's current options
|
||||
*
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
if (!isset($this->options)) {
|
||||
$this->options = new ReaderOptions();
|
||||
}
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tempFolder Temporary folder where the temporary files will be created
|
||||
* @return Reader
|
||||
*/
|
||||
public function setTempFolder($tempFolder)
|
||||
{
|
||||
$this->tempFolder = $tempFolder;
|
||||
$this->getOptions()->setTempFolder($tempFolder);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -62,14 +72,14 @@ class Reader extends AbstractReader
|
||||
$this->zip = new \ZipArchive();
|
||||
|
||||
if ($this->zip->open($filePath) === true) {
|
||||
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
|
||||
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->getOptions()->getTempFolder());
|
||||
|
||||
if ($this->sharedStringsHelper->hasSharedStrings()) {
|
||||
// Extracts all the strings from the sheets for easy access in the future
|
||||
$this->sharedStringsHelper->extractSharedStrings();
|
||||
}
|
||||
|
||||
$this->sheetIterator = new SheetIterator($filePath, $this->sharedStringsHelper, $this->globalFunctionsHelper, $this->shouldFormatDates);
|
||||
$this->sheetIterator = new SheetIterator($filePath, $this->getOptions(), $this->sharedStringsHelper, $this->globalFunctionsHelper);
|
||||
} else {
|
||||
throw new IOException("Could not open $filePath for reading.");
|
||||
}
|
||||
@@ -80,7 +90,7 @@ class Reader extends AbstractReader
|
||||
*
|
||||
* @return SheetIterator To iterate over sheets
|
||||
*/
|
||||
public function getConcreteSheetIterator()
|
||||
protected function getConcreteSheetIterator()
|
||||
{
|
||||
return $this->sheetIterator;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\XLSX;
|
||||
|
||||
/**
|
||||
* Class ReaderOptions
|
||||
* This class is used to customize the reader's behavior
|
||||
*
|
||||
* @package Box\Spout\Reader\XLSX
|
||||
*/
|
||||
class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions
|
||||
{
|
||||
/** @var string|null Temporary folder where the temporary files will be created */
|
||||
protected $tempFolder = null;
|
||||
|
||||
/**
|
||||
* @return string|null Temporary folder where the temporary files will be created
|
||||
*/
|
||||
public function getTempFolder()
|
||||
{
|
||||
return $this->tempFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $tempFolder Temporary folder where the temporary files will be created
|
||||
* @return ReaderOptions
|
||||
*/
|
||||
public function setTempFolder($tempFolder)
|
||||
{
|
||||
$this->tempFolder = $tempFolder;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
use Box\Spout\Reader\XLSX\Helper\CellHelper;
|
||||
use Box\Spout\Reader\XLSX\Helper\CellValueFormatter;
|
||||
use Box\Spout\Reader\XLSX\Helper\StyleHelper;
|
||||
use Box\Spout\Reader\Common\XMLProcessor;
|
||||
|
||||
/**
|
||||
* Class RowIterator
|
||||
@@ -26,6 +27,7 @@ class RowIterator implements IteratorInterface
|
||||
/** Definition of XML attributes used to parse data */
|
||||
const XML_ATTRIBUTE_REF = 'ref';
|
||||
const XML_ATTRIBUTE_SPANS = 'spans';
|
||||
const XML_ATTRIBUTE_ROW_INDEX = 'r';
|
||||
const XML_ATTRIBUTE_CELL_INDEX = 'r';
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
@@ -37,15 +39,24 @@ class RowIterator implements IteratorInterface
|
||||
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
|
||||
protected $xmlReader;
|
||||
|
||||
/** @var \Box\Spout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
|
||||
protected $xmlProcessor;
|
||||
|
||||
/** @var Helper\CellValueFormatter Helper to format cell values */
|
||||
protected $cellValueFormatter;
|
||||
|
||||
/** @var Helper\StyleHelper $styleHelper Helper to work with styles */
|
||||
protected $styleHelper;
|
||||
|
||||
/** @var int Number of read rows */
|
||||
/**
|
||||
* TODO: This variable can be deleted when row indices get preserved
|
||||
* @var int Number of read rows
|
||||
*/
|
||||
protected $numReadRows = 0;
|
||||
|
||||
/** @var array Contains the data for the currently processed row (key = cell index, value = cell value) */
|
||||
protected $currentlyProcessedRowData = [];
|
||||
|
||||
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
|
||||
protected $rowDataBuffer = null;
|
||||
|
||||
@@ -55,16 +66,25 @@ class RowIterator implements IteratorInterface
|
||||
/** @var int The number of columns the sheet has (0 meaning undefined) */
|
||||
protected $numColumns = 0;
|
||||
|
||||
/** @var bool Whether empty rows should be returned or skipped */
|
||||
protected $shouldPreserveEmptyRows;
|
||||
|
||||
/** @var int Last row index processed (one-based) */
|
||||
protected $lastRowIndexProcessed = 0;
|
||||
|
||||
/** @var int Row index to be processed next (one-based) */
|
||||
protected $nextRowIndexToBeProcessed = 0;
|
||||
|
||||
/** @var int Last column index processed (zero-based) */
|
||||
protected $lastColumnIndexProcessed = -1;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
*/
|
||||
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates)
|
||||
public function __construct($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
|
||||
@@ -72,7 +92,17 @@ class RowIterator implements IteratorInterface
|
||||
$this->xmlReader = new XMLReader();
|
||||
|
||||
$this->styleHelper = new StyleHelper($filePath);
|
||||
$this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $shouldFormatDates);
|
||||
$this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $options->shouldFormatDates());
|
||||
|
||||
$this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
|
||||
|
||||
// Register all callbacks to process different nodes when reading the XML file
|
||||
$this->xmlProcessor = new XMLProcessor($this->xmlReader);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_DIMENSION, XMLProcessor::NODE_TYPE_START, [$this, 'processDimensionStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
|
||||
$this->xmlProcessor->registerCallback(self::XML_NODE_WORKSHEET, XMLProcessor::NODE_TYPE_END, [$this, 'processWorksheetEndingNode']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,6 +134,8 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
$this->numReadRows = 0;
|
||||
$this->lastRowIndexProcessed = 0;
|
||||
$this->nextRowIndexToBeProcessed = 0;
|
||||
$this->rowDataBuffer = null;
|
||||
$this->hasReachedEndOfFile = false;
|
||||
$this->numColumns = 0;
|
||||
@@ -115,7 +147,7 @@ class RowIterator implements IteratorInterface
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
@@ -123,7 +155,7 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element. Empty rows will be skipped.
|
||||
* Move forward to next element. Reads data describing the next unprocessed row.
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void
|
||||
@@ -132,69 +164,165 @@ class RowIterator implements IteratorInterface
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$rowData = [];
|
||||
$this->nextRowIndexToBeProcessed++;
|
||||
|
||||
if ($this->doesNeedDataForNextRowToBeProcessed()) {
|
||||
$this->readDataForNextRow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we need data for the next row to be processed.
|
||||
* We don't need to read data if:
|
||||
* we have already read at least one row
|
||||
* AND
|
||||
* we need to preserve empty rows
|
||||
* AND
|
||||
* the last row that was read is not the row that need to be processed
|
||||
* (i.e. if we need to return empty rows)
|
||||
*
|
||||
* @return bool Whether we need data for the next row to be processed.
|
||||
*/
|
||||
protected function doesNeedDataForNextRowToBeProcessed()
|
||||
{
|
||||
$hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
|
||||
|
||||
return (
|
||||
!$hasReadAtLeastOneRow ||
|
||||
!$this->shouldPreserveEmptyRows ||
|
||||
$this->lastRowIndexProcessed < $this->nextRowIndexToBeProcessed
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
|
||||
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
|
||||
*/
|
||||
protected function readDataForNextRow()
|
||||
{
|
||||
$this->currentlyProcessedRowData = [];
|
||||
|
||||
try {
|
||||
while ($this->xmlReader->read()) {
|
||||
if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_DIMENSION)) {
|
||||
// Read dimensions of the sheet
|
||||
$dimensionRef = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
|
||||
if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
|
||||
$lastCellIndex = $matches[1];
|
||||
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
|
||||
}
|
||||
|
||||
} else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) {
|
||||
// Start of the row description
|
||||
|
||||
// Reset index of the last processed column
|
||||
$this->lastColumnIndexProcessed = -1;
|
||||
|
||||
// Read spans info if present
|
||||
$numberOfColumnsForRow = $this->numColumns;
|
||||
$spans = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
|
||||
if ($spans) {
|
||||
list(, $numberOfColumnsForRow) = explode(':', $spans);
|
||||
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
|
||||
}
|
||||
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
|
||||
|
||||
} else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
|
||||
// Start of a cell description
|
||||
$currentColumnIndex = $this->getCellIndex($this->xmlReader);
|
||||
|
||||
$node = $this->xmlReader->expand();
|
||||
$rowData[$currentColumnIndex] = $this->getCellValue($node);
|
||||
|
||||
$this->lastColumnIndexProcessed = $currentColumnIndex;
|
||||
|
||||
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
|
||||
// End of the row description
|
||||
// If needed, we fill the empty cells
|
||||
$rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
|
||||
$this->numReadRows++;
|
||||
break;
|
||||
|
||||
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_WORKSHEET)) {
|
||||
// The closing "</worksheet>" marks the end of the file
|
||||
$this->hasReachedEndOfFile = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->xmlProcessor->readUntilStopped();
|
||||
} catch (XMLProcessingException $exception) {
|
||||
throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
|
||||
}
|
||||
|
||||
$this->rowDataBuffer = $rowData;
|
||||
$this->rowDataBuffer = $this->currentlyProcessedRowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<c>" tag
|
||||
* @return int
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<dimension>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processDimensionStartingNode($xmlReader)
|
||||
{
|
||||
// Read dimensions of the sheet
|
||||
$dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
|
||||
if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
|
||||
$lastCellIndex = $matches[1];
|
||||
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
|
||||
}
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowStartingNode($xmlReader)
|
||||
{
|
||||
// Reset index of the last processed column
|
||||
$this->lastColumnIndexProcessed = -1;
|
||||
|
||||
// Mark the last processed row as the one currently being read
|
||||
$this->lastRowIndexProcessed = $this->getRowIndex($xmlReader);
|
||||
|
||||
// Read spans info if present
|
||||
$numberOfColumnsForRow = $this->numColumns;
|
||||
$spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
|
||||
if ($spans) {
|
||||
list(, $numberOfColumnsForRow) = explode(':', $spans);
|
||||
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
|
||||
}
|
||||
|
||||
$this->currentlyProcessedRowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<cell>" starting node
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processCellStartingNode($xmlReader)
|
||||
{
|
||||
$currentColumnIndex = $this->getColumnIndex($xmlReader);
|
||||
|
||||
$node = $xmlReader->expand();
|
||||
$this->currentlyProcessedRowData[$currentColumnIndex] = $this->getCellValue($node);
|
||||
$this->lastColumnIndexProcessed = $currentColumnIndex;
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processRowEndingNode()
|
||||
{
|
||||
// if the fetched row is empty and we don't want to preserve it..,
|
||||
if (!$this->shouldPreserveEmptyRows && $this->isEmptyRow($this->currentlyProcessedRowData)) {
|
||||
// ... skip it
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
|
||||
$this->numReadRows++;
|
||||
|
||||
// If needed, we fill the empty cells
|
||||
if ($this->numColumns === 0) {
|
||||
$this->currentlyProcessedRowData = CellHelper::fillMissingArrayIndexes($this->currentlyProcessedRowData);
|
||||
}
|
||||
|
||||
// at this point, we have all the data we need for the row
|
||||
// so that we can populate the buffer
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
protected function processWorksheetEndingNode()
|
||||
{
|
||||
// The closing "</worksheet>" marks the end of the file
|
||||
$this->hasReachedEndOfFile = true;
|
||||
|
||||
return XMLProcessor::PROCESSING_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" node
|
||||
* @return int Row index
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
|
||||
*/
|
||||
protected function getCellIndex($xmlReader)
|
||||
protected function getRowIndex($xmlReader)
|
||||
{
|
||||
// Get "r" attribute if present (from something like <row r="3"...>
|
||||
$currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX);
|
||||
|
||||
return ($currentRowIndex !== null) ?
|
||||
intval($currentRowIndex) :
|
||||
$this->lastRowIndexProcessed + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<c>" node
|
||||
* @return int Column index
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
|
||||
*/
|
||||
protected function getColumnIndex($xmlReader)
|
||||
{
|
||||
// Get "r" attribute if present (from something like <c r="A1"...>
|
||||
$currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
|
||||
@@ -216,25 +344,53 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element, from the buffer.
|
||||
* @param array $rowData
|
||||
* @return bool Whether the given row is empty
|
||||
*/
|
||||
protected function isEmptyRow($rowData)
|
||||
{
|
||||
return (count($rowData) === 1 && $rowData[0] === '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element, either an empty row or from the buffer.
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->rowDataBuffer;
|
||||
$rowDataForRowToBeProcessed = $this->rowDataBuffer;
|
||||
|
||||
if ($this->shouldPreserveEmptyRows) {
|
||||
// when we need to preserve empty rows, we will either return
|
||||
// an empty row or the last row read. This depends whether the
|
||||
// index of last row that was read matches the index of the last
|
||||
// row whose value should be returned.
|
||||
if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) {
|
||||
// return empty row if mismatch between last processed row
|
||||
// and the row that needs to be returned
|
||||
$rowDataForRowToBeProcessed = [''];
|
||||
}
|
||||
}
|
||||
|
||||
return $rowDataForRowToBeProcessed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
* Return the key of the current element. Here, the row index.
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->numReadRows;
|
||||
// TODO: This should return $this->nextRowIndexToBeProcessed
|
||||
// but to avoid a breaking change, the return value for
|
||||
// this function has been kept as the number of rows read.
|
||||
return $this->shouldPreserveEmptyRows ?
|
||||
$this->nextRowIndexToBeProcessed :
|
||||
$this->numReadRows;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,14 +24,14 @@ class Sheet implements SheetInterface
|
||||
/**
|
||||
* @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 Helper\SharedStringsHelper Helper to work with shared strings
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param string $sheetName Name of the sheet
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param Helper\SharedStringsHelper Helper to work with shared strings
|
||||
*/
|
||||
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates, $sheetIndex, $sheetName)
|
||||
public function __construct($filePath, $sheetDataXMLFilePath, $sheetIndex, $sheetName, $options, $sharedStringsHelper)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates);
|
||||
$this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper);
|
||||
$this->index = $sheetIndex;
|
||||
$this->name = $sheetName;
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@ class SheetIterator implements IteratorInterface
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options
|
||||
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper $sharedStringsHelper
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
|
||||
*/
|
||||
public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates)
|
||||
public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper)
|
||||
{
|
||||
// Fetch all available sheets
|
||||
$sheetHelper = new SheetHelper($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates);
|
||||
$sheetHelper = new SheetHelper($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper);
|
||||
$this->sheets = $sheetHelper->getSheets();
|
||||
|
||||
if (count($this->sheets) === 0) {
|
||||
@@ -53,7 +53,7 @@ class SheetIterator implements IteratorInterface
|
||||
* Checks if current position is valid
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace Box\Spout\Writer;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||
use Box\Spout\Common\Exception\SpoutException;
|
||||
use Box\Spout\Common\Helper\FileSystemHelper;
|
||||
use Box\Spout\Writer\Exception\WriterAlreadyOpenedException;
|
||||
use Box\Spout\Writer\Exception\WriterNotOpenedException;
|
||||
use Box\Spout\Writer\Style\StyleBuilder;
|
||||
@@ -199,13 +201,23 @@ abstract class AbstractWriter implements WriterInterface
|
||||
* @return AbstractWriter
|
||||
* @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\SpoutException If anything else goes wrong while writing data
|
||||
*/
|
||||
public function addRow(array $dataRow)
|
||||
{
|
||||
if ($this->isWriterOpened) {
|
||||
// empty $dataRow should not add an empty line
|
||||
if (!empty($dataRow)) {
|
||||
$this->addRowToWriter($dataRow, $this->rowStyle);
|
||||
try {
|
||||
$this->addRowToWriter($dataRow, $this->rowStyle);
|
||||
} catch (SpoutException $e) {
|
||||
// if an exception occurs while writing data,
|
||||
// close the writer and remove all files created so far.
|
||||
$this->closeAndAttemptToCleanupAllFiles();
|
||||
|
||||
// re-throw the exception to alert developers of the error
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
|
||||
@@ -346,5 +358,24 @@ abstract class AbstractWriter implements WriterInterface
|
||||
|
||||
$this->isWriterOpened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the writer and attempts to cleanup all files that were
|
||||
* created during the writing process (temp files & final file).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function closeAndAttemptToCleanupAllFiles()
|
||||
{
|
||||
// close the writer, which should remove all temp files
|
||||
$this->close();
|
||||
|
||||
// remove output file if it was created
|
||||
if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) {
|
||||
$outputFolderPath = dirname($this->outputFilePath);
|
||||
$fileSystemHelper = new FileSystemHelper($outputFolderPath);
|
||||
$fileSystemHelper->deleteFile($this->outputFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ abstract class AbstractStyleHelper
|
||||
protected function applyWrapTextIfCellContainsNewLine($style, $dataRow)
|
||||
{
|
||||
// if the "wrap text" option is already set, no-op
|
||||
if ($style->shouldWrapText()) {
|
||||
if ($style->hasSetWrapText()) {
|
||||
return $style;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,15 @@ class CellHelper
|
||||
return self::$columnIndexToCellIndexCache[$originalColumnIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return bool Whether the given value is considered "empty"
|
||||
*/
|
||||
public static function isEmpty($value)
|
||||
{
|
||||
return ($value === null || $value === '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return bool Whether the given value is a non empty string
|
||||
|
||||
@@ -75,7 +75,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
||||
*/
|
||||
protected function createRootFolder()
|
||||
{
|
||||
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('ods'));
|
||||
$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class Worksheet implements WorksheetInterface
|
||||
|
||||
/**
|
||||
* @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet
|
||||
* @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
|
||||
* @param string $worksheetFilesFolder Temporary folder where the files to create the ODS will be stored
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
||||
*/
|
||||
public function __construct($externalSheet, $worksheetFilesFolder)
|
||||
|
||||
@@ -20,7 +20,7 @@ class Writer extends AbstractMultiSheetsWriter
|
||||
/** @var string Temporary folder where the files to create the ODS will be stored */
|
||||
protected $tempFolder;
|
||||
|
||||
/** @var Internal\Workbook The workbook for the XLSX file */
|
||||
/** @var Internal\Workbook The workbook for the ODS file */
|
||||
protected $book;
|
||||
|
||||
/**
|
||||
|
||||
@@ -115,7 +115,7 @@ class Style
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldApplyBorder()
|
||||
{
|
||||
@@ -123,7 +123,7 @@ class Style
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontBold()
|
||||
{
|
||||
@@ -142,7 +142,7 @@ class Style
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontItalic()
|
||||
{
|
||||
@@ -161,7 +161,7 @@ class Style
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontUnderline()
|
||||
{
|
||||
@@ -180,7 +180,7 @@ class Style
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function isFontStrikethrough()
|
||||
{
|
||||
@@ -261,7 +261,7 @@ class Style
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldWrapText()
|
||||
{
|
||||
@@ -269,15 +269,24 @@ class Style
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|void $shouldWrap Should the text be wrapped
|
||||
* @return Style
|
||||
*/
|
||||
public function setShouldWrapText()
|
||||
public function setShouldWrapText($shouldWrap = true)
|
||||
{
|
||||
$this->shouldWrapText = true;
|
||||
$this->shouldWrapText = $shouldWrap;
|
||||
$this->hasSetWrapText = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSetWrapText()
|
||||
{
|
||||
return $this->hasSetWrapText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether specific font properties should be applied
|
||||
*/
|
||||
|
||||
@@ -109,15 +109,15 @@ class StyleBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the text wrap in the cell if it's too long or
|
||||
* on multiple lines.
|
||||
* Makes the text wrap in the cell if requested
|
||||
*
|
||||
* @api
|
||||
* @param bool $shouldWrap Should the text be wrapped
|
||||
* @return StyleBuilder
|
||||
*/
|
||||
public function setShouldWrapText()
|
||||
public function setShouldWrapText($shouldWrap = true)
|
||||
{
|
||||
$this->style->setShouldWrapText();
|
||||
$this->style->setShouldWrapText($shouldWrap);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class StyleBuilder
|
||||
*
|
||||
* @api
|
||||
* @param string $color ARGB color (@see Color)
|
||||
* @return StyleBuilder
|
||||
* @return StyleBuilder
|
||||
*/
|
||||
public function setBackgroundColor($color)
|
||||
{
|
||||
|
||||
@@ -94,7 +94,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
||||
*/
|
||||
protected function createRootFolder()
|
||||
{
|
||||
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx', true));
|
||||
$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('xlsx', true));
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,38 @@ EOD;
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
||||
*/
|
||||
public function addRow($dataRow, $style)
|
||||
{
|
||||
if (!$this->isEmptyRow($dataRow)) {
|
||||
$this->addNonEmptyRow($dataRow, $style);
|
||||
}
|
||||
|
||||
$this->lastWrittenRowIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given row is empty
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written. Cannot be empty.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @return bool Whether the given row is empty
|
||||
*/
|
||||
private function isEmptyRow($dataRow)
|
||||
{
|
||||
$numCells = count($dataRow);
|
||||
return ($numCells === 1 && CellHelper::isEmpty($dataRow[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds non empty row to the worksheet.
|
||||
*
|
||||
* @param array $dataRow Array containing data to be written. Cannot be empty.
|
||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
||||
*/
|
||||
private function addNonEmptyRow($dataRow, $style)
|
||||
{
|
||||
$cellNumber = 0;
|
||||
$rowIndex = $this->lastWrittenRowIndex + 1;
|
||||
@@ -149,9 +181,6 @@ EOD;
|
||||
if ($wasWriteSuccessful === false) {
|
||||
throw new IOException("Unable to write data in {$this->worksheetFilePath}");
|
||||
}
|
||||
|
||||
// only update the count if the write worked
|
||||
$this->lastWrittenRowIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -115,29 +115,40 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @return void
|
||||
*/
|
||||
public function dataProviderForTestReadShouldSkipEmptyLines()
|
||||
public function testReadShouldSkipEmptyLinesIfShouldPreserveEmptyRowsNotSet()
|
||||
{
|
||||
return [
|
||||
['csv_with_empty_line.csv'],
|
||||
['csv_with_empty_last_line.csv'],
|
||||
$allRows = $this->getAllRowsForFile('csv_with_multiple_empty_lines.csv');
|
||||
|
||||
$expectedRows = [
|
||||
// skipped row here
|
||||
['csv--21', 'csv--22', 'csv--23'],
|
||||
// skipped row here
|
||||
['csv--41', 'csv--42', 'csv--43'],
|
||||
// skipped row here
|
||||
// last row empty
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderForTestReadShouldSkipEmptyLines
|
||||
*
|
||||
* @param string $fileName
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSkipEmptyLines($fileName)
|
||||
public function testReadShouldReturnEmptyLinesIfShouldPreserveEmptyRowsSet()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile($fileName);
|
||||
$allRows = $this->getAllRowsForFile(
|
||||
'csv_with_multiple_empty_lines.csv',
|
||||
',', '"', "\n", EncodingHelper::ENCODING_UTF8,
|
||||
$shouldPreserveEmptyRows = true
|
||||
);
|
||||
|
||||
$expectedRows = [
|
||||
['csv--11', 'csv--12', 'csv--13'],
|
||||
['csv--31', 'csv--32', 'csv--33'],
|
||||
[''],
|
||||
['csv--21', 'csv--22', 'csv--23'],
|
||||
[''],
|
||||
['csv--41', 'csv--42', 'csv--43'],
|
||||
[''],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
@@ -204,6 +215,21 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('This is, a comma', $allRows[0][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadCustomEOLs()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('csv_with_CR_EOL.csv', ',', '"', "\r");
|
||||
|
||||
$expectedRows = [
|
||||
['csv--11', 'csv--12', 'csv--13'],
|
||||
['csv--21', 'csv--22', 'csv--23'],
|
||||
['csv--31', 'csv--32', 'csv--33'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -236,7 +262,7 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
*/
|
||||
public function testReadShouldSkipBom($fileName, $fileEncoding)
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile($fileName, ',', '"', $fileEncoding);
|
||||
$allRows = $this->getAllRowsForFile($fileName, ',', '"', "\n", $fileEncoding);
|
||||
|
||||
$expectedRows = [
|
||||
['csv--11', 'csv--12', 'csv--13'],
|
||||
@@ -275,6 +301,7 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$allRows = [];
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper|\PHPUnit_Framework_MockObject_MockObject $helperStub */
|
||||
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
|
||||
->setMethods(['function_exists'])
|
||||
->getMock();
|
||||
@@ -405,14 +432,18 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
* @param string $fileName
|
||||
* @param string|void $fieldDelimiter
|
||||
* @param string|void $fieldEnclosure
|
||||
* @param string|void $endOfLineCharacter
|
||||
* @param string|void $encoding
|
||||
* @param bool|void $shouldPreserveEmptyRows
|
||||
* @return array All the read rows the given file
|
||||
*/
|
||||
private function getAllRowsForFile(
|
||||
$fileName,
|
||||
$fieldDelimiter = ',',
|
||||
$fieldEnclosure = '"',
|
||||
$encoding = EncodingHelper::ENCODING_UTF8)
|
||||
$endOfLineCharacter = "\n",
|
||||
$encoding = EncodingHelper::ENCODING_UTF8,
|
||||
$shouldPreserveEmptyRows = false)
|
||||
{
|
||||
$allRows = [];
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
@@ -422,7 +453,9 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$reader
|
||||
->setFieldDelimiter($fieldDelimiter)
|
||||
->setFieldEnclosure($fieldEnclosure)
|
||||
->setEndOfLineCharacter($endOfLineCharacter)
|
||||
->setEncoding($encoding)
|
||||
->setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
|
||||
->open($resourcePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
|
||||
@@ -436,51 +469,6 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
return $allRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function dataProviderForTestReadCustomEOL()
|
||||
{
|
||||
return [
|
||||
['csv_with_CR_EOL.csv', "\r"],
|
||||
['csv_standard.csv', "\n"],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderForTestReadCustomEOL
|
||||
*
|
||||
* @param string $fileName
|
||||
* @param string $customEOL
|
||||
* @return void
|
||||
*/
|
||||
public function testReadCustomEOLs($fileName, $customEOL)
|
||||
{
|
||||
$allRows = [];
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
|
||||
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader
|
||||
->setEndOfLineCharacter($customEOL)
|
||||
->open($resourcePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
$allRows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
|
||||
$expectedRows = [
|
||||
['csv--11', 'csv--12', 'csv--13'],
|
||||
['csv--21', 'csv--22', 'csv--23'],
|
||||
['csv--31', 'csv--32', 'csv--33'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -78,6 +78,19 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals([['foo']], $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSupportNumberRowsRepeated()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_number_rows_repeated.ods');
|
||||
$expectedRows = [
|
||||
['foo', 10.43],
|
||||
['foo', 10.43],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -211,15 +224,39 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSkipEmptyRow()
|
||||
public function testReadShouldSkipEmptyRowsIfShouldPreserveEmptyRowsNotSet()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_row.ods');
|
||||
$this->assertEquals(2, count($allRows), 'There should be only 2 rows, because the empty row is skipped');
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_rows.ods');
|
||||
|
||||
$this->assertEquals(3, count($allRows), 'There should be only 3 rows, because the empty rows are skipped');
|
||||
|
||||
$expectedRows = [
|
||||
['ods--11', 'ods--12', 'ods--13'],
|
||||
// row skipped here
|
||||
// skipped row here
|
||||
['ods--21', 'ods--22', 'ods--23'],
|
||||
// skipped row here
|
||||
// skipped row here
|
||||
['ods--51', 'ods--52', 'ods--53'],
|
||||
['ods--61', 'ods--62', 'ods--63'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldReturnEmptyLinesIfShouldPreserveEmptyRowsSet()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_rows.ods', false, true);
|
||||
|
||||
$this->assertEquals(6, count($allRows), 'There should be 6 rows');
|
||||
|
||||
$expectedRows = [
|
||||
[''],
|
||||
['ods--21', 'ods--22', 'ods--23'],
|
||||
[''],
|
||||
[''],
|
||||
['ods--51', 'ods--52', 'ods--53'],
|
||||
['ods--61', 'ods--62', 'ods--63'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
@@ -485,15 +522,18 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @param bool|void $shouldFormatDates
|
||||
* @param bool|void $shouldPreserveEmptyRows
|
||||
* @return array All the read rows the given file
|
||||
*/
|
||||
private function getAllRowsForFile($fileName, $shouldFormatDates = false)
|
||||
private function getAllRowsForFile($fileName, $shouldFormatDates = false, $shouldPreserveEmptyRows = false)
|
||||
{
|
||||
$allRows = [];
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
|
||||
/** @var \Box\Spout\Reader\ODS\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::ODS);
|
||||
$reader->setShouldFormatDates($shouldFormatDates);
|
||||
$reader->setShouldPreserveEmptyRows($shouldPreserveEmptyRows);
|
||||
$reader->open($resourcePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
|
||||
|
||||
@@ -29,6 +29,8 @@ class DateFormatHelperTest extends \PHPUnit_Framework_TestCase
|
||||
['[$USD-F480]hh:mm AM/PM', 'h:i A'],
|
||||
['"Day " d', '\\D\\a\\y\\ j'],
|
||||
['yy "Year" m "Month"', 'y \\Y\\e\\a\\r n \\M\\o\\n\\t\\h'],
|
||||
['mmm-yy;@', 'M-y'],
|
||||
['[$-409]hh:mm AM/PM;"foo"@', 'h:i A'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,16 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* @param array $styleAttributes
|
||||
* @param array|void $styleAttributes
|
||||
* @param array|void $customNumberFormats
|
||||
* @return StyleHelper
|
||||
*/
|
||||
private function getStyleHelperMock($styleAttributes, $customNumberFormats = [])
|
||||
private function getStyleHelperMock($styleAttributes = [], $customNumberFormats = [])
|
||||
{
|
||||
/** @var StyleHelper $styleHelper */
|
||||
$styleHelper = $this->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\StyleHelper')
|
||||
->setConstructorArgs(['/path/to/file.xlsx'])
|
||||
->setMethods(['getCustomNumberFormats', 'getStylesAttributes'])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$styleHelper->method('getStylesAttributes')->willReturn($styleAttributes);
|
||||
@@ -34,27 +34,17 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
||||
*/
|
||||
public function testShouldFormatNumericValueAsDateWithDefaultStyle()
|
||||
{
|
||||
$styleHelper = $this->getStyleHelperMock([]);
|
||||
$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]]);
|
||||
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => false, 'numFmtId' => 14]]);
|
||||
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||
$this->assertFalse($shouldFormatAsDate);
|
||||
}
|
||||
@@ -69,6 +59,26 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertFalse($shouldFormatAsDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testShouldFormatNumericValueAsDateWithNonDateBuiltinFormat()
|
||||
{
|
||||
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 9]]);
|
||||
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||
$this->assertFalse($shouldFormatAsDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testShouldFormatNumericValueAsDateWithNoNumFmtId()
|
||||
{
|
||||
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => null]]);
|
||||
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||
$this->assertFalse($shouldFormatAsDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -84,6 +94,28 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testShouldFormatNumericValueAsDateWhenApplyNumberFormatNotSetAndUsingBuiltinDateFormat()
|
||||
{
|
||||
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => null, 'numFmtId' => 14]]);
|
||||
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||
|
||||
$this->assertTrue($shouldFormatAsDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testShouldFormatNumericValueAsDateWhenApplyNumberFormatNotSetAndUsingBuiltinNonDateFormat()
|
||||
{
|
||||
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => null, 'numFmtId' => 9]]);
|
||||
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
|
||||
|
||||
$this->assertFalse($shouldFormatAsDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -106,6 +138,7 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
||||
['[$-409]d\-mmm\-yy;@', true],
|
||||
['[$-409]d\-mmm\-yyyy;@', true],
|
||||
['mm/dd/yy;@', true],
|
||||
['MM/DD/YY;@', true],
|
||||
['[$-F800]dddd\,\ mmmm\ dd\,\ yyyy', true],
|
||||
['m/d;@', true],
|
||||
['m/d/yy;@', true],
|
||||
@@ -117,9 +150,11 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
||||
['[$-409]m/d/yy\ h:mm\ AM/PM;@', true],
|
||||
['m/d/yy\ h:mm;@', true],
|
||||
['[$-409]mmmmm;@', true],
|
||||
['[$-409]MMmmM;@', true],
|
||||
['[$-409]mmmmm\-yy;@', true],
|
||||
['m/d/yyyy;@', true],
|
||||
['[$-409]m/d/yy\--h:mm;@', true],
|
||||
['General', false],
|
||||
['GENERAL', false],
|
||||
['\ma\yb\e', false],
|
||||
['[Red]foo;', false],
|
||||
|
||||
@@ -279,6 +279,21 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldApplyCustomDateFormatNumberEvenIfApplyNumberFormatNotSpecified()
|
||||
{
|
||||
$shouldFormatDates = true;
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_custom_date_formats_and_no_apply_number_format.xlsx', $shouldFormatDates);
|
||||
|
||||
$expectedRows = [
|
||||
// "General", "GENERAL", "MM/DD/YYYY", "MM/dd/YYYY", "H:MM:SS"
|
||||
['42382', '42382', '01/13/2016', '01/13/2016', '4:43:25'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -337,16 +352,39 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSkipEmptyRows()
|
||||
public function testReadShouldSkipEmptyRowsIfShouldPreserveEmptyRowsNotSet()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_row.xlsx');
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_rows_and_missing_row_index.xlsx');
|
||||
|
||||
$this->assertEquals(2, count($allRows), 'There should be only 2 rows, because the empty row is skipped');
|
||||
$this->assertEquals(3, count($allRows), 'There should be only 3 rows, because the empty rows are skipped');
|
||||
|
||||
$expectedRows = [
|
||||
['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'],
|
||||
// skipped row here
|
||||
['s1--A3', 's1--B3', 's1--C3', 's1--D3', 's1--E3'],
|
||||
['s1--A2', 's1--B2', 's1--C2'],
|
||||
// skipped row here
|
||||
// skipped row here
|
||||
['s1--A5', 's1--B5', 's1--C5'],
|
||||
['s1--A6', 's1--B6', 's1--C6'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldReturnEmptyLinesIfShouldPreserveEmptyRowsSet()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_rows_and_missing_row_index.xlsx', false, true);
|
||||
|
||||
$this->assertEquals(6, count($allRows), 'There should be 6 rows');
|
||||
|
||||
$expectedRows = [
|
||||
[''],
|
||||
['s1--A2', 's1--B2', 's1--C2'],
|
||||
[''],
|
||||
[''],
|
||||
['s1--A5', 's1--B5', 's1--C5'],
|
||||
['s1--A6', 's1--B6', 's1--C6'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
@@ -580,15 +618,18 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @param bool|void $shouldFormatDates
|
||||
* @param bool|void $shouldPreserveEmptyRows
|
||||
* @return array All the read rows the given file
|
||||
*/
|
||||
private function getAllRowsForFile($fileName, $shouldFormatDates = false)
|
||||
private function getAllRowsForFile($fileName, $shouldFormatDates = false, $shouldPreserveEmptyRows = false)
|
||||
{
|
||||
$allRows = [];
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
|
||||
/** @var \Box\Spout\Reader\XLSX\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->setShouldFormatDates($shouldFormatDates);
|
||||
$reader->setShouldPreserveEmptyRows($shouldPreserveEmptyRows);
|
||||
$reader->open($resourcePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
|
||||
|
||||
@@ -18,6 +18,9 @@ trait TestUsingResource
|
||||
/** @var string Path to the test resources folder, that does not have writing permissions */
|
||||
private $generatedUnwritableResourcesPath = 'tests/resources/generated/unwritable';
|
||||
|
||||
/** @var string Path to the test temp folder */
|
||||
private $tempFolderPath = 'tests/resources/generated/temp';
|
||||
|
||||
/**
|
||||
* @param string $resourceName
|
||||
* @return string|null Path of the resource who matches the given name or null if resource not found
|
||||
@@ -86,6 +89,48 @@ trait TestUsingResource
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Path of the temp folder
|
||||
*/
|
||||
protected function getTempFolderPath()
|
||||
{
|
||||
return realpath($this->tempFolderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function recreateTempFolder()
|
||||
{
|
||||
if (file_exists($this->tempFolderPath)) {
|
||||
$this->deleteFolderRecursively($this->tempFolderPath);
|
||||
}
|
||||
|
||||
mkdir($this->tempFolderPath, 0777, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $folderPath
|
||||
* @return void
|
||||
*/
|
||||
private function deleteFolderRecursively($folderPath)
|
||||
{
|
||||
$itemIterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($itemIterator as $item) {
|
||||
if ($item->isDir()) {
|
||||
rmdir($item->getPathname());
|
||||
} else {
|
||||
unlink($item->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the OS on which PHP is installed is Windows
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,23 @@ class CellHelperTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedCellIndex, CellHelper::getCellIndexFromColumnIndex($columnIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function testIsEmpty()
|
||||
{
|
||||
$this->assertTrue(CellHelper::isEmpty(null));
|
||||
$this->assertTrue(CellHelper::isEmpty(""));
|
||||
|
||||
$this->assertFalse(CellHelper::isEmpty("string"));
|
||||
$this->assertFalse(CellHelper::isEmpty(0));
|
||||
$this->assertFalse(CellHelper::isEmpty(1));
|
||||
$this->assertFalse(CellHelper::isEmpty(true));
|
||||
$this->assertFalse(CellHelper::isEmpty(false));
|
||||
$this->assertFalse(CellHelper::isEmpty(["string"]));
|
||||
$this->assertFalse(CellHelper::isEmpty(new \stdClass()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Box\Spout\Writer\ODS;
|
||||
|
||||
use Box\Spout\Common\Exception\SpoutException;
|
||||
use Box\Spout\Common\Type;
|
||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||
use Box\Spout\TestUsingResource;
|
||||
@@ -91,6 +92,39 @@ class WriterTest extends \PHPUnit_Framework_TestCase
|
||||
$this->writeToODSFile($dataRows, $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testAddRowShouldCleanupAllFilesIfExceptionIsThrown()
|
||||
{
|
||||
$fileName = 'test_add_row_should_cleanup_all_files_if_exception_thrown.ods';
|
||||
$dataRows = [
|
||||
['wrong'],
|
||||
[new \stdClass()],
|
||||
];
|
||||
|
||||
$this->createGeneratedFolderIfNeeded($fileName);
|
||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||
|
||||
$this->recreateTempFolder();
|
||||
$tempFolderPath = $this->getTempFolderPath();
|
||||
|
||||
/** @var \Box\Spout\Writer\ODS\Writer $writer */
|
||||
$writer = WriterFactory::create(Type::ODS);
|
||||
$writer->setTempFolder($tempFolderPath);
|
||||
$writer->openToFile($resourcePath);
|
||||
|
||||
try {
|
||||
$writer->addRows($dataRows);
|
||||
$this->fail('Exception should have been thrown');
|
||||
} catch (SpoutException $e) {
|
||||
$this->assertFalse(file_exists($fileName), 'Output file should have been deleted');
|
||||
|
||||
$numFiles = iterator_count(new \FilesystemIterator($tempFolderPath, \FilesystemIterator::SKIP_DOTS));
|
||||
$this->assertEquals(0, $numFiles, 'All temp files should have been deleted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Box\Spout\Writer\XLSX;
|
||||
|
||||
use Box\Spout\Common\Exception\SpoutException;
|
||||
use Box\Spout\Common\Type;
|
||||
use Box\Spout\TestUsingResource;
|
||||
use Box\Spout\Writer\WriterFactory;
|
||||
@@ -104,6 +105,39 @@ class WriterTest extends \PHPUnit_Framework_TestCase
|
||||
$this->writeToXLSXFile($dataRows, $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testAddRowShouldCleanupAllFilesIfExceptionIsThrown()
|
||||
{
|
||||
$fileName = 'test_add_row_should_cleanup_all_files_if_exception_thrown.xlsx';
|
||||
$dataRows = [
|
||||
['wrong'],
|
||||
[new \stdClass()],
|
||||
];
|
||||
|
||||
$this->createGeneratedFolderIfNeeded($fileName);
|
||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||
|
||||
$this->recreateTempFolder();
|
||||
$tempFolderPath = $this->getTempFolderPath();
|
||||
|
||||
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->setTempFolder($tempFolderPath);
|
||||
$writer->openToFile($resourcePath);
|
||||
|
||||
try {
|
||||
$writer->addRows($dataRows);
|
||||
$this->fail('Exception should have been thrown');
|
||||
} catch (SpoutException $e) {
|
||||
$this->assertFalse(file_exists($fileName), 'Output file should have been deleted');
|
||||
|
||||
$numFiles = iterator_count(new \FilesystemIterator($tempFolderPath, \FilesystemIterator::SKIP_DOTS));
|
||||
$this->assertEquals(0, $numFiles, 'All temp files should have been deleted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -242,7 +276,7 @@ class WriterTest extends \PHPUnit_Framework_TestCase
|
||||
['foo' => 'xlsx--11', 'bar' => 'xlsx--12'],
|
||||
];
|
||||
|
||||
$this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = true);
|
||||
$this->writeToXLSXFile($dataRows, $fileName);
|
||||
|
||||
foreach ($dataRows as $dataRow) {
|
||||
foreach ($dataRow as $cellValue) {
|
||||
@@ -251,6 +285,29 @@ class WriterTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testAddRowShouldNotWriteEmptyRows()
|
||||
{
|
||||
$fileName = 'test_add_row_should_not_write_empty_rows.xlsx';
|
||||
$dataRows = [
|
||||
[''],
|
||||
['xlsx--21', 'xlsx--22'],
|
||||
[''],
|
||||
[''],
|
||||
['xlsx--51', 'xlsx--52'],
|
||||
];
|
||||
|
||||
$this->writeToXLSXFile($dataRows, $fileName);
|
||||
|
||||
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'row r="2"');
|
||||
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'row r="5"');
|
||||
$this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'row r="1"');
|
||||
$this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'row r="3"');
|
||||
$this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'row r="4"');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
csv--11,csv--12,csv--13
|
||||
csv--31,csv--32,csv--33
|
||||
|
@@ -1,3 +0,0 @@
|
||||
csv--11,csv--12,csv--13
|
||||
|
||||
csv--31,csv--32,csv--33
|
||||
|
@@ -0,0 +1,5 @@
|
||||
|
||||
csv--21,csv--22,csv--23
|
||||
|
||||
csv--41,csv--42,csv--43
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user