Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4cf853270 | ||
|
|
8bac924d48 | ||
|
|
591d86cf07 | ||
|
|
ba4a55292d | ||
|
|
d617a50b85 | ||
|
|
92981fa80b | ||
|
|
766c733466 | ||
|
|
46cae76546 | ||
|
|
03be58b84d | ||
|
|
b21bb86682 | ||
|
|
8a45cec220 | ||
|
|
04d41d7c9f | ||
|
|
f8f194bc8f | ||
|
|
044ad11596 | ||
|
|
935ba1fb5e | ||
|
|
fb0175d633 | ||
|
|
69190cd855 | ||
|
|
13a53fb43f | ||
|
|
a848be52de | ||
|
|
52faef0168 | ||
|
|
3b4dfba38e | ||
|
|
cfd3e0ffa3 | ||
|
|
c6e943041e | ||
|
|
502dd54c81 | ||
|
|
c6cfc4c80f | ||
|
|
d02013c82e | ||
|
|
71d9c18a81 | ||
|
|
e9ec4e745c | ||
|
|
e58284d27b | ||
|
|
27a47f509f | ||
|
|
8aeb883ebc | ||
|
|
05c504e72c | ||
|
|
abbc158877 | ||
|
|
3f3461b002 | ||
|
|
2074781852 | ||
|
|
fd5cfc5440 | ||
|
|
a538813827 | ||
|
|
3e5ef284a5 | ||
|
|
f770f38967 | ||
|
|
0edd9c434f | ||
|
|
3c9bf12a5f | ||
|
|
69c1436625 | ||
|
|
af940069bc | ||
|
|
d6155a4243 | ||
|
|
538f6109ad | ||
|
|
93cdd398dd | ||
|
|
66e5792a7e | ||
|
|
d4dbc32a53 | ||
|
|
09eac62c22 | ||
|
|
7bb5e02d2c | ||
|
|
aebdd1acd7 | ||
|
|
41a449f245 | ||
|
|
39c72a91b4 | ||
|
|
4ca1fc5851 | ||
|
|
419544a85f | ||
|
|
6e11a043c1 | ||
|
|
c24cdbb9be | ||
|
|
18f6e64910 | ||
|
|
f7a682dbd6 | ||
|
|
7301fc47fb | ||
|
|
ce296f0662 | ||
|
|
6bc9a18e9b |
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[{.travis.yml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
/tests/resources/generated
|
/tests/resources/generated
|
||||||
|
/tests/coverage
|
||||||
/vendor
|
/vendor
|
||||||
composer.lock
|
composer.lock
|
||||||
/.idea
|
/.idea
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ php:
|
|||||||
- 5.4
|
- 5.4
|
||||||
- 5.5
|
- 5.5
|
||||||
- 5.6
|
- 5.6
|
||||||
|
- nightly
|
||||||
|
- hhvm
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- composer self-update
|
- composer self-update
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Spout
|
# Spout
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/box/spout)
|
||||||
|
[](http://opensource.box.com/badges)
|
||||||
|
[](http://travis-ci.org/box/spout)
|
||||||
|
[](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
|
||||||
|
|
||||||
@@ -149,7 +151,7 @@ $writer->setShouldCreateNewSheetsAutomatically(true); // default value
|
|||||||
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
|
$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using custom temporary folder
|
#### Using custom temporary folder
|
||||||
|
|
||||||
Processing XLSX files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by sys_get_temp_dir()). It is possible to override this by explicitly setting it on the reader or writer:
|
Processing XLSX files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by sys_get_temp_dir()). It is possible to override this by explicitly setting it on the reader or writer:
|
||||||
```php
|
```php
|
||||||
@@ -180,6 +182,23 @@ It is also possible to retrieve all the sheets currently created:
|
|||||||
$sheets = $writer->getSheets();
|
$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();
|
||||||
|
$sheetName = $sheet->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessing the sheet name when writing
|
||||||
|
$sheet = $writer->getCurrentSheet();
|
||||||
|
$sheetName = $sheet->getName();
|
||||||
|
|
||||||
|
// Customizing the sheet name when writing
|
||||||
|
$sheet = $writer->getCurrentSheet();
|
||||||
|
$sheetName = $sheet->setName('My custom name');
|
||||||
|
```
|
||||||
|
|
||||||
### Fluent interface
|
### Fluent interface
|
||||||
|
|
||||||
Because fluent interfaces are great, you can use them with Spout:
|
Because fluent interfaces are great, you can use them with Spout:
|
||||||
@@ -231,6 +250,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,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.0",
|
|
||||||
"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",
|
||||||
|
|||||||
+10
@@ -14,4 +14,14 @@
|
|||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
|
<logging>
|
||||||
|
<log type="coverage-html" target="tests/coverage/"/>
|
||||||
|
</logging>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory suffix=".php">src/</directory>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
@@ -13,6 +13,8 @@ class CSV implements EscaperInterface
|
|||||||
/**
|
/**
|
||||||
* Escapes the given string to make it compatible with CSV
|
* Escapes the given string to make it compatible with CSV
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*
|
||||||
* @param string $string The string to escape
|
* @param string $string The string to escape
|
||||||
* @return string The escaped string
|
* @return string The escaped string
|
||||||
*/
|
*/
|
||||||
@@ -24,6 +26,8 @@ class CSV implements EscaperInterface
|
|||||||
/**
|
/**
|
||||||
* Unescapes the given string to make it compatible with CSV
|
* Unescapes the given string to make it compatible with CSV
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*
|
||||||
* @param string $string The string to unescape
|
* @param string $string The string to unescape
|
||||||
* @return string The unescaped string
|
* @return string The unescaped string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class XLSX implements EscaperInterface
|
|||||||
public function escape($string)
|
public function escape($string)
|
||||||
{
|
{
|
||||||
$escapedString = $this->escapeControlCharacters($string);
|
$escapedString = $this->escapeControlCharacters($string);
|
||||||
$escapedString = htmlspecialchars($escapedString, ENT_QUOTES, 'utf-8');
|
$escapedString = htmlspecialchars($escapedString, ENT_QUOTES);
|
||||||
|
|
||||||
return $escapedString;
|
return $escapedString;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace Box\Spout\Common\Helper;
|
|||||||
* Class GlobalFunctionsHelper
|
* Class GlobalFunctionsHelper
|
||||||
* This class wraps global functions to facilitate testing
|
* This class wraps global functions to facilitate testing
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*
|
||||||
* @package Box\Spout\Common\Helper
|
* @package Box\Spout\Common\Helper
|
||||||
*/
|
*/
|
||||||
class GlobalFunctionsHelper
|
class GlobalFunctionsHelper
|
||||||
@@ -89,6 +91,21 @@ class GlobalFunctionsHelper
|
|||||||
return fgetcsv($handle, $length, $delimiter, $enclosure);
|
return fgetcsv($handle, $length, $delimiter, $enclosure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around global function fputcsv()
|
||||||
|
* @see fputcsv()
|
||||||
|
*
|
||||||
|
* @param resource $handle
|
||||||
|
* @param array $fields
|
||||||
|
* @param string|void $delimiter
|
||||||
|
* @param string|void $enclosure
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
|
||||||
|
{
|
||||||
|
return fputcsv($handle, $fields, $delimiter, $enclosure);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around global function fclose()
|
* Wrapper around global function fclose()
|
||||||
* @see fclose()
|
* @see fclose()
|
||||||
@@ -117,7 +134,7 @@ class GlobalFunctionsHelper
|
|||||||
* Wrapper around global function file_exists()
|
* Wrapper around global function file_exists()
|
||||||
* @see file_exists()
|
* @see file_exists()
|
||||||
*
|
*
|
||||||
* @param string $filename
|
* @param string $fileName
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function file_exists($fileName)
|
public function file_exists($fileName)
|
||||||
@@ -125,11 +142,23 @@ class GlobalFunctionsHelper
|
|||||||
return file_exists($fileName);
|
return file_exists($fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around global function file_get_contents()
|
||||||
|
* @see file_get_contents()
|
||||||
|
*
|
||||||
|
* @param string $filePath
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function file_get_contents($filePath)
|
||||||
|
{
|
||||||
|
return file_get_contents($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around global function is_readable()
|
* Wrapper around global function is_readable()
|
||||||
* @see is_readable()
|
* @see is_readable()
|
||||||
*
|
*
|
||||||
* @param string $filename
|
* @param string $fileName
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function is_readable($fileName)
|
public function is_readable($fileName)
|
||||||
@@ -142,11 +171,12 @@ class GlobalFunctionsHelper
|
|||||||
* @see basename()
|
* @see basename()
|
||||||
*
|
*
|
||||||
* @param string $path
|
* @param string $path
|
||||||
|
* @param string|void $suffix
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function basename($path)
|
public function basename($path, $suffix = null)
|
||||||
{
|
{
|
||||||
return basename($path);
|
return basename($path, $suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use Box\Spout\Reader\Exception\EndOfFileReachedException;
|
|||||||
*/
|
*/
|
||||||
abstract class AbstractReader implements ReaderInterface
|
abstract class AbstractReader implements ReaderInterface
|
||||||
{
|
{
|
||||||
/** @var int Used to keep track of the row number */
|
/** @var int Used to keep track of the row index */
|
||||||
protected $currentRowIndex = 0;
|
protected $currentRowIndex = 0;
|
||||||
|
|
||||||
/** @var bool Indicates whether the stream is currently open */
|
/** @var bool Indicates whether the stream is currently open */
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class CSV extends AbstractReader
|
|||||||
* The file must be UTF-8 encoded.
|
* The file must be UTF-8 encoded.
|
||||||
* @TODO add encoding detection/conversion
|
* @TODO add encoding detection/conversion
|
||||||
*
|
*
|
||||||
* @param string $filePath Path of the XLSX file to be read
|
* @param string $filePath Path of the CSV file to be read
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException
|
* @throws \Box\Spout\Common\Exception\IOException
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CellHelper
|
|||||||
{
|
{
|
||||||
$existingIndexes = array_keys($dataArray);
|
$existingIndexes = array_keys($dataArray);
|
||||||
|
|
||||||
$newIndexes = array_fill_keys(range(min($existingIndexes), max($existingIndexes)), $fillValue);
|
$newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue);
|
||||||
$dataArray += $newIndexes;
|
$dataArray += $newIndexes;
|
||||||
|
|
||||||
ksort($dataArray);
|
ksort($dataArray);
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class SharedStringsHelper
|
|||||||
*/
|
*/
|
||||||
const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
|
const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
|
||||||
|
|
||||||
|
/** Value to use to escape the line feed character ("\n") */
|
||||||
|
const ESCAPED_LINE_FEED_CHARACTER = '_x000A_';
|
||||||
|
|
||||||
/** @var string Path of the XLSX file being read */
|
/** @var string Path of the XLSX file being read */
|
||||||
protected $filePath;
|
protected $filePath;
|
||||||
|
|
||||||
@@ -68,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'.
|
||||||
@@ -80,7 +101,6 @@ class SharedStringsHelper
|
|||||||
* Please note that SimpleXML does not provide such a functionality but since it is faster
|
* 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.
|
* and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose.
|
||||||
*
|
*
|
||||||
* @param string $filePath
|
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read
|
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read
|
||||||
*/
|
*/
|
||||||
@@ -91,7 +111,7 @@ class SharedStringsHelper
|
|||||||
$this->tempFilePointer = null;
|
$this->tempFilePointer = null;
|
||||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
|
||||||
$sharedStringsFilePath = 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH;
|
$sharedStringsFilePath = $this->getSharedStringsFilePath();
|
||||||
if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NONET) === false) {
|
if ($xmlReader->open($sharedStringsFilePath, null, 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 . '".');
|
||||||
}
|
}
|
||||||
@@ -120,7 +140,12 @@ class SharedStringsHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
$unescapedTextValue = $escaper->unescape($textValue);
|
$unescapedTextValue = $escaper->unescape($textValue);
|
||||||
$this->writeSharedStringToTempFile($unescapedTextValue, $sharedStringIndex);
|
|
||||||
|
// The shared string retrieval logic expects each cell data to be on one line only
|
||||||
|
// Encoding the line feed character allows to preserve this assumption
|
||||||
|
$lineFeedEncodedTextValue = $this->escapeLineFeed($unescapedTextValue);
|
||||||
|
|
||||||
|
$this->writeSharedStringToTempFile($lineFeedEncodedTextValue, $sharedStringIndex);
|
||||||
|
|
||||||
$sharedStringIndex++;
|
$sharedStringIndex++;
|
||||||
|
|
||||||
@@ -136,6 +161,15 @@ class SharedStringsHelper
|
|||||||
$xmlReader->close();
|
$xmlReader->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string The path to the shared strings XML file, working cross-platforms
|
||||||
|
*/
|
||||||
|
protected function getSharedStringsFilePath()
|
||||||
|
{
|
||||||
|
$sharedStringsXmlFilePath = str_replace('/', DIRECTORY_SEPARATOR, self::SHARED_STRINGS_XML_FILE_PATH);
|
||||||
|
return 'zip://' . $this->filePath . '#' . $sharedStringsXmlFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes nodes that should not be read, like the pronunciation of the Kanji characters.
|
* 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.
|
* By keeping them, their text content would be added to the read string.
|
||||||
@@ -246,16 +280,39 @@ class SharedStringsHelper
|
|||||||
|
|
||||||
$sharedString = null;
|
$sharedString = null;
|
||||||
if (array_key_exists($indexInFile, $this->inMemoryTempFileContents)) {
|
if (array_key_exists($indexInFile, $this->inMemoryTempFileContents)) {
|
||||||
$sharedString = $this->inMemoryTempFileContents[$indexInFile];
|
$escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
|
||||||
|
$sharedString = $this->unescapeLineFeed($escapedSharedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$sharedString) {
|
if ($sharedString === null) {
|
||||||
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
|
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
|
||||||
}
|
}
|
||||||
|
|
||||||
return rtrim($sharedString, PHP_EOL);
|
return rtrim($sharedString, PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes the line feed character (\n)
|
||||||
|
*
|
||||||
|
* @param string $unescapedString
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function escapeLineFeed($unescapedString)
|
||||||
|
{
|
||||||
|
return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescapes the line feed character (\n)
|
||||||
|
*
|
||||||
|
* @param string $escapedString
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function unescapeLineFeed($escapedString)
|
||||||
|
{
|
||||||
|
return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the created temporary folder and all its contents
|
* Deletes the created temporary folder and all its contents
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Box\Spout\Reader\Helper\XLSX;
|
namespace Box\Spout\Reader\Helper\XLSX;
|
||||||
|
|
||||||
use Box\Spout\Reader\Internal\XLSX\Worksheet;
|
use Box\Spout\Reader\Internal\XLSX\Worksheet;
|
||||||
|
use Box\Spout\Reader\Sheet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class WorksheetHelper
|
* Class WorksheetHelper
|
||||||
@@ -12,11 +13,18 @@ use Box\Spout\Reader\Internal\XLSX\Worksheet;
|
|||||||
*/
|
*/
|
||||||
class WorksheetHelper
|
class WorksheetHelper
|
||||||
{
|
{
|
||||||
/** Path of Content_Types XML file inside the XLSX file */
|
/** Extension for XML files */
|
||||||
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
const XML_EXTENSION = '.xml';
|
||||||
|
|
||||||
/** Main namespace for the [Content_Types].xml file */
|
/** Paths of XML files relative to the XLSX file root */
|
||||||
|
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
||||||
|
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
|
||||||
|
const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
|
||||||
|
|
||||||
|
/** Namespaces for the XML files */
|
||||||
const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
||||||
|
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 worksheets */
|
||||||
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
|
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
|
||||||
@@ -24,12 +32,23 @@ class WorksheetHelper
|
|||||||
/** @var string Path of the XLSX file being read */
|
/** @var string Path of the XLSX file being read */
|
||||||
protected $filePath;
|
protected $filePath;
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||||
|
protected $globalFunctionsHelper;
|
||||||
|
|
||||||
|
/** @var \SimpleXMLElement XML element representing the workbook.xml.rels file */
|
||||||
|
protected $workbookXMLRelsAsXMLElement;
|
||||||
|
|
||||||
|
/** @var \SimpleXMLElement XML element representing the workbook.xml file */
|
||||||
|
protected $workbookXMLAsXMLElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $filePath Path of the XLSX file being read
|
* @param string $filePath Path of the XLSX file being read
|
||||||
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
*/
|
*/
|
||||||
public function __construct($filePath)
|
public function __construct($filePath, $globalFunctionsHelper)
|
||||||
{
|
{
|
||||||
$this->filePath = $filePath;
|
$this->filePath = $filePath;
|
||||||
|
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,23 +61,140 @@ class WorksheetHelper
|
|||||||
{
|
{
|
||||||
$worksheets = [];
|
$worksheets = [];
|
||||||
|
|
||||||
$xmlContents = file_get_contents('zip://' . $this->filePath . '#' . self::CONTENT_TYPES_XML_FILE_PATH);
|
$contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||||
|
self::CONTENT_TYPES_XML_FILE_PATH,
|
||||||
$contentTypes = new \SimpleXMLElement($xmlContents);
|
self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML
|
||||||
$contentTypes->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML);
|
);
|
||||||
|
|
||||||
// find all nodes defining a worksheet
|
// find all nodes defining a worksheet
|
||||||
$sheetNodes = $contentTypes->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
|
$sheetNodes = $contentTypesAsXMLElement->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
|
||||||
|
|
||||||
for ($i = 0; $i < count($sheetNodes); $i++) {
|
for ($i = 0; $i < count($sheetNodes); $i++) {
|
||||||
$sheetNode = $sheetNodes[$i];
|
$sheetNode = $sheetNodes[$i];
|
||||||
$sheetDataXMLFilePath = (string) $sheetNode->attributes()->PartName;
|
$sheetDataXMLFilePath = (string) $sheetNode->attributes()->PartName;
|
||||||
$worksheets[] = new Worksheet($i, $sheetDataXMLFilePath);
|
|
||||||
|
$sheet = $this->getSheet($sheetDataXMLFilePath, $i);
|
||||||
|
$worksheets[] = new Worksheet($sheet, $i, $sheetDataXMLFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $worksheets;
|
return $worksheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of a sheet, given the path of its data XML file.
|
||||||
|
* We first look at "xl/_rels/workbook.xml.rels" to find the relationship ID of the sheet.
|
||||||
|
* Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID.
|
||||||
|
* The entry contains the ID and name of the sheet.
|
||||||
|
*
|
||||||
|
* If this piece of data can't be found by parsing the different XML files, the ID will default
|
||||||
|
* to the sheet index, based on order in [Content_Types].xml. Similarly, the sheet's name will
|
||||||
|
* default to the data sheet XML file name ("xl/worksheets/sheet2.xml" => "sheet2").
|
||||||
|
*
|
||||||
|
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||||
|
* @param int $sheetIndexZeroBased Index of the sheet, based on order in [Content_Types].xml (zero-based)
|
||||||
|
* @return \Box\Spout\Reader\Sheet Sheet instance
|
||||||
|
*/
|
||||||
|
protected function getSheet($sheetDataXMLFilePath, $sheetIndexZeroBased)
|
||||||
|
{
|
||||||
|
$sheetId = $sheetIndexZeroBased + 1;
|
||||||
|
$sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
|
||||||
|
* In workbook.xml.rels, it is only "worksheets/sheet1.xml"
|
||||||
|
*/
|
||||||
|
$sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/');
|
||||||
|
|
||||||
|
// find the node associated to the given file path
|
||||||
|
$workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement();
|
||||||
|
$relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]');
|
||||||
|
|
||||||
|
if (count($relationshipNodes) === 1) {
|
||||||
|
$relationshipNode = $relationshipNodes[0];
|
||||||
|
$sheetId = (string) $relationshipNode->attributes()->Id;
|
||||||
|
|
||||||
|
$workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
|
||||||
|
$sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $sheetId . '"]');
|
||||||
|
|
||||||
|
if (count($sheetNodes) === 1) {
|
||||||
|
$sheetNode = $sheetNodes[0];
|
||||||
|
$sheetId = (int) $sheetNode->attributes()->sheetId;
|
||||||
|
$escapedSheetName = (string) $sheetNode->attributes()->name;
|
||||||
|
|
||||||
|
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||||
|
$sheetName = $escaper->unescape($escapedSheetName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Sheet($sheetId, $sheetIndexZeroBased, $sheetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default name of the sheet whose data is located
|
||||||
|
* at the given path.
|
||||||
|
*
|
||||||
|
* @param $sheetDataXMLFilePath
|
||||||
|
* @return string The default sheet name
|
||||||
|
*/
|
||||||
|
protected function getDefaultSheetName($sheetDataXMLFilePath)
|
||||||
|
{
|
||||||
|
return $this->globalFunctionsHelper->basename($sheetDataXMLFilePath, self::XML_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a representation of the workbook.xml.rels file, ready to be parsed.
|
||||||
|
* The returned value is cached.
|
||||||
|
*
|
||||||
|
* @return \SimpleXMLElement XML element representating the workbook.xml.rels file
|
||||||
|
*/
|
||||||
|
protected function getWorkbookXMLRelsAsXMLElement()
|
||||||
|
{
|
||||||
|
if (!$this->workbookXMLRelsAsXMLElement) {
|
||||||
|
$this->workbookXMLRelsAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||||
|
self::WORKBOOK_XML_RELS_FILE_PATH,
|
||||||
|
self::MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workbookXMLRelsAsXMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a representation of the workbook.xml file, ready to be parsed.
|
||||||
|
* The returned value is cached.
|
||||||
|
*
|
||||||
|
* @return \SimpleXMLElement XML element representating the workbook.xml.rels file
|
||||||
|
*/
|
||||||
|
protected function getWorkbookXMLAsXMLElement()
|
||||||
|
{
|
||||||
|
if (!$this->workbookXMLAsXMLElement) {
|
||||||
|
$this->workbookXMLAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||||
|
self::WORKBOOK_XML_FILE_PATH,
|
||||||
|
self::MAIN_NAMESPACE_FOR_WORKBOOK_XML
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workbookXMLAsXMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the contents of the given file in an XML parser and register the given XPath namespace.
|
||||||
|
*
|
||||||
|
* @param string $xmlFilePath The path of the XML file inside the XLSX file
|
||||||
|
* @param string $mainNamespace The main XPath namespace to register
|
||||||
|
* @return \SimpleXMLElement The XML element representing the file
|
||||||
|
*/
|
||||||
|
protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace)
|
||||||
|
{
|
||||||
|
$normalizedXmlFilePath = str_replace('/', DIRECTORY_SEPARATOR, $xmlFilePath);
|
||||||
|
$xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $normalizedXmlFilePath);
|
||||||
|
|
||||||
|
$xmlElement = new \SimpleXMLElement($xmlContents);
|
||||||
|
$xmlElement->registerXPathNamespace('ns', $mainNamespace);
|
||||||
|
|
||||||
|
return $xmlElement;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether another worksheet exists after the current worksheet.
|
* Returns whether another worksheet exists after the current worksheet.
|
||||||
* The order is determined by the order of appearance in the [Content_Types].xml file.
|
* The order is determined by the order of appearance in the [Content_Types].xml file.
|
||||||
@@ -69,6 +205,6 @@ class WorksheetHelper
|
|||||||
*/
|
*/
|
||||||
public function hasNextWorksheet($currentWorksheet, $allWorksheets)
|
public function hasNextWorksheet($currentWorksheet, $allWorksheets)
|
||||||
{
|
{
|
||||||
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetNumber() + 1 < count($allWorksheets)));
|
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetIndex() + 1 < count($allWorksheets)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,35 +10,50 @@ namespace Box\Spout\Reader\Internal\XLSX;
|
|||||||
*/
|
*/
|
||||||
class Worksheet
|
class Worksheet
|
||||||
{
|
{
|
||||||
/** @var int Worksheet number, based on the order of appareance in [Content_Types].xml (zero-based) */
|
/** @var \Box\Spout\Reader\Sheet The "external" sheet */
|
||||||
protected $worksheetNumber;
|
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 */
|
/** @var string Path of the XML file containing the worksheet data */
|
||||||
protected $dataXmlFilePath;
|
protected $dataXmlFilePath;
|
||||||
|
|
||||||
/**
|
/**\
|
||||||
* @param int $worksheetNumber Worksheet number, based on the order of appareance in [Content_Types].xml (zero-based)
|
* @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
|
* @param string $dataXmlFilePath Path of the XML file containing the worksheet data
|
||||||
*/
|
*/
|
||||||
public function __construct($worksheetNumber, $dataXmlFilePath)
|
public function __construct($externalSheet, $worksheetIndex, $dataXmlFilePath)
|
||||||
{
|
{
|
||||||
$this->worksheetNumber = $worksheetNumber;
|
$this->externalSheet = $externalSheet;
|
||||||
|
$this->worksheetIndex = $worksheetIndex;
|
||||||
$this->dataXmlFilePath = $dataXmlFilePath;
|
$this->dataXmlFilePath = $dataXmlFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string Path of the XML file containing the worksheet data, without the leading slash
|
* @return string Path of the XML file containing the worksheet data,
|
||||||
|
* without the leading slash and working cross-platforms.
|
||||||
*/
|
*/
|
||||||
public function getDataXmlFilePath()
|
public function getDataXmlFilePath()
|
||||||
{
|
{
|
||||||
return ltrim($this->dataXmlFilePath, DIRECTORY_SEPARATOR);
|
$dataXmlFilePath = ltrim($this->dataXmlFilePath, '/');
|
||||||
|
return str_replace('/', DIRECTORY_SEPARATOR, $dataXmlFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Box\Spout\Reader\Sheet The "external" sheet
|
||||||
|
*/
|
||||||
|
public function getExternalSheet()
|
||||||
|
{
|
||||||
|
return $this->externalSheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getWorksheetNumber()
|
public function getWorksheetIndex()
|
||||||
{
|
{
|
||||||
return $this->worksheetNumber;
|
return $this->worksheetIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Sheet
|
||||||
|
* Represents a worksheet within a XLSX file
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader
|
||||||
|
*/
|
||||||
|
class Sheet
|
||||||
|
{
|
||||||
|
/** @var int ID of the sheet */
|
||||||
|
protected $id;
|
||||||
|
|
||||||
|
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||||
|
protected $index;
|
||||||
|
|
||||||
|
/** @var string Name of the sheet */
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
|
{
|
||||||
|
$this->id = $sheetId;
|
||||||
|
$this->index = $sheetIndex;
|
||||||
|
$this->name = $sheetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int ID of the sheet
|
||||||
|
*/
|
||||||
|
public function getId()
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||||
|
*/
|
||||||
|
public function getIndex()
|
||||||
|
{
|
||||||
|
return $this->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string Name of the sheet
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
-44
@@ -72,11 +72,15 @@ class XLSX extends AbstractReader
|
|||||||
$this->zip = new \ZipArchive();
|
$this->zip = new \ZipArchive();
|
||||||
|
|
||||||
if ($this->zip->open($filePath) === true) {
|
if ($this->zip->open($filePath) === true) {
|
||||||
// Extracts all the strings from the worksheets for easy access in the future
|
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
|
||||||
$this->extractSharedStrings($filePath);
|
|
||||||
|
if ($this->sharedStringsHelper->hasSharedStrings()) {
|
||||||
|
// Extracts all the strings from the worksheets for easy access in the future
|
||||||
|
$this->sharedStringsHelper->extractSharedStrings();
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch all available worksheets
|
// Fetch all available worksheets
|
||||||
$this->worksheetHelper = new WorksheetHelper($filePath);
|
$this->worksheetHelper = new WorksheetHelper($filePath, $this->globalFunctionsHelper);
|
||||||
$this->worksheets = $this->worksheetHelper->getWorksheets($filePath);
|
$this->worksheets = $this->worksheetHelper->getWorksheets($filePath);
|
||||||
|
|
||||||
if (count($this->worksheets) === 0) {
|
if (count($this->worksheets) === 0) {
|
||||||
@@ -87,19 +91,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.
|
||||||
*
|
*
|
||||||
@@ -119,29 +110,31 @@ class XLSX extends AbstractReader
|
|||||||
* Moves the pointer to the current worksheet.
|
* Moves the pointer to the current worksheet.
|
||||||
* Moving to another worksheet will stop the reading in the current worksheet.
|
* Moving to another worksheet will stop the reading in the current worksheet.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return \Box\Spout\Reader\Sheet The next sheet
|
||||||
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
|
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
|
||||||
* @throws Exception\EndOfWorksheetsReachedException If there is no more worksheets to read
|
* @throws Exception\EndOfWorksheetsReachedException If there is no more worksheets to read
|
||||||
*/
|
*/
|
||||||
public function nextSheet()
|
public function nextSheet()
|
||||||
{
|
{
|
||||||
if ($this->hasNextSheet()) {
|
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.');
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,15 +177,19 @@ 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 = [];
|
||||||
|
|
||||||
while ($this->xmlReader->read()) {
|
while ($this->xmlReader->read()) {
|
||||||
if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'dimension') {
|
if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'dimension') {
|
||||||
// Read dimensions of the worksheet
|
// Read dimensions of the worksheet
|
||||||
$dimensionRef = $this->xmlReader->getAttribute('ref'); // returns 'A1:M13' for instance
|
$dimensionRef = $this->xmlReader->getAttribute('ref'); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
|
||||||
list(, $lastCellIndex) = explode(':', $dimensionRef);
|
if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
|
||||||
$this->numberOfColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
|
$lastCellIndex = $matches[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;
|
||||||
@@ -205,20 +202,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
|
||||||
@@ -231,6 +223,58 @@ class XLSX extends AbstractReader
|
|||||||
return ($rowData !== []) ? $rowData : null;
|
return ($rowData !== []) ? $rowData : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the (unescaped) 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)
|
||||||
|
{
|
||||||
|
$cellValue = '';
|
||||||
|
|
||||||
|
// Default cell type is "n"
|
||||||
|
$cellType = $node->getAttribute('t') ?: 'n';
|
||||||
|
|
||||||
|
if ($cellType === 'inlineStr') {
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
// all other cell types should have a "v" tag containing the value.
|
||||||
|
// if not, the returned value should be empty string.
|
||||||
|
$vNode = $node->getElementsByTagName('v')->item(0);
|
||||||
|
|
||||||
|
if ($vNode !== null) {
|
||||||
|
if ($cellType === 's') {
|
||||||
|
// shared strings are formatted this way:
|
||||||
|
// <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
|
||||||
|
$sharedStringIndex = intval($vNode->nodeValue);
|
||||||
|
$escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
|
||||||
|
$cellValue = $escaper->unescape($escapedCellValue);
|
||||||
|
} else if ($cellType === 'b') {
|
||||||
|
// !! is similar to boolval()
|
||||||
|
$cellValue = !!$vNode->nodeValue;
|
||||||
|
} else if ($cellType === 'n') {
|
||||||
|
$nodeValue = $vNode->nodeValue;
|
||||||
|
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
|
||||||
|
} else if ($cellType === 'd') {
|
||||||
|
$cellValue = new \DateTime($vNode->nodeValue);
|
||||||
|
} else if ($cellType === 'e') {
|
||||||
|
$cellValue = null;
|
||||||
|
} else if ($cellType === 'str') {
|
||||||
|
$escapedCellValue = trim($vNode->nodeValue);
|
||||||
|
$cellValue = $escaper->unescape($escapedCellValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cellValue;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the reader. To be used after reading the file.
|
* Closes the reader. To be used after reading the file.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds data to the currently openned writer.
|
* Adds data to the currently openned writer.
|
||||||
|
* The data must be UTF-8 encoded.
|
||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be streamed.
|
* @param array $dataRow Array containing data to be streamed.
|
||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||||
@@ -88,6 +89,8 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
* Inits the writer and opens it to accept data.
|
* Inits the writer and opens it to accept data.
|
||||||
* By using this method, the data will be outputted directly to the browser.
|
* By using this method, the data will be outputted directly to the browser.
|
||||||
*
|
*
|
||||||
|
* @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
|
* @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 \Box\Spout\Writer\AbstractWriter
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
|
||||||
@@ -135,9 +138,11 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Write given data to the output. New data will be appended to end of stream.
|
* Write given data to the output. New data will be appended to end of stream.
|
||||||
|
* The data must be UTF-8 encoded.
|
||||||
*
|
*
|
||||||
* @param array $dataRow Array containing data to be streamed.
|
* @param array $dataRow Array containing data to be streamed.
|
||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* 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 \Box\Spout\Writer\AbstractWriter
|
||||||
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
|
||||||
@@ -146,7 +151,10 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
public function addRow(array $dataRow)
|
public function addRow(array $dataRow)
|
||||||
{
|
{
|
||||||
if ($this->isWriterOpened) {
|
if ($this->isWriterOpened) {
|
||||||
$this->addRowToWriter($dataRow);
|
// empty $dataRow should not add an empty line
|
||||||
|
if (!empty($dataRow)) {
|
||||||
|
$this->addRowToWriter($dataRow);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
|
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
|
||||||
}
|
}
|
||||||
@@ -156,12 +164,14 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Write given data to the output. New data will be appended to end of stream.
|
* Write given data to the output. New data will be appended to end of stream.
|
||||||
|
* The data must be UTF-8 encoded.
|
||||||
*
|
*
|
||||||
* @param array $dataRows Array of array containing data to be streamed.
|
* @param array $dataRows Array of array containing data to be streamed.
|
||||||
* Example $dataRow = [
|
* If a row is empty, it won't be added (i.e. not even as a blank row)
|
||||||
* ['data11', 12, , '', 'data13'],
|
* Example: $dataRows = [
|
||||||
* ['data21', 'data22', null],
|
* ['data11', 12, , '', 'data13'],
|
||||||
* ];
|
* ['data21', 'data22', null, false],
|
||||||
|
* ];
|
||||||
*
|
*
|
||||||
* @return \Box\Spout\Writer\AbstractWriter
|
* @return \Box\Spout\Writer\AbstractWriter
|
||||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class CSV extends AbstractWriter
|
|||||||
*/
|
*/
|
||||||
protected function addRowToWriter(array $dataRow)
|
protected function addRowToWriter(array $dataRow)
|
||||||
{
|
{
|
||||||
$wasWriteSuccessful = fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
|
$wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||||
if ($wasWriteSuccessful === false) {
|
if ($wasWriteSuccessful === false) {
|
||||||
throw new IOException('Unable to write data');
|
throw new IOException('Unable to write data');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,4 +34,38 @@ class CellHelper
|
|||||||
|
|
||||||
return $cellIndex;
|
return $cellIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
* @return bool Whether the given value is a non empty string
|
||||||
|
*/
|
||||||
|
public static function isNonEmptyString($value)
|
||||||
|
{
|
||||||
|
return (gettype($value) === 'string' && $value !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given value is numeric.
|
||||||
|
* A numeric value is from type "integer" or "double" ("float" is not returned by gettype).
|
||||||
|
*
|
||||||
|
* @param $value
|
||||||
|
* @return bool Whether the given value is numeric
|
||||||
|
*/
|
||||||
|
public static function isNumeric($value)
|
||||||
|
{
|
||||||
|
$valueType = gettype($value);
|
||||||
|
return ($valueType === 'integer' || $valueType === 'double');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given value is boolean.
|
||||||
|
* "true"/"false" and 0/1 are not booleans.
|
||||||
|
*
|
||||||
|
* @param $value
|
||||||
|
* @return bool Whether the given value is boolean
|
||||||
|
*/
|
||||||
|
public static function isBoolean($value)
|
||||||
|
{
|
||||||
|
return gettype($value) === 'boolean';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ namespace Box\Spout\Writer\Internal\XLSX;
|
|||||||
use Box\Spout\Writer\Exception\SheetNotFoundException;
|
use Box\Spout\Writer\Exception\SheetNotFoundException;
|
||||||
use Box\Spout\Writer\Helper\XLSX\FileSystemHelper;
|
use Box\Spout\Writer\Helper\XLSX\FileSystemHelper;
|
||||||
use Box\Spout\Writer\Helper\XLSX\SharedStringsHelper;
|
use Box\Spout\Writer\Helper\XLSX\SharedStringsHelper;
|
||||||
use Box\Spout\Writer\Helper\XLSX\ZipHelper;
|
|
||||||
use Box\Spout\Writer\Sheet;
|
use Box\Spout\Writer\Sheet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,8 +67,8 @@ class Workbook
|
|||||||
*/
|
*/
|
||||||
public function addNewSheet()
|
public function addNewSheet()
|
||||||
{
|
{
|
||||||
$newSheetNumber = count($this->worksheets);
|
$newSheetIndex = count($this->worksheets);
|
||||||
$sheet = new Sheet($newSheetNumber);
|
$sheet = new Sheet($newSheetIndex);
|
||||||
|
|
||||||
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
|
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
|
||||||
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings);
|
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Box\Spout\Writer\Internal\XLSX;
|
namespace Box\Spout\Writer\Internal\XLSX;
|
||||||
|
|
||||||
|
use Box\Spout\Common\Exception\InvalidArgumentException;
|
||||||
use Box\Spout\Common\Exception\IOException;
|
use Box\Spout\Common\Exception\IOException;
|
||||||
use Box\Spout\Writer\Helper\XLSX\CellHelper;
|
use Box\Spout\Writer\Helper\XLSX\CellHelper;
|
||||||
|
|
||||||
@@ -42,7 +43,8 @@ EOD;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \Box\Spout\Writer\Sheet $externalSheet The associated "external" sheet
|
* @param \Box\Spout\Writer\Sheet $externalSheet The associated "external" sheet
|
||||||
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
|
* @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 bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
* @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
|
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
||||||
*/
|
*/
|
||||||
@@ -94,8 +96,8 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
public function getId()
|
public function getId()
|
||||||
{
|
{
|
||||||
// sheet number is zero-based, while ID is 1-based
|
// sheet index is zero-based, while ID is 1-based
|
||||||
return $this->externalSheet->getSheetNumber() + 1;
|
return $this->externalSheet->getIndex() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +120,7 @@ EOD;
|
|||||||
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
|
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
|
||||||
|
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
||||||
*/
|
*/
|
||||||
public function addRow($dataRow)
|
public function addRow($dataRow)
|
||||||
{
|
{
|
||||||
@@ -131,19 +134,19 @@ EOD;
|
|||||||
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
||||||
$data .= ' <c r="' . $columnIndex . $rowIndex . '"';
|
$data .= ' <c r="' . $columnIndex . $rowIndex . '"';
|
||||||
|
|
||||||
if (empty($cellValue)) {
|
if (CellHelper::isNonEmptyString($cellValue)) {
|
||||||
|
if ($this->shouldUseInlineStrings) {
|
||||||
|
$data .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>' . PHP_EOL;
|
||||||
|
} else {
|
||||||
|
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
||||||
|
$data .= ' t="s"><v>' . $sharedStringId . '</v></c>' . PHP_EOL;
|
||||||
|
}
|
||||||
|
} else if (CellHelper::isNumeric($cellValue) || CellHelper::isBoolean($cellValue)) {
|
||||||
|
$data .= '><v>' . $cellValue . '</v></c>' . PHP_EOL;
|
||||||
|
} else if (empty($cellValue)) {
|
||||||
$data .= '/>' . PHP_EOL;
|
$data .= '/>' . PHP_EOL;
|
||||||
} else {
|
} else {
|
||||||
if (is_numeric($cellValue)) {
|
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
|
||||||
$data .= '><v>' . $cellValue . '</v></c>' . PHP_EOL;
|
|
||||||
} else {
|
|
||||||
if ($this->shouldUseInlineStrings) {
|
|
||||||
$data .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>' . PHP_EOL;
|
|
||||||
} else {
|
|
||||||
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
|
||||||
$data .= ' t="s"><v>' . $sharedStringId . '</v></c>' . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$cellNumber++;
|
$cellNumber++;
|
||||||
|
|||||||
@@ -12,27 +12,27 @@ class Sheet
|
|||||||
{
|
{
|
||||||
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
|
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
|
||||||
|
|
||||||
/** @var int Number of the sheet, based on order of creation (zero-based) */
|
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||||
protected $sheetNumber;
|
protected $index;
|
||||||
|
|
||||||
/** @var string Name of the sheet */
|
/** @var string Name of the sheet */
|
||||||
protected $name;
|
protected $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $sheetNumber Number of the sheet, based on order of creation (zero-based)
|
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||||
*/
|
*/
|
||||||
function __construct($sheetNumber)
|
function __construct($sheetIndex)
|
||||||
{
|
{
|
||||||
$this->sheetNumber = $sheetNumber;
|
$this->index = $sheetIndex;
|
||||||
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetNumber + 1);
|
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int Number of the sheet, based on order of creation (zero-based)
|
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||||
*/
|
*/
|
||||||
public function getSheetNumber()
|
public function getIndex()
|
||||||
{
|
{
|
||||||
return $this->sheetNumber;
|
return $this->index;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,4 +42,14 @@ class Sheet
|
|||||||
{
|
{
|
||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name Name of the sheet
|
||||||
|
* @return \Box\Spout\Writer\Sheet
|
||||||
|
*/
|
||||||
|
public function setName($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\Helper\XLSX;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CellHelperTest
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\Helper\XLSX
|
||||||
|
*/
|
||||||
|
class CellHelperTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testFillMissingArrayIndexes()
|
||||||
|
{
|
||||||
|
$arrayToFill = [1 => 1, 3 => 3];
|
||||||
|
$filledArray = CellHelper::fillMissingArrayIndexes($arrayToFill, 'FILL');
|
||||||
|
|
||||||
|
$expectedFilledArray = ['FILL', 1, 'FILL', 3];
|
||||||
|
$this->assertEquals($expectedFilledArray, $filledArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForTestGetColumnIndexFromCellIndex()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['A1', 0],
|
||||||
|
['Z3', 25],
|
||||||
|
['AA5', 26],
|
||||||
|
['AB24', 27],
|
||||||
|
['BC5', 54],
|
||||||
|
['BCZ99', 1455],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForTestGetColumnIndexFromCellIndex
|
||||||
|
*
|
||||||
|
* @param string $cellIndex
|
||||||
|
* @param int $expectedColumnIndex
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetColumnIndexFromCellIndex($cellIndex, $expectedColumnIndex)
|
||||||
|
{
|
||||||
|
$this->assertEquals($expectedColumnIndex, CellHelper::getColumnIndexFromCellIndex($cellIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetColumnIndexFromCellIndexShouldThrowIfInvalidCellIndex()
|
||||||
|
{
|
||||||
|
CellHelper::getColumnIndexFromCellIndex('InvalidCellIndex');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ class SharedStringsHelperTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals(1, count($filesInTempFolder), 'One temp file should have been created in the temp folder.');
|
$this->assertEquals(1, count($filesInTempFolder), 'One temp file should have been created in the temp folder.');
|
||||||
|
|
||||||
$tempFileContents = file_get_contents($filesInTempFolder[0]);
|
$tempFileContents = file_get_contents($filesInTempFolder[0]);
|
||||||
$tempFileContentsPerLine = explode("\n", $tempFileContents);
|
$tempFileContentsPerLine = explode(PHP_EOL, $tempFileContents);
|
||||||
|
|
||||||
$this->assertEquals('s1--A1', $tempFileContentsPerLine[0]);
|
$this->assertEquals('s1--A1', $tempFileContentsPerLine[0]);
|
||||||
$this->assertEquals('s1--E5', $tempFileContentsPerLine[24]);
|
$this->assertEquals('s1--E5', $tempFileContentsPerLine[24]);
|
||||||
@@ -96,4 +96,23 @@ class SharedStringsHelperTest extends \PHPUnit_Framework_TestCase
|
|||||||
$sharedString = $this->sharedStringsHelper->getStringAtIndex(24);
|
$sharedString = $this->sharedStringsHelper->getStringAtIndex(24);
|
||||||
$this->assertEquals('s1--E5', $sharedString);
|
$this->assertEquals('s1--E5', $sharedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetStringAtIndexShouldWorkWithMultilineStrings()
|
||||||
|
{
|
||||||
|
$resourcePath = $this->getResourcePath('one_sheet_with_shared_multiline_strings.xlsx');
|
||||||
|
$sharedStringsHelper = new SharedStringsHelper($resourcePath);
|
||||||
|
|
||||||
|
$sharedStringsHelper->extractSharedStrings();
|
||||||
|
|
||||||
|
$sharedString = $sharedStringsHelper->getStringAtIndex(0);
|
||||||
|
$this->assertEquals("s1\nA1", $sharedString);
|
||||||
|
|
||||||
|
$sharedString = $sharedStringsHelper->getStringAtIndex(24);
|
||||||
|
$this->assertEquals("s1\nE5", $sharedString);
|
||||||
|
|
||||||
|
$sharedStringsHelper->cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ReaderFactoryTest
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Writer
|
||||||
|
*/
|
||||||
|
class ReaderFactoryTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testCreateReaderShouldThrowWithUnsupportedType()
|
||||||
|
{
|
||||||
|
ReaderFactory::create('unsupportedType');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader;
|
||||||
|
|
||||||
|
use Box\Spout\Common\Type;
|
||||||
|
use Box\Spout\TestUsingResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SheetTest
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader
|
||||||
|
*/
|
||||||
|
class SheetTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
use TestUsingResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testNextSheetShouldReturnCorrectSheetInfos()
|
||||||
|
{
|
||||||
|
$sheets = $this->openFileAndReturnSheets('two_sheets_with_custom_names.xlsx');
|
||||||
|
|
||||||
|
$this->assertEquals('CustomName1', $sheets[0]->getName());
|
||||||
|
$this->assertEquals(0, $sheets[0]->getIndex());
|
||||||
|
$this->assertEquals(1, $sheets[0]->getId());
|
||||||
|
|
||||||
|
$this->assertEquals('CustomName2', $sheets[1]->getName());
|
||||||
|
$this->assertEquals(1, $sheets[1]->getIndex());
|
||||||
|
$this->assertEquals(2, $sheets[1]->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $fileName
|
||||||
|
* @return Sheet[]
|
||||||
|
*/
|
||||||
|
private function openFileAndReturnSheets($fileName)
|
||||||
|
{
|
||||||
|
$resourcePath = $this->getResourcePath($fileName);
|
||||||
|
$reader = ReaderFactory::create(Type::XLSX);
|
||||||
|
$reader->open($resourcePath);
|
||||||
|
|
||||||
|
$sheets = [];
|
||||||
|
while ($reader->hasNextSheet()) {
|
||||||
|
$sheets[] = $reader->nextSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader->close();
|
||||||
|
|
||||||
|
return $sheets;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,37 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->getAllRowsForFile($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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
@@ -69,6 +100,41 @@ 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,
|
||||||
|
],
|
||||||
|
['', '', '', '', '', '', '', '', ''],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -140,6 +206,32 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedRows, $allRows);
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldSupportEmptySharedString()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_empty_shared_string.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
['s1--A1', '', 's1--C1'],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldPreserveSpaceIfSpecified()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_preserve_space_shared_strings.xlsx');
|
||||||
|
|
||||||
|
$expectedRows = [
|
||||||
|
[' s1--A1', 's1--B1 ', ' s1--C1 '],
|
||||||
|
];
|
||||||
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -158,13 +250,37 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
{
|
{
|
||||||
$allRows = $this->getAllRowsForFile('billion_laughs_test_file.xlsx');
|
$allRows = $this->getAllRowsForFile('billion_laughs_test_file.xlsx');
|
||||||
|
|
||||||
$expectedMaxMemoryUsage = 10 * 1024 * 1024; // 10MB
|
$expectedMaxMemoryUsage = 20 * 1024 * 1024; // 20MB
|
||||||
$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'];
|
||||||
$this->assertEquals($expectedFirstRow, $allRows[0], 'Entities should be ignored when reading XML files.');
|
$this->assertEquals($expectedFirstRow, $allRows[0], 'Entities should be ignored when reading XML files.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadShouldBeAbleToProcessEmptySheets()
|
||||||
|
{
|
||||||
|
$allRows = $this->getAllRowsForFile('sheet_with_no_cells.xlsx');
|
||||||
|
$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
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ class CSVTest extends \PHPUnit_Framework_TestCase
|
|||||||
$writer->close();
|
$writer->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testAddRowsShouldThrowExceptionIfRowsAreNotArrayOfArrays()
|
||||||
|
{
|
||||||
|
$writer = WriterFactory::create(Type::CSV);
|
||||||
|
$writer->addRows(['csv--11', 'csv--12']);
|
||||||
|
$writer->close();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +86,22 @@ class CSVTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals('csv--11,,csv--13', $writtenContent, 'The null values should be replaced by empty values');
|
$this->assertEquals('csv--11,,csv--13', $writtenContent, 'The null values should be replaced by empty values');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWriteShouldSkipEmptyRows()
|
||||||
|
{
|
||||||
|
$allRows = [
|
||||||
|
['csv--11', 'csv--12'],
|
||||||
|
[],
|
||||||
|
['csv--31', 'csv--32'],
|
||||||
|
];
|
||||||
|
$writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_empty_rows.csv');
|
||||||
|
$writtenContent = $this->trimWrittenContent($writtenContent);
|
||||||
|
|
||||||
|
$this->assertEquals("csv--11,csv--12\ncsv--31,csv--32", $writtenContent, 'Empty rows should be skipped');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -135,6 +161,6 @@ class CSVTest extends \PHPUnit_Framework_TestCase
|
|||||||
private function trimWrittenContent($writtenContent)
|
private function trimWrittenContent($writtenContent)
|
||||||
{
|
{
|
||||||
// remove line feeds and UTF-8 BOM
|
// remove line feeds and UTF-8 BOM
|
||||||
return trim($writtenContent, "\n" . CSV::UTF8_BOM);
|
return trim($writtenContent, PHP_EOL . CSV::UTF8_BOM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,4 +34,62 @@ class CellHelperTest extends \PHPUnit_Framework_TestCase
|
|||||||
{
|
{
|
||||||
$this->assertEquals($expectedCellIndex, CellHelper::getCellIndexFromColumnIndex($columnIndex));
|
$this->assertEquals($expectedCellIndex, CellHelper::getCellIndexFromColumnIndex($columnIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function testIsNonEmptyString()
|
||||||
|
{
|
||||||
|
$this->assertTrue(CellHelper::isNonEmptyString("string"));
|
||||||
|
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(""));
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(0));
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(1));
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(true));
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(false));
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(["string"]));
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(new \stdClass()));
|
||||||
|
$this->assertFalse(CellHelper::isNonEmptyString(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function testIsNumeric()
|
||||||
|
{
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(0));
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(10));
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(10.1));
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(10.10000000000000000000001));
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(0x539));
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(02471));
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(0b10100111001));
|
||||||
|
$this->assertTrue(CellHelper::isNumeric(1337e0));
|
||||||
|
|
||||||
|
$this->assertFalse(CellHelper::isNumeric("0"));
|
||||||
|
$this->assertFalse(CellHelper::isNumeric("42"));
|
||||||
|
$this->assertFalse(CellHelper::isNumeric(true));
|
||||||
|
$this->assertFalse(CellHelper::isNumeric([2]));
|
||||||
|
$this->assertFalse(CellHelper::isNumeric(new \stdClass()));
|
||||||
|
$this->assertFalse(CellHelper::isNumeric(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function testIsBoolean()
|
||||||
|
{
|
||||||
|
$this->assertTrue(CellHelper::isBoolean(true));
|
||||||
|
$this->assertTrue(CellHelper::isBoolean(false));
|
||||||
|
|
||||||
|
$this->assertFalse(CellHelper::isBoolean(0));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean(1));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean("0"));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean("1"));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean("true"));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean("false"));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean([true]));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean(new \stdClass()));
|
||||||
|
$this->assertFalse(CellHelper::isBoolean(null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,13 +17,13 @@ class SheetTest extends \PHPUnit_Framework_TestCase
|
|||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testGetSheetNumber()
|
public function testGetSheetIndex()
|
||||||
{
|
{
|
||||||
$sheets = $this->writeDataAndReturnSheets('test_get_sheet_number.xlsx');
|
$sheets = $this->writeDataAndReturnSheets('test_get_sheet_index.xlsx');
|
||||||
|
|
||||||
$this->assertEquals(2, count($sheets), '2 sheets should have been created');
|
$this->assertEquals(2, count($sheets), '2 sheets should have been created');
|
||||||
$this->assertEquals(0, $sheets[0]->getSheetNumber(), 'The first sheet should be number 0');
|
$this->assertEquals(0, $sheets[0]->getIndex(), 'The first sheet should be index 0');
|
||||||
$this->assertEquals(1, $sheets[1]->getSheetNumber(), 'The second sheet should be number 1');
|
$this->assertEquals(1, $sheets[1]->getIndex(), 'The second sheet should be index 1');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +38,28 @@ class SheetTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet');
|
$this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSetSheetNameShouldCreateSheetWithCustomName()
|
||||||
|
{
|
||||||
|
$fileName = 'test_set_name_should_create_sheet_with_custom_name.xlsx';
|
||||||
|
$this->createGeneratedFolderIfNeeded($fileName);
|
||||||
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
|
|
||||||
|
$writer = WriterFactory::create(Type::XLSX);
|
||||||
|
$writer->openToFile($resourcePath);
|
||||||
|
|
||||||
|
$customSheetName = 'CustomName';
|
||||||
|
$sheet = $writer->getCurrentSheet();
|
||||||
|
$sheet->setName($customSheetName);
|
||||||
|
|
||||||
|
$writer->addRow(['xlsx--11', 'xlsx--12']);
|
||||||
|
$writer->close();
|
||||||
|
|
||||||
|
$this->assertSheetNameEquals($customSheetName, $resourcePath, "The sheet name should have been changed to '$customSheetName'");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @return Sheet[]
|
* @return Sheet[]
|
||||||
@@ -59,4 +81,27 @@ class SheetTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
return $writer->getSheets();
|
return $writer->getSheets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $expectedName
|
||||||
|
* @param string $resourcePath
|
||||||
|
* @param string $message
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function assertSheetNameEquals($expectedName, $resourcePath, $message = '')
|
||||||
|
{
|
||||||
|
$pathToWorkbookFile = $resourcePath . $this->normalizePath('#xl/workbook.xml');
|
||||||
|
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Writer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class WriterFactoryTest
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Writer
|
||||||
|
*/
|
||||||
|
class WriterFactoryTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\UnsupportedTypeException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testCreateWriterShouldThrowWithUnsupportedType()
|
||||||
|
{
|
||||||
|
WriterFactory::create('unsupportedType');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,19 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
$writer->close();
|
$writer->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Box\Spout\Common\Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testAddRowShouldThrowExceptionIfUnsupportedDataTypePassedIn()
|
||||||
|
{
|
||||||
|
$fileName = 'test_add_row_should_throw_exception_if_unsupported_data_type_passed_in.xlsx';
|
||||||
|
$dataRows = [
|
||||||
|
[new \stdClass()],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->writeToXLSXFile($dataRows, $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -96,7 +109,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
public function testAddRowShouldWriteGivenDataToSheetUsingInlineStrings()
|
public function testAddRowShouldWriteGivenDataToSheetUsingInlineStrings()
|
||||||
{
|
{
|
||||||
$fileName = 'test_add_row_should_write_given_data_to_sheet.xlsx';
|
$fileName = 'test_add_row_should_write_given_data_to_sheet_using_inline_strings.xlsx';
|
||||||
$dataRows = [
|
$dataRows = [
|
||||||
['xlsx--11', 'xlsx--12'],
|
['xlsx--11', 'xlsx--12'],
|
||||||
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
||||||
@@ -106,7 +119,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
foreach ($dataRows as $dataRow) {
|
foreach ($dataRows as $dataRow) {
|
||||||
foreach ($dataRow as $cellValue) {
|
foreach ($dataRow as $cellValue) {
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 1, $cellValue);
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, $cellValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +129,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
public function testAddRowShouldWriteGivenDataToTwoSheetUsingInlineStrings()
|
public function testAddRowShouldWriteGivenDataToTwoSheetUsingInlineStrings()
|
||||||
{
|
{
|
||||||
$fileName = 'test_add_row_should_write_given_data_to_sheet.xlsx';
|
$fileName = 'test_add_row_should_write_given_data_to_two_sheets_using_inline_strings.xlsx';
|
||||||
$dataRows = [
|
$dataRows = [
|
||||||
['xlsx--11', 'xlsx--12'],
|
['xlsx--11', 'xlsx--12'],
|
||||||
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
||||||
@@ -128,7 +141,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
for ($i = 1; $i <= $numSheets; $i++) {
|
for ($i = 1; $i <= $numSheets; $i++) {
|
||||||
foreach ($dataRows as $dataRow) {
|
foreach ($dataRows as $dataRow) {
|
||||||
foreach ($dataRow as $cellValue) {
|
foreach ($dataRow as $cellValue) {
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, $numSheets, $cellValue);
|
$this->assertInlineDataWasWrittenToSheet($fileName, $numSheets, $cellValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +152,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
public function testAddRowShouldWriteGivenDataToSheetUsingSharedStrings()
|
public function testAddRowShouldWriteGivenDataToSheetUsingSharedStrings()
|
||||||
{
|
{
|
||||||
$fileName = 'test_add_row_should_write_given_data_to_sheet.xlsx';
|
$fileName = 'test_add_row_should_write_given_data_to_sheet_using_shared_strings.xlsx';
|
||||||
$dataRows = [
|
$dataRows = [
|
||||||
['xlsx--11', 'xlsx--12'],
|
['xlsx--11', 'xlsx--12'],
|
||||||
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
||||||
@@ -159,7 +172,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
public function testAddRowShouldWriteGivenDataToTwoSheetUsingSharedStrings()
|
public function testAddRowShouldWriteGivenDataToTwoSheetUsingSharedStrings()
|
||||||
{
|
{
|
||||||
$fileName = 'test_add_row_should_write_given_data_to_two_sheet_using_shared_strings.xlsx';
|
$fileName = 'test_add_row_should_write_given_data_to_two_sheets_using_shared_strings.xlsx';
|
||||||
$dataRows = [
|
$dataRows = [
|
||||||
['xlsx--11', 'xlsx--12'],
|
['xlsx--11', 'xlsx--12'],
|
||||||
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
['xlsx--21', 'xlsx--22', 'xlsx--23'],
|
||||||
@@ -177,6 +190,24 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAddRowShouldSupportMultipleTypesOfData()
|
||||||
|
{
|
||||||
|
$fileName = 'test_add_row_should_support_multiple_types_of_data.xlsx';
|
||||||
|
$dataRows = [
|
||||||
|
['xlsx--11', true, '', 0, 10.2, null],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = false);
|
||||||
|
|
||||||
|
$this->assertSharedStringWasWritten($fileName, 'xlsx--11');
|
||||||
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 1); // true is converted to 1
|
||||||
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 0);
|
||||||
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 10.2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -219,17 +250,17 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
foreach ($dataRowsSheet1 as $dataRow) {
|
foreach ($dataRowsSheet1 as $dataRow) {
|
||||||
foreach ($dataRow as $cellValue) {
|
foreach ($dataRow as $cellValue) {
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($dataRowsSheet2 as $dataRow) {
|
foreach ($dataRowsSheet2 as $dataRow) {
|
||||||
foreach ($dataRow as $cellValue) {
|
foreach ($dataRow as $cellValue) {
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 2, $cellValue, 'Data should have been written in Sheet 2');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 2, $cellValue, 'Data should have been written in Sheet 2');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($dataRowsSheet1Again as $dataRow) {
|
foreach ($dataRowsSheet1Again as $dataRow) {
|
||||||
foreach ($dataRow as $cellValue) {
|
foreach ($dataRow as $cellValue) {
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,8 +283,8 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = true);
|
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = true);
|
||||||
$this->assertEquals(2, count($writer->getSheets()), '2 sheets should have been created.');
|
$this->assertEquals(2, count($writer->getSheets()), '2 sheets should have been created.');
|
||||||
|
|
||||||
$this->assertInlineStringWasNotWrittenToSheet($fileName, 1, 'xlsx--sheet2--11');
|
$this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'xlsx--sheet2--11');
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 2, 'xlsx--sheet2--11');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 2, 'xlsx--sheet2--11');
|
||||||
|
|
||||||
\ReflectionHelper::reset();
|
\ReflectionHelper::reset();
|
||||||
}
|
}
|
||||||
@@ -276,7 +307,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = false);
|
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = false);
|
||||||
$this->assertEquals(1, count($writer->getSheets()), 'Only 1 sheet should have been created.');
|
$this->assertEquals(1, count($writer->getSheets()), 'Only 1 sheet should have been created.');
|
||||||
|
|
||||||
$this->assertInlineStringWasNotWrittenToSheet($fileName, 1, 'xlsx--sheet1--31');
|
$this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'xlsx--sheet1--31');
|
||||||
|
|
||||||
\ReflectionHelper::reset();
|
\ReflectionHelper::reset();
|
||||||
}
|
}
|
||||||
@@ -293,8 +324,8 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$this->writeToXLSXFile($dataRows, $fileName);
|
$this->writeToXLSXFile($dataRows, $fileName);
|
||||||
|
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 1, 'I'm in "great" mood', 'Quotes should be escaped');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'I'm in "great" mood', 'Quotes should be escaped');
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 1, 'This <must> be escaped & tested', '<, > and & should be escaped');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'This <must> be escaped & tested', '<, > and & should be escaped');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,10 +340,9 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$this->writeToXLSXFile($dataRows, $fileName);
|
$this->writeToXLSXFile($dataRows, $fileName);
|
||||||
|
|
||||||
$this->assertInlineStringWasWrittenToSheet($fileName, 1, 'control's _x0015_ "character"');
|
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'control's _x0015_ "character"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $allRows
|
* @param array $allRows
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
@@ -370,34 +400,34 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @param int $sheetNumber
|
* @param int $sheetIndex
|
||||||
* @param string $inlineString
|
* @param mixed $inlineData
|
||||||
* @param string $message
|
* @param string $message
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function assertInlineStringWasWrittenToSheet($fileName, $sheetNumber, $inlineString, $message = '')
|
private function assertInlineDataWasWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
||||||
{
|
{
|
||||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetNumber . '.xml';
|
$pathToSheetFile = $resourcePath . $this->normalizePath('#xl/worksheets/sheet' . $sheetIndex . '.xml');
|
||||||
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
||||||
|
|
||||||
$this->assertContains($inlineString, $xmlContents, $message);
|
$this->assertContains((string)$inlineData, $xmlContents, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @param int $sheetNumber
|
* @param int $sheetIndex
|
||||||
* @param string $inlineString
|
* @param mixed $inlineData
|
||||||
* @param string $message
|
* @param string $message
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function assertInlineStringWasNotWrittenToSheet($fileName, $sheetNumber, $inlineString, $message = '')
|
private function assertInlineDataWasNotWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
||||||
{
|
{
|
||||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetNumber . '.xml';
|
$pathToSheetFile = $resourcePath . $this->normalizePath('#xl/worksheets/sheet' . $sheetIndex . '.xml');
|
||||||
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
||||||
|
|
||||||
$this->assertNotContains($inlineString, $xmlContents, $message);
|
$this->assertNotContains((string)$inlineData, $xmlContents, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -409,9 +439,18 @@ 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 . '#xl/sharedStrings.xml';
|
$pathToSharedStringsFile = $resourcePath . $this->normalizePath('#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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user