Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3edb056286 | ||
|
|
edc0c22009 | ||
|
|
0319e578cb | ||
|
|
1e2452934c | ||
|
|
3e1793d852 | ||
|
|
c1b1bd0b76 | ||
|
|
cf239905d7 | ||
|
|
7c8f9293cc | ||
|
|
503ba97e9d | ||
|
|
b3df57d2e5 | ||
|
|
c6ebf115fc | ||
|
|
7d922e6776 | ||
|
|
d4cf853270 | ||
|
|
8bac924d48 | ||
|
|
591d86cf07 | ||
|
|
ba4a55292d | ||
|
|
d617a50b85 | ||
|
|
92981fa80b | ||
|
|
766c733466 | ||
|
|
46cae76546 | ||
|
|
03be58b84d | ||
|
|
b21bb86682 | ||
|
|
8a45cec220 | ||
|
|
04d41d7c9f |
No files matched your search
+7
-3
@@ -8,7 +8,11 @@ php:
|
|||||||
- hhvm
|
- hhvm
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- composer self-update
|
- composer install --no-interaction
|
||||||
- composer install --prefer-source
|
|
||||||
|
|
||||||
script: phpunit
|
script:
|
||||||
|
- mkdir -p build/logs
|
||||||
|
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
|
||||||
|
|
||||||
|
after_script:
|
||||||
|
- php vendor/bin/coveralls -v
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
# Spout
|
# Spout
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/box/spout)
|
||||||
|
[](http://opensource.box.com/badges)
|
||||||
|
[](http://travis-ci.org/box/spout)
|
||||||
|
[](https://coveralls.io/r/box/spout?branch=master)
|
||||||
|
[](https://packagist.org/packages/box/spout)
|
||||||
|
[](https://packagist.org/packages/box/spout)
|
||||||
|
|
||||||
Spout is a PHP library to read and write CSV and XLSX files, in a fast and scalable way.
|
Spout is a PHP library to read and write CSV and XLSX files, in a fast and scalable way.
|
||||||
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
|
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
|
||||||
|
|
||||||
[](http://travis-ci.org/box/spout)
|
|
||||||
[](http://opensource.box.com/badges)
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -248,6 +252,11 @@ Here are a few numbers regarding the performance of Spout:
|
|||||||
| Read XLSX (using shared strings) | 1 second | 1-2 minutes | 35-40 minutes |
|
| Read XLSX (using shared strings) | 1 second | 1-2 minutes | 35-40 minutes |
|
||||||
| Write XLSX | 1 second | 20-25 seconds | 8-10 minutes |
|
| Write XLSX | 1 second | 20-25 seconds | 8-10 minutes |
|
||||||
|
|
||||||
|
#### Does Spout support charts or formulas?
|
||||||
|
|
||||||
|
No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used.
|
||||||
|
So the larger the file would be, the more memory would be consumed, preventing your code to scale well.
|
||||||
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,6 @@
|
|||||||
"name": "box/spout",
|
"name": "box/spout",
|
||||||
"description": "PHP Library to read and write CSV and XLSX files, in a fast and scalable way",
|
"description": "PHP Library to read and write CSV and XLSX files, in a fast and scalable way",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"version": "1.0.8",
|
|
||||||
"keywords": ["php","read","write","csv","xlsx","excel","spreadsheet","scale","memory","stream","ooxml"],
|
"keywords": ["php","read","write","csv","xlsx","excel","spreadsheet","scale","memory","stream","ooxml"],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"homepage": "https://www.github.com/box/spout",
|
"homepage": "https://www.github.com/box/spout",
|
||||||
@@ -19,7 +18,8 @@
|
|||||||
"ext-simplexml": "*"
|
"ext-simplexml": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": ">=3.7"
|
"phpunit/phpunit": ">=3.7",
|
||||||
|
"satooshi/php-coveralls": "^0.6.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
@@ -14,10 +14,6 @@
|
|||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
<logging>
|
|
||||||
<log type="coverage-html" target="tests/coverage/"/>
|
|
||||||
</logging>
|
|
||||||
|
|
||||||
<filter>
|
<filter>
|
||||||
<whitelist>
|
<whitelist>
|
||||||
<directory suffix=".php">src/</directory>
|
<directory suffix=".php">src/</directory>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class FileSystemHelper
|
|||||||
{
|
{
|
||||||
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
||||||
|
|
||||||
$folderPath = $parentFolderPath . DIRECTORY_SEPARATOR . $folderName;
|
$folderPath = $parentFolderPath . '/' . $folderName;
|
||||||
|
|
||||||
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
|
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
|
||||||
if (!$wasCreationSuccessful) {
|
if (!$wasCreationSuccessful) {
|
||||||
@@ -60,7 +60,7 @@ class FileSystemHelper
|
|||||||
{
|
{
|
||||||
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
$this->throwIfOperationNotInBaseFolder($parentFolderPath);
|
||||||
|
|
||||||
$filePath = $parentFolderPath . DIRECTORY_SEPARATOR . $fileName;
|
$filePath = $parentFolderPath . '/' . $fileName;
|
||||||
|
|
||||||
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
|
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
|
||||||
if (!$wasCreationSuccessful) {
|
if (!$wasCreationSuccessful) {
|
||||||
|
|||||||
@@ -71,6 +71,24 @@ class SharedStringsHelper
|
|||||||
$this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
|
$this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the XLSX file contains a shared strings XML file
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasSharedStrings()
|
||||||
|
{
|
||||||
|
$hasSharedStrings = false;
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
|
||||||
|
if ($zip->open($this->filePath) === true) {
|
||||||
|
$hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false);
|
||||||
|
$zip->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasSharedStrings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 worksheet.
|
||||||
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
|
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
|
||||||
@@ -94,7 +112,7 @@ class SharedStringsHelper
|
|||||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
|
||||||
$sharedStringsFilePath = $this->getSharedStringsFilePath();
|
$sharedStringsFilePath = $this->getSharedStringsFilePath();
|
||||||
if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NONET) === false) {
|
if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NOENT|LIBXML_NONET) === false) {
|
||||||
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
|
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,12 +162,11 @@ class SharedStringsHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string The path to the shared strings XML file, working cross-platforms
|
* @return string The path to the shared strings XML file
|
||||||
*/
|
*/
|
||||||
protected function getSharedStringsFilePath()
|
protected function getSharedStringsFilePath()
|
||||||
{
|
{
|
||||||
$sharedStringsXmlFilePath = str_replace('/', DIRECTORY_SEPARATOR, self::SHARED_STRINGS_XML_FILE_PATH);
|
return 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH;
|
||||||
return 'zip://' . $this->filePath . '#' . $sharedStringsXmlFilePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -232,7 +249,7 @@ class SharedStringsHelper
|
|||||||
protected function getSharedStringTempFilePath($sharedStringIndex)
|
protected function getSharedStringTempFilePath($sharedStringIndex)
|
||||||
{
|
{
|
||||||
$numTempFile = intval($sharedStringIndex / self::MAX_NUM_STRINGS_PER_TEMP_FILE);
|
$numTempFile = intval($sharedStringIndex / self::MAX_NUM_STRINGS_PER_TEMP_FILE);
|
||||||
return $this->tempFolder . DIRECTORY_SEPARATOR . 'sharedstrings' . $numTempFile;
|
return $this->tempFolder . '/sharedstrings' . $numTempFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -186,8 +186,7 @@ class WorksheetHelper
|
|||||||
*/
|
*/
|
||||||
protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace)
|
protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace)
|
||||||
{
|
{
|
||||||
$normalizedXmlFilePath = str_replace('/', DIRECTORY_SEPARATOR, $xmlFilePath);
|
$xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $xmlFilePath);
|
||||||
$xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $normalizedXmlFilePath);
|
|
||||||
|
|
||||||
$xmlElement = new \SimpleXMLElement($xmlContents);
|
$xmlElement = new \SimpleXMLElement($xmlContents);
|
||||||
$xmlElement->registerXPathNamespace('ns', $mainNamespace);
|
$xmlElement->registerXPathNamespace('ns', $mainNamespace);
|
||||||
|
|||||||
@@ -33,12 +33,11 @@ class Worksheet
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string Path of the XML file containing the worksheet data,
|
* @return string Path of the XML file containing the worksheet data,
|
||||||
* without the leading slash and working cross-platforms.
|
* without the leading slash.
|
||||||
*/
|
*/
|
||||||
public function getDataXmlFilePath()
|
public function getDataXmlFilePath()
|
||||||
{
|
{
|
||||||
$dataXmlFilePath = ltrim($this->dataXmlFilePath, '/');
|
return ltrim($this->dataXmlFilePath, '/');
|
||||||
return str_replace('/', DIRECTORY_SEPARATOR, $dataXmlFilePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+163
-24
@@ -19,6 +19,14 @@ use Box\Spout\Reader\Helper\XLSX\WorksheetHelper;
|
|||||||
*/
|
*/
|
||||||
class XLSX extends AbstractReader
|
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 */
|
/** @var string Real path of the file to read */
|
||||||
protected $filePath;
|
protected $filePath;
|
||||||
|
|
||||||
@@ -72,8 +80,12 @@ class XLSX extends AbstractReader
|
|||||||
$this->zip = new \ZipArchive();
|
$this->zip = new \ZipArchive();
|
||||||
|
|
||||||
if ($this->zip->open($filePath) === true) {
|
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
|
// Extracts all the strings from the worksheets for easy access in the future
|
||||||
$this->extractSharedStrings($filePath);
|
$this->sharedStringsHelper->extractSharedStrings();
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch all available worksheets
|
// Fetch all available worksheets
|
||||||
$this->worksheetHelper = new WorksheetHelper($filePath, $this->globalFunctionsHelper);
|
$this->worksheetHelper = new WorksheetHelper($filePath, $this->globalFunctionsHelper);
|
||||||
@@ -87,19 +99,6 @@ class XLSX extends AbstractReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Returns whether another worksheet exists after the current worksheet.
|
||||||
*
|
*
|
||||||
@@ -167,7 +166,7 @@ class XLSX extends AbstractReader
|
|||||||
$worksheetDataXMLFilePath = $worksheet->getDataXmlFilePath();
|
$worksheetDataXMLFilePath = $worksheet->getDataXmlFilePath();
|
||||||
|
|
||||||
$worksheetDataFilePath = 'zip://' . $this->filePath . '#' . $worksheetDataXMLFilePath;
|
$worksheetDataFilePath = 'zip://' . $this->filePath . '#' . $worksheetDataXMLFilePath;
|
||||||
if ($this->xmlReader->open($worksheetDataFilePath, null, LIBXML_NONET) === false) {
|
if ($this->xmlReader->open($worksheetDataFilePath, null, LIBXML_NOENT|LIBXML_NONET) === false) {
|
||||||
throw new IOException('Could not open "' . $worksheetDataXMLFilePath . '".');
|
throw new IOException('Could not open "' . $worksheetDataXMLFilePath . '".');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +185,7 @@ class XLSX extends AbstractReader
|
|||||||
throw new BadUsageException('You must call nextSheet() before calling hasNextRow() or nextRow()');
|
throw new BadUsageException('You must call nextSheet() before calling hasNextRow() or nextRow()');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
$isInsideRowTag = false;
|
$isInsideRowTag = false;
|
||||||
$rowData = [];
|
$rowData = [];
|
||||||
|
|
||||||
@@ -197,6 +197,7 @@ class XLSX extends AbstractReader
|
|||||||
$lastCellIndex = $matches[1];
|
$lastCellIndex = $matches[1];
|
||||||
$this->numberOfColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
|
$this->numberOfColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'row') {
|
} else if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'row') {
|
||||||
// Start of the row description
|
// Start of the row description
|
||||||
$isInsideRowTag = true;
|
$isInsideRowTag = true;
|
||||||
@@ -209,20 +210,15 @@ class XLSX extends AbstractReader
|
|||||||
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
|
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
|
||||||
}
|
}
|
||||||
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
|
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
|
||||||
|
|
||||||
} else if ($isInsideRowTag && $this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'c') {
|
} else if ($isInsideRowTag && $this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'c') {
|
||||||
// Start of a cell description
|
// Start of a cell description
|
||||||
$currentCellIndex = $this->xmlReader->getAttribute('r');
|
$currentCellIndex = $this->xmlReader->getAttribute('r');
|
||||||
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
|
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
|
||||||
$node = $this->xmlReader->expand();
|
|
||||||
|
|
||||||
$hasSharedString = ($this->xmlReader->getAttribute('t') === 's');
|
$node = $this->xmlReader->expand();
|
||||||
if ($hasSharedString) {
|
$rowData[$currentColumnIndex] = $this->getCellValue($node, $escaper);
|
||||||
$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') {
|
} else if ($this->xmlReader->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === 'row') {
|
||||||
// End of the row description
|
// End of the row description
|
||||||
// If needed, we fill the empty cells
|
// If needed, we fill the empty cells
|
||||||
@@ -235,6 +231,149 @@ class XLSX extends AbstractReader
|
|||||||
return ($rowData !== []) ? $rowData : null;
|
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.
|
* Closes the reader. To be used after reading the file.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function __construct($xlFolder)
|
public function __construct($xlFolder)
|
||||||
{
|
{
|
||||||
$sharedStringsFilePath = $xlFolder . DIRECTORY_SEPARATOR . self::SHARED_STRINGS_FILE_NAME;
|
$sharedStringsFilePath = $xlFolder . '/' . self::SHARED_STRINGS_FILE_NAME;
|
||||||
$this->sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w');
|
$this->sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w');
|
||||||
|
|
||||||
$this->throwIfSharedStringsFilePointerIsNotAvailable();
|
$this->throwIfSharedStringsFilePointerIsNotAvailable();
|
||||||
|
|||||||
@@ -33,18 +33,30 @@ class ZipHelper
|
|||||||
*/
|
*/
|
||||||
protected function addFolderToZip($zip, $folderPath)
|
protected function addFolderToZip($zip, $folderPath)
|
||||||
{
|
{
|
||||||
$folderRealPath = realpath($folderPath) . DIRECTORY_SEPARATOR;
|
$folderRealPath = $this->getNormalizedRealPath($folderPath) . '/';
|
||||||
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
|
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
|
||||||
foreach ($itemIterator as $itemInfo) {
|
foreach ($itemIterator as $itemInfo) {
|
||||||
$itemRealPath = realpath($itemInfo->getPathname());
|
$itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
|
||||||
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
|
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
|
||||||
|
|
||||||
if ($itemInfo->isFile()) {
|
if ($itemInfo->isFile()) {
|
||||||
$zip->addFile($itemInfo->getPathname(), $itemLocalPath);
|
$zip->addFile($itemRealPath, $itemLocalPath);
|
||||||
} else if ($itemInfo->isDir()) {
|
} else if ($itemInfo->isDir()) {
|
||||||
$zip->addEmptyDir($itemLocalPath);
|
$zip->addEmptyDir($itemLocalPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns canonicalized absolute pathname, containing only forward slashes.
|
||||||
|
*
|
||||||
|
* @param string $path Path to normalize
|
||||||
|
* @return string Normalized and canonicalized path
|
||||||
|
*/
|
||||||
|
protected function getNormalizedRealPath($path)
|
||||||
|
{
|
||||||
|
$realPath = realpath($path);
|
||||||
|
return str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ EOD;
|
|||||||
|
|
||||||
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
|
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
|
||||||
$this->worksheetFilePath = $worksheetFilesFolder . DIRECTORY_SEPARATOR . strtolower($this->externalSheet->getName()) . '.xml';
|
$this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml';
|
||||||
$this->startSheet();
|
$this->startSheet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,43 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldSupportFilesWithoutSharedStringsFile()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_no_shared_strings_file.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
[10, 11],
|
||||||
|
[20, 21],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldSupportAllCellTypes()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_all_cell_types.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
[
|
||||||
|
's1--A1', 's1--A2',
|
||||||
|
false, true,
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-06-03 13:21:58'),
|
||||||
|
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-06-01 00:00:00'),
|
||||||
|
10, 10.43,
|
||||||
|
null,
|
||||||
|
'weird string', // valid 'str' string
|
||||||
|
null, // invalid date
|
||||||
|
],
|
||||||
|
['', '', '', '', '', '', '', '', ''],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -215,7 +252,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
{
|
{
|
||||||
$allRows = $this->getAllRowsForFile('billion_laughs_test_file.xlsx');
|
$allRows = $this->getAllRowsForFile('billion_laughs_test_file.xlsx');
|
||||||
|
|
||||||
$expectedMaxMemoryUsage = 20 * 1024 * 1024; // 20MB
|
$expectedMaxMemoryUsage = 30 * 1024 * 1024; // 30MB
|
||||||
$this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.');
|
$this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.');
|
||||||
|
|
||||||
$expectedFirstRow = ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'];
|
$expectedFirstRow = ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'];
|
||||||
@@ -231,6 +268,21 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals([], $allRows, 'Sheet with no cells should be correctly processed.');
|
$this->assertEquals([], $allRows, 'Sheet with no cells should be correctly processed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldSkipFormulas()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_formulas.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['val1', 'val2', 'total1', 'total2'],
|
||||||
|
[10, 20, 30, 21],
|
||||||
|
[11, 21, 32, 41],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @return array All the read rows the given file
|
* @return array All the read rows the given file
|
||||||
|
|||||||
@@ -90,18 +90,9 @@ class SheetTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
private function assertSheetNameEquals($expectedName, $resourcePath, $message = '')
|
private function assertSheetNameEquals($expectedName, $resourcePath, $message = '')
|
||||||
{
|
{
|
||||||
$pathToWorkbookFile = $resourcePath . $this->normalizePath('#xl/workbook.xml');
|
$pathToWorkbookFile = $resourcePath . '#xl/workbook.xml';
|
||||||
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
|
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
|
||||||
|
|
||||||
$this->assertContains('<sheet name="' . $expectedName . '"', $xmlContents, $message);
|
$this->assertContains('<sheet name="' . $expectedName . '"', $xmlContents, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $path
|
|
||||||
* @return string The path with the correct directory separators, as defined for the current platform
|
|
||||||
*/
|
|
||||||
private function normalizePath($path)
|
|
||||||
{
|
|
||||||
return str_replace('/', DIRECTORY_SEPARATOR, $path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -408,7 +408,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
private function assertInlineDataWasWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
private function assertInlineDataWasWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
||||||
{
|
{
|
||||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
$pathToSheetFile = $resourcePath . $this->normalizePath('#xl/worksheets/sheet' . $sheetIndex . '.xml');
|
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml';
|
||||||
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
||||||
|
|
||||||
$this->assertContains((string)$inlineData, $xmlContents, $message);
|
$this->assertContains((string)$inlineData, $xmlContents, $message);
|
||||||
@@ -424,7 +424,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
private function assertInlineDataWasNotWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
private function assertInlineDataWasNotWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
||||||
{
|
{
|
||||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
$pathToSheetFile = $resourcePath . $this->normalizePath('#xl/worksheets/sheet' . $sheetIndex . '.xml');
|
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml';
|
||||||
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
||||||
|
|
||||||
$this->assertNotContains((string)$inlineData, $xmlContents, $message);
|
$this->assertNotContains((string)$inlineData, $xmlContents, $message);
|
||||||
@@ -439,18 +439,9 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
private function assertSharedStringWasWritten($fileName, $sharedString, $message = '')
|
private function assertSharedStringWasWritten($fileName, $sharedString, $message = '')
|
||||||
{
|
{
|
||||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
$pathToSharedStringsFile = $resourcePath . $this->normalizePath('#xl/sharedStrings.xml');
|
$pathToSharedStringsFile = $resourcePath . '#xl/sharedStrings.xml';
|
||||||
$xmlContents = file_get_contents('zip://' . $pathToSharedStringsFile);
|
$xmlContents = file_get_contents('zip://' . $pathToSharedStringsFile);
|
||||||
|
|
||||||
$this->assertContains($sharedString, $xmlContents, $message);
|
$this->assertContains($sharedString, $xmlContents, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $path
|
|
||||||
* @return string The path with the correct directory separators, as defined for the current platform
|
|
||||||
*/
|
|
||||||
private function normalizePath($path)
|
|
||||||
{
|
|
||||||
return str_replace('/', DIRECTORY_SEPARATOR, $path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user