Merge pull request #73 from box/iterators

Moved readers to iterators
This commit is contained in:
Adrien Loison 2015-07-27 00:26:26 -07:00
commit be3932af18
53 changed files with 1350 additions and 949 deletions

View File

@ -64,9 +64,10 @@ use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::CSV);
$reader->open($filePath);
while ($reader->hasNextRow()) {
$row = $reader->nextRow();
// do stuff
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($reader->getRowIterator() as $row) {
// do stuff
}
}
$reader->close();
@ -81,11 +82,8 @@ use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($filePath);
while ($reader->hasNextSheet()) {
$reader->nextSheet();
while ($reader->hasNextRow()) {
$row = $reader->nextRow();
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($reader->getRowIterator() as $row) {
// do stuff
}
}
@ -202,8 +200,7 @@ $sheets = $writer->getSheets();
If you rely on the sheet's name in your application, you can access it and customize it this way:
```php
// Accessing the sheet name when reading
while ($reader->hasNextSheet()) {
$sheet = $reader->nextSheet();
foreach ($reader->getSheetIterator() as $sheet) {
$sheetName = $sheet->getName();
}
@ -253,7 +250,7 @@ For information, the performance tests take about one hour to run (processing 2
When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them into several small temporary files that allows fast access.
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access.
#### How long does it take to generate a file with X rows?

View File

@ -5,7 +5,6 @@
colors="true"
convertErrorsToExceptions="false"
convertWarningsToExceptions="false"
strict="false"
verbose="false">
<testsuites>

View File

@ -63,7 +63,7 @@ class FileSystemHelper
$filePath = $parentFolderPath . '/' . $fileName;
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
if (!$wasCreationSuccessful) {
if ($wasCreationSuccessful === false) {
throw new IOException('Unable to create file: ' . $filePath);
}

View File

@ -160,7 +160,7 @@ class GlobalFunctionsHelper
* @see file_get_contents()
*
* @param string $filePath
* @return bool
* @return string
*/
public function file_get_contents($filePath)
{

View File

@ -4,7 +4,6 @@ namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
use Box\Spout\Reader\Exception\EndOfFileReachedException;
/**
* Class AbstractReader
@ -14,18 +13,9 @@ use Box\Spout\Reader\Exception\EndOfFileReachedException;
*/
abstract class AbstractReader implements ReaderInterface
{
/** @var int Used to keep track of the row index */
protected $currentRowIndex = 0;
/** @var bool Indicates whether the stream is currently open */
protected $isStreamOpened = false;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var array Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
@ -38,11 +28,11 @@ abstract class AbstractReader implements ReaderInterface
abstract protected function openReader($filePath);
/**
* Reads and returns next row if available.
* Returns an iterator to iterate over sheets.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
* @return \Iterator To iterate over sheets
*/
abstract protected function read();
abstract public function getConcreteSheetIterator();
/**
* Closes the reader. To be used after reading the file.
@ -80,9 +70,6 @@ abstract class AbstractReader implements ReaderInterface
}
}
$this->currentRowIndex = 0;
$this->hasReachedEndOfFile = false;
try {
$this->openReader($filePath);
$this->isStreamOpened = true;
@ -103,82 +90,18 @@ abstract class AbstractReader implements ReaderInterface
}
/**
* Returns whether all rows have been read (i.e. if we are at the end of the file).
* To know if the end of file has been reached, it uses a buffer. If the buffer is
* empty (meaning, nothing has been read or previous read line has been consumed), then
* it reads the next line, store it in the buffer for the next time or flip a variable if
* the end of file has been reached.
* Returns an iterator to iterate over sheets.
*
* @return bool Whether all rows have been read (i.e. if we are at the end of the file)
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If the stream was not opened first
* @return \Iterator To iterate over sheets
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
*/
public function hasNextRow()
public function getSheetIterator()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Stream should be opened first.');
throw new ReaderNotOpenedException('Reader should be opened first.');
}
if ($this->hasReachedEndOfFile) {
return false;
}
// if the buffer contains unprocessed row
if (!$this->isRowDataBufferEmpty()) {
return true;
}
// otherwise, try to read the next line line, and store it in the buffer
$this->rowDataBuffer = $this->read();
// if the buffer is still empty after reading a row, it means end of file was reached
$this->hasReachedEndOfFile = $this->isRowDataBufferEmpty();
return (!$this->hasReachedEndOfFile);
}
/**
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
* actually reading the file.
*
* @return array Array that contains the data for the read row
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
*/
public function nextRow()
{
if (!$this->hasNextRow()) {
throw new EndOfFileReachedException('End of file was reached. Cannot read more rows.');
}
// Get data from buffer (if the buffer was empty, it was filled by the call to hasNextRow())
$rowData = $this->rowDataBuffer;
// empty buffer to mark the row as consumed
$this->emptyRowDataBuffer();
$this->currentRowIndex++;
return $rowData;
}
/**
* Returns whether the buffer where the row data is stored is empty
*
* @return bool
*/
protected function isRowDataBufferEmpty()
{
return ($this->rowDataBuffer === null);
}
/**
* Empty the buffer that stores row data
*
* @return void
*/
protected function emptyRowDataBuffer()
{
$this->rowDataBuffer = null;
return $this->getConcreteSheetIterator();
}
/**
@ -190,6 +113,12 @@ abstract class AbstractReader implements ReaderInterface
{
if ($this->isStreamOpened) {
$this->closeReader();
$sheetIterator = $this->getConcreteSheetIterator();
if ($sheetIterator) {
$sheetIterator->end();
}
$this->isStreamOpened = false;
}
}

View File

@ -1,130 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
/**
* Class CSV
* This class provides support to read data from a CSV file.
*
* @package Box\Spout\Reader
*/
class CSV extends AbstractReader
{
const UTF8_BOM = "\xEF\xBB\xBF";
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @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 = '"';
/**
* Sets the field delimiter for the CSV
*
* @param string $fieldDelimiter Character that delimits fields
* @return CSV
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
return $this;
}
/**
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return CSV
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
return $this;
}
/**
* Opens the file at the given path to make it ready to be read.
* The file must be UTF-8 encoded.
* @TODO add encoding detection/conversion
*
* @param string $filePath Path of the CSV file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException
*/
protected function openReader($filePath)
{
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException('Could not open file ' . $filePath . ' for reading.');
}
$this->skipUtf8Bom();
}
/**
* This skips the UTF-8 BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*
* @return void
*/
protected function skipUtf8Bom()
{
$this->globalFunctionsHelper->rewind($this->filePointer);
$hasUtf8Bom = ($this->globalFunctionsHelper->fgets($this->filePointer, 4) === self::UTF8_BOM);
if ($hasUtf8Bom) {
// we skip the 2 first bytes (so start from the 3rd byte)
$this->globalFunctionsHelper->fseek($this->filePointer, 3);
} else {
// if no BOM, reset the pointer to read from the beginning
$this->globalFunctionsHelper->fseek($this->filePointer, 0);
}
}
/**
* Reads and returns next row if available.
* Empty rows are skipped.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
*/
protected function read()
{
$lineData = null;
if ($this->filePointer) {
do {
$lineData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, 0, $this->fieldDelimiter, $this->fieldEnclosure);
} while ($lineData && $this->isEmptyLine($lineData));
}
// When reaching the end of the file, return null instead of false
return ($lineData !== false) ? $lineData : null;
}
/**
* @param array $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (count($lineData) === 1 && $lineData[0] === null);
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\AbstractReader;
use Box\Spout\Common\Exception\IOException;
/**
* Class Reader
* This class provides support to read data from a CSV file.
*
* @package Box\Spout\Reader\CSV
*/
class Reader extends AbstractReader
{
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @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 Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure = '"';
/**
* Sets the field delimiter for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldDelimiter Character that delimits fields
* @return Reader
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
return $this;
}
/**
* Sets the field enclosure for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldEnclosure Character that enclose fields
* @return Reader
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
return $this;
}
/**
* Opens the file at the given path to make it ready to be read.
* The file must be UTF-8 encoded.
* @TODO add encoding detection/conversion
*
* @param string $filePath Path of the CSV file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException
*/
protected function openReader($filePath)
{
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException('Could not open file ' . $filePath . ' for reading.');
}
$this->sheetIterator = new SheetIterator($this->filePointer, $this->fieldDelimiter, $this->fieldEnclosure, $this->globalFunctionsHelper);
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
public function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\IteratorInterface;
/**
* Class RowIterator
* Iterate over CSV rows.
*
* @package Box\Spout\Reader\CSV
*/
class RowIterator implements IteratorInterface
{
const UTF8_BOM = "\xEF\xBB\xBF";
/** @var resource Pointer to the CSV file to read */
protected $filePointer;
/** @var int Number of read rows */
protected $numReadRows = 0;
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @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 \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $globalFunctionsHelper)
{
$this->filePointer = $filePointer;
$this->fieldDelimiter = $fieldDelimiter;
$this->fieldEnclosure = $fieldEnclosure;
$this->globalFunctionsHelper = $globalFunctionsHelper;
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->rewindAndSkipUtf8Bom();
$this->numReadRows = 0;
$this->rowDataBuffer = null;
$this->next();
}
/**
* This rewinds and skips the UTF-8 BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*
* @return void
*/
protected function rewindAndSkipUtf8Bom()
{
$this->globalFunctionsHelper->rewind($this->filePointer);
$hasUtf8Bom = ($this->globalFunctionsHelper->fgets($this->filePointer, 4) === self::UTF8_BOM);
if ($hasUtf8Bom) {
// we skip the 2 first bytes (so start from the 3rd byte)
$this->globalFunctionsHelper->fseek($this->filePointer, 3);
} else {
// if no BOM, reset the pointer to read from the beginning
$this->globalFunctionsHelper->fseek($this->filePointer, 0);
}
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return ($this->filePointer && !$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Empty rows are skipped.
* @link http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
$lineData = null;
$this->hasReachedEndOfFile = feof($this->filePointer);
if (!$this->hasReachedEndOfFile) {
do {
$lineData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, 0, $this->fieldDelimiter, $this->fieldEnclosure);
} while ($lineData === false || ($lineData !== null && $this->isEmptyLine($lineData)));
if ($lineData !== false && $lineData !== null) {
$this->rowDataBuffer = $lineData;
$this->numReadRows++;
}
}
}
/**
* @param array $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (count($lineData) === 1 && $lineData[0] === null);
}
/**
* Return the current element from the buffer
* @link http://php.net/manual/en/iterator.current.php
*
* @return array|null
*/
public function current()
{
return $this->rowDataBuffer;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->numReadRows;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// do nothing
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\SheetInterface;
/**
* Class Sheet
*
* @package Box\Spout\Reader\CSV
*/
class Sheet implements SheetInterface
{
/** @var RowIterator To iterate over the CSV's rows */
protected $rowIterator;
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $globalFunctionsHelper)
{
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $globalFunctionsHelper);
}
/**
* @return RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\IteratorInterface;
/**
* Class SheetIterator
* Iterate over CSV unique "sheet".
*
* @package Box\Spout\Reader\CSV
*/
class SheetIterator implements IteratorInterface
{
/** @var Sheet The CSV unique "sheet" */
protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */
protected $hasReadUniqueSheet = false;
/**
* @param resource $filePointer
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $globalFunctionsHelper)
{
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $globalFunctionsHelper);
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->hasReadUniqueSheet = false;
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return (!$this->hasReadUniqueSheet);
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
$this->hasReadUniqueSheet = true;
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
*
* @return Sheet
*/
public function current()
{
return $this->sheet;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// do nothing
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class EndOfWorksheetsReachedException
*
* @package Box\Spout\Reader\Exception
*/
class EndOfWorksheetsReachedException extends ReaderException
{
}

View File

@ -3,10 +3,10 @@
namespace Box\Spout\Reader\Exception;
/**
* Class EndOfFileReachedException
* Class NoSheetsFoundException
*
* @package Box\Spout\Reader\Exception
*/
class EndOfFileReachedException extends ReaderException
class NoSheetsFoundException extends ReaderException
{
}

View File

@ -1,12 +0,0 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class NoWorksheetsFoundException
*
* @package Box\Spout\Reader\Exception
*/
class NoWorksheetsFoundException extends ReaderException
{
}

View File

@ -1,58 +0,0 @@
<?php
namespace Box\Spout\Reader\Internal\XLSX;
/**
* Class Worksheet
* Represents a worksheet within a XLSX file
*
* @package Box\Spout\Reader\Internal\XLSX
*/
class Worksheet
{
/** @var \Box\Spout\Reader\Sheet The "external" sheet */
protected $externalSheet;
/** @var int Worksheet index, based on the order of appareance in [Content_Types].xml (zero-based) */
protected $worksheetIndex;
/** @var string Path of the XML file containing the worksheet data */
protected $dataXmlFilePath;
/**\
* @param \Box\Spout\Reader\Sheet $externalSheet The associated "external" sheet
* @param int $worksheetIndex Worksheet index, based on the order of appareance in [Content_Types].xml (zero-based)
* @param string $dataXmlFilePath Path of the XML file containing the worksheet data
*/
public function __construct($externalSheet, $worksheetIndex, $dataXmlFilePath)
{
$this->externalSheet = $externalSheet;
$this->worksheetIndex = $worksheetIndex;
$this->dataXmlFilePath = $dataXmlFilePath;
}
/**
* @return string Path of the XML file containing the worksheet data,
* without the leading slash.
*/
public function getDataXmlFilePath()
{
return ltrim($this->dataXmlFilePath, '/');
}
/**
* @return \Box\Spout\Reader\Sheet The "external" sheet
*/
public function getExternalSheet()
{
return $this->externalSheet;
}
/**
* @return int
*/
public function getWorksheetIndex()
{
return $this->worksheetIndex;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Box\Spout\Reader;
/**
* Interface IteratorInterface
*
* @package Box\Spout\Reader
*/
interface IteratorInterface extends \Iterator
{
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end();
}

View File

@ -19,7 +19,7 @@ class ReaderFactory
* This creates an instance of the appropriate reader, given the type of the file to be read
*
* @param string $readerType Type of the reader to instantiate
* @return \Box\Spout\Reader\CSV|\Box\Spout\Reader\XLSX
* @return ReaderInterface
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
*/
public static function create($readerType)
@ -28,10 +28,10 @@ class ReaderFactory
switch ($readerType) {
case Type::CSV:
$reader = new CSV();
$reader = new CSV\Reader();
break;
case Type::XLSX:
$reader = new XLSX();
$reader = new XLSX\Reader();
break;
default:
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);

View File

@ -20,26 +20,12 @@ interface ReaderInterface
public function open($filePath);
/**
* Returns whether all rows have been read (i.e. if we are at the end of the file).
* To know if the end of file has been reached, it uses a buffer. If the buffer is
* empty (meaning, nothing has been read or previous read line has been consumed), then
* it reads the next line, store it in the buffer for the next time or flip a variable if
* the end of file has been reached.
* Returns an iterator to iterate over sheets.
*
* @return bool
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @return \Iterator To iterate over sheets
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
*/
public function hasNextRow();
/**
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
* actually reading the file.
*
* @return array Array that contains the data for the read row
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
*/
public function nextRow();
public function getSheetIterator();
/**
* Closes the reader, preventing any additional reading

View File

@ -0,0 +1,18 @@
<?php
namespace Box\Spout\Reader;
/**
* Interface SheetInterface
*
* @package Box\Spout\Reader
*/
interface SheetInterface
{
/**
* Returns an iterator to iterate over the sheet's rows.
*
* @return \Iterator
*/
public function getRowIterator();
}

View File

@ -1,394 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\BadUsageException;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\EndOfWorksheetsReachedException;
use Box\Spout\Reader\Exception\NoWorksheetsFoundException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
use Box\Spout\Reader\Helper\XLSX\CellHelper;
use Box\Spout\Reader\Helper\XLSX\SharedStringsHelper;
use Box\Spout\Reader\Helper\XLSX\WorksheetHelper;
/**
* Class XLSX
* This class provides support to read data from a XLSX file
*
* @package Box\Spout\Reader
*/
class XLSX extends AbstractReader
{
const CELL_TYPE_INLINE_STRING = 'inlineStr';
const CELL_TYPE_STR = 'str';
const CELL_TYPE_SHARED_STRING = 's';
const CELL_TYPE_BOOLEAN = 'b';
const CELL_TYPE_NUMERIC = 'n';
const CELL_TYPE_DATE = 'd';
const CELL_TYPE_ERROR = 'e';
/** @var string Real path of the file to read */
protected $filePath;
/** @var string Temporary folder where the temporary files will be created */
protected $tempFolder;
/** @var \ZipArchive */
protected $zip;
/** @var Helper\XLSX\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var Helper\XLSX\WorksheetHelper Helper to work with worksheets */
protected $worksheetHelper;
/** @var Internal\XLSX\Worksheet[] The list of worksheets present in the file */
protected $worksheets;
/** @var Internal\XLSX\Worksheet The worksheet being read */
protected $currentWorksheet;
/** @var \XMLReader The XMLReader object that will help read sheets XML data */
protected $xmlReader;
/** @var int The number of columns the worksheet has (0 meaning undefined) */
protected $numberOfColumns = 0;
/**
* @param string $tempFolder Temporary folder where the temporary files will be created
* @return XLSX
*/
public function setTempFolder($tempFolder)
{
$this->tempFolder = $tempFolder;
return $this;
}
/**
* Opens the file at the given file path to make it ready to be read.
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
* and fetches all the available worksheets.
*
* @param string $filePath Path of the file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
* @throws Exception\NoWorksheetsFoundException If there are no worksheets in the file
*/
protected function openReader($filePath)
{
$this->filePath = $filePath;
$this->zip = new \ZipArchive();
if ($this->zip->open($filePath) === true) {
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
if ($this->sharedStringsHelper->hasSharedStrings()) {
// Extracts all the strings from the worksheets for easy access in the future
$this->sharedStringsHelper->extractSharedStrings();
}
// Fetch all available worksheets
$this->worksheetHelper = new WorksheetHelper($filePath, $this->globalFunctionsHelper);
$this->worksheets = $this->worksheetHelper->getWorksheets($filePath);
if (count($this->worksheets) === 0) {
throw new NoWorksheetsFoundException('The file must contain at least one worksheet.');
}
} else {
throw new IOException('Could not open ' . $filePath . ' for reading.');
}
}
/**
* Returns whether another worksheet exists after the current worksheet.
*
* @return bool Whether another worksheet exists after the current worksheet.
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
*/
public function hasNextSheet()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Stream should be opened first.');
}
return $this->worksheetHelper->hasNextWorksheet($this->currentWorksheet, $this->worksheets);
}
/**
* Moves the pointer to the current worksheet.
* Moving to another worksheet will stop the reading in the current worksheet.
*
* @return \Box\Spout\Reader\Sheet The next sheet
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
* @throws Exception\EndOfWorksheetsReachedException If there is no more worksheets to read
*/
public function nextSheet()
{
if (!$this->hasNextSheet()) {
throw new EndOfWorksheetsReachedException('End of worksheets was reached. Cannot read more worksheets.');
}
if ($this->currentWorksheet === null) {
$nextWorksheet = $this->worksheets[0];
} else {
$currentWorksheetIndex = $this->currentWorksheet->getWorksheetIndex();
$nextWorksheet = $this->worksheets[$currentWorksheetIndex + 1];
}
$this->initXmlReaderForWorksheetData($nextWorksheet);
$this->currentWorksheet = $nextWorksheet;
// make sure that we are ready to read more rows
$this->hasReachedEndOfFile = false;
$this->emptyRowDataBuffer();
return $this->currentWorksheet->getExternalSheet();
}
/**
* Initializes the XMLReader object that reads worksheet data for the given worksheet.
* If another worksheet was being read, it closes the reader before reopening it for the new worksheet.
* The XMLReader is configured to be safe from billion laughs attack.
*
* @param Internal\XLSX\Worksheet $worksheet The worksheet to initialize the XMLReader with
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the worksheet data XML cannot be read
*/
protected function initXmlReaderForWorksheetData($worksheet)
{
// if changing worksheet and the XMLReader was initialized for the current worksheet
if ($worksheet != $this->currentWorksheet && $this->xmlReader) {
$this->xmlReader->close();
} else if (!$this->xmlReader) {
$this->xmlReader = new \XMLReader();
}
$worksheetDataXMLFilePath = $worksheet->getDataXmlFilePath();
$worksheetDataFilePath = 'zip://' . $this->filePath . '#' . $worksheetDataXMLFilePath;
if ($this->xmlReader->open($worksheetDataFilePath, null, LIBXML_NONET) === false) {
throw new IOException('Could not open "' . $worksheetDataXMLFilePath . '".');
}
}
/**
* Reads and returns data of the line that comes after the last read line, on the current worksheet.
* Empty rows will be skipped.
*
* @return array|null Array that contains the data for the read line or null at the end of the file
* @throws \Box\Spout\Common\Exception\BadUsageException If the pointer to the current worksheet has not been set
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
*/
protected function read()
{
if (!$this->currentWorksheet) {
throw new BadUsageException('You must call nextSheet() before calling hasNextRow() or nextRow()');
}
$escaper = new \Box\Spout\Common\Escaper\XLSX();
$isInsideRowTag = false;
$rowData = [];
while ($this->xmlReader->read()) {
if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'dimension') {
// Read dimensions of the worksheet
$dimensionRef = $this->xmlReader->getAttribute('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->numberOfColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
}
} else if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'row') {
// Start of the row description
$isInsideRowTag = true;
// Read spans info if present
$numberOfColumnsForRow = $this->numberOfColumns;
$spans = $this->xmlReader->getAttribute('spans'); // returns '1:5' for instance
if ($spans) {
list(, $numberOfColumnsForRow) = explode(':', $spans);
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
}
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
} else if ($isInsideRowTag && $this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'c') {
// Start of a cell description
$currentCellIndex = $this->xmlReader->getAttribute('r');
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
$node = $this->xmlReader->expand();
$rowData[$currentColumnIndex] = $this->getCellValue($node, $escaper);
} else if ($this->xmlReader->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === 'row') {
// End of the row description
// If needed, we fill the empty cells
$rowData = ($this->numberOfColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
break;
}
}
// no data means "end of file"
return ($rowData !== []) ? $rowData : null;
}
/**
* Returns the cell's string value from a node's nested value node
*
* @param \DOMNode $node
* @return string The value associated with the cell
*/
protected function getVNodeValue($node)
{
// for cell types having a "v" tag containing the value.
// if not, the returned value should be empty string.
$vNode = $node->getElementsByTagName('v')->item(0);
if ($vNode !== null) {
return $vNode->nodeValue;
}
return "";
}
/**
* Returns the cell String value where string is inline.
*
* @param \DOMNode $node
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatInlineStringCellValue($node, $escaper)
{
// inline strings are formatted this way:
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
$tNode = $node->getElementsByTagName('t')->item(0);
$escapedCellValue = trim($tNode->nodeValue);
$cellValue = $escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value from shared-strings file using nodeValue index.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatSharedStringCellValue($nodeValue, $escaper)
{
// shared strings are formatted this way:
// <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
$sharedStringIndex = intval($nodeValue);
$escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
$cellValue = $escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value, where string is stored in value node.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatStrCellValue($nodeValue, $escaper)
{
$escapedCellValue = trim($nodeValue);
$cellValue = $escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell Numeric value from string of nodeValue.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return int|float The value associated with the cell
*/
protected function formatNumericCellValue($nodeValue)
{
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
return $cellValue;
}
/**
* Returns the cell Boolean value from a specific node's Value.
*
* @param string $nodeValue
* @return bool The value associated with the cell
*/
protected function formatBooleanCellValue($nodeValue)
{
// !! is similar to boolval()
$cellValue = !!$nodeValue;
return $cellValue;
}
/**
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return DateTime|null The value associated with the cell (null when the cell has an error)
*/
protected function formatDateCellValue($nodeValue)
{
try { // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
$cellValue = new \DateTime($nodeValue);
return $cellValue;
} catch (\Exception $e) {
return null;
}
}
/**
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
*
* @param \DOMNode $node
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string|int|float|bool|null The value associated with the cell (null when the cell has an error)
*/
protected function getCellValue($node, $escaper)
{
// Default cell type is "n"
$cellType = $node->getAttribute('t') ?: 'n';
$vNodeValue = $this->getVNodeValue($node);
if ( ($vNodeValue === "") && ($cellType !== self::CELL_TYPE_INLINE_STRING) ) {
return $vNodeValue;
}
switch ($cellType)
{
case self::CELL_TYPE_INLINE_STRING:
return $this->formatInlineStringCellValue($node, $escaper);
case self::CELL_TYPE_SHARED_STRING:
return $this->formatSharedStringCellValue($vNodeValue, $escaper);
case self::CELL_TYPE_STR:
return $this->formatStrCellValue($vNodeValue, $escaper);
case self::CELL_TYPE_BOOLEAN:
return $this->formatBooleanCellValue($vNodeValue);
case self::CELL_TYPE_NUMERIC:
return $this->formatNumericCellValue($vNodeValue);
case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($vNodeValue);
default:
return null;
}
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->xmlReader) {
$this->xmlReader->close();
}
if ($this->zip) {
$this->zip->close();
}
$this->sharedStringsHelper->cleanup();
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Common\Exception\InvalidArgumentException;
@ -8,7 +8,7 @@ use Box\Spout\Common\Exception\InvalidArgumentException;
* Class CellHelper
* This class provides helper functions when working with cells
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class CellHelper
{

View File

@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
/**
* Class CachingStrategyFactory
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class CachingStrategyFactory
{

View File

@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
/**
* Interface CachingStrategyInterface
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
interface CachingStrategyInterface
{

View File

@ -1,6 +1,6 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
use Box\Spout\Common\Helper\FileSystemHelper;
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
@ -13,7 +13,7 @@ use Box\Spout\Reader\Exception\SharedStringNotFoundException;
* Shared strings are stored in small files (with a max number of strings per file).
* This strategy is slower than an in-memory strategy but is used to avoid out of memory crashes.
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class FileBasedStrategy implements CachingStrategyInterface
{
@ -26,6 +26,9 @@ class FileBasedStrategy implements CachingStrategyInterface
/** @var \Box\Spout\Common\Helper\FileSystemHelper Helper to perform file system operations */
protected $fileSystemHelper;
/** @var string Temporary folder where the temporary files will be created */
protected $tempFolder;
/**
* @var int Maximum number of strings that can be stored in one temp file
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
@ -42,7 +45,7 @@ class FileBasedStrategy implements CachingStrategyInterface
protected $inMemoryTempFilePath;
/**
* @var string Contents of the temporary file that was last read
* @var array Contents of the temporary file that was last read
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
*/
protected $inMemoryTempFileContents;

View File

@ -1,6 +1,7 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
use Box\Spout\Reader\Exception\SharedStringNotFoundException;
/**
@ -9,7 +10,7 @@ use Box\Spout\Reader\Exception\SharedStringNotFoundException;
* This class implements the in-memory caching strategy for shared strings.
* This strategy is used when the number of unique strings is low, compared to the memory available.
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class InMemoryStrategy implements CachingStrategyInterface
{

View File

@ -1,16 +1,16 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyInterface;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyInterface;
/**
* Class SharedStringsHelper
* This class provides helper functions for reading sharedStrings XML file
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class SharedStringsHelper
{
@ -58,13 +58,13 @@ class SharedStringsHelper
}
/**
* Builds an in-memory array containing all the shared strings of the worksheet.
* Builds an in-memory array containing all the shared strings of the sheet.
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
* It is then accessed by the worksheet data, via the string index in the built table.
* It is then accessed by the sheet data, via the string index in the built table.
*
* More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx
*
* The XML file can be really big with worksheets containing a lot of data. That is why
* The XML file can be really big with sheets containing a lot of data. That is why
* we need to use a XML reader that provides streaming like the XMLReader library.
* Please note that SimpleXML does not provide such a functionality but since it is faster
* and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose.
@ -154,7 +154,8 @@ class SharedStringsHelper
$readError = libxml_get_last_error();
if ($readError !== false) {
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$readError->message}]");
$readErrorMessage = trim($readError->message);
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$readErrorMessage}]");
}
// reset the setting to display XML warnings/errors

View File

@ -1,17 +1,16 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Reader\Internal\XLSX\Worksheet;
use Box\Spout\Reader\Sheet;
use Box\Spout\Reader\XLSX\Sheet;
/**
* Class WorksheetHelper
* This class provides helper functions related to XLSX worksheets
* Class SheetHelper
* This class provides helper functions related to XLSX sheets
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class WorksheetHelper
class SheetHelper
{
/** Extension for XML files */
const XML_EXTENSION = '.xml';
@ -26,12 +25,15 @@ class WorksheetHelper
const MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS = 'http://schemas.openxmlformats.org/package/2006/relationships';
const MAIN_NAMESPACE_FOR_WORKBOOK_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
/** Value of the Override attribute used in [Content_Types].xml to define worksheets */
/** Value of the Override attribute used in [Content_Types].xml to define sheets */
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
/** @var string Path of the XLSX file being read */
protected $filePath;
/** @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;
@ -43,41 +45,43 @@ class WorksheetHelper
/**
* @param string $filePath Path of the XLSX file being read
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePath, $globalFunctionsHelper)
public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper)
{
$this->filePath = $filePath;
$this->sharedStringsHelper = $sharedStringsHelper;
$this->globalFunctionsHelper = $globalFunctionsHelper;
}
/**
* Returns the file paths of the worksheet data XML files within the XLSX file.
* The paths are read from the [Content_Types].xml file.
* Returns the sheets metadata of the file located at the previously given file path.
* The paths to the sheets' data are read from the [Content_Types].xml file.
*
* @return Worksheet[] Worksheets within the XLSX file
* @return Sheet[] Sheets within the XLSX file
*/
public function getWorksheets()
public function getSheets()
{
$worksheets = [];
$sheets = [];
$contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace(
self::CONTENT_TYPES_XML_FILE_PATH,
self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML
);
// find all nodes defining a worksheet
// find all nodes defining a sheet
$sheetNodes = $contentTypesAsXMLElement->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
$numSheetNodes = count($sheetNodes);
for ($i = 0; $i < count($sheetNodes); $i++) {
for ($i = 0; $i < $numSheetNodes; $i++) {
$sheetNode = $sheetNodes[$i];
$sheetDataXMLFilePath = (string) $sheetNode->attributes()->PartName;
$sheet = $this->getSheet($sheetDataXMLFilePath, $i);
$worksheets[] = new Worksheet($sheet, $i, $sheetDataXMLFilePath);
$sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath, $i);
}
return $worksheets;
return $sheets;
}
/**
@ -92,9 +96,9 @@ class WorksheetHelper
*
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param int $sheetIndexZeroBased Index of the sheet, based on order in [Content_Types].xml (zero-based)
* @return \Box\Spout\Reader\Sheet Sheet instance
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
*/
protected function getSheet($sheetDataXMLFilePath, $sheetIndexZeroBased)
protected function getSheetFromXML($sheetDataXMLFilePath, $sheetIndexZeroBased)
{
$sheetId = $sheetIndexZeroBased + 1;
$sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath);
@ -126,14 +130,14 @@ class WorksheetHelper
}
}
return new Sheet($sheetId, $sheetIndexZeroBased, $sheetName);
return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetId, $sheetIndexZeroBased, $sheetName);
}
/**
* Returns the default name of the sheet whose data is located
* at the given path.
*
* @param $sheetDataXMLFilePath
* @param string $sheetDataXMLFilePath Path of the sheet data XML file
* @return string The default sheet name
*/
protected function getDefaultSheetName($sheetDataXMLFilePath)
@ -193,17 +197,4 @@ class WorksheetHelper
return $xmlElement;
}
/**
* Returns whether another worksheet exists after the current worksheet.
* The order is determined by the order of appearance in the [Content_Types].xml file.
*
* @param Worksheet|null $currentWorksheet The worksheet being currently read or null if reading has not started yet
* @param Worksheet[] $allWorksheets A list of all worksheets in the XLSX file. Must contain at least one worksheet
* @return bool Whether another worksheet exists after the current sheet
*/
public function hasNextWorksheet($currentWorksheet, $allWorksheets)
{
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetIndex() + 1 < count($allWorksheets)));
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\AbstractReader;
use Box\Spout\Reader\XLSX\Helper\SharedStringsHelper;
/**
* Class Reader
* This class provides support to read data from a XLSX file
*
* @package Box\Spout\Reader\XLSX
*/
class Reader extends AbstractReader
{
/** @var string Temporary folder where the temporary files will be created */
protected $tempFolder;
/** @var \ZipArchive */
protected $zip;
/** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var SheetIterator To iterator over the XLSX sheets */
protected $sheetIterator;
/**
* @param string $tempFolder Temporary folder where the temporary files will be created
* @return Reader
*/
public function setTempFolder($tempFolder)
{
$this->tempFolder = $tempFolder;
return $this;
}
/**
* Opens the file at the given file path to make it ready to be read.
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
* and fetches all the available sheets.
*
* @param string $filePath Path of the file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
*/
protected function openReader($filePath)
{
$this->zip = new \ZipArchive();
if ($this->zip->open($filePath) === true) {
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
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);
} else {
throw new IOException('Could not open ' . $filePath . ' for reading.');
}
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
public function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->zip) {
$this->zip->close();
}
if ($this->sharedStringsHelper) {
$this->sharedStringsHelper->cleanup();
}
}
}

View File

@ -0,0 +1,369 @@
<?php
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\IteratorInterface;
use Box\Spout\Reader\XLSX\Helper\CellHelper;
/**
* Class RowIterator
*
* @package Box\Spout\Reader\XLSX
*/
class RowIterator implements IteratorInterface
{
/** Definition of all possible cell types */
const CELL_TYPE_INLINE_STRING = 'inlineStr';
const CELL_TYPE_STR = 'str';
const CELL_TYPE_SHARED_STRING = 's';
const CELL_TYPE_BOOLEAN = 'b';
const CELL_TYPE_NUMERIC = 'n';
const CELL_TYPE_DATE = 'd';
const CELL_TYPE_ERROR = 'e';
/** Definition of XML nodes names used to parse data */
const XML_NODE_DIMENSION = 'dimension';
const XML_NODE_WORKSHEET = 'worksheet';
const XML_NODE_ROW = 'row';
const XML_NODE_CELL = 'c';
const XML_NODE_VALUE = 'v';
const XML_NODE_INLINE_STRING_VALUE = 't';
/** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_REF = 'ref';
const XML_ATTRIBUTE_SPANS = 'spans';
const XML_ATTRIBUTE_CELL_INDEX = 'r';
const XML_ATTRIBUTE_TYPE = 't';
/** @var string Path of the XLSX file being read */
protected $filePath;
/** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */
protected $sheetDataXMLFilePath;
/** @var Helper\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var \XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader;
/** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
protected $escaper;
/** @var int Number of read rows */
protected $numReadRows = 0;
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var int The number of columns the sheet has (0 meaning undefined) */
protected $numColumns = 0;
/**
* @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 $sharedStringsHelper Helper to work with shared strings
*/
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper)
{
$this->filePath = $filePath;
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
$this->sharedStringsHelper = $sharedStringsHelper;
$this->xmlReader = new \XMLReader();
$this->escaper = new \Box\Spout\Common\Escaper\XLSX();
}
/**
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @return string Path of the XML file containing the sheet data,
* without the leading slash.
*/
protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
{
return ltrim($sheetDataXMLFilePath, '/');
}
/**
* Rewind the Iterator to the first element.
* Initializes the XMLReader object that reads the associated sheet data.
* The XMLReader is configured to be safe from billion laughs attack.
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
*/
public function rewind()
{
$this->xmlReader->close();
$sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath;
if ($this->xmlReader->open($sheetDataFilePath, null, LIBXML_NONET) === false) {
throw new IOException('Could not open "' . $this->sheetDataXMLFilePath . '".');
}
$this->numReadRows = 0;
$this->rowDataBuffer = null;
$this->hasReachedEndOfFile = false;
$this->numColumns = 0;
$this->next();
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return (!$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Empty rows will be skipped.
* @link http://php.net/manual/en/iterator.next.php
*
* @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
*/
public function next()
{
$isInsideRowTag = false;
$rowData = [];
// Use internal errors to avoid displaying lots of warning messages in case of invalid file
// For instance on HHVM, XMLReader->open() won't fail when trying to read a unexisting file within a zip...
// But the XMLReader->read() will fail!
libxml_clear_errors();
libxml_use_internal_errors(true);
while ($this->xmlReader->read()) {
if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 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->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) {
// Start of the row description
$isInsideRowTag = true;
// 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 ($isInsideRowTag && $this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_CELL) {
// Start of a cell description
$currentCellIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
$node = $this->xmlReader->expand();
$rowData[$currentColumnIndex] = $this->getCellValue($node);
} else if ($this->xmlReader->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === 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->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === self::XML_NODE_WORKSHEET) {
// The closing "</worksheet>" marks the end of the file
$this->hasReachedEndOfFile = true;
}
}
$readError = libxml_get_last_error();
if ($readError !== false) {
$readErrorMessage = trim($readError->message);
throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$readErrorMessage}]");
}
$this->rowDataBuffer = $rowData;
}
/**
* Returns the cell's string value from a node's nested value node
*
* @param \DOMNode $node
* @return string The value associated with the cell
*/
protected function getVNodeValue($node)
{
// for cell types having a "v" tag containing the value.
// if not, the returned value should be empty string.
$vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0);
if ($vNode !== null) {
return $vNode->nodeValue;
}
return "";
}
/**
* Returns the cell String value where string is inline.
*
* @param \DOMNode $node
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatInlineStringCellValue($node)
{
// inline strings are formatted this way:
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
$escapedCellValue = trim($tNode->nodeValue);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value from shared-strings file using nodeValue index.
*
* @param string $nodeValue
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatSharedStringCellValue($nodeValue)
{
// shared strings are formatted this way:
// <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
$sharedStringIndex = intval($nodeValue);
$escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value, where string is stored in value node.
*
* @param string $nodeValue
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatStrCellValue($nodeValue)
{
$escapedCellValue = trim($nodeValue);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell Numeric value from string of nodeValue.
*
* @param string $nodeValue
* @return int|float The value associated with the cell
*/
protected function formatNumericCellValue($nodeValue)
{
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
return $cellValue;
}
/**
* Returns the cell Boolean value from a specific node's Value.
*
* @param string $nodeValue
* @return bool The value associated with the cell
*/
protected function formatBooleanCellValue($nodeValue)
{
// !! is similar to boolval()
$cellValue = !!$nodeValue;
return $cellValue;
}
/**
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
*
* @param string $nodeValue
* @return \DateTime|null The value associated with the cell (null when the cell has an error)
*/
protected function formatDateCellValue($nodeValue)
{
// Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
try {
$cellValue = new \DateTime($nodeValue);
return $cellValue;
} catch (\Exception $e) {
return null;
}
}
/**
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
*
* @param \DOMNode $node
* @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
*/
protected function getCellValue($node)
{
// Default cell type is "n"
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
$vNodeValue = $this->getVNodeValue($node);
if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
return $vNodeValue;
}
switch ($cellType) {
case self::CELL_TYPE_INLINE_STRING:
return $this->formatInlineStringCellValue($node);
case self::CELL_TYPE_SHARED_STRING:
return $this->formatSharedStringCellValue($vNodeValue);
case self::CELL_TYPE_STR:
return $this->formatStrCellValue($vNodeValue);
case self::CELL_TYPE_BOOLEAN:
return $this->formatBooleanCellValue($vNodeValue);
case self::CELL_TYPE_NUMERIC:
return $this->formatNumericCellValue($vNodeValue);
case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($vNodeValue);
default:
return null;
}
}
/**
* Return the current element, from the buffer.
* @link http://php.net/manual/en/iterator.current.php
*
* @return array|null
*/
public function current()
{
return $this->rowDataBuffer;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->numReadRows;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
$this->xmlReader->close();
}
}

View File

@ -1,15 +1,20 @@
<?php
namespace Box\Spout\Reader;
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Reader\SheetInterface;
/**
* Class Sheet
* Represents a worksheet within a XLSX file
* Represents a sheet within a XLSX file
*
* @package Box\Spout\Reader
* @package Box\Spout\Reader\XLSX
*/
class Sheet
class Sheet implements SheetInterface
{
/** @var RowIterator To iterate over sheet's rows */
protected $rowIterator;
/** @var int ID of the sheet */
protected $id;
@ -20,17 +25,29 @@ class Sheet
protected $name;
/**
* @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 int $sheetId ID of the sheet
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
* @param string $sheetName Name of the sheet
*/
function __construct($sheetId, $sheetIndex, $sheetName)
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetId, $sheetIndex, $sheetName)
{
$this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper);
$this->id = $sheetId;
$this->index = $sheetIndex;
$this->name = $sheetName;
}
/**
* @return RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
/**
* @return int ID of the sheet
*/

View File

@ -0,0 +1,112 @@
<?php
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Reader\IteratorInterface;
use Box\Spout\Reader\XLSX\Helper\SheetHelper;
use Box\Spout\Reader\Exception\NoSheetsFoundException;
/**
* Class SheetIterator
* Iterate over XLSX sheet.
*
* @package Box\Spout\Reader\XLSX
*/
class SheetIterator implements IteratorInterface
{
/** @var Sheet[] The list of sheet present in the file */
protected $sheets;
/** @var int The index of the sheet being read (zero-based) */
protected $currentSheetIndex;
/**
* @param string $filePath Path of the file to be read
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper $sharedStringsHelper
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
*/
public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper)
{
// Fetch all available sheets
$sheetHelper = new SheetHelper($filePath, $sharedStringsHelper, $globalFunctionsHelper);
$this->sheets = $sheetHelper->getSheets();
if (count($this->sheets) === 0) {
throw new NoSheetsFoundException('The file must contain at least one sheet.');
}
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->currentSheetIndex = 0;
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return ($this->currentSheetIndex < count($this->sheets));
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
if (array_key_exists($this->currentSheetIndex, $this->sheets)) {
$currentSheet = $this->sheets[$this->currentSheetIndex];
$currentSheet->getRowIterator()->end();
$this->currentSheetIndex++;
}
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
*
* @return Sheet
*/
public function current()
{
return $this->sheets[$this->currentSheetIndex];
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->currentSheetIndex + 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// make sure we are not leaking memory in case the iteration stopped before the end
foreach ($this->sheets as $sheet) {
$sheet->getRowIterator()->end();
}
}
}

View File

@ -69,7 +69,7 @@ abstract class AbstractWriter implements WriterInterface
* By using this method, the data will be written to a file.
*
* @param string $outputFilePath Path of the output file that will contain the data
* @return \Box\Spout\Writer\AbstractWriter
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
*/
public function openToFile($outputFilePath)
@ -92,7 +92,7 @@ abstract class AbstractWriter implements WriterInterface
* @codeCoverageIgnore
*
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
* @return \Box\Spout\Writer\AbstractWriter
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
*/
public function openToBrowser($outputFileName)
@ -144,7 +144,7 @@ abstract class AbstractWriter implements WriterInterface
* If empty, no data is added (i.e. not even as a blank row)
* Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
*
* @return \Box\Spout\Writer\AbstractWriter
* @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
*/
@ -173,7 +173,7 @@ abstract class AbstractWriter implements WriterInterface
* ['data21', 'data22', null, false],
* ];
*
* @return \Box\Spout\Writer\AbstractWriter
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data

View File

@ -1,16 +1,17 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\CSV;
use Box\Spout\Writer\AbstractWriter;
use Box\Spout\Common\Exception\IOException;
/**
* Class CSV
* Class Writer
* This class provides support to write data to CSV files
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\CSV
*/
class CSV extends AbstractWriter
class Writer extends AbstractWriter
{
/** Number of rows to write before flushing */
const FLUSH_THRESHOLD = 500;
@ -32,7 +33,7 @@ class CSV extends AbstractWriter
* Sets the field delimiter for the CSV
*
* @param string $fieldDelimiter Character that delimits fields
* @return CSV
* @return Writer
*/
public function setFieldDelimiter($fieldDelimiter)
{
@ -44,7 +45,7 @@ class CSV extends AbstractWriter
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return CSV
* @return Writer
*/
public function setFieldEnclosure($fieldEnclosure)
{

View File

@ -19,7 +19,7 @@ class WriterFactory
* This creates an instance of the appropriate writer, given the type of the file to be read
*
* @param string $writerType Type of the writer to instantiate
* @return \Box\Spout\Writer\CSV|\Box\Spout\Writer\XLSX
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
*/
public static function create($writerType)
@ -28,10 +28,10 @@ class WriterFactory
switch ($writerType) {
case Type::CSV:
$writer = new CSV();
$writer = new CSV\Writer();
break;
case Type::XLSX:
$writer = new XLSX();
$writer = new XLSX\Writer();
break;
default:
throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType);

View File

@ -14,7 +14,7 @@ interface WriterInterface
* By using this method, the data will be written to a file.
*
* @param string $outputFilePath Path of the output file that will contain the data
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
*/
public function openToFile($outputFilePath);
@ -24,7 +24,7 @@ interface WriterInterface
* By using this method, the data will be outputted directly to the browser.
*
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
*/
public function openToBrowser($outputFileName);
@ -34,7 +34,7 @@ interface WriterInterface
*
* @param array $dataRow Array containing data to be streamed.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yetthe writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
@ -48,7 +48,7 @@ interface WriterInterface
* ['data11', 12, , '', 'data13'],
* ['data21', 'data22', null],
* ];
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
* @throws \Box\Spout\Common\Exception\IOException If unable to write data

View File

@ -1,12 +1,12 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class CellHelper
* This class provides helper functions when working with cells
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class CellHelper
{

View File

@ -1,15 +1,15 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
use Box\Spout\Writer\Internal\XLSX\Worksheet;
use Box\Spout\Writer\XLSX\Internal\Worksheet;
/**
* Class FileSystemHelper
* This class provides helper functions to help with the file system operations
* like files/folders creation & deletion for XLSX files
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
{

View File

@ -1,6 +1,6 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
use Box\Spout\Common\Exception\IOException;
@ -8,7 +8,7 @@ use Box\Spout\Common\Exception\IOException;
* Class SharedStringsHelper
* This class provides helper functions to write shared strings
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class SharedStringsHelper
{
@ -25,6 +25,9 @@ EOD;
*/
const DEFAULT_STRINGS_COUNT_PART = 'count="9999999999999" uniqueCount="9999999999999"';
/** @var resource Pointer to the sharedStrings.xml file */
protected $sharedStringsFilePointer;
/** @var int Number of shared strings already written */
protected $numSharedStrings = 0;

View File

@ -1,12 +1,12 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class ZipHelper
* This class provides helper functions to create zip files
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class ZipHelper
{

View File

@ -1,18 +1,18 @@
<?php
namespace Box\Spout\Writer\Internal\XLSX;
namespace Box\Spout\Writer\XLSX\Internal;
use Box\Spout\Writer\Exception\SheetNotFoundException;
use Box\Spout\Writer\Helper\XLSX\FileSystemHelper;
use Box\Spout\Writer\Helper\XLSX\SharedStringsHelper;
use Box\Spout\Writer\Sheet;
use Box\Spout\Writer\XLSX\Helper\FileSystemHelper;
use Box\Spout\Writer\XLSX\Helper\SharedStringsHelper;
use Box\Spout\Writer\XLSX\Sheet;
/**
* Class Book
* Represents a workbook within a XLSX file.
* It provides the functions to work with worksheets.
*
* @package Box\Spout\Writer\Internal\XLSX
* @package Box\Spout\Writer\XLSX\Internal
*/
class Workbook
{
@ -28,10 +28,10 @@ class Workbook
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
protected $shouldCreateNewSheetsAutomatically;
/** @var \Box\Spout\Writer\Helper\XLSX\FileSystemHelper Helper to perform file system operations */
/** @var \Box\Spout\Writer\XLSX\Helper\FileSystemHelper Helper to perform file system operations */
protected $fileSystemHelper;
/** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
protected $sharedStringsHelper;
/** @var Worksheet[] Array containing the workbook's sheets */
@ -114,7 +114,7 @@ class Workbook
* Sets the given sheet as the current one. New data will be written to this sheet.
* The writing will resume where it stopped (i.e. data won't be truncated).
*
* @param \Box\Spout\Writer\Sheet $sheet The "external" sheet to set as current
* @param \Box\Spout\Writer\XLSX\Sheet $sheet The "external" sheet to set as current
* @return void
* @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
*/
@ -140,7 +140,7 @@ class Workbook
/**
* Returns the worksheet associated to the given external sheet.
*
* @param \Box\Spout\Writer\Sheet $sheet
* @param \Box\Spout\Writer\XLSX\Sheet $sheet
* @return Worksheet|null The worksheet associated to the given external sheet or null if not found.
*/
protected function getWorksheetFromExternalSheet($sheet)

View File

@ -1,17 +1,17 @@
<?php
namespace Box\Spout\Writer\Internal\XLSX;
namespace Box\Spout\Writer\XLSX\Internal;
use Box\Spout\Common\Exception\InvalidArgumentException;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Writer\Helper\XLSX\CellHelper;
use Box\Spout\Writer\XLSX\Helper\CellHelper;
/**
* Class Worksheet
* Represents a worksheet within a XLSX file. The difference with the Sheet object is
* that this class provides an interface to write data
*
* @package Box\Spout\Writer\Internal\XLSX
* @package Box\Spout\Writer\XLSX\Internal
*/
class Worksheet
{
@ -20,13 +20,13 @@ class Worksheet
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
EOD;
/** @var \Box\Spout\Writer\Sheet The "external" sheet */
/** @var \Box\Spout\Writer\XLSX\Sheet The "external" sheet */
protected $externalSheet;
/** @var string Path to the XML file that will contain the sheet data */
protected $worksheetFilePath;
/** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
protected $sharedStringsHelper;
/** @var bool Whether inline or shared strings should be used */
@ -42,9 +42,9 @@ EOD;
protected $lastWrittenRowIndex = 0;
/**
* @param \Box\Spout\Writer\Sheet $externalSheet The associated "external" sheet
* @param \Box\Spout\Writer\XLSX\Sheet $externalSheet The associated "external" sheet
* @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
* @param \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper $sharedStringsHelper Helper for shared strings
* @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
*/
@ -76,7 +76,7 @@ EOD;
}
/**
* @return \Box\Spout\Writer\Sheet The "external" sheet
* @return \Box\Spout\Writer\XLSX\Sheet The "external" sheet
*/
public function getExternalSheet()
{

View File

@ -1,12 +1,12 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
/**
* Class Sheet
* Represents a worksheet within a XLSX file
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class Sheet
{
@ -21,7 +21,7 @@ class Sheet
/**
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
*/
function __construct($sheetIndex)
public function __construct($sheetIndex)
{
$this->index = $sheetIndex;
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1);
@ -45,7 +45,7 @@ class Sheet
/**
* @param string $name Name of the sheet
* @return \Box\Spout\Writer\Sheet
* @return Sheet
*/
public function setName($name)
{

View File

@ -1,17 +1,18 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Writer\AbstractWriter;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\Internal\XLSX\Workbook;
use Box\Spout\Writer\XLSX\Internal\Workbook;
/**
* Class XLSX
* Class Writer
* This class provides base support to write data to XLSX files
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class XLSX extends AbstractWriter
class Writer extends AbstractWriter
{
/** @var string Content-Type value for the header */
protected static $headerContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
@ -25,7 +26,7 @@ class XLSX extends AbstractWriter
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
protected $shouldCreateNewSheetsAutomatically = true;
/** @var Internal\XLSX\Workbook The workbook for the XLSX file */
/** @var Internal\Workbook The workbook for the XLSX file */
protected $book;
/** @var int */
@ -33,7 +34,7 @@ class XLSX extends AbstractWriter
/**
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
* @return XLSX
* @return Writer
*/
public function setTempFolder($tempFolder)
{
@ -45,7 +46,7 @@ class XLSX extends AbstractWriter
* Use inline string to be more memory efficient. If set to false, it will use shared strings.
*
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
* @return XLSX
* @return Writer
*/
public function setShouldUseInlineStrings($shouldUseInlineStrings)
{
@ -55,7 +56,7 @@ class XLSX extends AbstractWriter
/**
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
* @return XLSX
* @return Writer
*/
public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically)
{
@ -91,7 +92,7 @@ class XLSX extends AbstractWriter
$externalSheets = [];
$worksheets = $this->book->getWorksheets();
/** @var Internal\XLSX\Worksheet $worksheet */
/** @var Internal\Worksheet $worksheet */
foreach ($worksheets as $worksheet) {
$externalSheets[] = $worksheet->getExternalSheet();
}

View File

@ -1,16 +1,17 @@
<?php
namespace Box\Spout\Reader;
namespace Box\Spout\Reader\CSV;
use Box\Spout\Common\Type;
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\TestUsingResource;
/**
* Class CSVTest
* Class ReaderTest
*
* @package Box\Spout\Reader
* @package Box\Spout\Reader\CSV
*/
class CSVTest extends \PHPUnit_Framework_TestCase
class ReaderTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
@ -24,6 +25,16 @@ class CSVTest extends \PHPUnit_Framework_TestCase
ReaderFactory::create(Type::CSV)->open('/path/to/fake/file.csv');
}
/**
* @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException
*
* @return void
*/
public function testOpenShouldThrowExceptionIfTryingToReadBeforeOpeningReader()
{
ReaderFactory::create(Type::CSV)->getSheetIterator();
}
/**
* @expectedException \Box\Spout\Common\Exception\IOException
*
@ -44,33 +55,22 @@ class CSVTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException
* @expectedException \Box\Spout\Common\Exception\IOException
*
* @return void
*/
public function testReadShouldThrowExceptionIfReadBeforeReaderOpened()
public function testOpenShouldThrowExceptionIfCannotOpenFile()
{
$reader = ReaderFactory::create(Type::CSV);
$reader->hasNextRow();
}
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
->setMethods(['fopen'])
->getMock();
$helperStub->method('fopen')->willReturn(false);
/**
* @expectedException \Box\Spout\Reader\Exception\EndOfFileReachedException
*
* @return void
*/
public function testReadShouldThrowExceptionIfNextRowCalledAfterReadingDone()
{
$resourcePath = $this->getResourcePath('csv_standard.csv');
$reader = ReaderFactory::create(Type::CSV);
$reader->setGlobalFunctionsHelper($helperStub);
$reader->open($resourcePath);
while ($reader->hasNextRow()) {
$reader->nextRow();
}
$reader->nextRow();
}
@ -180,6 +180,50 @@ class CSVTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadMultipleTimesShouldRewindReader()
{
$allRows = [];
$resourcePath = $this->getResourcePath('csv_standard.csv');
$reader = ReaderFactory::create(Type::CSV);
$reader->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheet) {
// do nothing
}
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
}
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
}
$reader->close();
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--11', 'csv--12', 'csv--13'],
['csv--11', 'csv--12', 'csv--13'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @param string $fileName
* @param string|void $fieldDelimiter
@ -197,8 +241,10 @@ class CSVTest extends \PHPUnit_Framework_TestCase
$reader->open($resourcePath);
while ($reader->hasNextRow()) {
$allRows[] = $reader->nextRow();
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
foreach ($sheet->getRowIterator() as $rowIndex => $row) {
$allRows[] = $row;
}
}
$reader->close();

View File

@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
/**
* Class CellHelperTest
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class CellHelperTest extends \PHPUnit_Framework_TestCase
{

View File

@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
/**
* Class CachingStrategyFactoryTest
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class CachingStrategyFactoryTest extends \PHPUnit_Framework_TestCase
{
@ -36,18 +36,18 @@ class CachingStrategyFactoryTest extends \PHPUnit_Framework_TestCase
{
/** @var CachingStrategyFactory|\PHPUnit_Framework_MockObject_MockObject $factoryStub */
$factoryStub = $this
->getMockBuilder('\Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory')
->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory')
->disableOriginalConstructor()
->setMethods(['getMemoryLimitInKB'])
->getMock();
$factoryStub->method('getMemoryLimitInKB')->willReturn($memoryLimitInKB);
\ReflectionHelper::setStaticValue('\Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory', 'instance', $factoryStub);
\ReflectionHelper::setStaticValue('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory', 'instance', $factoryStub);
$strategy = $factoryStub->getBestCachingStrategy($sharedStringsUniqueCount, null);
$fullExpectedStrategyClassName = 'Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\\' . $expectedStrategyClassName;
$fullExpectedStrategyClassName = 'Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\\' . $expectedStrategyClassName;
$this->assertEquals($fullExpectedStrategyClassName, get_class($strategy));
$strategy->clearCache();
@ -85,7 +85,7 @@ class CachingStrategyFactoryTest extends \PHPUnit_Framework_TestCase
{
/** @var CachingStrategyFactory|\PHPUnit_Framework_MockObject_MockObject $factoryStub */
$factoryStub = $this
->getMockBuilder('\Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory')
->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory')
->disableOriginalConstructor()
->setMethods(['getMemoryLimitFromIni'])
->getMock();

View File

@ -1,16 +1,16 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\FileBasedStrategy;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\InMemoryStrategy;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\FileBasedStrategy;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\InMemoryStrategy;
use Box\Spout\TestUsingResource;
/**
* Class SharedStringsHelperTest
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class SharedStringsHelperTest extends \PHPUnit_Framework_TestCase
{

View File

@ -1,17 +1,19 @@
<?php
namespace Box\Spout\Reader;
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Type;
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\TestUsingResource;
use Symfony\Component\Config\Definition\Exception\Exception;
/**
* Class XLSXTest
* Class ReaderTest
*
* @package Box\Spout\Reader
* @package Box\Spout\Reader\XLSX
*/
class XLSXTest extends \PHPUnit_Framework_TestCase
class ReaderTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
@ -23,6 +25,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
return [
['/path/to/fake/file.xlsx'],
['file_with_no_sheets_in_content_types.xlsx'],
['file_with_sheet_xml_not_matching_content_types.xlsx'],
['file_corrupted.xlsx'],
];
}
@ -36,38 +39,8 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
*/
public function testReadShouldThrowException($filePath)
{
$this->getAllRowsForFile($filePath);
}
/**
* @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException
*
* @return void
*/
public function testHasNextSheetShouldThrowExceptionIfReaderNotOpened()
{
$reader = ReaderFactory::create(Type::XLSX);
$reader->hasNextSheet();
}
/**
* @expectedException \Box\Spout\Reader\Exception\EndOfWorksheetsReachedException
*
* @return void
*/
public function testNextSheetShouldThrowExceptionIfNoMoreSheetsToRead()
{
$fileName = 'one_sheet_with_shared_strings.xlsx';
$resourcePath = $this->getResourcePath($fileName);
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($resourcePath);
while ($reader->hasNextSheet()) {
$reader->nextSheet();
}
$reader->nextSheet();
// using @ to prevent warnings/errors from being displayed
@$this->getAllRowsForFile($filePath);
}
/**
@ -120,6 +93,9 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
*/
public function testReadShouldSupportAllCellTypes()
{
// make sure dates are always created with the same timezone
date_default_timezone_set('UTC');
$allRows = $this->getAllRowsForFile('sheet_with_all_cell_types.xlsx');
$expectedRows = [
@ -270,7 +246,8 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$startTime = microtime(true);
try {
$this->getAllRowsForFile($fileName);
// using @ to prevent warnings/errors from being displayed
@$this->getAllRowsForFile($fileName);
$this->fail('An exception should have been thrown');
} catch (IOException $exception) {
$duration = microtime(true) - $startTime;
@ -305,6 +282,60 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadMultipleTimesShouldRewindReader()
{
$allRows = [];
$resourcePath = $this->getResourcePath('two_sheets_with_inline_strings.xlsx');
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheet) {
// do nothing
}
foreach ($reader->getSheetIterator() as $sheet) {
// this loop should only add the first row of the first sheet
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
// this loop should rewind the iterator and restart reading from the 1st row again
// therefore, it should only add the first row of the first sheet
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
// not reading any more sheets
break;
}
foreach ($reader->getSheetIterator() as $sheet) {
// this loop should only add the first row of the current sheet
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
// not breaking, so we keep reading the next sheets
}
$reader->close();
$expectedRows = [
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
['s2 - A1', 's2 - B1', 's2 - C1', 's2 - D1', 's2 - E1'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @param string $fileName
* @return array All the read rows the given file
@ -317,11 +348,9 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($resourcePath);
while ($reader->hasNextSheet()) {
$reader->nextSheet();
while ($reader->hasNextRow()) {
$allRows[] = $reader->nextRow();
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
foreach ($sheet->getRowIterator() as $rowIndex => $row) {
$allRows[] = $row;
}
}

View File

@ -1,14 +1,15 @@
<?php
namespace Box\Spout\Reader;
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\TestUsingResource;
/**
* Class SheetTest
*
* @package Box\Spout\Reader
* @package Box\Spout\Reader\XLSX
*/
class SheetTest extends \PHPUnit_Framework_TestCase
{
@ -41,8 +42,8 @@ class SheetTest extends \PHPUnit_Framework_TestCase
$reader->open($resourcePath);
$sheets = [];
while ($reader->hasNextSheet()) {
$sheets[] = $reader->nextSheet();
foreach ($reader->getSheetIterator() as $sheet) {
$sheets[] = $sheet;
}
$reader->close();

View File

@ -1,16 +1,17 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\CSV;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\WriterFactory;
/**
* Class CSVTest
* Class WriterTest
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\CSV
*/
class CSVTest extends \PHPUnit_Framework_TestCase
class WriterTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
@ -69,7 +70,7 @@ class CSVTest extends \PHPUnit_Framework_TestCase
];
$writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_utf8_bom.csv');
$this->assertContains(CSV::UTF8_BOM, $writtenContent, 'The CSV file should contain a UTF-8 BOM');
$this->assertContains(Writer::UTF8_BOM, $writtenContent, 'The CSV file should contain a UTF-8 BOM');
}
/**
@ -161,6 +162,6 @@ class CSVTest extends \PHPUnit_Framework_TestCase
private function trimWrittenContent($writtenContent)
{
// remove line feeds and UTF-8 BOM
return trim($writtenContent, PHP_EOL . CSV::UTF8_BOM);
return trim($writtenContent, PHP_EOL . Writer::UTF8_BOM);
}
}

View File

@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class CellHelperTest
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class CellHelperTest extends \PHPUnit_Framework_TestCase
{

View File

@ -1,14 +1,15 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\WriterFactory;
/**
* Class SheetTest
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class SheetTest extends \PHPUnit_Framework_TestCase
{
@ -69,7 +70,7 @@ class SheetTest extends \PHPUnit_Framework_TestCase
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);

View File

@ -1,16 +1,17 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\WriterFactory;
/**
* Class XLSXTest
*
* @package Box\Spout\Writer
*/
class XLSXTest extends \PHPUnit_Framework_TestCase
class WriterTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
@ -230,7 +231,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings(true);
@ -278,7 +279,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
];
// set the maxRowsPerSheet limit to 2
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\Internal\XLSX\Workbook', 'maxRowsPerWorksheet', 2);
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\XLSX\Internal\Workbook', 'maxRowsPerWorksheet', 2);
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = true);
$this->assertEquals(2, count($writer->getSheets()), '2 sheets should have been created.');
@ -302,7 +303,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
];
// set the maxRowsPerSheet limit to 2
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\Internal\XLSX\Workbook', 'maxRowsPerWorksheet', 2);
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\XLSX\Internal\Workbook', 'maxRowsPerWorksheet', 2);
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = false);
$this->assertEquals(1, count($writer->getSheets()), 'Only 1 sheet should have been created.');
@ -348,14 +349,14 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
* @param string $fileName
* @param bool $shouldUseInlineStrings
* @param bool $shouldCreateSheetsAutomatically
* @return XLSX
* @return Writer
*/
private function writeToXLSXFile($allRows, $fileName, $shouldUseInlineStrings = true, $shouldCreateSheetsAutomatically = true)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings($shouldUseInlineStrings);
$writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically);
@ -373,14 +374,14 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
* @param string $fileName
* @param bool $shouldUseInlineStrings
* @param bool $shouldCreateSheetsAutomatically
* @return XLSX
* @return Writer
*/
private function writeToMultipleSheetsInXLSXFile($allRows, $numSheets, $fileName, $shouldUseInlineStrings = true, $shouldCreateSheetsAutomatically = true)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings($shouldUseInlineStrings);
$writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically);