Compare commits

..
37 Commits
Author SHA1 Message Date
Adrien Loison 444308d42c Merge pull request #84 from box/rename_strikethrough
Rename StrikeThrough to Strikethrough
2015-08-13 23:18:26 -07:00
Adrien Loison 353d4e86a5 Merge pull request #83 from box/wrap_text_on_multiline_strings
Set wrap text style when multiline string encountered
2015-08-13 23:18:11 -07:00
Adrien Loison f043f8d4d0 Rename StrikeThrough to Strikethrough 2015-08-13 23:09:43 -07:00
Adrien Loison c8ddcf5441 Set wrap text style when multiline string encountered
Fixes #10
If a cell contains a multiline string, "wrap text" style option should
automatically be set.
2015-08-13 23:03:28 -07:00
Adrien Loison 4a346cef0c Update README.md
Display style options as a table instead of bullet points
2015-08-13 23:02:37 -07:00
Adrien Loison 1c8934790d Update README.md
Added "How to style a row" section
2015-08-13 20:33:37 -07:00
Adrien Loison 2a1925bc51 Merge pull request #82 from box/fix_double_equals
Replace == with ===
2015-08-10 19:21:18 -07:00
Adrien Loison 2183ff6738 Replace == with === 2015-08-10 19:13:40 -07:00
Adrien Loison 380ee524a5 Merge pull request #80 from box/add_style_support
Add support for styling
2015-08-10 19:11:59 -07:00
Adrien Loison 21263a0730 Add support for styling
Added top level methods on the Writer:
 - addRowWithStyle()
 - addRowsWithStyle()

Added a style builder, to easily create new styles.
Each writer can specify its own default style and all styles will automatically inherit from it.

For now, the style properties supported are:
 - bold
 - italic
 - underline
 - strikethrough
 - font size
 - font name
 - wrap text (alignment)
2015-08-07 20:39:17 -07:00
Adrien Loison 0104714cbd Merge pull request #79 from jtreminio/master
getRowIterator() is on $sheet variable
2015-07-29 20:44:56 -07:00
Juan Treminio 611a091290 getRowIterator() is on $sheet variable 2015-07-29 22:04:47 -05:00
Adrien Loison 40a86c4b6c Merge pull request #77 from box/fix_csv_reader_empty_last_line
Fix CSV reader when last line is empty
2015-07-29 10:27:44 -07:00
Adrien Loison 8a3b895afc Fix CSV reader when last line is empty
If the last line was empty, it would create an infinite loop...
2015-07-29 10:17:51 -07:00
Adrien Loison e3f7ecfa64 Merge pull request #78 from box/fix_xml_reader_open_overriding
Fix XMLReader open() overriding
2015-07-29 10:17:34 -07:00
Adrien Loison 93d7aafe8b Fix XMLReader open() overriding
This is to avoid a warning in PHP7 (and also because that's how it should be!)
2015-07-29 09:59:33 -07:00
Adrien Loison dc53b6aa20 Update README.md
Change Travis badge from PNG to SVG
2015-07-27 22:26:36 -07:00
Adrien Loison 30aa8970d5 Update README.md
Bump "require" version in preparation of the 2.0.0 release
2015-07-27 22:21:33 -07:00
Adrien Loison 2ec12dd16b Merge pull request #76 from box/csv_multiple_encodings
Csv multiple encodings
2015-07-27 21:02:52 -07:00
Adrien Loison fd84c6f1c8 Update README 2015-07-27 20:59:21 -07:00
Adrien Loison 5e1cfbfdbd Attempt to convert the non UTF-8 strings to UTF-8 2015-07-27 20:59:12 -07:00
Adrien Loison d946f12951 Support for multiple BOMs depending on the selected encoding 2015-07-27 09:36:55 -07:00
Adrien Loison 03d1917080 Merge pull request #75 from box/xml_reader_wrappers
Add wrappers around XMLReader and SimpleXMLElement
2015-07-27 09:36:13 -07:00
Adrien Loison 1ba10ed2b0 Add wrappers around XMLReader and SimpleXMLElement to improve error handling 2015-07-27 00:49:43 -07:00
Adrien Loison be3932af18 Merge pull request #73 from box/iterators
Moved readers to iterators
2015-07-27 00:26:26 -07:00
Adrien Loison 37d87a8a27 Fix various problems 2015-07-27 00:23:18 -07:00
Adrien Loison 86a4c3790a Adding more tests 2015-07-26 23:53:49 -07:00
Adrien Loison 2345a80784 Update README for iterators 2015-07-26 23:53:18 -07:00
Adrien Loison 15aab7902a Factory should return Interface 2015-07-26 23:53:17 -07:00
Adrien Loison c672558a18 Update Writer folder structure to match Reader new structure 2015-07-26 23:53:17 -07:00
Adrien Loison c52dd7bde8 Remove old reader files 2015-07-26 23:53:17 -07:00
Adrien Loison ae3ee357ff Moved readers to iterators
Instead of the hasNext() / next() syntax, readers now implements the PHP iterator pattern.
It allows readers to be used with a foreach() loop.

All readers now share the same structure (CSV is treated as having exactly one sheet):
- one concrete Reader
- one SheetIterator, exposed by the Reader
- one or more Sheets, returned at every iteration
- one RowIterator, exposed by the Sheet

Introducing the concept of sheets for CSV may be kind of confusing but it makes Spout way more consistent.
Also, this confusion may be resolved by creating a wrapper around the readers if needed.

-- This commit does not delete the old files, not change the folder structure for Writers. This will be done in another commit.
2015-07-26 23:53:17 -07:00
Adrien Loison 322c3d0738 Merge pull request #74 from box/scrutinizer
Use Scrutinizer instead of Coveralls
2015-07-26 23:52:30 -07:00
Adrien Loison 0adbf439f5 Use Scrutinizer instead of Coveralls 2015-07-26 23:49:19 -07:00
Adrien Loison ad6b881685 Update README.md 2015-07-16 23:16:31 -07:00
Adrien Loison d410224e2d Merge pull request #71 from box/manual_autoloader
Autoloader for manual installation
2015-07-16 00:51:50 -07:00
Adrien Loison 79982a6107 Autoloader for manual installation
Added PSR4 Autoloader
Updated README with manual installation instructions
2015-07-16 00:46:15 -07:00
85 changed files with 6318 additions and 1258 deletions

No files matched your search

-1
View File
@@ -1,6 +1,5 @@
/tests/resources/generated
/tests/coverage
/vendor
composer.lock
/.idea
*.iml
+54
View File
@@ -0,0 +1,54 @@
filter:
excluded_paths: [vendor/*, tests/*]
tools:
external_code_coverage:
timeout: 600 # Wait 10 minutes for results
runs: 3 # Merge results for 5.4, 5.5 and 5.6 jobs
php_mess_detector: true
php_code_sniffer:
config:
standard: PSR4
filter:
paths: ['src']
sensiolabs_security_checker: true
php_pdepend: true
php_loc:
enabled: true
excluded_dirs: [vendor, tests]
php_cpd:
enabled: true
excluded_dirs: [vendor, tests]
build_failure_conditions:
- 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9
- 'project.metric_change("scrutinizer.test_coverage", < -0.005)' # Code Coverage decreased by more than 0.5%
- 'project.metric("scrutinizer.test_coverage", < 0.97)' # Code Coverage drops below 97%
checks:
php:
remove_extra_empty_lines: true
remove_php_closing_tag: true
remove_trailing_whitespace: true
fix_use_statements:
remove_unused: true
preserve_multiple: false
preserve_blanklines: true
fix_php_opening_tag: true
fix_linefeed: true
fix_line_ending: true
fix_identation_4spaces: true
fix_doc_comments: true
uppercase_constants: true
use_self_instead_of_fqcn: true
simplify_boolean_return: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
phpunit_assertions: true
parameters_in_camelcaps: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
optional_parameters_at_the_end: true
newline_at_end_of_file: true
encourage_single_quotes: true
+3 -3
View File
@@ -4,15 +4,15 @@ php:
- 5.4
- 5.5
- 5.6
- nightly
- 7.0
- hhvm
install:
- composer install --no-interaction
- composer install --no-interaction --prefer-source
script:
- mkdir -p build/logs
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
after_script:
- php vendor/bin/coveralls -v
- if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover build/logs/clover.xml; fi
+73 -16
View File
@@ -2,8 +2,8 @@
[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges)
[![Build Status](https://travis-ci.org/box/spout.png?branch=master)](http://travis-ci.org/box/spout)
[![Coverage Status](https://coveralls.io/repos/box/spout/badge.svg?branch=master)](https://coveralls.io/r/box/spout?branch=master)
[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](http://travis-ci.org/box/spout)
[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
[![License](https://poser.pugx.org/box/spout/license)](https://packagist.org/packages/box/spout)
@@ -13,12 +13,14 @@ Contrary to other file readers or writers, it is capable of processing very larg
## Installation
The Spout library can be installed directly from [Composer](https://getcomposer.org/).
### Composer (recommended)
Spout can be installed directly from [Composer](https://getcomposer.org/).
Add "box/spout" as a dependency in your project's composer.json file:
```json
"require": {
"box/spout": "*"
"box/spout": "~2.0"
}
```
@@ -27,6 +29,19 @@ Then run the install command from Composer:
php composer.phar install
```
### Manual installation
If you can't use Composer, no worries! You can still install Spout manually.
> Before starting, make sure your system meets the [requirements](#requirements).
1. Download the source code from the [Releases page](https://github.com/box/spout/releases)
2. Extract the downloaded content into your project.
3. Add this code to the top controller (index.php) or wherever it may be more appropriate:
```php
require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path!
```
## Requirements
@@ -49,9 +64,10 @@ use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::CSV);
$reader->open($filePath);
while ($reader->hasNextRow()) {
$row = $reader->nextRow();
// do stuff
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
// do stuff
}
}
$reader->close();
@@ -66,11 +82,8 @@ use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($filePath);
while ($reader->hasNextSheet()) {
$reader->nextSheet();
while ($reader->hasNextRow()) {
$row = $reader->nextRow();
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
// do stuff
}
}
@@ -113,7 +126,7 @@ For XLSX files, the number of rows per sheet is limited to 1,048,576 (see [Offic
### Configuring the CSV reader and writer
It is possible to configure the both the CSV reader and writer to specify the field separator as well as the field enclosure:
It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
```php
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\Common\Type;
@@ -123,8 +136,53 @@ $reader->setFieldDelimiter('|');
$reader->setFieldEnclosure('@');
```
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
```php
$reader->setEncoding('UTF-16LE');
```
The writer always generate CSV files encoded in UTF-8, with a BOM.
### Configuring the XLSX writer
#### Row styling
It is possible to apply some formatting options to a row. Spout supports fonts as well as alignment styles.
```php
use Box\Spout\Common\Type;
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Writer\Style\StyleBuilder;
$style = (new StyleBuilder())
->setFontBold()
->setFontSize(15)
->setShouldWrapText()
->build();
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($filePath);
$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row
$writer->addRow($otherSingleRow); // no style will be applied
$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows
$writer->close();
```
Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
Category | Property | API
----------|---------------|---------------------------------------
Font | Bold | `StyleBuilder::setFontBold()`
| Italic | `StyleBuilder::setFontItalic()`
| Underline | `StyleBuilder::setFontUnderline()`
| Strikethrough | `StyleBuilder::setFontStrikethrough()`
| Font name | `StyleBuilder::setFontName('Arial')`
| Font size | `StyleBuilder::setFontSize(14)`
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
#### Strings storage
XLSX files support different ways to store the string values:
@@ -187,8 +245,7 @@ $sheets = $writer->getSheets();
If you rely on the sheet's name in your application, you can access it and customize it this way:
```php
// Accessing the sheet name when reading
while ($reader->hasNextSheet()) {
$sheet = $reader->nextSheet();
foreach ($reader->getSheetIterator() as $sheet) {
$sheetName = $sheet->getName();
}
@@ -238,7 +295,7 @@ For information, the performance tests take about one hour to run (processing 2
When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed.
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them into several small temporary files that allows fast access.
Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access.
#### How long does it take to generate a file with X rows?
+5 -1
View File
@@ -19,7 +19,11 @@
},
"require-dev": {
"phpunit/phpunit": ">=3.7",
"satooshi/php-coveralls": "^0.6.1"
"scrutinizer/ocular": "~1.1"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
"ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
},
"autoload": {
"psr-4": {
Generated
+1647
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -5,7 +5,6 @@
colors="true"
convertErrorsToExceptions="false"
convertWarningsToExceptions="false"
strict="false"
verbose="false">
<testsuites>
@@ -17,6 +16,9 @@
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
<exclude>
<directory>src/Spout/Autoloader</directory>
</exclude>
</whitelist>
</filter>
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace Box\Spout\Autoloader;
/**
* Class Psr4Autoloader
* @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example
*
* @package Box\Spout\Autoloader
*/
class Psr4Autoloader
{
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
*
* @var array
*/
protected $prefixes = array();
/**
* Register loader with SPL autoloader stack.
*
* @return void
*/
public function register()
{
spl_autoload_register(array($this, 'loadClass'));
}
/**
* Adds a base directory for a namespace prefix.
*
* @param string $prefix The namespace prefix.
* @param string $baseDir A base directory for class files in the
* namespace.
* @param bool $prepend If true, prepend the base directory to the stack
* instead of appending it; this causes it to be searched first rather
* than last.
* @return void
*/
public function addNamespace($prefix, $baseDir, $prepend = false)
{
// normalize namespace prefix
$prefix = trim($prefix, '\\') . '\\';
// normalize the base directory with a trailing separator
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
// initialize the namespace prefix array
if (isset($this->prefixes[$prefix]) === false) {
$this->prefixes[$prefix] = array();
}
// retain the base directory for the namespace prefix
if ($prepend) {
array_unshift($this->prefixes[$prefix], $baseDir);
} else {
array_push($this->prefixes[$prefix], $baseDir);
}
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name.
* @return mixed The mapped file name on success, or boolean false on
* failure.
*/
public function loadClass($class)
{
// the current namespace prefix
$prefix = $class;
// work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name
while (false !== $pos = strrpos($prefix, '\\')) {
// retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1);
// the rest is the relative class name
$relativeClass = substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
if ($mappedFile !== false) {
return $mappedFile;
}
// remove the trailing namespace separator for the next iteration
// of strrpos()
$prefix = rtrim($prefix, '\\');
}
// never found a mapped file
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix.
* @param string $relativeClass The relative class name.
* @return mixed Boolean false if no mapped file can be loaded, or the
* name of the mapped file that was loaded.
*/
protected function loadMappedFile($prefix, $relativeClass)
{
// are there any base directories for this namespace prefix?
if (isset($this->prefixes[$prefix]) === false) {
return false;
}
// look through base directories for this namespace prefix
foreach ($this->prefixes[$prefix] as $baseDir) {
// replace the namespace prefix with the base directory,
// replace namespace separators with directory separators
// in the relative class name, append with .php
$file = $baseDir
. str_replace('\\', '/', $relativeClass)
. '.php';
// if the mapped file exists, require it
if ($this->requireFile($file)) {
// yes, we're done
return $file;
}
}
// never found it
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file The file to require.
* @return bool True if the file exists, false if not.
*/
protected function requireFile($file)
{
if (file_exists($file)) {
require $file;
return true;
}
return false;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Box\Spout\Autoloader;
require_once 'Psr4Autoloader.php';
/**
* @var string $srcBaseDirectory
* Full path to "src/Spout" which is what we want "Box\Spout" to map to.
*/
$srcBaseDirectory = dirname(dirname(__FILE__));
$loader = new Psr4Autoloader();
$loader->register();
$loader->addNamespace('Box\Spout', $srcBaseDirectory);
+3
View File
@@ -13,6 +13,9 @@ class XLSX implements EscaperInterface
/** @var string[] Control characters to be escaped */
protected $controlCharactersEscapingMap;
/**
*
*/
public function __construct()
{
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class EncodingConversionException
*
* @package Box\Spout\Common\Exception
*/
class EncodingConversionException extends SpoutException
{
}
+175
View File
@@ -0,0 +1,175 @@
<?php
namespace Box\Spout\Common\Helper;
use Box\Spout\Common\Exception\EncodingConversionException;
/**
* Class EncodingHelper
* This class provides helper functions to work with encodings.
*
* @package Box\Spout\Common\Helper
*/
class EncodingHelper
{
/** Definition of the encodings that can have a BOM */
const ENCODING_UTF8 = 'UTF-8';
const ENCODING_UTF16_LE = 'UTF-16LE';
const ENCODING_UTF16_BE = 'UTF-16BE';
const ENCODING_UTF32_LE = 'UTF-32LE';
const ENCODING_UTF32_BE = 'UTF-32BE';
/** Definition of the BOMs for the different encodings */
const BOM_UTF8 = "\xEF\xBB\xBF";
const BOM_UTF16_LE = "\xFF\xFE";
const BOM_UTF16_BE = "\xFE\xFF";
const BOM_UTF32_LE = "\xFF\xFE\x00\x00";
const BOM_UTF32_BE = "\x00\x00\xFE\xFF";
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */
protected $supportedEncodingsWithBom;
/**
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($globalFunctionsHelper)
{
$this->globalFunctionsHelper = $globalFunctionsHelper;
$this->supportedEncodingsWithBom = [
self::ENCODING_UTF8 => self::BOM_UTF8,
self::ENCODING_UTF16_LE => self::BOM_UTF16_LE,
self::ENCODING_UTF16_BE => self::BOM_UTF16_BE,
self::ENCODING_UTF32_LE => self::BOM_UTF32_LE,
self::ENCODING_UTF32_BE => self::BOM_UTF32_BE,
];
}
/**
* Returns the number of bytes to use as offset in order to skip the BOM.
*
* @param resource $filePointer Pointer to the file to check
* @param string $encoding Encoding of the file to check
* @return int Bytes offset to apply to skip the BOM (0 means no BOM)
*/
public function getBytesOffsetToSkipBOM($filePointer, $encoding)
{
$byteOffsetToSkipBom = 0;
if ($this->hasBom($filePointer, $encoding)) {
$bomUsed = $this->supportedEncodingsWithBom[$encoding];
// we skip the N first bytes
$byteOffsetToSkipBom = strlen($bomUsed);
}
return $byteOffsetToSkipBom;
}
/**
* Returns whether the file identified by the given pointer has a BOM.
*
* @param resource $filePointer Pointer to the file to check
* @param string $encoding Encoding of the file to check
* @return bool TRUE if the file has a BOM, FALSE otherwise
*/
protected function hasBOM($filePointer, $encoding)
{
$hasBOM = false;
$this->globalFunctionsHelper->rewind($filePointer);
if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
$potentialBom = $this->supportedEncodingsWithBom[$encoding];
$numBytesInBom = strlen($potentialBom);
$hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
}
return $hasBOM;
}
/**
* Attempts to convert a non UTF-8 string into UTF-8.
*
* @param string $string Non UTF-8 string to be converted
* @param string $sourceEncoding The encoding used to encode the source string
* @return string The converted, UTF-8 string
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
*/
public function attemptConversionToUTF8($string, $sourceEncoding)
{
return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8);
}
/**
* Attempts to convert a UTF-8 string into the given encoding.
*
* @param string $string UTF-8 string to be converted
* @param string $targetEncoding The encoding the string should be re-encoded into
* @return string The converted string, encoded with the given encoding
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
*/
public function attemptConversionFromUTF8($string, $targetEncoding)
{
return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding);
}
/**
* Attempts to convert the given string to the given encoding.
* Depending on what is installed on the server, we will try to iconv or mbstring.
*
* @param string $string string to be converted
* @param string $sourceEncoding The encoding used to encode the source string
* @param string $targetEncoding The encoding the string should be re-encoded into
* @return string The converted string, encoded with the given encoding
* @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
*/
protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
{
// if source and target encodings are the same, it's a no-op
if ($sourceEncoding === $targetEncoding) {
return $string;
}
$convertedString = null;
if ($this->canUseIconv()) {
$convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
} else if ($this->canUseMbString()) {
$convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
} else {
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\".");
}
if ($convertedString === false) {
throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed.");
}
return $convertedString;
}
/**
* Returns whether "iconv" can be used.
*
* @return bool TRUE if "iconv" is available and can be used, FALSE otherwise
*/
protected function canUseIconv()
{
return $this->globalFunctionsHelper->function_exists('iconv');
}
/**
* Returns whether "mb_string" functions can be used.
* These functions come with the PHP Intl package.
*
* @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise
*/
protected function canUseMbString()
{
return $this->globalFunctionsHelper->function_exists('mb_convert_encoding');
}
}
+4 -4
View File
@@ -40,7 +40,7 @@ class FileSystemHelper
$wasCreationSuccessful = mkdir($folderPath, 0777, true);
if (!$wasCreationSuccessful) {
throw new IOException('Unable to create folder: ' . $folderPath);
throw new IOException("Unable to create folder: $folderPath");
}
return $folderPath;
@@ -63,8 +63,8 @@ class FileSystemHelper
$filePath = $parentFolderPath . '/' . $fileName;
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
if (!$wasCreationSuccessful) {
throw new IOException('Unable to create file: ' . $filePath);
if ($wasCreationSuccessful === false) {
throw new IOException("Unable to create file: $filePath");
}
return $filePath;
@@ -126,7 +126,7 @@ class FileSystemHelper
{
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0);
if (!$isInBaseFolder) {
throw new IOException('Cannot perform I/O operation outside of the base folder: ' . $this->baseFolderPath);
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderPath}");
}
}
}
@@ -160,13 +160,25 @@ class GlobalFunctionsHelper
* @see file_get_contents()
*
* @param string $filePath
* @return bool
* @return string
*/
public function file_get_contents($filePath)
{
return file_get_contents($filePath);
}
/**
* Wrapper around global function feof()
* @see feof()
*
* @param resource
* @return bool
*/
public function feof($handle)
{
return feof($handle);
}
/**
* Wrapper around global function is_readable()
* @see is_readable()
@@ -203,4 +215,73 @@ class GlobalFunctionsHelper
{
header($string);
}
/**
* Wrapper around global function iconv()
* @see iconv()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
* @return string|bool the converted string or FALSE on failure.
*/
public function iconv($string, $sourceEncoding, $targetEncoding)
{
return iconv($sourceEncoding, $targetEncoding, $string);
}
/**
* Wrapper around global function mb_convert_encoding()
* @see mb_convert_encoding()
*
* @param string $string The string to be converted
* @param string $sourceEncoding The encoding of the source string
* @param string $targetEncoding The encoding the source string should be converted to
* @return string|bool the converted string or FALSE on failure.
*/
public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
{
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
}
/**
* Wrapper around global function stream_get_line()
* @see stream_get_line()
*
* @param resource $handle
* @param int $length
* @param string|void $ending
* @return string|bool
*/
public function stream_get_line($handle, $length, $ending = null)
{
return stream_get_line($handle, $length, $ending);
}
/**
* Wrapper around global function str_getcsv()
* @see str_getcsv()
*
* @param string $input
* @param string|void $delimiter
* @param string|void $enclosure
* @param string|void $escape
* @return array
*/
public function str_getcsv($input, $delimiter = null, $enclosure = null, $escape = null)
{
return str_getcsv($input, $delimiter, $enclosure, $escape);
}
/**
* Wrapper around global function function_exists()
* @see function_exists()
*
* @param string $functionName
* @return bool
*/
public function function_exists($functionName)
{
return function_exists($functionName);
}
}
+18 -89
View File
@@ -4,7 +4,6 @@ namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
use Box\Spout\Reader\Exception\EndOfFileReachedException;
/**
* Class AbstractReader
@@ -14,18 +13,9 @@ use Box\Spout\Reader\Exception\EndOfFileReachedException;
*/
abstract class AbstractReader implements ReaderInterface
{
/** @var int Used to keep track of the row index */
protected $currentRowIndex = 0;
/** @var bool Indicates whether the stream is currently open */
protected $isStreamOpened = false;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var array Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
@@ -38,11 +28,11 @@ abstract class AbstractReader implements ReaderInterface
abstract protected function openReader($filePath);
/**
* Reads and returns next row if available.
* Returns an iterator to iterate over sheets.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
* @return \Iterator To iterate over sheets
*/
abstract protected function read();
abstract public function getConcreteSheetIterator();
/**
* Closes the reader. To be used after reading the file.
@@ -74,20 +64,17 @@ abstract class AbstractReader implements ReaderInterface
if (!$this->isPhpStream($filePath)) {
// we skip the checks if the provided file path points to a PHP stream
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
throw new IOException('Could not open ' . $filePath . ' for reading! File does not exist.');
throw new IOException("Could not open $filePath for reading! File does not exist.");
} else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
throw new IOException('Could not open ' . $filePath . ' for reading! File is not readable.');
throw new IOException("Could not open $filePath for reading! File is not readable.");
}
}
$this->currentRowIndex = 0;
$this->hasReachedEndOfFile = false;
try {
$this->openReader($filePath);
$this->isStreamOpened = true;
} catch (\Exception $exception) {
throw new IOException('Could not open ' . $filePath . ' for reading! (' . $exception->getMessage() . ')');
throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
}
}
@@ -103,82 +90,18 @@ abstract class AbstractReader implements ReaderInterface
}
/**
* Returns whether all rows have been read (i.e. if we are at the end of the file).
* To know if the end of file has been reached, it uses a buffer. If the buffer is
* empty (meaning, nothing has been read or previous read line has been consumed), then
* it reads the next line, store it in the buffer for the next time or flip a variable if
* the end of file has been reached.
* Returns an iterator to iterate over sheets.
*
* @return bool Whether all rows have been read (i.e. if we are at the end of the file)
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If the stream was not opened first
* @return \Iterator To iterate over sheets
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
*/
public function hasNextRow()
public function getSheetIterator()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Stream should be opened first.');
throw new ReaderNotOpenedException('Reader should be opened first.');
}
if ($this->hasReachedEndOfFile) {
return false;
}
// if the buffer contains unprocessed row
if (!$this->isRowDataBufferEmpty()) {
return true;
}
// otherwise, try to read the next line line, and store it in the buffer
$this->rowDataBuffer = $this->read();
// if the buffer is still empty after reading a row, it means end of file was reached
$this->hasReachedEndOfFile = $this->isRowDataBufferEmpty();
return (!$this->hasReachedEndOfFile);
}
/**
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
* actually reading the file.
*
* @return array Array that contains the data for the read row
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
*/
public function nextRow()
{
if (!$this->hasNextRow()) {
throw new EndOfFileReachedException('End of file was reached. Cannot read more rows.');
}
// Get data from buffer (if the buffer was empty, it was filled by the call to hasNextRow())
$rowData = $this->rowDataBuffer;
// empty buffer to mark the row as consumed
$this->emptyRowDataBuffer();
$this->currentRowIndex++;
return $rowData;
}
/**
* Returns whether the buffer where the row data is stored is empty
*
* @return bool
*/
protected function isRowDataBufferEmpty()
{
return ($this->rowDataBuffer === null);
}
/**
* Empty the buffer that stores row data
*
* @return void
*/
protected function emptyRowDataBuffer()
{
$this->rowDataBuffer = null;
return $this->getConcreteSheetIterator();
}
/**
@@ -190,6 +113,12 @@ abstract class AbstractReader implements ReaderInterface
{
if ($this->isStreamOpened) {
$this->closeReader();
$sheetIterator = $this->getConcreteSheetIterator();
if ($sheetIterator) {
$sheetIterator->end();
}
$this->isStreamOpened = false;
}
}
-130
View File
@@ -1,130 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
/**
* Class CSV
* This class provides support to read data from a CSV file.
*
* @package Box\Spout\Reader
*/
class CSV extends AbstractReader
{
const UTF8_BOM = "\xEF\xBB\xBF";
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @var string Defines the character used to delimit fields (one character only) */
protected $fieldDelimiter = ',';
/** @var string Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure = '"';
/**
* Sets the field delimiter for the CSV
*
* @param string $fieldDelimiter Character that delimits fields
* @return CSV
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
return $this;
}
/**
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return CSV
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
return $this;
}
/**
* Opens the file at the given path to make it ready to be read.
* The file must be UTF-8 encoded.
* @TODO add encoding detection/conversion
*
* @param string $filePath Path of the CSV file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException
*/
protected function openReader($filePath)
{
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException('Could not open file ' . $filePath . ' for reading.');
}
$this->skipUtf8Bom();
}
/**
* This skips the UTF-8 BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*
* @return void
*/
protected function skipUtf8Bom()
{
$this->globalFunctionsHelper->rewind($this->filePointer);
$hasUtf8Bom = ($this->globalFunctionsHelper->fgets($this->filePointer, 4) === self::UTF8_BOM);
if ($hasUtf8Bom) {
// we skip the 2 first bytes (so start from the 3rd byte)
$this->globalFunctionsHelper->fseek($this->filePointer, 3);
} else {
// if no BOM, reset the pointer to read from the beginning
$this->globalFunctionsHelper->fseek($this->filePointer, 0);
}
}
/**
* Reads and returns next row if available.
* Empty rows are skipped.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
*/
protected function read()
{
$lineData = null;
if ($this->filePointer) {
do {
$lineData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, 0, $this->fieldDelimiter, $this->fieldEnclosure);
} while ($lineData && $this->isEmptyLine($lineData));
}
// When reaching the end of the file, return null instead of false
return ($lineData !== false) ? $lineData : null;
}
/**
* @param array $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (count($lineData) === 1 && $lineData[0] === null);
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\AbstractReader;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\EncodingHelper;
/**
* Class Reader
* This class provides support to read data from a CSV file.
*
* @package Box\Spout\Reader\CSV
*/
class Reader extends AbstractReader
{
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @var SheetIterator To iterator over the CSV unique "sheet" */
protected $sheetIterator;
/** @var string Defines the character used to delimit fields (one character only) */
protected $fieldDelimiter = ',';
/** @var string Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure = '"';
/** @var string Encoding of the CSV file to be read */
protected $encoding = EncodingHelper::ENCODING_UTF8;
/**
* Sets the field delimiter for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldDelimiter Character that delimits fields
* @return Reader
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
return $this;
}
/**
* Sets the field enclosure for the CSV.
* Needs to be called before opening the reader.
*
* @param string $fieldEnclosure Character that enclose fields
* @return Reader
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
return $this;
}
/**
* Sets the encoding of the CSV file to be read.
* Needs to be called before opening the reader.
*
* @param string $encoding Encoding of the CSV file to be read
* @return Reader
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
/**
* Opens the file at the given path to make it ready to be read.
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
*
* @param string $filePath Path of the CSV file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException
*/
protected function openReader($filePath)
{
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException("Could not open file $filePath for reading.");
}
$this->sheetIterator = new SheetIterator(
$this->filePointer,
$this->fieldDelimiter,
$this->fieldEnclosure,
$this->encoding,
$this->globalFunctionsHelper
);
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
public function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
}
}
+213
View File
@@ -0,0 +1,213 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\IteratorInterface;
use Box\Spout\Common\Helper\EncodingHelper;
/**
* Class RowIterator
* Iterate over CSV rows.
*
* @package Box\Spout\Reader\CSV
*/
class RowIterator implements IteratorInterface
{
/** @var resource Pointer to the CSV file to read */
protected $filePointer;
/** @var int Number of read rows */
protected $numReadRows = 0;
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var string Defines the character used to delimit fields (one character only) */
protected $fieldDelimiter;
/** @var string Defines the character used to enclose fields (one character only) */
protected $fieldEnclosure;
/** @var string Encoding of the CSV file to be read */
protected $encoding;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */
protected $encodingHelper;
/** @var string End of line delimiter, encoded using the same encoding as the CSV */
protected $encodedEOLDelimiter;
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param string $encoding Encoding of the CSV file to be read
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
{
$this->filePointer = $filePointer;
$this->fieldDelimiter = $fieldDelimiter;
$this->fieldEnclosure = $fieldEnclosure;
$this->encoding = $encoding;
$this->globalFunctionsHelper = $globalFunctionsHelper;
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->rewindAndSkipBom();
$this->numReadRows = 0;
$this->rowDataBuffer = null;
$this->next();
}
/**
* This rewinds and skips the BOM if inserted at the beginning of the file
* by moving the file pointer after it, so that it is not read.
*
* @return void
*/
protected function rewindAndSkipBom()
{
$byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding);
// sets the cursor after the BOM (0 means no BOM, so rewind it)
$this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom);
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return ($this->filePointer && !$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Empty rows are skipped.
* @link http://php.net/manual/en/iterator.next.php
*
* @return void
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
*/
public function next()
{
$lineData = false;
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if (!$this->hasReachedEndOfFile) {
do {
$utf8EncodedLineData = $this->getNextUTF8EncodedLine();
if ($utf8EncodedLineData !== false) {
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
}
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
} while (($lineData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($lineData));
if ($lineData !== false) {
$this->rowDataBuffer = $lineData;
$this->numReadRows++;
} else {
// If we reach this point, it means end of file was reached.
// This happens when the last lines are empty lines.
$this->hasReachedEndOfFile = $hasNowReachedEndOfFile;
}
}
}
/**
* Returns the next line, converted if necessary to UTF-8.
* Neither fgets nor fgetcsv don't work with non UTF-8 data... so we need to do some things manually.
*
* @return string|false The next line for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
*/
protected function getNextUTF8EncodedLine()
{
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, 0, $encodedEOLDelimiter);
// If the line could have been read, it can be converted to UTF-8
$utf8EncodedLineData = ($encodedLineData !== false) ?
$this->encodingHelper->attemptConversionToUTF8($encodedLineData, $this->encoding) :
false;
return $utf8EncodedLineData;
}
/**
* Returns the end of line delimiter, encoded using the same encoding as the CSV.
* The return value is cached.
*
* @return string
*/
protected function getEncodedEOLDelimiter()
{
if (!isset($this->encodedEOLDelimiter)) {
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8("\n", $this->encoding);
}
return $this->encodedEOLDelimiter;
}
/**
* @param array $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null);
}
/**
* Return the current element from the buffer
* @link http://php.net/manual/en/iterator.current.php
*
* @return array|null
*/
public function current()
{
return $this->rowDataBuffer;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->numReadRows;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// do nothing
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\SheetInterface;
/**
* Class Sheet
*
* @package Box\Spout\Reader\CSV
*/
class Sheet implements SheetInterface
{
/** @var RowIterator To iterate over the CSV's rows */
protected $rowIterator;
/**
* @param resource $filePointer Pointer to the CSV file to read
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param string $encoding Encoding of the CSV file to be read
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
{
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
}
/**
* @return RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\IteratorInterface;
/**
* Class SheetIterator
* Iterate over CSV unique "sheet".
*
* @package Box\Spout\Reader\CSV
*/
class SheetIterator implements IteratorInterface
{
/** @var Sheet The CSV unique "sheet" */
protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */
protected $hasReadUniqueSheet = false;
/**
* @param resource $filePointer
* @param string $fieldDelimiter Character that delimits fields
* @param string $fieldEnclosure Character that enclose fields
* @param string $encoding Encoding of the CSV file to be read
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
{
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->hasReadUniqueSheet = false;
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return (!$this->hasReadUniqueSheet);
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
$this->hasReadUniqueSheet = true;
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
*
* @return Sheet
*/
public function current()
{
return $this->sheet;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// do nothing
}
}
@@ -1,12 +0,0 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class EndOfWorksheetsReachedException
*
* @package Box\Spout\Reader\Exception
*/
class EndOfWorksheetsReachedException extends ReaderException
{
}
@@ -3,10 +3,10 @@
namespace Box\Spout\Reader\Exception;
/**
* Class EndOfFileReachedException
* Class NoSheetsFoundException
*
* @package Box\Spout\Reader\Exception
*/
class EndOfFileReachedException extends ReaderException
class NoSheetsFoundException extends ReaderException
{
}
@@ -3,10 +3,10 @@
namespace Box\Spout\Reader\Exception;
/**
* Class NoWorksheetsFoundException
* Class XMLProcessingException
*
* @package Box\Spout\Reader\Exception
*/
class NoWorksheetsFoundException extends ReaderException
class XMLProcessingException extends ReaderException
{
}
@@ -1,58 +0,0 @@
<?php
namespace Box\Spout\Reader\Internal\XLSX;
/**
* Class Worksheet
* Represents a worksheet within a XLSX file
*
* @package Box\Spout\Reader\Internal\XLSX
*/
class Worksheet
{
/** @var \Box\Spout\Reader\Sheet The "external" sheet */
protected $externalSheet;
/** @var int Worksheet index, based on the order of appareance in [Content_Types].xml (zero-based) */
protected $worksheetIndex;
/** @var string Path of the XML file containing the worksheet data */
protected $dataXmlFilePath;
/**\
* @param \Box\Spout\Reader\Sheet $externalSheet The associated "external" sheet
* @param int $worksheetIndex Worksheet index, based on the order of appareance in [Content_Types].xml (zero-based)
* @param string $dataXmlFilePath Path of the XML file containing the worksheet data
*/
public function __construct($externalSheet, $worksheetIndex, $dataXmlFilePath)
{
$this->externalSheet = $externalSheet;
$this->worksheetIndex = $worksheetIndex;
$this->dataXmlFilePath = $dataXmlFilePath;
}
/**
* @return string Path of the XML file containing the worksheet data,
* without the leading slash.
*/
public function getDataXmlFilePath()
{
return ltrim($this->dataXmlFilePath, '/');
}
/**
* @return \Box\Spout\Reader\Sheet The "external" sheet
*/
public function getExternalSheet()
{
return $this->externalSheet;
}
/**
* @return int
*/
public function getWorksheetIndex()
{
return $this->worksheetIndex;
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Box\Spout\Reader;
/**
* Interface IteratorInterface
*
* @package Box\Spout\Reader
*/
interface IteratorInterface extends \Iterator
{
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end();
}
+3 -3
View File
@@ -19,7 +19,7 @@ class ReaderFactory
* This creates an instance of the appropriate reader, given the type of the file to be read
*
* @param string $readerType Type of the reader to instantiate
* @return \Box\Spout\Reader\CSV|\Box\Spout\Reader\XLSX
* @return ReaderInterface
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
*/
public static function create($readerType)
@@ -28,10 +28,10 @@ class ReaderFactory
switch ($readerType) {
case Type::CSV:
$reader = new CSV();
$reader = new CSV\Reader();
break;
case Type::XLSX:
$reader = new XLSX();
$reader = new XLSX\Reader();
break;
default:
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
+4 -18
View File
@@ -20,26 +20,12 @@ interface ReaderInterface
public function open($filePath);
/**
* Returns whether all rows have been read (i.e. if we are at the end of the file).
* To know if the end of file has been reached, it uses a buffer. If the buffer is
* empty (meaning, nothing has been read or previous read line has been consumed), then
* it reads the next line, store it in the buffer for the next time or flip a variable if
* the end of file has been reached.
* Returns an iterator to iterate over sheets.
*
* @return bool
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @return \Iterator To iterate over sheets
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
*/
public function hasNextRow();
/**
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
* actually reading the file.
*
* @return array Array that contains the data for the read row
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
*/
public function nextRow();
public function getSheetIterator();
/**
* Closes the reader, preventing any additional reading
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Box\Spout\Reader;
/**
* Interface SheetInterface
*
* @package Box\Spout\Reader
*/
interface SheetInterface
{
/**
* Returns an iterator to iterate over the sheet's rows.
*
* @return \Iterator
*/
public function getRowIterator();
}
@@ -0,0 +1,161 @@
<?php
namespace Box\Spout\Reader\Wrapper;
use Box\Spout\Reader\Exception\XMLProcessingException;
/**
* Class SimpleXMLElement
* Wrapper around the built-in SimpleXMLElement. This class does not extend \SimpleXMLElement
* because it its constructor is final... Instead, it is used as a passthrough.
* @see \SimpleXMLElement
*
* @package Box\Spout\Reader\Wrapper
*/
class SimpleXMLElement
{
use XMLInternalErrorsHelper;
/** @var \SimpleXMLElement Instance of the wrapped SimpleXMLElement object */
protected $simpleXMLElement;
/**
* Creates a new SimpleXMLElement object
* @see \SimpleXMLElement::__construct
*
* @param string $xmlData A well-formed XML string
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If the XML string is not well-formed
*/
public function __construct($xmlData)
{
$this->useXMLInternalErrors();
try {
$this->simpleXMLElement = new \SimpleXMLElement($xmlData);
} catch (\Exception $exception) {
// if the data is invalid, the constructor will throw an Exception
$this->resetXMLInternalErrorsSetting();
throw new XMLProcessingException($this->getLastXMLErrorMessage());
}
$this->resetXMLInternalErrorsSetting();
return $this->simpleXMLElement;
}
/**
* Returns the attribute for the given name.
*
* @param string $name Attribute name
* @param string|null|void $namespace An optional namespace for the retrieved attributes
* @return string|null The attribute value or NULL if attribute not found
*/
public function getAttribute($name, $namespace = null)
{
$isPrefix = ($namespace !== null);
$attributes = $this->simpleXMLElement->attributes($namespace, $isPrefix);
$attributeValue = $attributes->{$name};
return ($attributeValue !== null) ? (string) $attributeValue : null;
}
/**
* Creates a prefix/ns context for the next XPath query
* @see \SimpleXMLElement::registerXPathNamespace
*
* @param string $prefix The namespace prefix to use in the XPath query for the namespace given in "namespace".
* @param string $namespace The namespace to use for the XPath query. This must match a namespace in
* use by the XML document or the XPath query using "prefix" will not return any results.
* @return bool TRUE on success or FALSE on failure.
*/
public function registerXPathNamespace($prefix, $namespace)
{
return $this->simpleXMLElement->registerXPathNamespace($prefix, $namespace);
}
/**
* Runs XPath query on XML data
* @see \SimpleXMLElement::xpath
*
* @param string $path An XPath path
* @return SimpleXMLElement[]|bool an array of SimpleXMLElement objects or FALSE in case of an error.
*/
public function xpath($path)
{
$elements = $this->simpleXMLElement->xpath($path);
if ($elements !== false) {
$wrappedElements = [];
foreach ($elements as $element) {
$wrappedElement = $this->wrapSimpleXMLElement($element);
if ($wrappedElement !== null) {
$wrappedElements[] = $this->wrapSimpleXMLElement($element);
}
}
$elements = $wrappedElements;
}
return $elements;
}
/**
* Wraps the given element into an instance of the wrapper
*
* @param \SimpleXMLElement $element Element to be wrapped
* @return SimpleXMLElement|null The wrapped element or NULL if the given element is invalid
*/
protected function wrapSimpleXMLElement(\SimpleXMLElement $element)
{
$wrappedElement = null;
$elementAsXML = $element->asXML();
if ($elementAsXML !== false) {
$wrappedElement = new SimpleXMLElement($elementAsXML);
}
return $wrappedElement;
}
/**
* Remove all nodes matching the given XPath query.
* It does not map to any \SimpleXMLElement function.
*
* @param string $path An XPath path
* @return void
*/
public function removeNodesMatchingXPath($path)
{
$nodesToRemove = $this->simpleXMLElement->xpath($path);
foreach ($nodesToRemove as $nodeToRemove) {
unset($nodeToRemove[0]);
}
}
/**
* Returns the first child matching the given tag name
*
* @param string $tagName
* @return SimpleXMLElement|null The first child matching the tag name or NULL if none found
*/
public function getFirstChildByTagName($tagName)
{
$doesElementExist = isset($this->simpleXMLElement->{$tagName});
/** @var \SimpleXMLElement $realElement */
$realElement = $this->simpleXMLElement->{$tagName};
return $doesElementExist ? $this->wrapSimpleXMLElement($realElement) : null;
}
/**
* @return string
*/
public function __toString()
{
return $this->simpleXMLElement->__toString();
}
}
@@ -0,0 +1,82 @@
<?php
namespace Box\Spout\Reader\Wrapper;
use Box\Spout\Reader\Exception\XMLProcessingException;
/**
* Trait XMLInternalErrorsHelper
*
* @package Box\Spout\Reader\Wrapper
*/
trait XMLInternalErrorsHelper
{
/** @var bool Stores whether XML errors were initially stored internally - used to reset */
protected $initialUseInternalErrorsValue;
/**
* To avoid displaying lots of warning/error messages on screen,
* stores errors internally instead.
*
* @return void
*/
protected function useXMLInternalErrors()
{
libxml_clear_errors();
$this->initialUseInternalErrorsValue = libxml_use_internal_errors(true);
}
/**
* Throws an XMLProcessingException if an error occured.
* It also always resets the "libxml_use_internal_errors" setting back to its initial value.
*
* @return void
* @throws \Box\Spout\Reader\Exception\XMLProcessingException
*/
protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured()
{
if ($this->hasXMLErrorOccured()) {
$this->resetXMLInternalErrorsSetting();
throw new XMLProcessingException($this->getLastXMLErrorMessage());
}
$this->resetXMLInternalErrorsSetting();
}
/**
* Returns whether the a XML error has occured since the last time errors were cleared.
*
* @return bool TRUE if an error occured, FALSE otherwise
*/
private function hasXMLErrorOccured()
{
return (libxml_get_last_error() !== false);
}
/**
* Returns the error message for the last XML error that occured.
* @see libxml_get_last_error
*
* @return String|null Last XML error message or null if no error
*/
private function getLastXMLErrorMessage()
{
$errorMessage = null;
$error = libxml_get_last_error();
if ($error !== false) {
$errorMessage = trim($error->message);
}
return $errorMessage;
}
/**
* @return void
*/
protected function resetXMLInternalErrorsSetting()
{
libxml_use_internal_errors($this->initialUseInternalErrorsValue);
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace Box\Spout\Reader\Wrapper;
/**
* Class XMLReader
* Wrapper around the built-in XMLReader
* @see \XMLReader
*
* @package Box\Spout\Reader\Wrapper
*/
class XMLReader extends \XMLReader
{
use XMLInternalErrorsHelper;
/**
* Set the URI containing the XML to parse
* @see \XMLReader::open
*
* @param string $URI URI pointing to the document
* @param string|null|void $encoding The document encoding
* @param int $options A bitmask of the LIBXML_* constants
* @return bool TRUE on success or FALSE on failure
*/
public function open($URI, $encoding = null, $options = 0)
{
$wasOpenSuccessful = false;
// HHVM does not check if file exists within zip file
// @link https://github.com/facebook/hhvm/issues/5779
if ($this->isRunningHHVM() && $this->isZipStream($URI)) {
if ($this->fileExistsWithinZip($URI)) {
$wasOpenSuccessful = parent::open($URI, $encoding, $options|LIBXML_NONET);
}
} else {
$wasOpenSuccessful = parent::open($URI, $encoding, $options|LIBXML_NONET);
}
return $wasOpenSuccessful;
}
/**
* Returns whether the given URI is a zip stream.
*
* @param string $URI URI pointing to a document
* @return bool TRUE if URI is a zip stream, FALSE otherwise
*/
protected function isZipStream($URI)
{
return (strpos($URI, 'zip://') === 0);
}
/**
* Returns whether the current environment is HHVM
*
* @return bool TRUE if running on HHVM, FALSE otherwise
*/
protected function isRunningHHVM()
{
return defined('HHVM_VERSION');
}
/**
* Returns whether the file at the given location exists
*
* @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml"
* @return bool TRUE if the file exists, FALSE otherwise
*/
protected function fileExistsWithinZip($zipStreamURI)
{
$doesFileExists = false;
$pattern = '/zip:\/\/([^#]+)#(.*)/';
if (preg_match($pattern, $zipStreamURI, $matches)) {
$zipFilePath = $matches[1];
$innerFilePath = $matches[2];
$zip = new \ZipArchive();
if ($zip->open($zipFilePath) === true) {
$doesFileExists = ($zip->locateName($innerFilePath) !== false);
$zip->close();
}
}
return $doesFileExists;
}
/**
* Move to next node in document
* @see \XMLReader::read
*
* @return bool TRUE on success or FALSE on failure
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
*/
public function read()
{
$this->useXMLInternalErrors();
$wasReadSuccessful = parent::read();
$this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
return $wasReadSuccessful;
}
/**
* Move cursor to next node skipping all subtrees
* @see \XMLReader::next
*
* @param string|void $localName The name of the next node to move to
* @return bool TRUE on success or FALSE on failure
* @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
*/
public function next($localName = null)
{
$this->useXMLInternalErrors();
$wasNextSuccessful = parent::next($localName);
$this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
return $wasNextSuccessful;
}
}
-394
View File
@@ -1,394 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\BadUsageException;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\EndOfWorksheetsReachedException;
use Box\Spout\Reader\Exception\NoWorksheetsFoundException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
use Box\Spout\Reader\Helper\XLSX\CellHelper;
use Box\Spout\Reader\Helper\XLSX\SharedStringsHelper;
use Box\Spout\Reader\Helper\XLSX\WorksheetHelper;
/**
* Class XLSX
* This class provides support to read data from a XLSX file
*
* @package Box\Spout\Reader
*/
class XLSX extends AbstractReader
{
const CELL_TYPE_INLINE_STRING = 'inlineStr';
const CELL_TYPE_STR = 'str';
const CELL_TYPE_SHARED_STRING = 's';
const CELL_TYPE_BOOLEAN = 'b';
const CELL_TYPE_NUMERIC = 'n';
const CELL_TYPE_DATE = 'd';
const CELL_TYPE_ERROR = 'e';
/** @var string Real path of the file to read */
protected $filePath;
/** @var string Temporary folder where the temporary files will be created */
protected $tempFolder;
/** @var \ZipArchive */
protected $zip;
/** @var Helper\XLSX\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var Helper\XLSX\WorksheetHelper Helper to work with worksheets */
protected $worksheetHelper;
/** @var Internal\XLSX\Worksheet[] The list of worksheets present in the file */
protected $worksheets;
/** @var Internal\XLSX\Worksheet The worksheet being read */
protected $currentWorksheet;
/** @var \XMLReader The XMLReader object that will help read sheets XML data */
protected $xmlReader;
/** @var int The number of columns the worksheet has (0 meaning undefined) */
protected $numberOfColumns = 0;
/**
* @param string $tempFolder Temporary folder where the temporary files will be created
* @return XLSX
*/
public function setTempFolder($tempFolder)
{
$this->tempFolder = $tempFolder;
return $this;
}
/**
* Opens the file at the given file path to make it ready to be read.
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
* and fetches all the available worksheets.
*
* @param string $filePath Path of the file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
* @throws Exception\NoWorksheetsFoundException If there are no worksheets in the file
*/
protected function openReader($filePath)
{
$this->filePath = $filePath;
$this->zip = new \ZipArchive();
if ($this->zip->open($filePath) === true) {
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
if ($this->sharedStringsHelper->hasSharedStrings()) {
// Extracts all the strings from the worksheets for easy access in the future
$this->sharedStringsHelper->extractSharedStrings();
}
// Fetch all available worksheets
$this->worksheetHelper = new WorksheetHelper($filePath, $this->globalFunctionsHelper);
$this->worksheets = $this->worksheetHelper->getWorksheets($filePath);
if (count($this->worksheets) === 0) {
throw new NoWorksheetsFoundException('The file must contain at least one worksheet.');
}
} else {
throw new IOException('Could not open ' . $filePath . ' for reading.');
}
}
/**
* Returns whether another worksheet exists after the current worksheet.
*
* @return bool Whether another worksheet exists after the current worksheet.
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
*/
public function hasNextSheet()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Stream should be opened first.');
}
return $this->worksheetHelper->hasNextWorksheet($this->currentWorksheet, $this->worksheets);
}
/**
* Moves the pointer to the current worksheet.
* Moving to another worksheet will stop the reading in the current worksheet.
*
* @return \Box\Spout\Reader\Sheet The next sheet
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
* @throws Exception\EndOfWorksheetsReachedException If there is no more worksheets to read
*/
public function nextSheet()
{
if (!$this->hasNextSheet()) {
throw new EndOfWorksheetsReachedException('End of worksheets was reached. Cannot read more worksheets.');
}
if ($this->currentWorksheet === null) {
$nextWorksheet = $this->worksheets[0];
} else {
$currentWorksheetIndex = $this->currentWorksheet->getWorksheetIndex();
$nextWorksheet = $this->worksheets[$currentWorksheetIndex + 1];
}
$this->initXmlReaderForWorksheetData($nextWorksheet);
$this->currentWorksheet = $nextWorksheet;
// make sure that we are ready to read more rows
$this->hasReachedEndOfFile = false;
$this->emptyRowDataBuffer();
return $this->currentWorksheet->getExternalSheet();
}
/**
* Initializes the XMLReader object that reads worksheet data for the given worksheet.
* If another worksheet was being read, it closes the reader before reopening it for the new worksheet.
* The XMLReader is configured to be safe from billion laughs attack.
*
* @param Internal\XLSX\Worksheet $worksheet The worksheet to initialize the XMLReader with
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the worksheet data XML cannot be read
*/
protected function initXmlReaderForWorksheetData($worksheet)
{
// if changing worksheet and the XMLReader was initialized for the current worksheet
if ($worksheet != $this->currentWorksheet && $this->xmlReader) {
$this->xmlReader->close();
} else if (!$this->xmlReader) {
$this->xmlReader = new \XMLReader();
}
$worksheetDataXMLFilePath = $worksheet->getDataXmlFilePath();
$worksheetDataFilePath = 'zip://' . $this->filePath . '#' . $worksheetDataXMLFilePath;
if ($this->xmlReader->open($worksheetDataFilePath, null, LIBXML_NONET) === false) {
throw new IOException('Could not open "' . $worksheetDataXMLFilePath . '".');
}
}
/**
* Reads and returns data of the line that comes after the last read line, on the current worksheet.
* Empty rows will be skipped.
*
* @return array|null Array that contains the data for the read line or null at the end of the file
* @throws \Box\Spout\Common\Exception\BadUsageException If the pointer to the current worksheet has not been set
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
*/
protected function read()
{
if (!$this->currentWorksheet) {
throw new BadUsageException('You must call nextSheet() before calling hasNextRow() or nextRow()');
}
$escaper = new \Box\Spout\Common\Escaper\XLSX();
$isInsideRowTag = false;
$rowData = [];
while ($this->xmlReader->read()) {
if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'dimension') {
// Read dimensions of the worksheet
$dimensionRef = $this->xmlReader->getAttribute('ref'); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
$lastCellIndex = $matches[1];
$this->numberOfColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
}
} else if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'row') {
// Start of the row description
$isInsideRowTag = true;
// Read spans info if present
$numberOfColumnsForRow = $this->numberOfColumns;
$spans = $this->xmlReader->getAttribute('spans'); // returns '1:5' for instance
if ($spans) {
list(, $numberOfColumnsForRow) = explode(':', $spans);
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
}
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
} else if ($isInsideRowTag && $this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'c') {
// Start of a cell description
$currentCellIndex = $this->xmlReader->getAttribute('r');
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
$node = $this->xmlReader->expand();
$rowData[$currentColumnIndex] = $this->getCellValue($node, $escaper);
} else if ($this->xmlReader->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === 'row') {
// End of the row description
// If needed, we fill the empty cells
$rowData = ($this->numberOfColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
break;
}
}
// no data means "end of file"
return ($rowData !== []) ? $rowData : null;
}
/**
* Returns the cell's string value from a node's nested value node
*
* @param \DOMNode $node
* @return string The value associated with the cell
*/
protected function getVNodeValue($node)
{
// for cell types having a "v" tag containing the value.
// if not, the returned value should be empty string.
$vNode = $node->getElementsByTagName('v')->item(0);
if ($vNode !== null) {
return $vNode->nodeValue;
}
return "";
}
/**
* Returns the cell String value where string is inline.
*
* @param \DOMNode $node
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatInlineStringCellValue($node, $escaper)
{
// inline strings are formatted this way:
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
$tNode = $node->getElementsByTagName('t')->item(0);
$escapedCellValue = trim($tNode->nodeValue);
$cellValue = $escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value from shared-strings file using nodeValue index.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatSharedStringCellValue($nodeValue, $escaper)
{
// shared strings are formatted this way:
// <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
$sharedStringIndex = intval($nodeValue);
$escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
$cellValue = $escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value, where string is stored in value node.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatStrCellValue($nodeValue, $escaper)
{
$escapedCellValue = trim($nodeValue);
$cellValue = $escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell Numeric value from string of nodeValue.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return int|float The value associated with the cell
*/
protected function formatNumericCellValue($nodeValue)
{
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
return $cellValue;
}
/**
* Returns the cell Boolean value from a specific node's Value.
*
* @param string $nodeValue
* @return bool The value associated with the cell
*/
protected function formatBooleanCellValue($nodeValue)
{
// !! is similar to boolval()
$cellValue = !!$nodeValue;
return $cellValue;
}
/**
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
*
* @param string $nodeValue
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return DateTime|null The value associated with the cell (null when the cell has an error)
*/
protected function formatDateCellValue($nodeValue)
{
try { // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
$cellValue = new \DateTime($nodeValue);
return $cellValue;
} catch (\Exception $e) {
return null;
}
}
/**
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
*
* @param \DOMNode $node
* @param \Box\Spout\Common\Escaper\XLSX $escaper
* @return string|int|float|bool|null The value associated with the cell (null when the cell has an error)
*/
protected function getCellValue($node, $escaper)
{
// Default cell type is "n"
$cellType = $node->getAttribute('t') ?: 'n';
$vNodeValue = $this->getVNodeValue($node);
if ( ($vNodeValue === "") && ($cellType !== self::CELL_TYPE_INLINE_STRING) ) {
return $vNodeValue;
}
switch ($cellType)
{
case self::CELL_TYPE_INLINE_STRING:
return $this->formatInlineStringCellValue($node, $escaper);
case self::CELL_TYPE_SHARED_STRING:
return $this->formatSharedStringCellValue($vNodeValue, $escaper);
case self::CELL_TYPE_STR:
return $this->formatStrCellValue($vNodeValue, $escaper);
case self::CELL_TYPE_BOOLEAN:
return $this->formatBooleanCellValue($vNodeValue);
case self::CELL_TYPE_NUMERIC:
return $this->formatNumericCellValue($vNodeValue);
case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($vNodeValue);
default:
return null;
}
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->xmlReader) {
$this->xmlReader->close();
}
if ($this->zip) {
$this->zip->close();
}
$this->sharedStringsHelper->cleanup();
}
}
@@ -1,6 +1,6 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Common\Exception\InvalidArgumentException;
@@ -8,7 +8,7 @@ use Box\Spout\Common\Exception\InvalidArgumentException;
* Class CellHelper
* This class provides helper functions when working with cells
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class CellHelper
{
@@ -71,7 +71,7 @@ class CellHelper
{
$currentColumnIndex = ord($single_cell_index) - $capitalAAsciiValue;
if ($columnLength == 1) {
if ($columnLength === 1) {
$columnIndex += $currentColumnIndex;
} else {
$columnIndex += pow($step, ($columnLength - 1)) * ($currentColumnIndex + 1);
@@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
/**
* Class CachingStrategyFactory
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class CachingStrategyFactory
{
@@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
/**
* Interface CachingStrategyInterface
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
interface CachingStrategyInterface
{
@@ -1,6 +1,6 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
use Box\Spout\Common\Helper\FileSystemHelper;
use Box\Spout\Common\Helper\GlobalFunctionsHelper;
@@ -13,7 +13,7 @@ use Box\Spout\Reader\Exception\SharedStringNotFoundException;
* Shared strings are stored in small files (with a max number of strings per file).
* This strategy is slower than an in-memory strategy but is used to avoid out of memory crashes.
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class FileBasedStrategy implements CachingStrategyInterface
{
@@ -26,6 +26,9 @@ class FileBasedStrategy implements CachingStrategyInterface
/** @var \Box\Spout\Common\Helper\FileSystemHelper Helper to perform file system operations */
protected $fileSystemHelper;
/** @var string Temporary folder where the temporary files will be created */
protected $tempFolder;
/**
* @var int Maximum number of strings that can be stored in one temp file
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
@@ -42,7 +45,7 @@ class FileBasedStrategy implements CachingStrategyInterface
protected $inMemoryTempFilePath;
/**
* @var string Contents of the temporary file that was last read
* @var array Contents of the temporary file that was last read
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
*/
protected $inMemoryTempFileContents;
@@ -1,6 +1,7 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
use Box\Spout\Reader\Exception\SharedStringNotFoundException;
/**
@@ -9,7 +10,7 @@ use Box\Spout\Reader\Exception\SharedStringNotFoundException;
* This class implements the in-memory caching strategy for shared strings.
* This strategy is used when the number of unique strings is low, compared to the memory available.
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class InMemoryStrategy implements CachingStrategyInterface
{
@@ -1,16 +1,19 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyInterface;
use Box\Spout\Reader\Exception\XMLProcessingException;
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
use Box\Spout\Reader\Wrapper\XMLReader;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyInterface;
/**
* Class SharedStringsHelper
* This class provides helper functions for reading sharedStrings XML file
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class SharedStringsHelper
{
@@ -58,13 +61,13 @@ class SharedStringsHelper
}
/**
* Builds an in-memory array containing all the shared strings of the worksheet.
* Builds an in-memory array containing all the shared strings of the sheet.
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
* It is then accessed by the worksheet data, via the string index in the built table.
* It is then accessed by the sheet data, via the string index in the built table.
*
* More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx
*
* The XML file can be really big with worksheets containing a lot of data. That is why
* The XML file can be really big with sheets containing a lot of data. That is why
* we need to use a XML reader that provides streaming like the XMLReader library.
* Please note that SimpleXML does not provide such a functionality but since it is faster
* and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose.
@@ -74,7 +77,7 @@ class SharedStringsHelper
*/
public function extractSharedStrings()
{
$xmlReader = new \XMLReader();
$xmlReader = new XMLReader();
$sharedStringIndex = 0;
$escaper = new \Box\Spout\Common\Escaper\XLSX();
@@ -83,39 +86,44 @@ class SharedStringsHelper
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
}
$sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader);
$this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount);
try {
$sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader);
$this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount);
while ($xmlReader->read() && $xmlReader->name !== 'si') {
// do nothing until a 'si' tag is reached
}
while ($xmlReader->name === 'si') {
$node = $this->getSimpleXmlElementNodeFromXMLReader($xmlReader);
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
// removes nodes that should not be read, like the pronunciation of the Kanji characters
$cleanNode = $this->removeSuperfluousTextNodes($node);
// find all text nodes 't'; there can be multiple if the cell contains formatting
$textNodes = $cleanNode->xpath('//ns:t');
$textValue = '';
foreach ($textNodes as $textNode) {
if ($this->shouldPreserveWhitespace($textNode)) {
$textValue .= $textNode->__toString();
} else {
$textValue .= trim($textNode->__toString());
}
while ($xmlReader->read() && $xmlReader->name !== 'si') {
// do nothing until a 'si' tag is reached
}
$unescapedTextValue = $escaper->unescape($textValue);
$this->cachingStrategy->addStringForIndex($unescapedTextValue, $sharedStringIndex);
while ($xmlReader->name === 'si') {
$node = $this->getSimpleXmlElementNodeFromXMLReader($xmlReader);
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
$sharedStringIndex++;
// removes nodes that should not be read, like the pronunciation of the Kanji characters
$cleanNode = $this->removeSuperfluousTextNodes($node);
// jump to the next 'si' tag
$xmlReader->next('si');
// find all text nodes 't'; there can be multiple if the cell contains formatting
$textNodes = $cleanNode->xpath('//ns:t');
$textValue = '';
foreach ($textNodes as $textNode) {
if ($this->shouldPreserveWhitespace($textNode)) {
$textValue .= $textNode->__toString();
} else {
$textValue .= trim($textNode->__toString());
}
}
$unescapedTextValue = $escaper->unescape($textValue);
$this->cachingStrategy->addStringForIndex($unescapedTextValue, $sharedStringIndex);
$sharedStringIndex++;
// jump to the next 'si' tag
$xmlReader->next('si');
}
} catch (XMLProcessingException $exception) {
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
}
$this->cachingStrategy->closeCache();
@@ -134,32 +142,19 @@ class SharedStringsHelper
/**
* Returns the shared strings unique count, as specified in <sst> tag.
*
* @param \XMLReader $xmlReader XMLReader instance
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance
* @return int Number of unique shared strings in the sharedStrings.xml file
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read
*/
protected function getSharedStringsUniqueCount($xmlReader)
{
// Use internal errors to avoid displaying lots of warning messages in case of invalid file
// For instance, if the file is used to perform a "Billion Laughs" or "Quadratic Blowup" attacks
libxml_clear_errors();
libxml_use_internal_errors(true);
$xmlReader->next('sst');
// Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE)
while ($xmlReader->name === 'sst' && $xmlReader->nodeType !== \XMLReader::ELEMENT) {
while ($xmlReader->name === 'sst' && $xmlReader->nodeType !== XMLReader::ELEMENT) {
$xmlReader->read();
}
$readError = libxml_get_last_error();
if ($readError !== false) {
throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$readError->message}]");
}
// reset the setting to display XML warnings/errors
libxml_use_internal_errors(false);
return intval($xmlReader->getAttribute('uniqueCount'));
}
@@ -179,29 +174,19 @@ class SharedStringsHelper
* Returns a SimpleXMLElement node from the current node in the given XMLReader instance.
* This is to simplify the parsing of the subtree.
*
* @param \XMLReader $xmlReader
* @return \SimpleXMLElement
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement
* @throws \Box\Spout\Common\Exception\IOException If the current node cannot be read
*/
protected function getSimpleXmlElementNodeFromXMLReader($xmlReader)
{
// Use internal errors to avoid displaying lots of warning messages in case of error found in the XML node.
// For instance, if the file is used to perform a "Billion Laughs" or "Quadratic Blowup" attacks
libxml_clear_errors();
libxml_use_internal_errors(true);
$node = null;
try {
$node = new \SimpleXMLElement($xmlReader->readOuterXml());
} catch (\Exception $exception) {
$error = libxml_get_last_error();
libxml_use_internal_errors(false);
throw new IOException('The sharedStrings.xml file contains unreadable data [' . trim($error->message) . '].');
$node = new SimpleXMLElement($xmlReader->readOuterXml());
} catch (XMLProcessingException $exception) {
throw new IOException("The sharedStrings.xml file contains unreadable data [{$exception->getMessage()}].");
}
libxml_use_internal_errors(false);
return $node;
}
@@ -209,8 +194,8 @@ class SharedStringsHelper
* Removes nodes that should not be read, like the pronunciation of the Kanji characters.
* By keeping them, their text content would be added to the read string.
*
* @param \SimpleXMLElement $parentNode Parent node that may contain nodes to remove
* @return \SimpleXMLElement Cleaned parent node
* @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $parentNode Parent node that may contain nodes to remove
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement Cleaned parent node
*/
protected function removeSuperfluousTextNodes($parentNode)
{
@@ -220,12 +205,7 @@ class SharedStringsHelper
foreach ($tagsToRemove as $tagToRemove) {
$xpath = '//ns:' . $tagToRemove;
$nodesToRemove = $parentNode->xpath($xpath);
foreach ($nodesToRemove as $nodeToRemove) {
// This is how to remove a node from the XML
unset($nodeToRemove[0]);
}
$parentNode->removeNodesMatchingXPath($xpath);
}
return $parentNode;
@@ -234,24 +214,13 @@ class SharedStringsHelper
/**
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
*
* @param \SimpleXMLElement $textNode The text node element (<t>) whitespace may be preserved
* @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $textNode The text node element (<t>) whitespace may be preserved
* @return bool Whether whitespace should be preserved
*/
protected function shouldPreserveWhitespace($textNode)
{
$shouldPreserveWhitespace = false;
$attributes = $textNode->attributes('xml', true);
if ($attributes) {
foreach ($attributes as $attributeName => $attributeValue) {
if ($attributeName === 'space' && $attributeValue->__toString() === 'preserve') {
$shouldPreserveWhitespace = true;
break;
}
}
}
return $shouldPreserveWhitespace;
$spaceValue = $textNode->getAttribute('space', 'xml');
return ($spaceValue === 'preserve');
}
/**
@@ -1,17 +1,17 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Reader\Internal\XLSX\Worksheet;
use Box\Spout\Reader\Sheet;
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
use Box\Spout\Reader\XLSX\Sheet;
/**
* Class WorksheetHelper
* This class provides helper functions related to XLSX worksheets
* Class SheetHelper
* This class provides helper functions related to XLSX sheets
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class WorksheetHelper
class SheetHelper
{
/** Extension for XML files */
const XML_EXTENSION = '.xml';
@@ -26,58 +26,63 @@ class WorksheetHelper
const MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS = 'http://schemas.openxmlformats.org/package/2006/relationships';
const MAIN_NAMESPACE_FOR_WORKBOOK_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
/** Value of the Override attribute used in [Content_Types].xml to define worksheets */
/** Value of the Override attribute used in [Content_Types].xml to define sheets */
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
/** @var string Path of the XLSX file being read */
protected $filePath;
/** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @var \SimpleXMLElement XML element representing the workbook.xml.rels file */
/** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml.rels file */
protected $workbookXMLRelsAsXMLElement;
/** @var \SimpleXMLElement XML element representing the workbook.xml file */
/** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml file */
protected $workbookXMLAsXMLElement;
/**
* @param string $filePath Path of the XLSX file being read
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
*/
public function __construct($filePath, $globalFunctionsHelper)
public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper)
{
$this->filePath = $filePath;
$this->sharedStringsHelper = $sharedStringsHelper;
$this->globalFunctionsHelper = $globalFunctionsHelper;
}
/**
* Returns the file paths of the worksheet data XML files within the XLSX file.
* The paths are read from the [Content_Types].xml file.
* Returns the sheets metadata of the file located at the previously given file path.
* The paths to the sheets' data are read from the [Content_Types].xml file.
*
* @return Worksheet[] Worksheets within the XLSX file
* @return Sheet[] Sheets within the XLSX file
*/
public function getWorksheets()
public function getSheets()
{
$worksheets = [];
$sheets = [];
$contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace(
self::CONTENT_TYPES_XML_FILE_PATH,
self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML
);
// find all nodes defining a worksheet
// find all nodes defining a sheet
$sheetNodes = $contentTypesAsXMLElement->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
$numSheetNodes = count($sheetNodes);
for ($i = 0; $i < count($sheetNodes); $i++) {
for ($i = 0; $i < $numSheetNodes; $i++) {
$sheetNode = $sheetNodes[$i];
$sheetDataXMLFilePath = (string) $sheetNode->attributes()->PartName;
$sheetDataXMLFilePath = $sheetNode->getAttribute('PartName');
$sheet = $this->getSheet($sheetDataXMLFilePath, $i);
$worksheets[] = new Worksheet($sheet, $i, $sheetDataXMLFilePath);
$sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath, $i);
}
return $worksheets;
return $sheets;
}
/**
@@ -92,9 +97,9 @@ class WorksheetHelper
*
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param int $sheetIndexZeroBased Index of the sheet, based on order in [Content_Types].xml (zero-based)
* @return \Box\Spout\Reader\Sheet Sheet instance
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
*/
protected function getSheet($sheetDataXMLFilePath, $sheetIndexZeroBased)
protected function getSheetFromXML($sheetDataXMLFilePath, $sheetIndexZeroBased)
{
$sheetId = $sheetIndexZeroBased + 1;
$sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath);
@@ -111,29 +116,29 @@ class WorksheetHelper
if (count($relationshipNodes) === 1) {
$relationshipNode = $relationshipNodes[0];
$sheetId = (string) $relationshipNode->attributes()->Id;
$sheetId = $relationshipNode->getAttribute('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;
$sheetId = (int) $sheetNode->getAttribute('sheetId');
$escapedSheetName = $sheetNode->getAttribute('name');
$escaper = new \Box\Spout\Common\Escaper\XLSX();
$sheetName = $escaper->unescape($escapedSheetName);
}
}
return new Sheet($sheetId, $sheetIndexZeroBased, $sheetName);
return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetId, $sheetIndexZeroBased, $sheetName);
}
/**
* Returns the default name of the sheet whose data is located
* at the given path.
*
* @param $sheetDataXMLFilePath
* @param string $sheetDataXMLFilePath Path of the sheet data XML file
* @return string The default sheet name
*/
protected function getDefaultSheetName($sheetDataXMLFilePath)
@@ -145,7 +150,7 @@ class WorksheetHelper
* 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
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file
*/
protected function getWorkbookXMLRelsAsXMLElement()
{
@@ -163,7 +168,7 @@ class WorksheetHelper
* 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
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file
*/
protected function getWorkbookXMLAsXMLElement()
{
@@ -182,28 +187,15 @@ class WorksheetHelper
*
* @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
* @return \Box\Spout\Reader\Wrapper\SimpleXMLElement The XML element representing the file
*/
protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace)
{
$xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $xmlFilePath);
$xmlElement = new \SimpleXMLElement($xmlContents);
$xmlElement = new SimpleXMLElement($xmlContents);
$xmlElement->registerXPathNamespace('ns', $mainNamespace);
return $xmlElement;
}
/**
* Returns whether another worksheet exists after the current worksheet.
* The order is determined by the order of appearance in the [Content_Types].xml file.
*
* @param Worksheet|null $currentWorksheet The worksheet being currently read or null if reading has not started yet
* @param Worksheet[] $allWorksheets A list of all worksheets in the XLSX file. Must contain at least one worksheet
* @return bool Whether another worksheet exists after the current sheet
*/
public function hasNextWorksheet($currentWorksheet, $allWorksheets)
{
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetIndex() + 1 < count($allWorksheets)));
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\AbstractReader;
use Box\Spout\Reader\XLSX\Helper\SharedStringsHelper;
/**
* Class Reader
* This class provides support to read data from a XLSX file
*
* @package Box\Spout\Reader\XLSX
*/
class Reader extends AbstractReader
{
/** @var string Temporary folder where the temporary files will be created */
protected $tempFolder;
/** @var \ZipArchive */
protected $zip;
/** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var SheetIterator To iterator over the XLSX sheets */
protected $sheetIterator;
/**
* @param string $tempFolder Temporary folder where the temporary files will be created
* @return Reader
*/
public function setTempFolder($tempFolder)
{
$this->tempFolder = $tempFolder;
return $this;
}
/**
* Opens the file at the given file path to make it ready to be read.
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
* and fetches all the available sheets.
*
* @param string $filePath Path of the file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
*/
protected function openReader($filePath)
{
$this->zip = new \ZipArchive();
if ($this->zip->open($filePath) === true) {
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
if ($this->sharedStringsHelper->hasSharedStrings()) {
// Extracts all the strings from the sheets for easy access in the future
$this->sharedStringsHelper->extractSharedStrings();
}
$this->sheetIterator = new SheetIterator($filePath, $this->sharedStringsHelper, $this->globalFunctionsHelper);
} else {
throw new IOException("Could not open $filePath for reading.");
}
}
/**
* Returns an iterator to iterate over sheets.
*
* @return SheetIterator To iterate over sheets
*/
public function getConcreteSheetIterator()
{
return $this->sheetIterator;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->zip) {
$this->zip->close();
}
if ($this->sharedStringsHelper) {
$this->sharedStringsHelper->cleanup();
}
}
}
+364
View File
@@ -0,0 +1,364 @@
<?php
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\XMLProcessingException;
use Box\Spout\Reader\IteratorInterface;
use Box\Spout\Reader\Wrapper\XMLReader;
use Box\Spout\Reader\XLSX\Helper\CellHelper;
/**
* Class RowIterator
*
* @package Box\Spout\Reader\XLSX
*/
class RowIterator implements IteratorInterface
{
/** Definition of all possible cell types */
const CELL_TYPE_INLINE_STRING = 'inlineStr';
const CELL_TYPE_STR = 'str';
const CELL_TYPE_SHARED_STRING = 's';
const CELL_TYPE_BOOLEAN = 'b';
const CELL_TYPE_NUMERIC = 'n';
const CELL_TYPE_DATE = 'd';
const CELL_TYPE_ERROR = 'e';
/** Definition of XML nodes names used to parse data */
const XML_NODE_DIMENSION = 'dimension';
const XML_NODE_WORKSHEET = 'worksheet';
const XML_NODE_ROW = 'row';
const XML_NODE_CELL = 'c';
const XML_NODE_VALUE = 'v';
const XML_NODE_INLINE_STRING_VALUE = 't';
/** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_REF = 'ref';
const XML_ATTRIBUTE_SPANS = 'spans';
const XML_ATTRIBUTE_CELL_INDEX = 'r';
const XML_ATTRIBUTE_TYPE = 't';
/** @var string Path of the XLSX file being read */
protected $filePath;
/** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */
protected $sheetDataXMLFilePath;
/** @var Helper\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader;
/** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
protected $escaper;
/** @var int Number of read rows */
protected $numReadRows = 0;
/** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var int The number of columns the sheet has (0 meaning undefined) */
protected $numColumns = 0;
/**
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
*/
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper)
{
$this->filePath = $filePath;
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
$this->sharedStringsHelper = $sharedStringsHelper;
$this->xmlReader = new XMLReader();
$this->escaper = new \Box\Spout\Common\Escaper\XLSX();
}
/**
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @return string Path of the XML file containing the sheet data,
* without the leading slash.
*/
protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
{
return ltrim($sheetDataXMLFilePath, '/');
}
/**
* Rewind the Iterator to the first element.
* Initializes the XMLReader object that reads the associated sheet data.
* The XMLReader is configured to be safe from billion laughs attack.
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
*/
public function rewind()
{
$this->xmlReader->close();
$sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath;
if ($this->xmlReader->open($sheetDataFilePath) === false) {
throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
}
$this->numReadRows = 0;
$this->rowDataBuffer = null;
$this->hasReachedEndOfFile = false;
$this->numColumns = 0;
$this->next();
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return (!$this->hasReachedEndOfFile);
}
/**
* Move forward to next element. Empty rows will be skipped.
* @link http://php.net/manual/en/iterator.next.php
*
* @return void
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
* @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
*/
public function next()
{
$isInsideRowTag = false;
$rowData = [];
try {
while ($this->xmlReader->read()) {
if ($this->xmlReader->nodeType === XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_DIMENSION) {
// Read dimensions of the sheet
$dimensionRef = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
$lastCellIndex = $matches[1];
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
}
} else if ($this->xmlReader->nodeType === XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) {
// Start of the row description
$isInsideRowTag = true;
// Read spans info if present
$numberOfColumnsForRow = $this->numColumns;
$spans = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
if ($spans) {
list(, $numberOfColumnsForRow) = explode(':', $spans);
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
}
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
} else if ($isInsideRowTag && $this->xmlReader->nodeType === XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_CELL) {
// Start of a cell description
$currentCellIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
$node = $this->xmlReader->expand();
$rowData[$currentColumnIndex] = $this->getCellValue($node);
} else if ($this->xmlReader->nodeType === XMLReader::END_ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) {
// End of the row description
// If needed, we fill the empty cells
$rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
$this->numReadRows++;
break;
} else if ($this->xmlReader->nodeType === XMLReader::END_ELEMENT && $this->xmlReader->name === self::XML_NODE_WORKSHEET) {
// The closing "</worksheet>" marks the end of the file
$this->hasReachedEndOfFile = true;
}
}
} catch (XMLProcessingException $exception) {
throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
}
$this->rowDataBuffer = $rowData;
}
/**
* Returns the cell's string value from a node's nested value node
*
* @param \DOMNode $node
* @return string The value associated with the cell
*/
protected function getVNodeValue($node)
{
// for cell types having a "v" tag containing the value.
// if not, the returned value should be empty string.
$vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0);
if ($vNode !== null) {
return $vNode->nodeValue;
}
return "";
}
/**
* Returns the cell String value where string is inline.
*
* @param \DOMNode $node
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatInlineStringCellValue($node)
{
// inline strings are formatted this way:
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
$escapedCellValue = trim($tNode->nodeValue);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value from shared-strings file using nodeValue index.
*
* @param string $nodeValue
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatSharedStringCellValue($nodeValue)
{
// shared strings are formatted this way:
// <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
$sharedStringIndex = intval($nodeValue);
$escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell String value, where string is stored in value node.
*
* @param string $nodeValue
* @return string The value associated with the cell (null when the cell has an error)
*/
protected function formatStrCellValue($nodeValue)
{
$escapedCellValue = trim($nodeValue);
$cellValue = $this->escaper->unescape($escapedCellValue);
return $cellValue;
}
/**
* Returns the cell Numeric value from string of nodeValue.
*
* @param string $nodeValue
* @return int|float The value associated with the cell
*/
protected function formatNumericCellValue($nodeValue)
{
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
return $cellValue;
}
/**
* Returns the cell Boolean value from a specific node's Value.
*
* @param string $nodeValue
* @return bool The value associated with the cell
*/
protected function formatBooleanCellValue($nodeValue)
{
// !! is similar to boolval()
$cellValue = !!$nodeValue;
return $cellValue;
}
/**
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
*
* @param string $nodeValue
* @return \DateTime|null The value associated with the cell (null when the cell has an error)
*/
protected function formatDateCellValue($nodeValue)
{
// Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
try {
$cellValue = new \DateTime($nodeValue);
return $cellValue;
} catch (\Exception $e) {
return null;
}
}
/**
* Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
*
* @param \DOMNode $node
* @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
*/
protected function getCellValue($node)
{
// Default cell type is "n"
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
$vNodeValue = $this->getVNodeValue($node);
if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
return $vNodeValue;
}
switch ($cellType) {
case self::CELL_TYPE_INLINE_STRING:
return $this->formatInlineStringCellValue($node);
case self::CELL_TYPE_SHARED_STRING:
return $this->formatSharedStringCellValue($vNodeValue);
case self::CELL_TYPE_STR:
return $this->formatStrCellValue($vNodeValue);
case self::CELL_TYPE_BOOLEAN:
return $this->formatBooleanCellValue($vNodeValue);
case self::CELL_TYPE_NUMERIC:
return $this->formatNumericCellValue($vNodeValue);
case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($vNodeValue);
default:
return null;
}
}
/**
* Return the current element, from the buffer.
* @link http://php.net/manual/en/iterator.current.php
*
* @return array|null
*/
public function current()
{
return $this->rowDataBuffer;
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->numReadRows;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
$this->xmlReader->close();
}
}
@@ -1,15 +1,20 @@
<?php
namespace Box\Spout\Reader;
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Reader\SheetInterface;
/**
* Class Sheet
* Represents a worksheet within a XLSX file
* Represents a sheet within a XLSX file
*
* @package Box\Spout\Reader
* @package Box\Spout\Reader\XLSX
*/
class Sheet
class Sheet implements SheetInterface
{
/** @var RowIterator To iterate over sheet's rows */
protected $rowIterator;
/** @var int ID of the sheet */
protected $id;
@@ -20,17 +25,29 @@ class Sheet
protected $name;
/**
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param Helper\SharedStringsHelper Helper to work with shared strings
* @param int $sheetId ID of the sheet
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
* @param string $sheetName Name of the sheet
*/
function __construct($sheetId, $sheetIndex, $sheetName)
public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetId, $sheetIndex, $sheetName)
{
$this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper);
$this->id = $sheetId;
$this->index = $sheetIndex;
$this->name = $sheetName;
}
/**
* @return RowIterator
*/
public function getRowIterator()
{
return $this->rowIterator;
}
/**
* @return int ID of the sheet
*/
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Reader\IteratorInterface;
use Box\Spout\Reader\XLSX\Helper\SheetHelper;
use Box\Spout\Reader\Exception\NoSheetsFoundException;
/**
* Class SheetIterator
* Iterate over XLSX sheet.
*
* @package Box\Spout\Reader\XLSX
*/
class SheetIterator implements IteratorInterface
{
/** @var Sheet[] The list of sheet present in the file */
protected $sheets;
/** @var int The index of the sheet being read (zero-based) */
protected $currentSheetIndex;
/**
* @param string $filePath Path of the file to be read
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper $sharedStringsHelper
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
*/
public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper)
{
// Fetch all available sheets
$sheetHelper = new SheetHelper($filePath, $sharedStringsHelper, $globalFunctionsHelper);
$this->sheets = $sheetHelper->getSheets();
if (count($this->sheets) === 0) {
throw new NoSheetsFoundException('The file must contain at least one sheet.');
}
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
*
* @return void
*/
public function rewind()
{
$this->currentSheetIndex = 0;
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
*
* @return boolean
*/
public function valid()
{
return ($this->currentSheetIndex < count($this->sheets));
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
*
* @return void
*/
public function next()
{
if (array_key_exists($this->currentSheetIndex, $this->sheets)) {
$currentSheet = $this->sheets[$this->currentSheetIndex];
$currentSheet->getRowIterator()->end();
$this->currentSheetIndex++;
}
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
*
* @return Sheet
*/
public function current()
{
return $this->sheets[$this->currentSheetIndex];
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
*
* @return int
*/
public function key()
{
return $this->currentSheetIndex + 1;
}
/**
* Cleans up what was created to iterate over the object.
*
* @return void
*/
public function end()
{
// make sure we are not leaking memory in case the iteration stopped before the end
foreach ($this->sheets as $sheet) {
$sheet->getRowIterator()->end();
}
}
}
+106 -10
View File
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Exception\InvalidArgumentException;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\Style\StyleBuilder;
/**
* Class AbstractWriter
@@ -26,6 +27,12 @@ abstract class AbstractWriter implements WriterInterface
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @var Style\Style Style to be applied to the next written row(s) */
protected $rowStyle;
/** @var Style\Style Default row style. Each writer can have its own default style */
protected $defaultRowStyle;
/** @var string Content-Type value for the header - to be defined by child class */
protected static $headerContentType;
@@ -39,13 +46,13 @@ abstract class AbstractWriter implements WriterInterface
/**
* Adds data to the currently openned writer.
* The data must be UTF-8 encoded.
*
* @param array $dataRow Array containing data to be streamed.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @param Style\Style $style Style to be applied to the written row
* @return void
*/
abstract protected function addRowToWriter(array $dataRow);
abstract protected function addRowToWriter(array $dataRow, $style);
/**
* Closes the streamer, preventing any additional writing.
@@ -55,7 +62,16 @@ abstract class AbstractWriter implements WriterInterface
abstract protected function closeWriter();
/**
* @param $globalFunctionsHelper
*
*/
public function __construct()
{
$this->defaultRowStyle = $this->getDefaultRowStyle();
$this->resetRowStyleToDefault();
}
/**
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
* @return AbstractWriter
*/
public function setGlobalFunctionsHelper($globalFunctionsHelper)
@@ -69,7 +85,7 @@ abstract class AbstractWriter implements WriterInterface
* By using this method, the data will be written to a file.
*
* @param string $outputFilePath Path of the output file that will contain the data
* @return \Box\Spout\Writer\AbstractWriter
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
*/
public function openToFile($outputFilePath)
@@ -92,7 +108,7 @@ abstract class AbstractWriter implements WriterInterface
* @codeCoverageIgnore
*
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
* @return \Box\Spout\Writer\AbstractWriter
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
*/
public function openToBrowser($outputFileName)
@@ -138,13 +154,12 @@ abstract class AbstractWriter implements WriterInterface
/**
* 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.
* If empty, no data is added (i.e. not even as a blank row)
* Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
*
* @return \Box\Spout\Writer\AbstractWriter
* @return AbstractWriter
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
@@ -153,7 +168,7 @@ abstract class AbstractWriter implements WriterInterface
if ($this->isWriterOpened) {
// empty $dataRow should not add an empty line
if (!empty($dataRow)) {
$this->addRowToWriter($dataRow);
$this->addRowToWriter($dataRow, $this->rowStyle);
}
} else {
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
@@ -162,9 +177,32 @@ abstract class AbstractWriter implements WriterInterface
return $this;
}
/**
* Write given data to the output and apply the given style.
* @see addRow
*
* @param array $dataRow Array of array containing data to be streamed.
* @param Style\Style $style Style to be applied to the row.
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRowWithStyle(array $dataRow, $style)
{
if (!$style instanceof Style\Style) {
throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
}
$this->setRowStyle($style);
$this->addRow($dataRow);
$this->resetRowStyleToDefault();
return $this;
}
/**
* 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.
* If a row is empty, it won't be added (i.e. not even as a blank row)
@@ -173,7 +211,7 @@ abstract class AbstractWriter implements WriterInterface
* ['data21', 'data22', null, false],
* ];
*
* @return \Box\Spout\Writer\AbstractWriter
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
@@ -193,6 +231,64 @@ abstract class AbstractWriter implements WriterInterface
return $this;
}
/**
* Write given data to the output and apply the given style.
* @see addRows
*
* @param array $dataRows Array of array containing data to be streamed.
* @param Style\Style $style Style to be applied to the rows.
* @return AbstractWriter
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRowsWithStyle(array $dataRows, $style)
{
if (!$style instanceof Style\Style) {
throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
}
$this->setRowStyle($style);
$this->addRows($dataRows);
$this->resetRowStyleToDefault();
return $this;
}
/**
* Returns the default style to be applied to rows.
* Can be overriden by children to have a custom style.
*
* @return Style\Style
*/
protected function getDefaultRowStyle()
{
return (new StyleBuilder())->build();
}
/**
* Sets the style to be applied to the next written rows
* until it is changed or reset.
*
* @param Style\Style $style
* @return void
*/
private function setRowStyle($style)
{
// Merge given style with the default one to inherit custom properties
$this->rowStyle = $style->mergeWith($this->defaultRowStyle);
}
/**
* Resets the style to be applied to the next written rows.
*
* @return void
*/
private function resetRowStyleToDefault()
{
$this->rowStyle = $this->defaultRowStyle;
}
/**
* Closes the writer. This will close the streamer as well, preventing new data
* to be written to the file.
@@ -1,20 +1,21 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\CSV;
use Box\Spout\Writer\AbstractWriter;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\EncodingHelper;
/**
* Class CSV
* Class Writer
* This class provides support to write data to CSV files
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\CSV
*/
class CSV extends AbstractWriter
class Writer extends AbstractWriter
{
/** Number of rows to write before flushing */
const FLUSH_THRESHOLD = 500;
const UTF8_BOM = "\xEF\xBB\xBF";
/** @var string Content-Type value for the header */
protected static $headerContentType = 'text/csv; charset=UTF-8';
@@ -32,7 +33,7 @@ class CSV extends AbstractWriter
* Sets the field delimiter for the CSV
*
* @param string $fieldDelimiter Character that delimits fields
* @return CSV
* @return Writer
*/
public function setFieldDelimiter($fieldDelimiter)
{
@@ -44,7 +45,7 @@ class CSV extends AbstractWriter
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return CSV
* @return Writer
*/
public function setFieldEnclosure($fieldEnclosure)
{
@@ -60,7 +61,7 @@ class CSV extends AbstractWriter
protected function openWriter()
{
// Adds UTF-8 BOM for Unicode compatibility
$this->globalFunctionsHelper->fputs($this->filePointer, self::UTF8_BOM);
$this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8);
}
/**
@@ -68,10 +69,11 @@ class CSV extends AbstractWriter
*
* @param array $dataRow Array containing data to be written.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @param \Box\Spout\Writer\Style\Style $style Ignored here since CSV does not support styling.
* @return void
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
protected function addRowToWriter(array $dataRow)
protected function addRowToWriter(array $dataRow, $style)
{
$wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
if ($wasWriteSuccessful === false) {
+277
View File
@@ -0,0 +1,277 @@
<?php
namespace Box\Spout\Writer\Style;
/**
* Class Style
* Represents a style to be applied to a cell
*
* @package Box\Spout\Writer\Style
*/
class Style
{
/** Default font values */
const DEFAULT_FONT_SIZE = 11;
const DEFAULT_FONT_NAME = 'Arial';
/** @var int|null Style ID */
protected $id = null;
/** @var bool Whether the font should be bold */
protected $fontBold = false;
/** @var bool Whether the bold property was set */
protected $hasSetFontBold = false;
/** @var bool Whether the font should be italic */
protected $fontItalic = false;
/** @var bool Whether the italic property was set */
protected $hasSetFontItalic = false;
/** @var bool Whether the font should be underlined */
protected $fontUnderline = false;
/** @var bool Whether the underline property was set */
protected $hasSetFontUnderline = false;
/** @var bool Whether the font should be struck through */
protected $fontStrikethrough = false;
/** @var bool Whether the strikethrough property was set */
protected $hasSetFontStrikethrough = false;
/** @var int Font size */
protected $fontSize = self::DEFAULT_FONT_SIZE;
/** @var bool Whether the font size property was set */
protected $hasSetFontSize = false;
/** @var string Font name */
protected $fontName = self::DEFAULT_FONT_NAME;
/** @var bool Whether the font name property was set */
protected $hasSetFontName = false;
/** @var bool Whether specific font properties should be applied */
protected $shouldApplyFont = false;
/** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */
protected $shouldWrapText = false;
/** @var bool Whether the wrap text property was set */
protected $hasSetWrapText = false;
/**
* @return int|null
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
* @return Style
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return boolean
*/
public function isFontBold()
{
return $this->fontBold;
}
/**
* @return Style
*/
public function setFontBold()
{
$this->fontBold = true;
$this->hasSetFontBold = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function isFontItalic()
{
return $this->fontItalic;
}
/**
* @return Style
*/
public function setFontItalic()
{
$this->fontItalic = true;
$this->hasSetFontItalic = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function isFontUnderline()
{
return $this->fontUnderline;
}
/**
* @return Style
*/
public function setFontUnderline()
{
$this->fontUnderline = true;
$this->hasSetFontUnderline = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function isFontStrikethrough()
{
return $this->fontStrikethrough;
}
/**
* @return Style
*/
public function setFontStrikethrough()
{
$this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return int
*/
public function getFontSize()
{
return $this->fontSize;
}
/**
* @param int $fontSize Font size, in pixels
* @return Style
*/
public function setFontSize($fontSize)
{
$this->fontSize = $fontSize;
$this->hasSetFontSize = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return string
*/
public function getFontName()
{
return $this->fontName;
}
/**
* @param string $fontName Name of the font to use
* @return Style
*/
public function setFontName($fontName)
{
$this->fontName = $fontName;
$this->hasSetFontName = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function shouldWrapText()
{
return $this->shouldWrapText;
}
/**
* @return Style
*/
public function setShouldWrapText()
{
$this->shouldWrapText = true;
$this->hasSetWrapText = true;
return $this;
}
/**
* @return bool Whether specific font properties should be applied
*/
public function shouldApplyFont()
{
return $this->shouldApplyFont;
}
/**
* Serializes the style for future comparison with other styles.
* The ID is excluded from the comparison, as we only care about
* actual style properties.
*
* @return string The serialized style
*/
public function serialize()
{
// In order to be able to properly compare style, set static ID value
$currentId = $this->id;
$this->setId(0);
$serializedStyle = serialize($this);
$this->setId($currentId);
return $serializedStyle;
}
/**
* Merges the current style with the given style, using the given style as a base. This means that:
* - if current style and base style both have property A set, use current style property's value
* - if current style has property A set but base style does not, use current style property's value
* - if base style has property A set but current style does not, use base style property's value
*
* @NOTE: This function returns a new style.
*
* @param Style $baseStyle
* @return Style New style corresponding to the merge of the 2 styles
*/
public function mergeWith($baseStyle)
{
$mergedStyle = clone $this;
if (!$this->hasSetFontBold && $baseStyle->isFontBold()) {
$mergedStyle->setFontBold();
}
if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) {
$mergedStyle->setFontItalic();
}
if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) {
$mergedStyle->setFontUnderline();
}
if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) {
$mergedStyle->setFontStrikethrough();
}
if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
$mergedStyle->setFontSize($baseStyle->getFontSize());
}
if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
$mergedStyle->setFontName($baseStyle->getFontName());
}
if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) {
$mergedStyle->setShouldWrapText();
}
return $mergedStyle;
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace Box\Spout\Writer\Style;
/**
* Class StyleBuilder
* Builder to create new styles
*
* @package Box\Spout\Writer\Style
*/
class StyleBuilder
{
/** @var Style Style to be created */
protected $style;
/**
*
*/
public function __construct()
{
$this->style = new Style();
}
/**
* Makes the font bold.
*
* @return StyleBuilder
*/
public function setFontBold()
{
$this->style->setFontBold();
return $this;
}
/**
* Makes the font italic.
*
* @return StyleBuilder
*/
public function setFontItalic()
{
$this->style->setFontItalic();
return $this;
}
/**
* Makes the font underlined.
*
* @return StyleBuilder
*/
public function setFontUnderline()
{
$this->style->setFontUnderline();
return $this;
}
/**
* Makes the font struck through.
*
* @return StyleBuilder
*/
public function setFontStrikethrough()
{
$this->style->setFontStrikethrough();
return $this;
}
/**
* Sets the font size.
*
* @param int $fontSize Font size, in pixels
* @return StyleBuilder
*/
public function setFontSize($fontSize)
{
$this->style->setFontSize($fontSize);
return $this;
}
/**
* Sets the font name.
*
* @param string $fontName Name of the font to use
* @return StyleBuilder
*/
public function setFontName($fontName)
{
$this->style->setFontName($fontName);
return $this;
}
/**
* Makes the text wrap in the cell if it's too long or
* on multiple lines.
*
* @return StyleBuilder
*/
public function setShouldWrapText()
{
$this->style->setShouldWrapText();
return $this;
}
/**
* Returns the configured style. The style is cached and can be reused.
*
* @return Style
*/
public function build()
{
return $this->style;
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ class WriterFactory
* This creates an instance of the appropriate writer, given the type of the file to be read
*
* @param string $writerType Type of the writer to instantiate
* @return \Box\Spout\Writer\CSV|\Box\Spout\Writer\XLSX
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
*/
public static function create($writerType)
@@ -28,10 +28,10 @@ class WriterFactory
switch ($writerType) {
case Type::CSV:
$writer = new CSV();
$writer = new CSV\Writer();
break;
case Type::XLSX:
$writer = new XLSX();
$writer = new XLSX\Writer();
break;
default:
throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType);
+30 -4
View File
@@ -14,7 +14,7 @@ interface WriterInterface
* By using this method, the data will be written to a file.
*
* @param string $outputFilePath Path of the output file that will contain the data
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
*/
public function openToFile($outputFilePath);
@@ -24,7 +24,7 @@ interface WriterInterface
* By using this method, the data will be outputted directly to the browser.
*
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
*/
public function openToBrowser($outputFileName);
@@ -34,12 +34,25 @@ interface WriterInterface
*
* @param array $dataRow Array containing data to be streamed.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yetthe writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRow(array $dataRow);
/**
* Write given data to the output and apply the given style.
* @see addRow
*
* @param array $dataRow Array of array containing data to be streamed.
* @param Style\Style $style Style to be applied to the row.
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRowWithStyle(array $dataRow, $style);
/**
* Write given data to the output. New data will be appended to end of stream.
*
@@ -48,13 +61,26 @@ interface WriterInterface
* ['data11', 12, , '', 'data13'],
* ['data21', 'data22', null],
* ];
* @return \Box\Spout\Writer\WriterInterface
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRows(array $dataRows);
/**
* Write given data to the output and apply the given style.
* @see addRows
*
* @param array $dataRows Array of array containing data to be streamed.
* @param Style\Style $style Style to be applied to the rows.
* @return WriterInterface
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRowsWithStyle(array $dataRows, $style);
/**
* Closes the writer. This will close the streamer as well, preventing new data
* to be written to the file.
@@ -1,12 +1,12 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class CellHelper
* This class provides helper functions when working with cells
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class CellHelper
{
@@ -1,15 +1,15 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
use Box\Spout\Writer\Internal\XLSX\Worksheet;
use Box\Spout\Writer\XLSX\Internal\Worksheet;
/**
* Class FileSystemHelper
* This class provides helper functions to help with the file system operations
* like files/folders creation & deletion for XLSX files
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
{
@@ -26,6 +26,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml';
const WORKBOOK_XML_FILE_NAME = 'workbook.xml';
const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels';
const STYLES_XML_FILE_NAME = 'styles.xml';
/** @var string Path to the root folder inside the temp folder where the files to create the XLSX will be stored */
protected $rootFolder;
@@ -256,6 +257,7 @@ EOD;
}
$contentTypesXmlFileContents .= <<<EOD
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/>
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>
<Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>
<Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>
@@ -312,6 +314,7 @@ EOD;
$workbookRelsXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rIdStyles" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/>
<Relationship Id="rIdSharedStrings" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"/>
EOD;
@@ -329,6 +332,20 @@ EOD;
return $this;
}
/**
* Creates the "styles.xml" file under the "xl" folder
*
* @param StyleHelper $styleHelper
* @return FileSystemHelper
*/
public function createStylesFile($styleHelper)
{
$stylesXmlFileContents = $styleHelper->getStylesXMLFileContent();
$this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
return $this;
}
/**
* Zips the root folder and streams the contents of the zip into the given stream
*
@@ -1,6 +1,6 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
use Box\Spout\Common\Exception\IOException;
@@ -8,7 +8,7 @@ use Box\Spout\Common\Exception\IOException;
* Class SharedStringsHelper
* This class provides helper functions to write shared strings
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class SharedStringsHelper
{
@@ -25,6 +25,9 @@ EOD;
*/
const DEFAULT_STRINGS_COUNT_PART = 'count="9999999999999" uniqueCount="9999999999999"';
/** @var resource Pointer to the sharedStrings.xml file */
protected $sharedStringsFilePointer;
/** @var int Number of shared strings already written */
protected $numSharedStrings = 0;
@@ -0,0 +1,275 @@
<?php
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class StyleHelper
* This class provides helper functions to manage styles
*
* @package Box\Spout\Writer\XLSX\Helper
*/
class StyleHelper
{
/** @var array [SERIALIZED_STYLE] => [STYLE_ID] mapping table, keeping track of the registered styles */
protected $serializedStyleToStyleIdMappingTable = [];
/** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */
protected $styleIdToStyleMappingTable = [];
/**
* @param \Box\Spout\Writer\Style\Style $defaultStyle
*/
public function __construct($defaultStyle)
{
// This ensures that the default style is the first one to be registered
$this->registerStyle($defaultStyle);
}
/**
* Registers the given style as a used style.
* Duplicate styles won't be registered more than once.
*
* @param \Box\Spout\Writer\Style\Style $style The style to be registered
* @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID.
*/
public function registerStyle($style)
{
$serializedStyle = $style->serialize();
if (!$this->hasStyleAlreadyBeenRegistered($style)) {
$nextStyleId = count($this->serializedStyleToStyleIdMappingTable);
$style->setId($nextStyleId);
$this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
$this->styleIdToStyleMappingTable[$nextStyleId] = $style;
}
return $this->getStyleFromSerializedStyle($serializedStyle);
}
/**
* Returns whether the given style has already been registered.
*
* @param \Box\Spout\Writer\Style\Style $style
* @return bool
*/
protected function hasStyleAlreadyBeenRegistered($style)
{
$serializedStyle = $style->serialize();
return array_key_exists($serializedStyle, $this->serializedStyleToStyleIdMappingTable);
}
/**
* Returns the registered style associated to the given serialization.
*
* @param string $serializedStyle The serialized style from which the actual style should be fetched from
* @return \Box\Spout\Writer\Style\Style
*/
protected function getStyleFromSerializedStyle($serializedStyle)
{
$styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle];
return $this->styleIdToStyleMappingTable[$styleId];
}
/**
* Apply additional styles if the given row needs it.
* Typically, set "wrap text" if a cell contains a new line.
*
* @param \Box\Spout\Writer\Style\Style $style The original style
* @param array $dataRow The row the style will be applied to
* @return \Box\Spout\Writer\Style\Style The updated style
*/
public function applyExtraStylesIfNeeded($style, $dataRow)
{
$updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow);
return $updatedStyle;
}
/**
* Set the "wrap text" option if a cell of the given row contains a new line.
*
* @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines
* are ignored even when the "wrap text" option is set. This only occurs with
* inline strings (shared strings do work fine).
* A workaround would be to encode "\n" as "_x000D_" but it does not work
* on the Windows version of Excel...
*
* @param \Box\Spout\Writer\Style\Style $style The original style
* @param array $dataRow The row the style will be applied to
* @return \Box\Spout\Writer\Style\Style The eventually updated style
*/
protected function applyWrapTextIfCellContainsNewLine($style, $dataRow)
{
// if the "wrap text" option is already set, no-op
if ($style->shouldWrapText()) {
return $style;
}
foreach ($dataRow as $cell) {
if (is_string($cell) && strpos($cell, "\n") !== false) {
$style->setShouldWrapText();
break;
}
}
return $style;
}
/**
* Returns the content of the "styles.xml" file, given a list of styles.
* @return string
*/
public function getStylesXMLFileContent()
{
$content = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
EOD;
$content .= $this->getFontsSectionContent();
$content .= $this->getFillsSectionContent();
$content .= $this->getBordersSectionContent();
$content .= $this->getCellStyleXfsSectionContent();
$content .= $this->getCellXfsSectionContent();
$content .= $this->getCellStylesSectionContent();
$content .= <<<EOD
</styleSheet>
EOD;
return $content;
}
/**
* Returns the content of the "<fonts>" section.
* @return string
*/
protected function getFontsSectionContent()
{
$content = ' <fonts count="' . count($this->styleIdToStyleMappingTable) . '">' . PHP_EOL;
foreach ($this->styleIdToStyleMappingTable as $style) {
$content .= ' <font>' . PHP_EOL;
if ($style->isFontBold()) {
$content .= ' <b/>' . PHP_EOL;
}
if ($style->isFontItalic()) {
$content .= ' <i/>' . PHP_EOL;
}
if ($style->isFontUnderline()) {
$content .= ' <u/>' . PHP_EOL;
}
if ($style->isFontStrikethrough()) {
$content .= ' <strike/>' . PHP_EOL;
}
$content .= ' <sz val="' . $style->getFontSize() . '"/>' . PHP_EOL;
$content .= ' <name val="' . $style->getFontName() . '"/>' . PHP_EOL;
$content .= ' </font>' . PHP_EOL;
}
$content .= ' </fonts>' . PHP_EOL;
return $content;
}
/**
* Returns the content of the "<fills>" section.
*
* @return string
*/
protected function getFillsSectionContent()
{
return <<<EOD
<fills count="1">
<fill>
<patternFill patternType="none"/>
</fill>
</fills>
EOD;
}
/**
* Returns the content of the "<borders>" section.
*
* @return string
*/
protected function getBordersSectionContent()
{
return <<<EOD
<borders count="1">
<border>
<left/>
<right/>
<top/>
<bottom/>
<diagonal/>
</border>
</borders>
EOD;
}
/**
* Returns the content of the "<cellStyleXfs>" section.
*
* @return string
*/
protected function getCellStyleXfsSectionContent()
{
return <<<EOD
<cellStyleXfs count="1">
<xf borderId="0" fillId="0" fontId="0" numFmtId="0"/>
</cellStyleXfs>
EOD;
}
/**
* Returns the content of the "<cellXfs>" section.
* @return string
*/
protected function getCellXfsSectionContent()
{
$content = ' <cellXfs count="' . count($this->styleIdToStyleMappingTable) . '">' . PHP_EOL;
foreach ($this->styleIdToStyleMappingTable as $styleId => $style) {
$content .= ' <xf numFmtId="0" fontId="' . $styleId . '" fillId="0" borderId="0" xfId="0"';
if ($style->shouldApplyFont()) {
$content .= ' applyFont="1"';
}
if ($style->shouldWrapText()) {
$content .= ' applyAlignment="1">' . PHP_EOL;
$content .= ' <alignment wrapText="1"/>' . PHP_EOL;
$content .= ' </xf>' . PHP_EOL;
} else {
$content .= '/>' . PHP_EOL;
}
}
$content .= ' </cellXfs>' . PHP_EOL;
return $content;
}
/**
* Returns the content of the "<cellStyles>" section.
*
* @return string
*/
protected function getCellStylesSectionContent()
{
return <<<EOD
<cellStyles count="1">
<cellStyle builtinId="0" name="Normal" xfId="0"/>
</cellStyles>
EOD;
}
}
@@ -1,12 +1,12 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class ZipHelper
* This class provides helper functions to create zip files
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class ZipHelper
{
@@ -1,18 +1,19 @@
<?php
namespace Box\Spout\Writer\Internal\XLSX;
namespace Box\Spout\Writer\XLSX\Internal;
use Box\Spout\Writer\Exception\SheetNotFoundException;
use Box\Spout\Writer\Helper\XLSX\FileSystemHelper;
use Box\Spout\Writer\Helper\XLSX\SharedStringsHelper;
use Box\Spout\Writer\Sheet;
use Box\Spout\Writer\XLSX\Helper\FileSystemHelper;
use Box\Spout\Writer\XLSX\Helper\SharedStringsHelper;
use Box\Spout\Writer\XLSX\Helper\StyleHelper;
use Box\Spout\Writer\XLSX\Sheet;
/**
* Class Book
* Represents a workbook within a XLSX file.
* It provides the functions to work with worksheets.
*
* @package Box\Spout\Writer\Internal\XLSX
* @package Box\Spout\Writer\XLSX\Internal
*/
class Workbook
{
@@ -28,12 +29,15 @@ class Workbook
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
protected $shouldCreateNewSheetsAutomatically;
/** @var \Box\Spout\Writer\Helper\XLSX\FileSystemHelper Helper to perform file system operations */
/** @var \Box\Spout\Writer\XLSX\Helper\FileSystemHelper Helper to perform file system operations */
protected $fileSystemHelper;
/** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
protected $sharedStringsHelper;
/** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles */
protected $styleHelper;
/** @var Worksheet[] Array containing the workbook's sheets */
protected $worksheets = [];
@@ -44,9 +48,10 @@ class Workbook
* @param string $tempFolder
* @param bool $shouldUseInlineStrings
* @param bool $shouldCreateNewSheetsAutomatically
* @param \Box\Spout\Writer\Style\Style $defaultRowStyle
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
*/
public function __construct($tempFolder, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically)
public function __construct($tempFolder, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically, $defaultRowStyle)
{
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
@@ -54,6 +59,8 @@ class Workbook
$this->fileSystemHelper = new FileSystemHelper($tempFolder);
$this->fileSystemHelper->createBaseFilesAndFolders();
$this->styleHelper = new StyleHelper($defaultRowStyle);
// This helper will be shared by all sheets
$xlFolder = $this->fileSystemHelper->getXlFolder();
$this->sharedStringsHelper = new SharedStringsHelper($xlFolder);
@@ -114,7 +121,7 @@ class Workbook
* Sets the given sheet as the current one. New data will be written to this sheet.
* The writing will resume where it stopped (i.e. data won't be truncated).
*
* @param \Box\Spout\Writer\Sheet $sheet The "external" sheet to set as current
* @param \Box\Spout\Writer\XLSX\Sheet $sheet The "external" sheet to set as current
* @return void
* @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
*/
@@ -140,7 +147,7 @@ class Workbook
/**
* Returns the worksheet associated to the given external sheet.
*
* @param \Box\Spout\Writer\Sheet $sheet
* @param \Box\Spout\Writer\XLSX\Sheet $sheet
* @return Worksheet|null The worksheet associated to the given external sheet or null if not found.
*/
protected function getWorksheetFromExternalSheet($sheet)
@@ -148,7 +155,7 @@ class Workbook
$worksheetFound = null;
foreach ($this->worksheets as $worksheet) {
if ($worksheet->getExternalSheet() == $sheet) {
if ($worksheet->getExternalSheet() === $sheet) {
$worksheetFound = $worksheet;
break;
}
@@ -164,11 +171,12 @@ class Workbook
*
* @param array $dataRow Array containing data to be written.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
* @return void
* @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing
* @throws \Box\Spout\Writer\Exception\WriterException If unable to write data
*/
public function addRowToCurrentWorksheet($dataRow)
public function addRowToCurrentWorksheet($dataRow, $style)
{
$currentWorksheet = $this->getCurrentWorksheet();
$hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows();
@@ -178,12 +186,17 @@ class Workbook
// ... continue writing in a new sheet if option set
if ($this->shouldCreateNewSheetsAutomatically) {
$currentWorksheet = $this->addNewSheetAndMakeItCurrent();
$currentWorksheet->addRow($dataRow);
$updatedStyle = $this->styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
$registeredStyle = $this->styleHelper->registerStyle($updatedStyle);
$currentWorksheet->addRow($dataRow, $registeredStyle);
} else {
// otherwise, do nothing as the data won't be read anyways
}
} else {
$currentWorksheet->addRow($dataRow);
$updatedStyle = $this->styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
$registeredStyle = $this->styleHelper->registerStyle($updatedStyle);
$currentWorksheet->addRow($dataRow, $registeredStyle);
}
}
@@ -217,6 +230,7 @@ class Workbook
->createContentTypesFile($this->worksheets)
->createWorkbookFile($this->worksheets)
->createWorkbookRelsFile($this->worksheets)
->createStylesFile($this->styleHelper)
->zipRootFolderAndCopyToStream($finalFilePointer);
$this->cleanupTempFolder();
@@ -1,17 +1,17 @@
<?php
namespace Box\Spout\Writer\Internal\XLSX;
namespace Box\Spout\Writer\XLSX\Internal;
use Box\Spout\Common\Exception\InvalidArgumentException;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Writer\Helper\XLSX\CellHelper;
use Box\Spout\Writer\XLSX\Helper\CellHelper;
/**
* Class Worksheet
* Represents a worksheet within a XLSX file. The difference with the Sheet object is
* that this class provides an interface to write data
*
* @package Box\Spout\Writer\Internal\XLSX
* @package Box\Spout\Writer\XLSX\Internal
*/
class Worksheet
{
@@ -20,13 +20,13 @@ class Worksheet
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
EOD;
/** @var \Box\Spout\Writer\Sheet The "external" sheet */
/** @var \Box\Spout\Writer\XLSX\Sheet The "external" sheet */
protected $externalSheet;
/** @var string Path to the XML file that will contain the sheet data */
protected $worksheetFilePath;
/** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
protected $sharedStringsHelper;
/** @var bool Whether inline or shared strings should be used */
@@ -38,13 +38,13 @@ EOD;
/** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */
protected $sheetFilePointer;
/** @var int */
/** @var int Index of the last written row */
protected $lastWrittenRowIndex = 0;
/**
* @param \Box\Spout\Writer\Sheet $externalSheet The associated "external" sheet
* @param \Box\Spout\Writer\XLSX\Sheet $externalSheet The associated "external" sheet
* @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
* @param \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper $sharedStringsHelper Helper for shared strings
* @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
*/
@@ -76,7 +76,7 @@ EOD;
}
/**
* @return \Box\Spout\Writer\Sheet The "external" sheet
* @return \Box\Spout\Writer\XLSX\Sheet The "external" sheet
*/
public function getExternalSheet()
{
@@ -118,11 +118,12 @@ EOD;
*
* @param array $dataRow Array containing data to be written.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
*/
public function addRow($dataRow)
public function addRow($dataRow, $style)
{
$cellNumber = 0;
$rowIndex = $this->lastWrittenRowIndex + 1;
@@ -133,6 +134,7 @@ EOD;
foreach($dataRow as $cellValue) {
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
$data .= ' <c r="' . $columnIndex . $rowIndex . '"';
$data .= ' s="' . $style->getId() . '"';
if (CellHelper::isNonEmptyString($cellValue)) {
if ($this->shouldUseInlineStrings) {
@@ -156,7 +158,7 @@ EOD;
$wasWriteSuccessful = fwrite($this->sheetFilePointer, $data);
if ($wasWriteSuccessful === false) {
throw new IOException('Unable to write data in ' . $this->worksheetFilePath);
throw new IOException("Unable to write data in {$this->worksheetFilePath}");
}
// only update the count if the write worked
@@ -1,12 +1,12 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
/**
* Class Sheet
* Represents a worksheet within a XLSX file
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class Sheet
{
@@ -21,7 +21,7 @@ class Sheet
/**
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
*/
function __construct($sheetIndex)
public function __construct($sheetIndex)
{
$this->index = $sheetIndex;
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1);
@@ -45,7 +45,7 @@ class Sheet
/**
* @param string $name Name of the sheet
* @return \Box\Spout\Writer\Sheet
* @return Sheet
*/
public function setName($name)
{
@@ -1,18 +1,24 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Writer\AbstractWriter;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\Internal\XLSX\Workbook;
use Box\Spout\Writer\Style\StyleBuilder;
use Box\Spout\Writer\XLSX\Internal\Workbook;
/**
* Class XLSX
* Class Writer
* This class provides base support to write data to XLSX files
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class XLSX extends AbstractWriter
class Writer extends AbstractWriter
{
/** Default style font values */
const DEFAULT_FONT_SIZE = 12;
const DEFAULT_FONT_NAME = 'Calibri';
/** @var string Content-Type value for the header */
protected static $headerContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
@@ -25,7 +31,7 @@ class XLSX extends AbstractWriter
/** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
protected $shouldCreateNewSheetsAutomatically = true;
/** @var Internal\XLSX\Workbook The workbook for the XLSX file */
/** @var Internal\Workbook The workbook for the XLSX file */
protected $book;
/** @var int */
@@ -33,7 +39,7 @@ class XLSX extends AbstractWriter
/**
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
* @return XLSX
* @return Writer
*/
public function setTempFolder($tempFolder)
{
@@ -45,7 +51,7 @@ class XLSX extends AbstractWriter
* Use inline string to be more memory efficient. If set to false, it will use shared strings.
*
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
* @return XLSX
* @return Writer
*/
public function setShouldUseInlineStrings($shouldUseInlineStrings)
{
@@ -55,7 +61,7 @@ class XLSX extends AbstractWriter
/**
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
* @return XLSX
* @return Writer
*/
public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically)
{
@@ -73,7 +79,7 @@ class XLSX extends AbstractWriter
{
if (!$this->book) {
$tempFolder = ($this->tempFolder) ? : sys_get_temp_dir();
$this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically);
$this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle);
$this->book->addNewSheetAndMakeItCurrent();
}
}
@@ -91,7 +97,7 @@ class XLSX extends AbstractWriter
$externalSheets = [];
$worksheets = $this->book->getWorksheets();
/** @var Internal\XLSX\Worksheet $worksheet */
/** @var Internal\Worksheet $worksheet */
foreach ($worksheets as $worksheet) {
$externalSheets[] = $worksheet->getExternalSheet();
}
@@ -160,14 +166,28 @@ class XLSX extends AbstractWriter
*
* @param array $dataRow Array containing data to be written.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
* @return void
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
protected function addRowToWriter(array $dataRow)
protected function addRowToWriter(array $dataRow, $style)
{
$this->throwIfBookIsNotAvailable();
$this->book->addRowToCurrentWorksheet($dataRow);
$this->book->addRowToCurrentWorksheet($dataRow, $style);
}
/**
* Returns the default style to be applied to rows.
*
* @return \Box\Spout\Writer\Style\Style
*/
protected function getDefaultRowStyle()
{
return (new StyleBuilder())
->setFontSize(self::DEFAULT_FONT_SIZE)
->setFontName(self::DEFAULT_FONT_NAME)
->build();
}
/**
@@ -0,0 +1,223 @@
<?php
namespace Box\Spout\Common\Helper;
use Box\Spout\TestUsingResource;
/**
* Class EncodingHelperTest
*
* @package Box\Spout\Common\Helper
*/
class EncodingHelperTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
/**
* @return array
*/
public function dataProviderForTestGetBytesOffsetToSkipBOM()
{
return [
['csv_with_utf8_bom.csv', EncodingHelper::ENCODING_UTF8, 3],
['csv_with_utf16be_bom.csv', EncodingHelper::ENCODING_UTF16_BE, 2],
['csv_with_utf32le_bom.csv', EncodingHelper::ENCODING_UTF32_LE, 4],
['csv_with_encoding_utf16le_no_bom.csv', EncodingHelper::ENCODING_UTF16_LE, 0],
['csv_standard.csv', EncodingHelper::ENCODING_UTF8, 0],
];
}
/**
* @dataProvider dataProviderForTestGetBytesOffsetToSkipBOM
*
* @param string $fileName
* @param string $encoding
* @param int $expectedBytesOffset
* @return void
*/
public function testGetBytesOffsetToSkipBOM($fileName, $encoding, $expectedBytesOffset)
{
$resourcePath = $this->getResourcePath($fileName);
$filePointer = fopen($resourcePath, 'r');
$encodingHelper = new EncodingHelper(new GlobalFunctionsHelper());
$bytesOffset = $encodingHelper->getBytesOffsetToSkipBOM($filePointer, $encoding);
$this->assertEquals($expectedBytesOffset, $bytesOffset);
}
/**
* @return array
*/
public function dataProviderForIconvOrMbstringUsage()
{
return [
[$shouldUseIconv = true],
[$shouldNotUseIconv = false],
];
}
/**
* @dataProvider dataProviderForIconvOrMbstringUsage
* @expectedException \Box\Spout\Common\Exception\EncodingConversionException
*
* @param bool $shouldUseIconv
* @return void
*/
public function testAttemptConversionToUTF8ShouldThrowIfConversionFailed($shouldUseIconv)
{
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
->setMethods(['iconv', 'mb_convert_encoding'])
->getMock();
$helperStub->method('iconv')->willReturn(false);
$helperStub->method('mb_convert_encoding')->willReturn(false);
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->setConstructorArgs([$helperStub])
->setMethods(['canUseIconv', 'canUseMbString'])
->getMock();
$encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv);
$encodingHelperStub->method('canUseMbString')->willReturn(true);
$encodingHelperStub->attemptConversionToUTF8('input', EncodingHelper::ENCODING_UTF16_LE);
}
/**
* @expectedException \Box\Spout\Common\Exception\EncodingConversionException
*
* @return void
*/
public function testAttemptConversionToUTF8ShouldThrowIfConversionNotSupported()
{
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->disableOriginalConstructor()
->setMethods(['canUseIconv', 'canUseMbString'])
->getMock();
$encodingHelperStub->method('canUseIconv')->willReturn(false);
$encodingHelperStub->method('canUseMbString')->willReturn(false);
$encodingHelperStub->attemptConversionToUTF8('input', EncodingHelper::ENCODING_UTF16_LE);
}
/**
* @dataProvider dataProviderForIconvOrMbstringUsage
*
* @param bool $shouldUseIconv
* @return void
*/
public function testAttemptConversionToUTF8ShouldReturnReencodedString($shouldUseIconv)
{
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->setConstructorArgs([new GlobalFunctionsHelper()])
->setMethods(['canUseIconv', 'canUseMbString'])
->getMock();
$encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv);
$encodingHelperStub->method('canUseMbString')->willReturn(true);
$encodedString = iconv(EncodingHelper::ENCODING_UTF8, EncodingHelper::ENCODING_UTF16_LE, 'input');
$decodedString = $encodingHelperStub->attemptConversionToUTF8($encodedString, EncodingHelper::ENCODING_UTF16_LE);
$this->assertEquals('input', $decodedString);
}
/**
* @return void
*/
public function testAttemptConversionToUTF8ShouldBeNoopWhenTargetIsUTF8()
{
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->disableOriginalConstructor()
->setMethods(['canUseIconv'])
->getMock();
$encodingHelperStub->expects($this->never())->method('canUseIconv');
$decodedString = $encodingHelperStub->attemptConversionToUTF8('input', EncodingHelper::ENCODING_UTF8);
$this->assertEquals('input', $decodedString);
}
/**
* @dataProvider dataProviderForIconvOrMbstringUsage
* @expectedException \Box\Spout\Common\Exception\EncodingConversionException
*
* @param bool $shouldUseIconv
* @return void
*/
public function testAttemptConversionFromUTF8ShouldThrowIfConversionFailed($shouldUseIconv)
{
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
->setMethods(['iconv', 'mb_convert_encoding'])
->getMock();
$helperStub->method('iconv')->willReturn(false);
$helperStub->method('mb_convert_encoding')->willReturn(false);
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->setConstructorArgs([$helperStub])
->setMethods(['canUseIconv', 'canUseMbString'])
->getMock();
$encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv);
$encodingHelperStub->method('canUseMbString')->willReturn(true);
$encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF16_LE);
}
/**
* @expectedException \Box\Spout\Common\Exception\EncodingConversionException
*
* @return void
*/
public function testAttemptConversionFromUTF8ShouldThrowIfConversionNotSupported()
{
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->disableOriginalConstructor()
->setMethods(['canUseIconv', 'canUseMbString'])
->getMock();
$encodingHelperStub->method('canUseIconv')->willReturn(false);
$encodingHelperStub->method('canUseMbString')->willReturn(false);
$encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF16_LE);
}
/**
* @dataProvider dataProviderForIconvOrMbstringUsage
*
* @param bool $shouldUseIconv
* @return void
*/
public function testAttemptConversionFromUTF8ShouldReturnReencodedString($shouldUseIconv)
{
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->setConstructorArgs([new GlobalFunctionsHelper()])
->setMethods(['canUseIconv', 'canUseMbString'])
->getMock();
$encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv);
$encodingHelperStub->method('canUseMbString')->willReturn(true);
$encodedString = $encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF16_LE);
$encodedStringWithIconv = iconv(EncodingHelper::ENCODING_UTF8, EncodingHelper::ENCODING_UTF16_LE, 'input');
$this->assertEquals($encodedStringWithIconv, $encodedString);
}
/**
* @return void
*/
public function testAttemptConversionFromUTF8ShouldBeNoopWhenTargetIsUTF8()
{
/** @var EncodingHelper $encodingHelperStub */
$encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper')
->disableOriginalConstructor()
->setMethods(['canUseIconv'])
->getMock();
$encodingHelperStub->expects($this->never())->method('canUseIconv');
$encodedString = $encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF8);
$this->assertEquals('input', $encodedString);
}
}
+357
View File
@@ -0,0 +1,357 @@
<?php
namespace Box\Spout\Reader\CSV;
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\Common\Type;
use Box\Spout\Common\Helper\EncodingHelper;
use Box\Spout\TestUsingResource;
/**
* Class ReaderTest
*
* @package Box\Spout\Reader\CSV
*/
class ReaderTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
/**
* @expectedException \Box\Spout\Common\Exception\IOException
*
* @return void
*/
public function testOpenShouldThrowExceptionIfFileDoesNotExist()
{
ReaderFactory::create(Type::CSV)->open('/path/to/fake/file.csv');
}
/**
* @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException
*
* @return void
*/
public function testOpenShouldThrowExceptionIfTryingToReadBeforeOpeningReader()
{
ReaderFactory::create(Type::CSV)->getSheetIterator();
}
/**
* @expectedException \Box\Spout\Common\Exception\IOException
*
* @return void
*/
public function testOpenShouldThrowExceptionIfFileNotReadable()
{
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
->setMethods(['is_readable'])
->getMock();
$helperStub->method('is_readable')->willReturn(false);
$resourcePath = $this->getResourcePath('csv_standard.csv');
$reader = ReaderFactory::create(Type::CSV);
$reader->setGlobalFunctionsHelper($helperStub);
$reader->open($resourcePath);
}
/**
* @expectedException \Box\Spout\Common\Exception\IOException
*
* @return void
*/
public function testOpenShouldThrowExceptionIfCannotOpenFile()
{
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
->setMethods(['fopen'])
->getMock();
$helperStub->method('fopen')->willReturn(false);
$resourcePath = $this->getResourcePath('csv_standard.csv');
$reader = ReaderFactory::create(Type::CSV);
$reader->setGlobalFunctionsHelper($helperStub);
$reader->open($resourcePath);
}
/**
* @return void
*/
public function testReadStandardCSV()
{
$allRows = $this->getAllRowsForFile('csv_standard.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22', 'csv--23'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldNotStopAtCommaIfEnclosed()
{
$allRows = $this->getAllRowsForFile('csv_with_comma_enclosed.csv');
$this->assertEquals('This is, a comma', $allRows[0][0]);
}
/**
* @return void
*/
public function testReadShouldKeepEmptyCells()
{
$allRows = $this->getAllRowsForFile('csv_with_empty_cells.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', '', 'csv--23'],
['csv--31', 'csv--32', ''],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return array
*/
public function dataProviderForTestReadShouldSkipEmptyLines()
{
return [
['csv_with_empty_line.csv'],
['csv_with_empty_last_line.csv'],
];
}
/**
* @dataProvider dataProviderForTestReadShouldSkipEmptyLines
*
* @param string $fileName
* @return void
*/
public function testReadShouldSkipEmptyLines($fileName)
{
$allRows = $this->getAllRowsForFile($fileName);
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldHaveTheRightNumberOfCells()
{
$allRows = $this->getAllRowsForFile('csv_with_different_cells_number.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22'],
['csv--31'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldSupportCustomFieldDelimiter()
{
$allRows = $this->getAllRowsForFile('csv_delimited_with_pipes.csv', '|');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22', 'csv--23'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldSupportCustomFieldEnclosure()
{
$allRows = $this->getAllRowsForFile('csv_text_enclosed_with_pound.csv', ',', '#');
$this->assertEquals('This is, a comma', $allRows[0][0]);
}
/**
* @return array
*/
public function dataProviderForTestReadShouldSkipBom()
{
return [
['csv_with_utf8_bom.csv', EncodingHelper::ENCODING_UTF8],
['csv_with_utf16le_bom.csv', EncodingHelper::ENCODING_UTF16_LE],
['csv_with_utf16be_bom.csv', EncodingHelper::ENCODING_UTF16_BE],
['csv_with_utf32le_bom.csv', EncodingHelper::ENCODING_UTF32_LE],
['csv_with_utf32be_bom.csv', EncodingHelper::ENCODING_UTF32_BE],
];
}
/**
* @dataProvider dataProviderForTestReadShouldSkipBom
*
* @param string $fileName
* @param string $fileEncoding
* @return void
*/
public function testReadShouldSkipBom($fileName, $fileEncoding)
{
$allRows = $this->getAllRowsForFile($fileName, ',', '"', $fileEncoding);
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22', 'csv--23'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return array
*/
public function dataProviderForTestReadShouldSupportNonUTF8FilesWithoutBOMs()
{
$shouldUseIconv = true;
$shouldNotUseIconv = false;
return [
['csv_with_encoding_utf16le_no_bom.csv', EncodingHelper::ENCODING_UTF16_LE, $shouldUseIconv],
['csv_with_encoding_utf16le_no_bom.csv', EncodingHelper::ENCODING_UTF16_LE, $shouldNotUseIconv],
['csv_with_encoding_cp1252.csv', 'CP1252', $shouldUseIconv],
['csv_with_encoding_cp1252.csv', 'CP1252', $shouldNotUseIconv],
];
}
/**
* @dataProvider dataProviderForTestReadShouldSupportNonUTF8FilesWithoutBOMs
*
* @param string $fileName
* @param string $fileEncoding
* @param bool $shouldUseIconv
* @return void
*/
public function testReadShouldSupportNonUTF8FilesWithoutBOMs($fileName, $fileEncoding, $shouldUseIconv)
{
$allRows = [];
$resourcePath = $this->getResourcePath($fileName);
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
->setMethods(['function_exists'])
->getMock();
$returnValueMap = [
['iconv', $shouldUseIconv],
['mb_convert_encoding', true],
];
$helperStub->method('function_exists')->will($this->returnValueMap($returnValueMap));
/** @var \Box\Spout\Reader\CSV\Reader $reader */
$reader = ReaderFactory::create(Type::CSV);
$reader
->setGlobalFunctionsHelper($helperStub)
->setEncoding($fileEncoding)
->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
}
}
$reader->close();
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22', 'csv--23'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadMultipleTimesShouldRewindReader()
{
$allRows = [];
$resourcePath = $this->getResourcePath('csv_standard.csv');
$reader = ReaderFactory::create(Type::CSV);
$reader->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheet) {
// do nothing
}
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
}
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
}
$reader->close();
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--11', 'csv--12', 'csv--13'],
['csv--11', 'csv--12', 'csv--13'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @param string $fileName
* @param string|void $fieldDelimiter
* @param string|void $fieldEnclosure
* @param string|void $encoding
* @return array All the read rows the given file
*/
private function getAllRowsForFile(
$fileName,
$fieldDelimiter = ',',
$fieldEnclosure = '"',
$encoding = EncodingHelper::ENCODING_UTF8)
{
$allRows = [];
$resourcePath = $this->getResourcePath($fileName);
/** @var \Box\Spout\Reader\CSV\Reader $reader */
$reader = ReaderFactory::create(Type::CSV);
$reader
->setFieldDelimiter($fieldDelimiter)
->setFieldEnclosure($fieldEnclosure)
->setEncoding($encoding)
->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
foreach ($sheet->getRowIterator() as $rowIndex => $row) {
$allRows[] = $row;
}
}
$reader->close();
return $allRows;
}
}
-208
View File
@@ -1,208 +0,0 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
/**
* Class CSVTest
*
* @package Box\Spout\Reader
*/
class CSVTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
/**
* @expectedException \Box\Spout\Common\Exception\IOException
*
* @return void
*/
public function testOpenShouldThrowExceptionIfFileDoesNotExist()
{
ReaderFactory::create(Type::CSV)->open('/path/to/fake/file.csv');
}
/**
* @expectedException \Box\Spout\Common\Exception\IOException
*
* @return void
*/
public function testOpenShouldThrowExceptionIfFileNotReadable()
{
$helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper')
->setMethods(['is_readable'])
->getMock();
$helperStub->method('is_readable')->willReturn(false);
$resourcePath = $this->getResourcePath('csv_standard.csv');
$reader = ReaderFactory::create(Type::CSV);
$reader->setGlobalFunctionsHelper($helperStub);
$reader->open($resourcePath);
}
/**
* @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException
*
* @return void
*/
public function testReadShouldThrowExceptionIfReadBeforeReaderOpened()
{
$reader = ReaderFactory::create(Type::CSV);
$reader->hasNextRow();
}
/**
* @expectedException \Box\Spout\Reader\Exception\EndOfFileReachedException
*
* @return void
*/
public function testReadShouldThrowExceptionIfNextRowCalledAfterReadingDone()
{
$resourcePath = $this->getResourcePath('csv_standard.csv');
$reader = ReaderFactory::create(Type::CSV);
$reader->open($resourcePath);
while ($reader->hasNextRow()) {
$reader->nextRow();
}
$reader->nextRow();
}
/**
* @return void
*/
public function testReadStandardCSV()
{
$allRows = $this->getAllRowsForFile('csv_standard.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22', 'csv--23'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldNotStopAtCommaIfEnclosed()
{
$allRows = $this->getAllRowsForFile('csv_with_comma_enclosed.csv');
$this->assertEquals('This is, a comma', $allRows[0][0]);
}
/**
* @return void
*/
public function testReadShouldKeepEmptyCells()
{
$allRows = $this->getAllRowsForFile('csv_with_empty_cells.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', '', 'csv--23'],
['csv--31', 'csv--32', ''],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldSkipEmptyLines()
{
$allRows = $this->getAllRowsForFile('csv_with_empty_line.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldHaveTheRightNumberOfCells()
{
$allRows = $this->getAllRowsForFile('csv_with_different_cells_number.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22'],
['csv--31'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldSupportCustomFieldDelimiter()
{
$allRows = $this->getAllRowsForFile('csv_delimited_with_pipes.csv', '|');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22', 'csv--23'],
['csv--31', 'csv--32', 'csv--33'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldSupportCustomFieldEnclosure()
{
$allRows = $this->getAllRowsForFile('csv_text_enclosed_with_pound.csv', ',', '#');
$this->assertEquals('This is, a comma', $allRows[0][0]);
}
/**
* @return void
*/
public function testReadShouldSkipUtf8Bom()
{
$allRows = $this->getAllRowsForFile('csv_with_utf8_bom.csv');
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
['csv--21', 'csv--22', 'csv--23'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @param string $fileName
* @param string|void $fieldDelimiter
* @param string|void $fieldEnclosure
* @return array All the read rows the given file
*/
private function getAllRowsForFile($fileName, $fieldDelimiter = ",", $fieldEnclosure = '"')
{
$allRows = [];
$resourcePath = $this->getResourcePath($fileName);
$reader = ReaderFactory::create(Type::CSV);
$reader->setFieldDelimiter($fieldDelimiter);
$reader->setFieldEnclosure($fieldEnclosure);
$reader->open($resourcePath);
while ($reader->hasNextRow()) {
$allRows[] = $reader->nextRow();
}
$reader->close();
return $allRows;
}
}
@@ -0,0 +1,127 @@
<?php
namespace Box\Spout\Reader\Wrapper;
use Box\Spout\TestUsingResource;
/**
* Class SimpleXMLElementTest
*
* @package Box\Spout\Reader\Wrapper
*/
class SimpleXMLElementTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
/**
* @expectedException \Box\Spout\Reader\Exception\XMLProcessingException
*
* @return void
*/
public function testConstructShouldThrowExceptionIfInvalidData()
{
$invalidXML = '<invalid><xml></invalid>';
new SimpleXMLElement($invalidXML);
}
/**
* @return array
*/
public function dataProviderForTestGetAttribute()
{
$xmlWithoutNamespace = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<worksheet foo="bar" type="test" />
XML;
$xmlWithHalfNamespace = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<worksheet
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
foo="bar" r:type="test" />
XML;
$xmlWithFullNamespace = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<worksheet
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
r:foo="bar" r:type="test" />
XML;
return [
[$xmlWithoutNamespace, null, ['foo' => 'bar', 'type' => 'test']],
[$xmlWithHalfNamespace, null, ['foo' => 'bar', 'type' => null]],
[$xmlWithFullNamespace, null, ['foo' => null, 'type' => null]],
[$xmlWithoutNamespace, 'r', ['foo' => null, 'type' => null]],
[$xmlWithHalfNamespace, 'r', ['foo' => null, 'type' => 'test']],
[$xmlWithFullNamespace, 'r', ['foo' => 'bar', 'type' => 'test']],
];
}
/**
* @dataProvider dataProviderForTestGetAttribute
*
* @param string $xml
* @param string|null $namespace
* @param array $expectedAttributes
* @return void
*/
public function testGetAttribute($xml, $namespace, $expectedAttributes)
{
$element = new SimpleXMLElement($xml);
foreach ($expectedAttributes as $name => $expectedValue) {
$value = $element->getAttribute($name, $namespace);
$this->assertEquals($expectedValue, $value);
}
}
/**
* @return void
*/
public function testXPath()
{
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<worksheet>
<sheetData>
<row r="1">
<c r="A1"><v>0</v></c>
<c r="A2"><v>1</v></c>
</row>
</sheetData>
</worksheet>
XML;
$element = new SimpleXMLElement($xml);
$matchedElements = $element->xpath('//c');
$this->assertEquals(2, count($matchedElements));
$this->assertTrue($matchedElements[0] instanceof SimpleXMLElement, 'The SimpleXMLElement should be wrapped');
$this->assertEquals('A2', $matchedElements[1]->getAttribute('r'));
}
/**
* @return void
*/
public function testRemoveNodeMatchingXPath()
{
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<worksheet>
<sheetData>
<row r="1">
<c r="A1"><v>0</v></c>
<c r="A2"><v>1</v></c>
</row>
</sheetData>
</worksheet>
XML;
$element = new SimpleXMLElement($xml);
$this->assertNotNull($element->getFirstChildByTagName('sheetData'));
$element->removeNodesMatchingXPath('//sheetData');
$this->assertNull($element->getFirstChildByTagName('sheetData'));
}
}
@@ -0,0 +1,166 @@
<?php
namespace Box\Spout\Reader\Wrapper;
use Box\Spout\TestUsingResource;
use Box\Spout\Reader\Exception\XMLProcessingException;
/**
* Class XMLReaderTest
*
* @package Box\Spout\Reader\Wrapper
*/
class XMLReaderTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
/**
* @return void
*/
public function testOpenShouldFailIfFileInsideZipDoesNotExist()
{
$resourcePath = $this->getResourcePath('one_sheet_with_inline_strings.xlsx');
$nonExistingXMLFilePath = 'zip://' . $resourcePath . '#path/to/fake/file.xml';
$xmlReader = new XMLReader();
// using "@" to prevent errors/warning to be displayed
$wasOpenSuccessful = @$xmlReader->open($nonExistingXMLFilePath);
$this->assertTrue($wasOpenSuccessful === false);
}
/**
* Testing a HHVM bug: https://github.com/facebook/hhvm/issues/5779
* The associated code in XMLReader::open() can be removed when the issue is fixed (and this test starts failing).
* @see XMLReader::open()
*
* @return void
*/
public function testHHVMStillDoesNotComplainWhenCallingOpenWithFileInsideZipNotExisting()
{
// Test should only be run on HHVM
if ($this->isRunningHHVM()) {
$resourcePath = $this->getResourcePath('one_sheet_with_inline_strings.xlsx');
$nonExistingXMLFilePath = 'zip://' . $resourcePath . '#path/to/fake/file.xml';
libxml_clear_errors();
$initialUseInternalErrorsSetting = libxml_use_internal_errors(true);
// using the built-in XMLReader
$xmlReader = new \XMLReader();
$this->assertTrue($xmlReader->open($nonExistingXMLFilePath) !== false);
$this->assertTrue(libxml_get_last_error() === false);
libxml_use_internal_errors($initialUseInternalErrorsSetting);
}
}
/**
* @return bool TRUE if running on HHVM, FALSE otherwise
*/
private function isRunningHHVM()
{
return defined('HHVM_VERSION');
}
/**
* @expectedException \Box\Spout\Reader\Exception\XMLProcessingException
*
* @return void
*/
public function testReadShouldThrowExceptionOnError()
{
$resourcePath = $this->getResourcePath('one_sheet_with_invalid_xml_characters.xlsx');
$sheetDataXMLFilePath = 'zip://' . $resourcePath . '#xl/worksheets/sheet1.xml';
$xmlReader = new XMLReader();
if ($xmlReader->open($sheetDataXMLFilePath) === false) {
$this->fail();
}
// using "@" to prevent errors/warning to be displayed
while (@$xmlReader->read()) {
// do nothing
}
}
/**
* @expectedException \Box\Spout\Reader\Exception\XMLProcessingException
*
* @return void
*/
public function testNextShouldThrowExceptionOnError()
{
// The sharedStrings.xml file in "attack_billion_laughs.xlsx" contains
// a doctype element that causes read errors
$resourcePath = $this->getResourcePath('attack_billion_laughs.xlsx');
$sheetDataXMLFilePath = 'zip://' . $resourcePath . '#xl/sharedStrings.xml';
$xmlReader = new XMLReader();
if ($xmlReader->open($sheetDataXMLFilePath) !== false) {
@$xmlReader->next('sst');
}
}
/**
* @return array
*/
public function dataProviderForTestIsZipStream()
{
return [
['/absolute/path/to/file.xlsx', false],
['relative/path/to/file.xlsx', false],
['php://temp', false],
['zip:///absolute/path/to/file.xlsx', true],
['zip://relative/path/to/file.xlsx', true],
];
}
/**
* @dataProvider dataProviderForTestIsZipStream
*
* @param string $URI
* @param bool $expectedResult
* @return void
*/
public function testIsZipStream($URI, $expectedResult)
{
$xmlReader = new XMLReader();
$isZipStream = \ReflectionHelper::callMethodOnObject($xmlReader, 'isZipStream', $URI);
$this->assertEquals($expectedResult, $isZipStream);
}
/**
* @return array
*/
public function dataProviderForTestFileExistsWithinZip()
{
return [
['[Content_Types].xml', true],
['xl/sharedStrings.xml', true],
['xl/worksheets/sheet1.xml', true],
['/invalid/file.xml', false],
['another/invalid/file.xml', false],
];
}
/**
* @dataProvider dataProviderForTestFileExistsWithinZip
*
* @param string $innerFilePath
* @param bool $expectedResult
* @return void
*/
public function testFileExistsWithinZip($innerFilePath, $expectedResult)
{
$resourcePath = $this->getResourcePath('one_sheet_with_inline_strings.xlsx');
$zipStreamURI = 'zip://' . $resourcePath . '#' . $innerFilePath;
$xmlReader = new XMLReader();
$isZipStream = \ReflectionHelper::callMethodOnObject($xmlReader, 'fileExistsWithinZip', $zipStreamURI);
$this->assertEquals($expectedResult, $isZipStream);
}
}
@@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
/**
* Class CellHelperTest
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class CellHelperTest extends \PHPUnit_Framework_TestCase
{
@@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX\SharedStringsCaching;
namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching;
/**
* Class CachingStrategyFactoryTest
*
* @package Box\Spout\Reader\Helper\XLSX\SharedStringsCaching
* @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching
*/
class CachingStrategyFactoryTest extends \PHPUnit_Framework_TestCase
{
@@ -36,18 +36,18 @@ class CachingStrategyFactoryTest extends \PHPUnit_Framework_TestCase
{
/** @var CachingStrategyFactory|\PHPUnit_Framework_MockObject_MockObject $factoryStub */
$factoryStub = $this
->getMockBuilder('\Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory')
->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory')
->disableOriginalConstructor()
->setMethods(['getMemoryLimitInKB'])
->getMock();
$factoryStub->method('getMemoryLimitInKB')->willReturn($memoryLimitInKB);
\ReflectionHelper::setStaticValue('\Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory', 'instance', $factoryStub);
\ReflectionHelper::setStaticValue('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory', 'instance', $factoryStub);
$strategy = $factoryStub->getBestCachingStrategy($sharedStringsUniqueCount, null);
$fullExpectedStrategyClassName = 'Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\\' . $expectedStrategyClassName;
$fullExpectedStrategyClassName = 'Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\\' . $expectedStrategyClassName;
$this->assertEquals($fullExpectedStrategyClassName, get_class($strategy));
$strategy->clearCache();
@@ -85,7 +85,7 @@ class CachingStrategyFactoryTest extends \PHPUnit_Framework_TestCase
{
/** @var CachingStrategyFactory|\PHPUnit_Framework_MockObject_MockObject $factoryStub */
$factoryStub = $this
->getMockBuilder('\Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory')
->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory')
->disableOriginalConstructor()
->setMethods(['getMemoryLimitFromIni'])
->getMock();
@@ -1,16 +1,16 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\FileBasedStrategy;
use Box\Spout\Reader\Helper\XLSX\SharedStringsCaching\InMemoryStrategy;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\FileBasedStrategy;
use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\InMemoryStrategy;
use Box\Spout\TestUsingResource;
/**
* Class SharedStringsHelperTest
*
* @package Box\Spout\Reader\Helper\XLSX
* @package Box\Spout\Reader\XLSX\Helper
*/
class SharedStringsHelperTest extends \PHPUnit_Framework_TestCase
{
@@ -1,17 +1,18 @@
<?php
namespace Box\Spout\Reader;
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Type;
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\TestUsingResource;
/**
* Class XLSXTest
* Class ReaderTest
*
* @package Box\Spout\Reader
* @package Box\Spout\Reader\XLSX
*/
class XLSXTest extends \PHPUnit_Framework_TestCase
class ReaderTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
@@ -23,6 +24,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
return [
['/path/to/fake/file.xlsx'],
['file_with_no_sheets_in_content_types.xlsx'],
['file_with_sheet_xml_not_matching_content_types.xlsx'],
['file_corrupted.xlsx'],
];
}
@@ -36,38 +38,8 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
*/
public function testReadShouldThrowException($filePath)
{
$this->getAllRowsForFile($filePath);
}
/**
* @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException
*
* @return void
*/
public function testHasNextSheetShouldThrowExceptionIfReaderNotOpened()
{
$reader = ReaderFactory::create(Type::XLSX);
$reader->hasNextSheet();
}
/**
* @expectedException \Box\Spout\Reader\Exception\EndOfWorksheetsReachedException
*
* @return void
*/
public function testNextSheetShouldThrowExceptionIfNoMoreSheetsToRead()
{
$fileName = 'one_sheet_with_shared_strings.xlsx';
$resourcePath = $this->getResourcePath($fileName);
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($resourcePath);
while ($reader->hasNextSheet()) {
$reader->nextSheet();
}
$reader->nextSheet();
// using @ to prevent warnings/errors from being displayed
@$this->getAllRowsForFile($filePath);
}
/**
@@ -120,6 +92,9 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
*/
public function testReadShouldSupportAllCellTypes()
{
// make sure dates are always created with the same timezone
date_default_timezone_set('UTC');
$allRows = $this->getAllRowsForFile('sheet_with_all_cell_types.xlsx');
$expectedRows = [
@@ -270,7 +245,8 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$startTime = microtime(true);
try {
$this->getAllRowsForFile($fileName);
// using @ to prevent warnings/errors from being displayed
@$this->getAllRowsForFile($fileName);
$this->fail('An exception should have been thrown');
} catch (IOException $exception) {
$duration = microtime(true) - $startTime;
@@ -305,6 +281,60 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadMultipleTimesShouldRewindReader()
{
$allRows = [];
$resourcePath = $this->getResourcePath('two_sheets_with_inline_strings.xlsx');
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheet) {
// do nothing
}
foreach ($reader->getSheetIterator() as $sheet) {
// this loop should only add the first row of the first sheet
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
// this loop should rewind the iterator and restart reading from the 1st row again
// therefore, it should only add the first row of the first sheet
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
// not reading any more sheets
break;
}
foreach ($reader->getSheetIterator() as $sheet) {
// this loop should only add the first row of the current sheet
foreach ($sheet->getRowIterator() as $row) {
$allRows[] = $row;
break;
}
// not breaking, so we keep reading the next sheets
}
$reader->close();
$expectedRows = [
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
['s2 - A1', 's2 - B1', 's2 - C1', 's2 - D1', 's2 - E1'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @param string $fileName
* @return array All the read rows the given file
@@ -317,11 +347,9 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$reader = ReaderFactory::create(Type::XLSX);
$reader->open($resourcePath);
while ($reader->hasNextSheet()) {
$reader->nextSheet();
while ($reader->hasNextRow()) {
$allRows[] = $reader->nextRow();
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
foreach ($sheet->getRowIterator() as $rowIndex => $row) {
$allRows[] = $row;
}
}
@@ -1,14 +1,15 @@
<?php
namespace Box\Spout\Reader;
namespace Box\Spout\Reader\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\Reader\ReaderFactory;
use Box\Spout\TestUsingResource;
/**
* Class SheetTest
*
* @package Box\Spout\Reader
* @package Box\Spout\Reader\XLSX
*/
class SheetTest extends \PHPUnit_Framework_TestCase
{
@@ -41,8 +42,8 @@ class SheetTest extends \PHPUnit_Framework_TestCase
$reader->open($resourcePath);
$sheets = [];
while ($reader->hasNextSheet()) {
$sheets[] = $reader->nextSheet();
foreach ($reader->getSheetIterator() as $sheet) {
$sheets[] = $sheet;
}
$reader->close();
@@ -1,16 +1,18 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\CSV;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Common\Type;
use Box\Spout\Common\Helper\EncodingHelper;
use Box\Spout\Writer\WriterFactory;
/**
* Class CSVTest
* Class WriterTest
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\CSV
*/
class CSVTest extends \PHPUnit_Framework_TestCase
class WriterTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
@@ -69,7 +71,7 @@ class CSVTest extends \PHPUnit_Framework_TestCase
];
$writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_utf8_bom.csv');
$this->assertContains(CSV::UTF8_BOM, $writtenContent, 'The CSV file should contain a UTF-8 BOM');
$this->assertContains(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should contain a UTF-8 BOM');
}
/**
@@ -161,6 +163,6 @@ class CSVTest extends \PHPUnit_Framework_TestCase
private function trimWrittenContent($writtenContent)
{
// remove line feeds and UTF-8 BOM
return trim($writtenContent, PHP_EOL . CSV::UTF8_BOM);
return trim($writtenContent, PHP_EOL . EncodingHelper::BOM_UTF8);
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace Box\Spout\Writer\Style;
/**
* Class StyleTest
*
* @package Box\Spout\Writer\Style
*/
class StyleTest extends \PHPUnit_Framework_TestCase
{
/**
* @return void
*/
public function testSerializeShouldNotTakeIntoAccountId()
{
$style1 = (new StyleBuilder())->setFontBold()->build();
$style1->setId(1);
$style2 = (new StyleBuilder())->setFontBold()->build();
$style2->setId(2);
$this->assertEquals($style1->serialize(), $style2->serialize());
}
/**
* @return void
*/
public function testMergeWithShouldReturnACopy()
{
$baseStyle = (new StyleBuilder())->build();
$currentStyle = (new StyleBuilder())->build();
$mergedStyle = $currentStyle->mergeWith($baseStyle);
$this->assertNotSame($mergedStyle, $currentStyle);
}
/**
* @return void
*/
public function testMergeWithShouldMergeSetProperties()
{
$baseStyle = (new StyleBuilder())->setFontSize(99)->setFontBold()->build();
$currentStyle = (new StyleBuilder())->setFontName('Font')->setFontUnderline()->build();
$mergedStyle = $currentStyle->mergeWith($baseStyle);
$this->assertNotEquals(99, $currentStyle->getFontSize());
$this->assertFalse($currentStyle->isFontBold());
$this->assertEquals(99, $mergedStyle->getFontSize());
$this->assertTrue($mergedStyle->isFontBold());
$this->assertEquals('Font', $mergedStyle->getFontName());
$this->assertTrue($mergedStyle->isFontUnderline());
}
/**
* @return void
*/
public function testMergeWithShouldPreferCurrentStylePropertyIfSetOnCurrentAndOnBase()
{
$baseStyle = (new StyleBuilder())->setFontSize(10)->build();
$currentStyle = (new StyleBuilder())->setFontSize(99)->build();
$mergedStyle = $currentStyle->mergeWith($baseStyle);
$this->assertEquals(99, $mergedStyle->getFontSize());
}
/**
* @return void
*/
public function testMergeWithShouldPreferCurrentStylePropertyIfSetOnCurrentButNotOnBase()
{
$baseStyle = (new StyleBuilder())->build();
$currentStyle = (new StyleBuilder())->setFontItalic()->setFontStrikethrough()->build();
$mergedStyle = $currentStyle->mergeWith($baseStyle);
$this->assertFalse($baseStyle->isFontItalic());
$this->assertFalse($baseStyle->isFontStrikethrough());
$this->assertTrue($mergedStyle->isFontItalic());
$this->assertTrue($mergedStyle->isFontStrikethrough());
}
/**
* @return void
*/
public function testMergeWithShouldPreferBaseStylePropertyIfSetOnBaseButNotOnCurrent()
{
$baseStyle = (new StyleBuilder())
->setFontItalic()
->setFontUnderline()
->setFontStrikethrough()
->setShouldWrapText()
->build();
$currentStyle = (new StyleBuilder())->build();
$mergedStyle = $currentStyle->mergeWith($baseStyle);
$this->assertFalse($currentStyle->isFontUnderline());
$this->assertTrue($mergedStyle->isFontUnderline());
$this->assertFalse($currentStyle->shouldWrapText());
$this->assertTrue($mergedStyle->shouldWrapText());
}
/**
* @return void
*/
public function testMergeWithShouldDoNothingIfStylePropertyNotSetOnBaseNorCurrent()
{
$baseStyle = (new StyleBuilder())->build();
$currentStyle = (new StyleBuilder())->build();
$mergedStyle = $currentStyle->mergeWith($baseStyle);
$this->assertTrue($baseStyle->serialize() === $currentStyle->serialize());
$this->assertTrue($currentStyle->serialize() === $mergedStyle->serialize());
}
/**
* @return void
*/
public function testMergeWithShouldDoNothingIfStylePropertyNotSetOnCurrentAndIsDefaultValueOnBase()
{
$baseStyle = (new StyleBuilder())
->setFontName(Style::DEFAULT_FONT_NAME)
->setFontSize(Style::DEFAULT_FONT_SIZE)
->build();
$currentStyle = (new StyleBuilder())->build();
$mergedStyle = $currentStyle->mergeWith($baseStyle);
$this->assertTrue($currentStyle->serialize() === $mergedStyle->serialize());
}
}
@@ -1,11 +1,11 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class CellHelperTest
*
* @package Box\Spout\Writer\Helper\XLSX
* @package Box\Spout\Writer\XLSX\Helper
*/
class CellHelperTest extends \PHPUnit_Framework_TestCase
{
@@ -0,0 +1,89 @@
<?php
namespace Box\Spout\Writer\XLSX\Helper;
use Box\Spout\Writer\Style\StyleBuilder;
/**
* Class StyleHelperTest
*
* @package Box\Spout\Writer\XLSX\Helper
*/
class StyleHelperTest extends \PHPUnit_Framework_TestCase
{
/** @var \Box\Spout\Writer\Style\Style */
protected $defaultStyle;
/**
* @return void
*/
public function setUp()
{
$this->defaultStyle = (new StyleBuilder())->build();
}
/**
* @return void
*/
public function testRegisterStyleShouldUpdateId()
{
$style1 = (new StyleBuilder())->setFontBold()->build();
$style2 = (new StyleBuilder())->setFontUnderline()->build();
$this->assertEquals(0, $this->defaultStyle->getId(), 'Default style ID should be 0');
$this->assertNull($style1->getId());
$this->assertNull($style2->getId());
$styleHelper = new StyleHelper($this->defaultStyle);
$registeredStyle1 = $styleHelper->registerStyle($style1);
$registeredStyle2 = $styleHelper->registerStyle($style2);
$this->assertEquals(1, $registeredStyle1->getId());
$this->assertEquals(2, $registeredStyle2->getId());
}
/**
* @return void
*/
public function testRegisterStyleShouldReuseAlreadyRegisteredStyles()
{
$style = (new StyleBuilder())->setFontBold()->build();
$styleHelper = new StyleHelper($this->defaultStyle);
$registeredStyle1 = $styleHelper->registerStyle($style);
$registeredStyle2 = $styleHelper->registerStyle($style);
$this->assertEquals(1, $registeredStyle1->getId());
$this->assertEquals(1, $registeredStyle2->getId());
}
/**
* @return void
*/
public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine()
{
$style = clone $this->defaultStyle;
$styleHelper = new StyleHelper($this->defaultStyle);
$this->assertFalse($style->shouldWrapText());
$updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, [12, 'single line', "multi\nlines", null]);
$this->assertTrue($updatedStyle->shouldWrapText());
}
/**
* @return void
*/
public function testApplyExtraStylesIfNeededShouldDoNothingIfWrapTextAlreadyApplied()
{
$style = (new StyleBuilder())->setShouldWrapText()->build();
$styleHelper = new StyleHelper($this->defaultStyle);
$this->assertTrue($style->shouldWrapText());
$updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, ["multi\nlines"]);
$this->assertTrue($updatedStyle->shouldWrapText());
}
}
@@ -1,14 +1,15 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\WriterFactory;
/**
* Class SheetTest
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class SheetTest extends \PHPUnit_Framework_TestCase
{
@@ -69,7 +70,7 @@ class SheetTest extends \PHPUnit_Framework_TestCase
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
@@ -1,16 +1,17 @@
<?php
namespace Box\Spout\Writer;
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\WriterFactory;
/**
* Class XLSXTest
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class XLSXTest extends \PHPUnit_Framework_TestCase
class WriterTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
@@ -25,8 +26,6 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$writer = WriterFactory::create(Type::XLSX);
@$writer->openToFile($filePath);
$writer->addRow(['xlsx--11', 'xlsx--12']);
$writer->close();
}
/**
@@ -36,17 +35,15 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
{
$writer = WriterFactory::create(Type::XLSX);
$writer->addRow(['xlsx--11', 'xlsx--12']);
$writer->close();
}
/**
* @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException
*/
public function testAddRowShouldThrowExceptionIfCallAddRowsBeforeOpeningWriter()
public function testAddRowShouldThrowExceptionIfCalledBeforeOpeningWriter()
{
$writer = WriterFactory::create(Type::XLSX);
$writer->addRows([['xlsx--11', 'xlsx--12']]);
$writer->close();
}
/**
@@ -230,7 +227,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings(true);
@@ -278,7 +275,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
];
// set the maxRowsPerSheet limit to 2
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\Internal\XLSX\Workbook', 'maxRowsPerWorksheet', 2);
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\XLSX\Internal\Workbook', 'maxRowsPerWorksheet', 2);
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = true);
$this->assertEquals(2, count($writer->getSheets()), '2 sheets should have been created.');
@@ -302,7 +299,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
];
// set the maxRowsPerSheet limit to 2
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\Internal\XLSX\Workbook', 'maxRowsPerWorksheet', 2);
\ReflectionHelper::setStaticValue('\Box\Spout\Writer\XLSX\Internal\Workbook', 'maxRowsPerWorksheet', 2);
$writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = false);
$this->assertEquals(1, count($writer->getSheets()), 'Only 1 sheet should have been created.');
@@ -348,14 +345,14 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
* @param string $fileName
* @param bool $shouldUseInlineStrings
* @param bool $shouldCreateSheetsAutomatically
* @return XLSX
* @return Writer
*/
private function writeToXLSXFile($allRows, $fileName, $shouldUseInlineStrings = true, $shouldCreateSheetsAutomatically = true)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings($shouldUseInlineStrings);
$writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically);
@@ -373,14 +370,14 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
* @param string $fileName
* @param bool $shouldUseInlineStrings
* @param bool $shouldCreateSheetsAutomatically
* @return XLSX
* @return Writer
*/
private function writeToMultipleSheetsInXLSXFile($allRows, $numSheets, $fileName, $shouldUseInlineStrings = true, $shouldCreateSheetsAutomatically = true)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX $writer */
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings($shouldUseInlineStrings);
$writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically);
@@ -0,0 +1,356 @@
<?php
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\Style\StyleBuilder;
use Box\Spout\Writer\WriterFactory;
/**
* Class WriterWithStyleTest
*
* @package Box\Spout\Writer\XLSX
*/
class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
/** @var \Box\Spout\Writer\Style\Style */
protected $defaultStyle;
/**
* @return void
*/
public function setUp()
{
$this->defaultStyle = (new StyleBuilder())->build();
}
/**
* @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException
*/
public function testAddRowWithStyleShouldThrowExceptionIfCallAddRowBeforeOpeningWriter()
{
$writer = WriterFactory::create(Type::XLSX);
$writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $this->defaultStyle);
}
/**
* @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException
*/
public function testAddRowWithStyleShouldThrowExceptionIfCalledBeforeOpeningWriter()
{
$writer = WriterFactory::create(Type::XLSX);
$writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $this->defaultStyle);
}
/**
* @return array
*/
public function dataProviderForInvalidStyle()
{
return [
['style'],
[new \stdClass()],
[null],
];
}
/**
* @dataProvider dataProviderForInvalidStyle
* @expectedException \Box\Spout\Common\Exception\InvalidArgumentException
*
* @param \Box\Spout\Writer\Style\Style $style
*/
public function testAddRowWithStyleShouldThrowExceptionIfInvalidStyleGiven($style)
{
$fileName = 'test_add_row_with_style_should_throw_exception.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
$writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $style);
}
/**
* @dataProvider dataProviderForInvalidStyle
* @expectedException \Box\Spout\Common\Exception\InvalidArgumentException
*
* @param \Box\Spout\Writer\Style\Style $style
*/
public function testAddRowsWithStyleShouldThrowExceptionIfInvalidStyleGiven($style)
{
$fileName = 'test_add_row_with_style_should_throw_exception.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
$writer->addRowsWithStyle([['xlsx--11', 'xlsx--12']], $style);
}
/**
* @return void
*/
public function testAddRowWithStyleShouldListAllUsedFontsInCreateStylesXmlFile()
{
$fileName = 'test_add_row_with_style_should_list_all_used_fonts.xlsx';
$dataRows = [
['xlsx--11', 'xlsx--12'],
['xlsx--21', 'xlsx--22'],
];
$style = (new StyleBuilder())
->setFontBold()
->setFontItalic()
->setFontUnderline()
->setFontStrikethrough()
->build();
$style2 = (new StyleBuilder())
->setFontSize(15)
->setFontName('Arial')
->build();
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]);
$fontsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fonts');
$this->assertEquals(3, $fontsDomElement->getAttribute('count'), 'There should be 3 fonts, including the default one.');
$fontElements = $fontsDomElement->getElementsByTagName('font');
$this->assertEquals(3, $fontElements->length, 'There should be 3 associated "font" elements, including the default one.');
// First font should be the default one
$defaultFontElement = $fontElements->item(0);
$this->assertChildrenNumEquals(2, $defaultFontElement, 'The default font should only have 2 properties.');
$this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $defaultFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $defaultFontElement, 'name', 'val');
// Second font should contain data from the first created style
$secondFontElement = $fontElements->item(1);
$this->assertChildrenNumEquals(6, $secondFontElement, 'The font should only have 6 properties (4 custom styles + 2 default styles).');
$this->assertChildExists($secondFontElement, 'b');
$this->assertChildExists($secondFontElement, 'i');
$this->assertChildExists($secondFontElement, 'u');
$this->assertChildExists($secondFontElement, 'strike');
$this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $secondFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $secondFontElement, 'name', 'val');
// Third font should contain data from the second created style
$thirdFontElement = $fontElements->item(2);
$this->assertChildrenNumEquals(2, $thirdFontElement, 'The font should only have 2 properties.');
$this->assertFirstChildHasAttributeEquals('15', $thirdFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals('Arial', $thirdFontElement, 'name', 'val');
}
/**
* @return void
*/
public function testAddRowWithStyleShouldAddWrapTextAlignmentInfoInStylesXmlFileIfSpecified()
{
$fileName = 'test_add_row_with_style_should_add_wrap_text_alignment.xlsx';
$dataRows = [
['xlsx--11', 'xlsx--12'],
];
$style = (new StyleBuilder())->setShouldWrapText()->build();
$this->writeToXLSXFile($dataRows, $fileName,$style);
$cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
$xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1);
$this->assertEquals(1, $xfElement->getAttribute('applyAlignment'));
$this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText');
}
/**
* @return void
*/
public function testAddRowWithStyleShouldApplyStyleToCells()
{
$fileName = 'test_add_row_with_style_should_apply_style_to_cells.xlsx';
$dataRows = [
['xlsx--11'],
['xlsx--21'],
['xlsx--31'],
];
$style = (new StyleBuilder())->setFontBold()->build();
$style2 = (new StyleBuilder())->setFontSize(15)->build();
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style, $style2, null]);
$cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName);
$this->assertEquals(3, count($cellDomElements), 'There should be 3 cells.');
$this->assertEquals('1', $cellDomElements[0]->getAttribute('s'));
$this->assertEquals('2', $cellDomElements[1]->getAttribute('s'));
$this->assertEquals('0', $cellDomElements[2]->getAttribute('s'));
}
/**
* @return void
*/
public function testAddRowWithStyleShouldReuseDuplicateStyles()
{
$fileName = 'test_add_row_with_style_should_reuse_duplicate_styles.xlsx';
$dataRows = [
['xlsx--11'],
['xlsx--21'],
];
$style = (new StyleBuilder())->setFontBold()->build();
$this->writeToXLSXFile($dataRows, $fileName, $style);
$cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName);
$this->assertEquals('1', $cellDomElements[0]->getAttribute('s'));
$this->assertEquals('1', $cellDomElements[1]->getAttribute('s'));
}
/**
* @return void
*/
public function testAddRowWithStyleShouldApplyWrapTextIfCellContainsNewLine()
{
$fileName = 'test_add_row_with_style_should_apply_wrap_text_if_new_lines.xlsx';
$dataRows = [
["xlsx--11\nxlsx--11"],
['xlsx--21'],
];
$this->writeToXLSXFile($dataRows, $fileName, $this->defaultStyle);
$cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
$xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1);
$this->assertEquals(1, $xfElement->getAttribute('applyAlignment'));
$this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText');
}
/**
* @param array $allRows
* @param string $fileName
* @param \Box\Spout\Writer\Style\Style $style
* @return Writer
*/
private function writeToXLSXFile($allRows, $fileName, $style)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings(true);
$writer->openToFile($resourcePath);
$writer->addRowsWithStyle($allRows, $style);
$writer->close();
return $writer;
}
/**
* @param array $allRows
* @param string $fileName
* @param \Box\Spout\Writer\Style\Style|null[] $styles
* @return Writer
*/
private function writeToXLSXFileWithMultipleStyles($allRows, $fileName, $styles)
{
// there should be as many rows as there are styles passed in
$this->assertEquals(count($allRows), count($styles));
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings(true);
$writer->openToFile($resourcePath);
for ($i = 0; $i < count($allRows); $i++) {
if ($styles[$i] === null) {
$writer->addRow($allRows[$i]);
} else {
$writer->addRowWithStyle($allRows[$i], $styles[$i]);
}
}
$writer->close();
return $writer;
}
/**
* @param string $fileName
* @param string $section
* @return \DomElement
*/
private function getXmlSectionFromStylesXmlFile($fileName, $section)
{
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToStylesXmlFile = $resourcePath . '#xl/styles.xml';
$xmlReader = new \XMLReader();
$xmlReader->open('zip://' . $pathToStylesXmlFile);
while ($xmlReader->read() && ($xmlReader->nodeType !== \XMLReader::ELEMENT || $xmlReader->name !== $section)) {
// do nothing
}
return $xmlReader->expand();
}
/**
* @param string $fileName
* @return \DOMNode[]
*/
private function getCellElementsFromSheetXmlFile($fileName)
{
$cellElements = [];
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToStylesXmlFile = $resourcePath . '#xl/worksheets/sheet1.xml';
$xmlReader = new \XMLReader();
$xmlReader->open('zip://' . $pathToStylesXmlFile);
while ($xmlReader->read()) {
if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'c') {
$cellElements[] = $xmlReader->expand();
}
}
return $cellElements;
}
/**
* @param string $expectedValue
* @param \DOMNode $parentElement
* @param string $childTagName
* @param string $attributeName
* @return void
*/
private function assertFirstChildHasAttributeEquals($expectedValue, $parentElement, $childTagName, $attributeName)
{
$this->assertEquals($expectedValue, $parentElement->getElementsByTagName($childTagName)->item(0)->getAttribute($attributeName));
}
/**
* @param int $expectedNumber
* @param \DOMNode $parentElement
* @param string $message
* @return void
*/
private function assertChildrenNumEquals($expectedNumber, $parentElement, $message)
{
$this->assertEquals($expectedNumber, $parentElement->getElementsByTagName('*')->length, $message);
}
/**
* @param \DOMNode $parentElement
* @param string $childTagName
* @return void
*/
private function assertChildExists($parentElement, $childTagName)
{
$this->assertEquals(1, $parentElement->getElementsByTagName($childTagName)->length);
}
}
@@ -0,0 +1,2 @@
csv--11,csv--12,csv--13
csv--31,csv--32,csv--33
1 csv--11 csv--12 csv--13
2 csv--31 csv--32 csv--33
@@ -0,0 +1,3 @@
csv--11,csv--12,csv--13
csv--21,csv--22,csv--23
csv--31,csv--32,csv--33
1 csv--11 csv--12 csv--13
2 csv--21 csv--22 csv--23
3 csv--31 csv--32 csv--33
Binary file not shown.
1 c�s�v�-�-�1�1� �c�s�v�-�-�1�2� �c�s�v�-�-�1�3�
2 �c�s�v�-�-�2�1� �c�s�v�-�-�2�2� �c�s�v�-�-�2�3�
3 �c�s�v�-�-�3�1� �c�s�v�-�-�3�2� �c�s�v�-�-�3�3�
Binary file not shown.
1 csv--11 csv--12 csv--13
2 csv--21 csv--22 csv--23
3 csv--31 csv--32 csv--33
Binary file not shown.
1 csv--11 csv--12 csv--13
2 csv--21 csv--22 csv--23
3 csv--31 csv--32 csv--33
Binary file not shown.
1 csv--11 csv--12 csv--13
2 csv--21 csv--22 csv--23
3 csv--31 csv--32 csv--33
Binary file not shown.
1 csv--11 csv--12 csv--13
2 csv--21 csv--22 csv--23
3 csv--31 csv--32 csv--33
+2 -1
View File
@@ -1,2 +1,3 @@
csv--11,csv--12,csv--13
csv--21,csv--22,csv--23
csv--21,csv--22,csv--23
csv--31,csv--32,csv--33
1 csv--11 csv--12 csv--13
2 csv--21 csv--22 csv--23
3 csv--31 csv--32 csv--33