First external release
This commit is contained in:
commit
5e199009e6
81 files changed
+5447
No files matched your search
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
|
||||
use Box\Spout\Reader\Exception\EndOfFileReachedException;
|
||||
|
||||
/**
|
||||
* Class AbstractReader
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
* @abstract
|
||||
*/
|
||||
abstract class AbstractReader implements ReaderInterface
|
||||
{
|
||||
/** @var int Used to keep track of the row number */
|
||||
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;
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function openReader($filePath);
|
||||
|
||||
/**
|
||||
* Reads and returns next row if available.
|
||||
*
|
||||
* @return array|null Array that contains the data for the read row or null at the end of the file
|
||||
*/
|
||||
abstract protected function read();
|
||||
|
||||
/**
|
||||
* Closes the reader. To be used after reading the file.
|
||||
*
|
||||
* @return AbstractReader
|
||||
*/
|
||||
abstract protected function closeReader();
|
||||
|
||||
/**
|
||||
* @param $globalFunctionsHelper
|
||||
* @return AbstractReader
|
||||
*/
|
||||
public function setGlobalFunctionsHelper($globalFunctionsHelper)
|
||||
{
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the reader to read the given file. It also makes sure
|
||||
* that the file exists and is readable.
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
|
||||
*/
|
||||
public function open($filePath)
|
||||
{
|
||||
if (!$this->isPhpStream($filePath)) {
|
||||
// we skip the checks if the provided file path points to a PHP stream
|
||||
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
||||
throw new IOException('Could not open ' . $filePath . ' for reading! File does not exist.');
|
||||
} else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
|
||||
throw new IOException('Could not open ' . $filePath . ' for reading! File is not readable.');
|
||||
}
|
||||
}
|
||||
|
||||
$this->currentRowIndex = 0;
|
||||
$this->hasReachedEndOfFile = false;
|
||||
|
||||
try {
|
||||
$this->openReader($filePath);
|
||||
$this->isStreamOpened = true;
|
||||
} catch (\Exception $exception) {
|
||||
throw new IOException('Could not open ' . $filePath . ' for reading! (' . $exception->getMessage() . ')');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path maps to a PHP stream
|
||||
*/
|
||||
protected function isPhpStream($filePath)
|
||||
{
|
||||
return (strpos($filePath, 'php://') === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public function hasNextRow()
|
||||
{
|
||||
if (!$this->isStreamOpened) {
|
||||
throw new ReaderNotOpenedException('Stream 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader, preventing any additional reading
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if ($this->isStreamOpened) {
|
||||
$this->closeReader();
|
||||
$this->isStreamOpened = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?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 XLSX 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class EndOfFileReachedException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class EndOfFileReachedException extends ReaderException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class EndOfWorksheetsReachedException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class EndOfWorksheetsReachedException extends ReaderException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class NoWorksheetsFoundException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class NoWorksheetsFoundException extends ReaderException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
use Box\Spout\Common\Exception\SpoutException;
|
||||
|
||||
/**
|
||||
* Class ReaderException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
* @abstract
|
||||
*/
|
||||
abstract class ReaderException extends SpoutException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class ReaderNotOpenedException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class ReaderNotOpenedException extends ReaderException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Exception;
|
||||
|
||||
/**
|
||||
* Class SharedStringNotFoundException
|
||||
*
|
||||
* @package Box\Spout\Reader\Exception
|
||||
*/
|
||||
class SharedStringNotFoundException extends ReaderException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Helper\XLSX;
|
||||
|
||||
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class CellHelper
|
||||
* This class provides helper functions when working with cells
|
||||
*
|
||||
* @package Box\Spout\Reader\Helper\XLSX
|
||||
*/
|
||||
class CellHelper
|
||||
{
|
||||
/**
|
||||
* Fills the missing indexes of an array with a given value.
|
||||
* For instance, $dataArray = []; $a[1] = 1; $a[3] = 3;
|
||||
* Calling fillMissingArrayIndexes($dataArray, 'FILL') will return this array: ['FILL', 1, 'FILL', 3]
|
||||
*
|
||||
* @param array $dataArray The array to fill
|
||||
* @param string|void $fillValue optional
|
||||
* @return array
|
||||
*/
|
||||
public static function fillMissingArrayIndexes($dataArray, $fillValue = '')
|
||||
{
|
||||
$existingIndexes = array_keys($dataArray);
|
||||
|
||||
$newIndexes = array_fill_keys(range(min($existingIndexes), max($existingIndexes)), $fillValue);
|
||||
$dataArray += $newIndexes;
|
||||
|
||||
ksort($dataArray);
|
||||
|
||||
return $dataArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base 10 column index associated to the cell index (base 26).
|
||||
* Excel uses A to Z letters for column indexing, where A is the 1st column,
|
||||
* Z is the 26th and AA is the 27th.
|
||||
* The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26.
|
||||
*
|
||||
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
|
||||
* @return int
|
||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
|
||||
*/
|
||||
public static function getColumnIndexFromCellIndex($cellIndex)
|
||||
{
|
||||
if (!self::isValidCellIndex($cellIndex)) {
|
||||
throw new InvalidArgumentException('Cannot get column index from an invalid cell index.');
|
||||
}
|
||||
|
||||
$columnIndex = 0;
|
||||
$capitalAAsciiValue = ord('A');
|
||||
$capitalZAsciiValue = ord('Z');
|
||||
$step = $capitalZAsciiValue - $capitalAAsciiValue + 1;
|
||||
|
||||
// Remove row information
|
||||
$column = preg_replace('/\d/', '', $cellIndex);
|
||||
$columnLength = strlen($column);
|
||||
|
||||
/*
|
||||
* This is how the following loop will process the data:
|
||||
* A => 0
|
||||
* Z => 25
|
||||
* AA => 26 : (26^(2-1) * (0+1)) + 0
|
||||
* AB => 27 : (26^(2-1) * (0+1)) + 1
|
||||
* BC => 54 : (26^(2-1) * (1+1)) + 2
|
||||
* BCZ => 1455 : (26^(3-1) * (1+1)) + (26^(2-1) * (2+1)) + 25
|
||||
*/
|
||||
foreach (str_split($column) as $single_cell_index)
|
||||
{
|
||||
$currentColumnIndex = ord($single_cell_index) - $capitalAAsciiValue;
|
||||
|
||||
if ($columnLength == 1) {
|
||||
$columnIndex += $currentColumnIndex;
|
||||
} else {
|
||||
$columnIndex += pow($step, ($columnLength - 1)) * ($currentColumnIndex + 1);
|
||||
}
|
||||
|
||||
$columnLength--;
|
||||
}
|
||||
|
||||
return $columnIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a cell index is valid, in an Excel world.
|
||||
* To be valid, the cell index should start with capital letters and be followed by numbers.
|
||||
*
|
||||
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isValidCellIndex($cellIndex)
|
||||
{
|
||||
return (preg_match('/^[A-Z]+\d+$/', $cellIndex) === 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Helper\XLSX;
|
||||
|
||||
use Box\Spout\Common\Exception\IOException;
|
||||
use Box\Spout\Common\Helper\FileSystemHelper;
|
||||
use Box\Spout\Reader\Exception\SharedStringNotFoundException;
|
||||
|
||||
/**
|
||||
* Class SharedStringsHelper
|
||||
* This class provides helper functions for reading sharedStrings XML file
|
||||
*
|
||||
* @package Box\Spout\Reader\Helper\XLSX
|
||||
*/
|
||||
class SharedStringsHelper
|
||||
{
|
||||
/** Path of sharedStrings XML file inside the XLSX file */
|
||||
const SHARED_STRINGS_XML_FILE_PATH = 'xl/sharedStrings.xml';
|
||||
|
||||
/** Main namespace for the sharedStrings.xml file */
|
||||
const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||
|
||||
/**
|
||||
* To avoid running out of memory when extracting the shared strings, they will be saved to temporary files
|
||||
* instead of in memory. Then, when accessing a string, the corresponding file contents will be loaded in memory
|
||||
* and the string will be quickly retrieved.
|
||||
* The performance bottleneck is not when creating these temporary files, but rather when loading their content.
|
||||
* Because the contents of the last loaded file stays in memory until another file needs to be loaded, it works
|
||||
* best when the indexes of the shared strings are sorted in the sheet data.
|
||||
* 10,000 was chosen because it creates small files that are fast to be loaded in memory.
|
||||
*/
|
||||
const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var string Temporary folder where the temporary files to store shared strings will be stored */
|
||||
protected $tempFolder;
|
||||
|
||||
/** @var \Box\Spout\Writer\Helper\XLSX\FileSystemHelper Helper to perform file system operations */
|
||||
protected $fileSystemHelper;
|
||||
|
||||
/** @var resource Pointer to the last temp file a shared string was written to */
|
||||
protected $tempFilePointer;
|
||||
|
||||
/**
|
||||
* @var string Path of the temporary file whose contents is currently stored in memory
|
||||
* @see MAX_NUM_STRINGS_PER_TEMP_FILE
|
||||
*/
|
||||
protected $inMemoryTempFilePath;
|
||||
|
||||
/**
|
||||
* @var string Contents of the temporary file that was last read
|
||||
* @see MAX_NUM_STRINGS_PER_TEMP_FILE
|
||||
*/
|
||||
protected $inMemoryTempFileContents;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
|
||||
*/
|
||||
public function __construct($filePath, $tempFolder = null)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
|
||||
$rootTempFolder = ($tempFolder) ?: sys_get_temp_dir();
|
||||
$this->fileSystemHelper = new FileSystemHelper($rootTempFolder);
|
||||
$this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an in-memory array containing all the shared strings of the worksheet.
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read
|
||||
*/
|
||||
public function extractSharedStrings()
|
||||
{
|
||||
$xmlReader = new \XMLReader();
|
||||
$sharedStringIndex = 0;
|
||||
$this->tempFilePointer = null;
|
||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||
|
||||
$sharedStringsFilePath = 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH;
|
||||
if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NONET) === false) {
|
||||
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
|
||||
}
|
||||
|
||||
while ($xmlReader->read() && $xmlReader->name !== 'si') {
|
||||
// do nothing until a 'si' tag is reached
|
||||
}
|
||||
|
||||
while ($xmlReader->name === 'si') {
|
||||
$node = new \SimpleXMLElement($xmlReader->readOuterXml());
|
||||
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
|
||||
|
||||
// removes nodes that should not be read, like the pronunciation of the Kanji characters
|
||||
$cleanNode = $this->removeSuperfluousTextNodes($node);
|
||||
|
||||
// find all text nodes 't'; there can be multiple if the cell contains formatting
|
||||
$textNodes = $cleanNode->xpath('//ns:t');
|
||||
|
||||
$textValue = '';
|
||||
foreach ($textNodes as $textNode) {
|
||||
if ($this->shouldPreserveWhitespace($textNode)) {
|
||||
$textValue .= $textNode->__toString();
|
||||
} else {
|
||||
$textValue .= trim($textNode->__toString());
|
||||
}
|
||||
}
|
||||
|
||||
$unescapedTextValue = $escaper->unescape($textValue);
|
||||
$this->writeSharedStringToTempFile($unescapedTextValue, $sharedStringIndex);
|
||||
|
||||
$sharedStringIndex++;
|
||||
|
||||
// jump to the next 'si' tag
|
||||
$xmlReader->next('si');
|
||||
}
|
||||
|
||||
// close pointer to the last temp file that was written
|
||||
if ($this->tempFilePointer) {
|
||||
fclose($this->tempFilePointer);
|
||||
}
|
||||
|
||||
$xmlReader->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes nodes that should not be read, like the pronunciation of the Kanji characters.
|
||||
* By keeping them, their text content would be added to the read string.
|
||||
*
|
||||
* @param \SimpleXMLElement $parentNode Parent node that may contain nodes to remove
|
||||
* @return \SimpleXMLElement Cleaned parent node
|
||||
*/
|
||||
protected function removeSuperfluousTextNodes($parentNode)
|
||||
{
|
||||
$tagsToRemove = [
|
||||
'rPh', // Pronunciation of the text
|
||||
];
|
||||
|
||||
foreach ($tagsToRemove as $tagToRemove) {
|
||||
$xpath = '//ns:' . $tagToRemove;
|
||||
$nodesToRemove = $parentNode->xpath($xpath);
|
||||
|
||||
foreach ($nodesToRemove as $nodeToRemove) {
|
||||
// This is how to remove a node from the XML
|
||||
unset($nodeToRemove[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return $parentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
|
||||
*
|
||||
* @param \SimpleXMLElement $textNode The text node element (<t>) whitespace may be preserved
|
||||
* @return bool Whether whitespace should be preserved
|
||||
*/
|
||||
protected function shouldPreserveWhitespace($textNode)
|
||||
{
|
||||
$shouldPreserveWhitespace = false;
|
||||
|
||||
$attributes = $textNode->attributes('xml', true);
|
||||
if ($attributes) {
|
||||
foreach ($attributes as $attributeName => $attributeValue) {
|
||||
if ($attributeName === 'space' && $attributeValue->__toString() === 'preserve') {
|
||||
$shouldPreserveWhitespace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $shouldPreserveWhitespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given string to its associated temp file.
|
||||
* A new temporary file is created when the previous one has reached its max capacity.
|
||||
*
|
||||
* @param string $sharedString Shared string to write to the temp file
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return void
|
||||
*/
|
||||
protected function writeSharedStringToTempFile($sharedString, $sharedStringIndex)
|
||||
{
|
||||
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
|
||||
|
||||
if (!file_exists($tempFilePath)) {
|
||||
if ($this->tempFilePointer) {
|
||||
fclose($this->tempFilePointer);
|
||||
}
|
||||
$this->tempFilePointer = fopen($tempFilePath, 'w');
|
||||
}
|
||||
|
||||
fwrite($this->tempFilePointer, $sharedString . PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path for the temp file that should contain the string for the given index
|
||||
*
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return string The temp file path for the given index
|
||||
*/
|
||||
protected function getSharedStringTempFilePath($sharedStringIndex)
|
||||
{
|
||||
$numTempFile = intval($sharedStringIndex / self::MAX_NUM_STRINGS_PER_TEMP_FILE);
|
||||
return $this->tempFolder . DIRECTORY_SEPARATOR . 'sharedstrings' . $numTempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared string at the given index.
|
||||
* Because the strings have been split into different files, it looks for the value in the correct file.
|
||||
*
|
||||
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
|
||||
* @return string The shared string at the given index
|
||||
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
|
||||
*/
|
||||
public function getStringAtIndex($sharedStringIndex)
|
||||
{
|
||||
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
|
||||
$indexInFile = $sharedStringIndex % self::MAX_NUM_STRINGS_PER_TEMP_FILE;
|
||||
|
||||
if (!file_exists($tempFilePath)) {
|
||||
throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
|
||||
}
|
||||
|
||||
if ($this->inMemoryTempFilePath !== $tempFilePath) {
|
||||
// free memory
|
||||
unset($this->inMemoryTempFileContents);
|
||||
|
||||
$this->inMemoryTempFileContents = explode(PHP_EOL, file_get_contents($tempFilePath));
|
||||
$this->inMemoryTempFilePath = $tempFilePath;
|
||||
}
|
||||
|
||||
$sharedString = null;
|
||||
if (array_key_exists($indexInFile, $this->inMemoryTempFileContents)) {
|
||||
$sharedString = $this->inMemoryTempFileContents[$indexInFile];
|
||||
}
|
||||
|
||||
if (!$sharedString) {
|
||||
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
|
||||
}
|
||||
|
||||
return rtrim($sharedString, PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the created temporary folder and all its contents
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cleanup()
|
||||
{
|
||||
$this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\Helper\XLSX;
|
||||
|
||||
use Box\Spout\Reader\Internal\XLSX\Worksheet;
|
||||
|
||||
/**
|
||||
* Class WorksheetHelper
|
||||
* This class provides helper functions related to XLSX worksheets
|
||||
*
|
||||
* @package Box\Spout\Reader\Helper\XLSX
|
||||
*/
|
||||
class WorksheetHelper
|
||||
{
|
||||
/** Path of Content_Types XML file inside the XLSX file */
|
||||
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
||||
|
||||
/** Main namespace for the [Content_Types].xml file */
|
||||
const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
||||
|
||||
/** Value of the Override attribute used in [Content_Types].xml to define worksheets */
|
||||
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
*/
|
||||
public function __construct($filePath)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file paths of the worksheet data XML files within the XLSX file.
|
||||
* The paths are read from the [Content_Types].xml file.
|
||||
*
|
||||
* @return Worksheet[] Worksheets within the XLSX file
|
||||
*/
|
||||
public function getWorksheets()
|
||||
{
|
||||
$worksheets = [];
|
||||
|
||||
$xmlContents = file_get_contents('zip://' . $this->filePath . '#' . self::CONTENT_TYPES_XML_FILE_PATH);
|
||||
|
||||
$contentTypes = new \SimpleXMLElement($xmlContents);
|
||||
$contentTypes->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML);
|
||||
|
||||
// find all nodes defining a worksheet
|
||||
$sheetNodes = $contentTypes->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
|
||||
|
||||
for ($i = 0; $i < count($sheetNodes); $i++) {
|
||||
$sheetNode = $sheetNodes[$i];
|
||||
$sheetDataXMLFilePath = (string) $sheetNode->attributes()->PartName;
|
||||
$worksheets[] = new Worksheet($i, $sheetDataXMLFilePath);
|
||||
}
|
||||
|
||||
return $worksheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->getWorksheetNumber() + 1 < count($allWorksheets)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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 int Worksheet number, based on the order of appareance in [Content_Types].xml (zero-based) */
|
||||
protected $worksheetNumber;
|
||||
|
||||
/** @var string Path of the XML file containing the worksheet data */
|
||||
protected $dataXmlFilePath;
|
||||
|
||||
/**
|
||||
* @param int $worksheetNumber Worksheet number, 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($worksheetNumber, $dataXmlFilePath)
|
||||
{
|
||||
$this->worksheetNumber = $worksheetNumber;
|
||||
$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, DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getWorksheetNumber()
|
||||
{
|
||||
return $this->worksheetNumber;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
use Box\Spout\Common\Exception\UnsupportedTypeException;
|
||||
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
|
||||
use Box\Spout\Common\Type;
|
||||
|
||||
/**
|
||||
* Class ReaderFactory
|
||||
* This factory is used to create readers, based on the type of the file to be read.
|
||||
* It supports CSV and XLSX formats.
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
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
|
||||
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||
*/
|
||||
public static function create($readerType)
|
||||
{
|
||||
$reader = null;
|
||||
|
||||
switch ($readerType) {
|
||||
case Type::CSV:
|
||||
$reader = new CSV();
|
||||
break;
|
||||
case Type::XLSX:
|
||||
$reader = new XLSX();
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
|
||||
}
|
||||
|
||||
$reader->setGlobalFunctionsHelper(new GlobalFunctionsHelper());
|
||||
|
||||
return $reader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
/**
|
||||
* Interface ReaderInterface
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
interface ReaderInterface
|
||||
{
|
||||
/**
|
||||
* Prepares the reader to read the given file. It also makes sure
|
||||
* that the file exists and is readable.
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
* Closes the reader, preventing any additional reading
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close();
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?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
|
||||
{
|
||||
/** @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) {
|
||||
// Extracts all the strings from the worksheets for easy access in the future
|
||||
$this->extractSharedStrings($filePath);
|
||||
|
||||
// Fetch all available worksheets
|
||||
$this->worksheetHelper = new WorksheetHelper($filePath);
|
||||
$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.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an in-memory array containing all the shared strings of the worksheets.
|
||||
*
|
||||
* @param string $filePath Path of the XLSX file to be read
|
||||
* @return void
|
||||
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings XML file can't be read
|
||||
*/
|
||||
protected function extractSharedStrings($filePath)
|
||||
{
|
||||
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
|
||||
$this->sharedStringsHelper->extractSharedStrings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 void
|
||||
* @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()) {
|
||||
if ($this->currentWorksheet === null) {
|
||||
$nextWorksheet = $this->worksheets[0];
|
||||
} else {
|
||||
$currentWorksheetNumber = $this->currentWorksheet->getWorksheetNumber();
|
||||
$nextWorksheet = $this->worksheets[$currentWorksheetNumber + 1];
|
||||
}
|
||||
|
||||
$this->initXmlReaderForWorksheetData($nextWorksheet);
|
||||
$this->currentWorksheet = $nextWorksheet;
|
||||
|
||||
// make sure that we are ready to read more rows
|
||||
$this->hasReachedEndOfFile = false;
|
||||
$this->emptyRowDataBuffer();
|
||||
} else {
|
||||
throw new EndOfWorksheetsReachedException('End of worksheets was reached. Cannot read more worksheets.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()');
|
||||
}
|
||||
|
||||
$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
|
||||
list(, $lastCellIndex) = explode(':', $dimensionRef);
|
||||
$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();
|
||||
|
||||
$hasSharedString = ($this->xmlReader->getAttribute('t') === 's');
|
||||
if ($hasSharedString) {
|
||||
$sharedStringIndex = intval($node->nodeValue);
|
||||
$rowData[$currentColumnIndex] = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
|
||||
} else {
|
||||
// for inline strings or numbers, just get the value
|
||||
$rowData[$currentColumnIndex] = trim($node->nodeValue);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user