Compare commits

..
32 Commits
Author SHA1 Message Date
Adrien Loison b4ace972e7 Update README.md 2015-08-22 19:39:43 -07:00
Adrien Loison c428daff5a Update README.md 2015-08-22 18:14:33 -07:00
Adrien Loison 5908443583 Update README.md 2015-08-22 18:13:36 -07:00
Adrien Loison 050672755d Update README.md 2015-08-22 12:37:24 -07:00
Adrien Loison e1928eb68a Merge pull request #93 from box/font_color
Add support for font color
2015-08-21 21:04:32 -07:00
Adrien Loison 9467b5a810 Add support for font color 2015-08-21 20:58:21 -07:00
Adrien Loison 2699ffbcae Merge pull request #92 from box/invalid_sheet_names_contd
Detection of invalid sheet name - continued
2015-08-21 16:51:02 -07:00
Adrien Loison 3559bc8834 Detection of invalid sheet name - continued
Invalid names can also be triggered by:
- character ":"
- single quote at the beginning of the name
- single quote at the end of the name

Introduced a StringHelper, wrapping multibyte strings functions
2015-08-21 16:44:13 -07:00
Adrien Loison 506d682e74 Merge pull request #91 from box/invalid_sheet_name_detection
Detection of invalid sheet name
2015-08-21 15:36:25 -07:00
Adrien Loison 7efab5576d Detection of invalid sheet name
Based on Excel requirements:
 - it should not be blank
 - it should not exceed 31 characters
 - it should not contain these characters: \ / ? * [ or ]
 - it should be unique
2015-08-21 15:21:36 -07:00
Adrien Loison 366f121eb0 Update README.md 2015-08-21 10:58:11 -07:00
Adrien Loison f8d0ac2682 Merge pull request #89 from box/fix_ios_issue
Fix OfficeImportErrorDomain issue on iOS
2015-08-20 20:32:41 -07:00
Adrien Loison 82377403ff Fix OfficeImportErrorDomain issue on iOS 2015-08-20 20:28:34 -07:00
Adrien Loison e0c84f77b1 Merge pull request #87 from box/use_travis_cache
Use Travis cache
2015-08-18 21:56:51 -07:00
Adrien Loison b78227c370 Use Travis cache
This is to avoid re-downloading all dependencies for every build.
2015-08-18 21:52:25 -07:00
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
32 changed files with 2152 additions and 107 deletions

No files matched your search

+4
View File
@@ -7,6 +7,10 @@ php:
- 7.0
- hhvm
cache:
directories:
- $HOME/.composer/cache
install:
- composer install --no-interaction --prefer-source
+62 -3
View File
@@ -2,7 +2,7 @@
[![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)
[![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)
@@ -10,6 +10,7 @@
Spout is a PHP library to read and write CSV and XLSX files, in a fast and scalable way.
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Installation
@@ -65,7 +66,7 @@ $reader = ReaderFactory::create(Type::CSV);
$reader->open($filePath);
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($reader->getRowIterator() as $row) {
foreach ($sheet->getRowIterator() as $row) {
// do stuff
}
}
@@ -83,7 +84,7 @@ $reader = ReaderFactory::create(Type::XLSX);
$reader->open($filePath);
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($reader->getRowIterator() as $row) {
foreach ($sheet->getRowIterator() as $row) {
// do stuff
}
}
@@ -145,6 +146,47 @@ 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;
use Box\Spout\Writer\Style\Color;
$style = (new StyleBuilder())
->setFontBold()
->setFontSize(15)
->setFontColor(Color::BLUE)
->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)`
| Font color | `StyleBuilder::setFontSize(Color::BLUE)`<br>`StyleBuilder::setFontSize(Color::rgb(0, 128, 255))`
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
#### Strings storage
XLSX files support different ways to store the string values:
@@ -161,6 +203,11 @@ $writer->setShouldUseInlineStrings(true); // default (and recommended) value
$writer->setShouldUseInlineStrings(false); // will use shared strings
```
> ##### Note on Apple Numbers and iOS support
>
> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings!
#### New sheet creation
It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
@@ -220,6 +267,16 @@ $sheet = $writer->getCurrentSheet();
$sheetName = $sheet->setName('My custom name');
```
> Please note that Excel has some restrictions on the sheet's name:
> * it must not be blank
> * it must not exceed 31 characters
> * it must not contain these characters: \ / ? * : [ or ]
> * it must not start or end with a single quote
> * it must be unique
>
> Handling these restrictions is the developer's responsibility. Spout does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in.
### Fluent interface
Because fluent interfaces are great, you can use them with Spout:
@@ -281,6 +338,8 @@ So the larger the file would be, the more memory would be consumed, preventing y
Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject.
You can also ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Copyright and License
Generated
+49 -49
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "3c0d36250693792d14bc19f14250fec6",
"hash": "0866be323931eaa6e9431b3bbf0a817a",
"packages": [],
"packages-dev": [
{
@@ -585,16 +585,16 @@
},
{
"name": "phpspec/prophecy",
"version": "v1.4.1",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373"
"reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373",
"reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7",
"reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7",
"shasum": ""
},
"require": {
@@ -641,20 +641,20 @@
"spy",
"stub"
],
"time": "2015-04-27 22:15:08"
"time": "2015-08-13 10:07:40"
},
{
"name": "phpunit/php-code-coverage",
"version": "2.1.9",
"version": "2.2.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "5bd48b86cd282da411bb80baac1398ce3fefac41"
"reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5bd48b86cd282da411bb80baac1398ce3fefac41",
"reference": "5bd48b86cd282da411bb80baac1398ce3fefac41",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2d7c03c0e4e080901b8f33b2897b0577be18a13c",
"reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c",
"shasum": ""
},
"require": {
@@ -662,7 +662,7 @@
"phpunit/php-file-iterator": "~1.3",
"phpunit/php-text-template": "~1.2",
"phpunit/php-token-stream": "~1.3",
"sebastian/environment": "~1.0",
"sebastian/environment": "^1.3.2",
"sebastian/version": "~1.0"
},
"require-dev": {
@@ -677,7 +677,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
"dev-master": "2.2.x-dev"
}
},
"autoload": {
@@ -703,7 +703,7 @@
"testing",
"xunit"
],
"time": "2015-07-26 12:54:47"
"time": "2015-08-04 03:42:39"
},
{
"name": "phpunit/php-file-iterator",
@@ -836,16 +836,16 @@
},
{
"name": "phpunit/php-token-stream",
"version": "1.4.3",
"version": "1.4.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9"
"reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/7a9b0969488c3c54fd62b4d504b3ec758fd005d9",
"reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3ab72c62e550370a6cd5dc873e1a04ab57562f5b",
"reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b",
"shasum": ""
},
"require": {
@@ -881,20 +881,20 @@
"keywords": [
"tokenizer"
],
"time": "2015-06-19 03:43:16"
"time": "2015-08-16 08:51:00"
},
{
"name": "phpunit/phpunit",
"version": "4.7.7",
"version": "4.8.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "9b97f9d807b862c2de2a36e86690000801c85724"
"reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b97f9d807b862c2de2a36e86690000801c85724",
"reference": "9b97f9d807b862c2de2a36e86690000801c85724",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b7417edaf28059ea63d86be941e6004dbfcc0cc",
"reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc",
"shasum": ""
},
"require": {
@@ -904,7 +904,7 @@
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.3.3",
"phpspec/prophecy": "~1.3,>=1.3.1",
"phpspec/prophecy": "^1.3.1",
"phpunit/php-code-coverage": "~2.1",
"phpunit/php-file-iterator": "~1.4",
"phpunit/php-text-template": "~1.2",
@@ -912,7 +912,7 @@
"phpunit/phpunit-mock-objects": "~2.3",
"sebastian/comparator": "~1.1",
"sebastian/diff": "~1.2",
"sebastian/environment": "~1.2",
"sebastian/environment": "~1.3",
"sebastian/exporter": "~1.2",
"sebastian/global-state": "~1.0",
"sebastian/version": "~1.0",
@@ -927,7 +927,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.7.x-dev"
"dev-master": "4.8.x-dev"
}
},
"autoload": {
@@ -953,24 +953,24 @@
"testing",
"xunit"
],
"time": "2015-07-13 11:28:34"
"time": "2015-08-19 09:20:57"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "2.3.6",
"version": "2.3.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42"
"reference": "5e2645ad49d196e020b85598d7c97e482725786a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/18dfbcb81d05e2296c0bcddd4db96cade75e6f42",
"reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5e2645ad49d196e020b85598d7c97e482725786a",
"reference": "5e2645ad49d196e020b85598d7c97e482725786a",
"shasum": ""
},
"require": {
"doctrine/instantiator": "~1.0,>=1.0.2",
"doctrine/instantiator": "^1.0.2",
"php": ">=5.3.3",
"phpunit/php-text-template": "~1.2",
"sebastian/exporter": "~1.2"
@@ -1009,7 +1009,7 @@
"mock",
"xunit"
],
"time": "2015-07-10 06:54:24"
"time": "2015-08-19 09:14:08"
},
{
"name": "scrutinizer/ocular",
@@ -1165,16 +1165,16 @@
},
{
"name": "sebastian/environment",
"version": "1.3.0",
"version": "1.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "4fe0a44cddd8cc19583a024bdc7374eb2fef0b87"
"reference": "6324c907ce7a52478eeeaede764f48733ef5ae44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4fe0a44cddd8cc19583a024bdc7374eb2fef0b87",
"reference": "4fe0a44cddd8cc19583a024bdc7374eb2fef0b87",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44",
"reference": "6324c907ce7a52478eeeaede764f48733ef5ae44",
"shasum": ""
},
"require": {
@@ -1211,7 +1211,7 @@
"environment",
"hhvm"
],
"time": "2015-07-26 06:42:57"
"time": "2015-08-03 06:14:51"
},
{
"name": "sebastian/exporter",
@@ -1420,16 +1420,16 @@
},
{
"name": "symfony/console",
"version": "v2.7.2",
"version": "v2.7.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/Console.git",
"reference": "8cf484449130cabfd98dcb4694ca9945802a21ed"
"reference": "d6cf02fe73634c96677e428f840704bfbcaec29e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/Console/zipball/8cf484449130cabfd98dcb4694ca9945802a21ed",
"reference": "8cf484449130cabfd98dcb4694ca9945802a21ed",
"url": "https://api.github.com/repos/symfony/Console/zipball/d6cf02fe73634c96677e428f840704bfbcaec29e",
"reference": "d6cf02fe73634c96677e428f840704bfbcaec29e",
"shasum": ""
},
"require": {
@@ -1473,11 +1473,11 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2015-07-09 16:07:40"
"time": "2015-07-28 15:18:12"
},
{
"name": "symfony/event-dispatcher",
"version": "v2.7.2",
"version": "v2.7.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/EventDispatcher.git",
@@ -1535,7 +1535,7 @@
},
{
"name": "symfony/process",
"version": "v2.7.2",
"version": "v2.7.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/Process.git",
@@ -1584,16 +1584,16 @@
},
{
"name": "symfony/yaml",
"version": "v2.7.2",
"version": "v2.7.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/Yaml.git",
"reference": "4bfbe0ed3909bfddd75b70c094391ec1f142f860"
"reference": "71340e996171474a53f3d29111d046be4ad8a0ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/Yaml/zipball/4bfbe0ed3909bfddd75b70c094391ec1f142f860",
"reference": "4bfbe0ed3909bfddd75b70c094391ec1f142f860",
"url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff",
"reference": "71340e996171474a53f3d29111d046be4ad8a0ff",
"shasum": ""
},
"require": {
@@ -1629,7 +1629,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2015-07-01 11:25:50"
"time": "2015-07-28 14:07:07"
}
],
"aliases": [],
+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();
@@ -167,6 +167,18 @@ class GlobalFunctionsHelper
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()
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace Box\Spout\Common\Helper;
/**
* Class StringHelper
* This class provides helper functions to work with strings and multibyte strings.
*
* @codeCoverageIgnore
*
* @package Box\Spout\Common\Helper
*/
class StringHelper
{
/** @var bool Whether the mbstring extension is loaded */
protected $hasMbstringSupport;
/**
*
*/
public function __construct()
{
$this->hasMbstringSupport = extension_loaded('mbstring');
}
/**
* Returns the length of the given string.
* It uses the multi-bytes function is available.
* @see strlen
* @see mb_strlen
*
* @param string $string
* @return int
*/
public function getStringLength($string)
{
return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string);
}
/**
* Returns the position of the first occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
* @see strpos
* @see mb_strpos
*
* @param string $char Needle
* @param string $string Haystack
* @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharFirstOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char);
return ($position !== false) ? $position : -1;
}
/**
* Returns the position of the last occurrence of the given character/substring within the given string.
* It uses the multi-bytes function is available.
* @see strrpos
* @see mb_strrpos
*
* @param string $char Needle
* @param string $string Haystack
* @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found
*/
public function getCharLastOccurrencePosition($char, $string)
{
$position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char);
return ($position !== false) ? $position : -1;
}
}
+37 -10
View File
@@ -40,6 +40,9 @@ class RowIterator implements IteratorInterface
/** @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
@@ -108,18 +111,25 @@ class RowIterator implements IteratorInterface
*/
public function next()
{
$lineData = null;
$this->hasReachedEndOfFile = feof($this->filePointer);
$lineData = false;
$this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
if (!$this->hasReachedEndOfFile) {
do {
$utf8EncodedLineData = $this->getNextUTF8EncodedLine();
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
} while ($lineData === false || ($lineData !== null && $this->isEmptyLine($lineData)));
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 && $lineData !== null) {
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;
}
}
}
@@ -128,28 +138,45 @@ class RowIterator implements IteratorInterface
* 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 The next line for the current file pointer, encoded in UTF-8
* @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->encodingHelper->attemptConversionFromUTF8("\n", $this->encoding);
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, 0, $encodedEOLDelimiter);
// Once the line has been read, it can be converted to UTF-8
$utf8EncodedLineData = $this->encodingHelper->attemptConversionToUTF8($encodedLineData, $this->encoding);
// 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 (count($lineData) === 1 && $lineData[0] === null);
return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null);
}
/**
+5 -3
View File
@@ -19,9 +19,11 @@ class XMLReader extends \XMLReader
* @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)
public function open($URI, $encoding = null, $options = 0)
{
$wasOpenSuccessful = false;
@@ -29,10 +31,10 @@ class XMLReader extends \XMLReader
// @link https://github.com/facebook/hhvm/issues/5779
if ($this->isRunningHHVM() && $this->isZipStream($URI)) {
if ($this->fileExistsWithinZip($URI)) {
$wasOpenSuccessful = parent::open($URI, null, LIBXML_NONET);
$wasOpenSuccessful = parent::open($URI, $encoding, $options|LIBXML_NONET);
}
} else {
$wasOpenSuccessful = parent::open($URI, null, LIBXML_NONET);
$wasOpenSuccessful = parent::open($URI, $encoding, $options|LIBXML_NONET);
}
return $wasOpenSuccessful;
+1 -1
View File
@@ -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);
+5 -5
View File
@@ -142,7 +142,7 @@ class RowIterator implements IteratorInterface
try {
while ($this->xmlReader->read()) {
if ($this->xmlReader->nodeType == XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_DIMENSION) {
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)) {
@@ -150,7 +150,7 @@ class RowIterator implements IteratorInterface
$this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
}
} else if ($this->xmlReader->nodeType == XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) {
} else if ($this->xmlReader->nodeType === XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) {
// Start of the row description
$isInsideRowTag = true;
@@ -163,7 +163,7 @@ class RowIterator implements IteratorInterface
}
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
} else if ($isInsideRowTag && $this->xmlReader->nodeType == XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_CELL) {
} 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);
@@ -171,14 +171,14 @@ class RowIterator implements IteratorInterface
$node = $this->xmlReader->expand();
$rowData[$currentColumnIndex] = $this->getCellValue($node);
} else if ($this->xmlReader->nodeType == XMLReader::END_ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) {
} 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) {
} 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;
}
+102 -6
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)
@@ -138,7 +154,6 @@ 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)
@@ -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)
@@ -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.
+2 -1
View File
@@ -69,10 +69,11 @@ class Writer 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) {
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Writer\Exception;
/**
* Class InvalidColorException
*
* @package Box\Spout\Writer\Exception
*/
class InvalidColorException extends WriterException
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Writer\Exception;
/**
* Class InvalidSheetNameException
*
* @package Box\Spout\Writer\Exception
*/
class InvalidSheetNameException extends WriterException
{
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Box\Spout\Writer\Style;
use Box\Spout\Writer\Exception\InvalidColorException;
/**
* Class Color
* This class provides constants and functions to work with colors
*
* @package Box\Spout\Writer\Style
*/
class Color
{
/** Standard colors - based on Office Online */
const BLACK = 'FF000000';
const WHITE = 'FFFFFFFF';
const RED = 'FFFF0000';
const DARK_RED = 'FFC00000';
const ORANGE = 'FFFFC000';
const YELLOW = 'FFFFFF00';
const LIGHT_GREEN = 'FF92D040';
const GREEN = 'FF00B050';
const LIGHT_BLUE = 'FF00B0E0';
const BLUE = 'FF0070C0';
const DARK_BLUE = 'FF002060';
const PURPLE = 'FF7030A0';
/**
* Returns an ARGB color from R, G and B values
* Alpha is assumed to always be 1
*
* @param int $red Red component, 0 - 255
* @param int $green Green component, 0 - 255
* @param int $blue Blue component, 0 - 255
* @return string ARGB color
*/
public static function rgb($red, $green, $blue)
{
self::throwIfInvalidColorComponentValue($red);
self::throwIfInvalidColorComponentValue($green);
self::throwIfInvalidColorComponentValue($blue);
return strtoupper(
'FF' .
self::convertColorComponentToHex($red) .
self::convertColorComponentToHex($green) .
self::convertColorComponentToHex($blue)
);
}
/**
* Throws an exception is the color component value is outside of bounds (0 - 255)
*
* @param int $colorComponent
* @return void
* @throws \Box\Spout\Writer\Exception\InvalidColorException
*/
protected static function throwIfInvalidColorComponentValue($colorComponent)
{
if (!is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) {
throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent");
}
}
/**
* Converts the color component to its corresponding hexadecimal value
*
* @param int $colorComponent Color component, 0 - 255
* @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d"
*/
protected static function convertColorComponentToHex($colorComponent)
{
return str_pad(dechex($colorComponent), 2, '0', 0);
}
}
+308
View File
@@ -0,0 +1,308 @@
<?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_COLOR = Color::BLACK;
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 color */
protected $fontColor = self::DEFAULT_FONT_COLOR;
/** @var bool Whether the font color property was set */
protected $hasSetFontColor = 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 getFontColor()
{
return $this->fontColor;
}
/**
* Sets the font color.
*
* @param string $fontColor ARGB color (@see Color)
* @return Style
*/
public function setFontColor($fontColor)
{
$this->fontColor = $fontColor;
$this->hasSetFontColor = 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->hasSetFontSize && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) {
$mergedStyle->setFontColor($baseStyle->getFontColor());
}
if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
$mergedStyle->setFontName($baseStyle->getFontName());
}
if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) {
$mergedStyle->setShouldWrapText();
}
return $mergedStyle;
}
}
+125
View File
@@ -0,0 +1,125 @@
<?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 color.
*
* @param string $fontColor ARGB color (@see Color)
* @return StyleBuilder
*/
public function setFontColor($fontColor)
{
$this->style->setFontColor($fontColor);
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;
}
}
+26
View File
@@ -40,6 +40,19 @@ interface WriterInterface
*/
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.
*
@@ -55,6 +68,19 @@ interface WriterInterface
*/
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.
@@ -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;
@@ -179,10 +180,10 @@ EOD;
*/
protected function createCoreXmlFile()
{
$createdDate = (new \DateTime())->format('c');
$createdDate = (new \DateTime())->format(\DateTime::W3C);
$coreXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">$createdDate</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">$createdDate</dcterms:modified>
<cp:revision>0</cp:revision>
@@ -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
*
@@ -0,0 +1,278 @@
<?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;
/** @var \Box\Spout\Writer\Style\Style $style */
foreach ($this->styleIdToStyleMappingTable as $style) {
$content .= ' <font>' . PHP_EOL;
$content .= ' <sz val="' . $style->getFontSize() . '"/>' . PHP_EOL;
$content .= ' <color rgb="' . $style->getFontColor() . '"/>' . PHP_EOL;
$content .= ' <name val="' . $style->getFontName() . '"/>' . 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 .= ' </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;
}
}
+19 -5
View File
@@ -5,6 +5,7 @@ namespace Box\Spout\Writer\XLSX\Internal;
use Box\Spout\Writer\Exception\SheetNotFoundException;
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;
/**
@@ -34,6 +35,9 @@ class Workbook
/** @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);
@@ -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();
+4 -2
View File
@@ -38,7 +38,7 @@ 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;
/**
@@ -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) {
+103 -2
View File
@@ -2,9 +2,12 @@
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Helper\StringHelper;
use Box\Spout\Writer\Exception\InvalidSheetNameException;
/**
* Class Sheet
* Represents a worksheet within a XLSX file
* External representation of a worksheet within a XLSX file
*
* @package Box\Spout\Writer\XLSX
*/
@@ -12,19 +15,32 @@ class Sheet
{
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
/** Sheet name should not exceed 31 characters */
const MAX_LENGTH_SHEET_NAME = 31;
/** @var array Invalid characters that cannot be contained in the sheet name */
private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', ':', '[', ']'];
/** @var array Associative array [SHEET_INDEX] => [SHEET_NAME] keeping track of sheets' name to enforce uniqueness */
protected static $SHEETS_NAME_USED = [];
/** @var int Index of the sheet, based on order of creation (zero-based) */
protected $index;
/** @var string Name of the sheet */
protected $name;
/** @var \Box\Spout\Common\Helper\StringHelper */
protected $stringHelper;
/**
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
*/
public function __construct($sheetIndex)
{
$this->index = $sheetIndex;
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1);
$this->stringHelper = new StringHelper();
$this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1));
}
/**
@@ -44,12 +60,97 @@ class Sheet
}
/**
* Sets the name of the sheet. Note that Excel has some restrictions on the name:
* - it should not be blank
* - it should not exceed 31 characters
* - it should not contain these characters: \ / ? * : [ or ]
* - it should be unique
*
* @param string $name Name of the sheet
* @return Sheet
* @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
*/
public function setName($name)
{
if (!$this->isNameValid($name)) {
$errorMessage = "The sheet's name is invalid. It did not meet at least one of these requirements:\n";
$errorMessage .= " - It should not be blank\n";
$errorMessage .= " - It should not exceed 31 characters\n";
$errorMessage .= " - It should not contain these characters: \\ / ? * : [ or ]\n";
$errorMessage .= " - It should be unique";
throw new InvalidSheetNameException($errorMessage);
}
$this->name = $name;
self::$SHEETS_NAME_USED[$this->index] = $name;
return $this;
}
/**
* Returns whether the given sheet's name is valid.
* @see Sheet::setName for validity rules.
*
* @param string $name
* @return bool TRUE if the name is valid, FALSE otherwise.
*/
protected function isNameValid($name)
{
if (!is_string($name)) {
return false;
}
$nameLength = $this->stringHelper->getStringLength($name);
return (
$nameLength > 0 &&
$nameLength <= self::MAX_LENGTH_SHEET_NAME &&
!$this->doesContainInvalidCharacters($name) &&
$this->isNameUnique($name) &&
!$this->doesStartOrEndWithSingleQuote($name)
);
}
/**
* Returns whether the given name contains at least one invalid character.
* @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list.
*
* @param string $name
* @return bool TRUE if the name contains invalid characters, FALSE otherwise.
*/
protected function doesContainInvalidCharacters($name)
{
return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name);
}
/**
* Returns whether the given name starts or ends with a single quote
*
* @param string $name
* @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise.
*/
protected function doesStartOrEndWithSingleQuote($name)
{
$startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0);
$endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1));
return ($startsWithSingleQuote || $endsWithSingleQuote);
}
/**
* Returns whether the given name is unique.
*
* @param string $name
* @return bool TRUE if the name is unique, FALSE otherwise.
*/
protected function isNameUnique($name)
{
foreach (self::$SHEETS_NAME_USED as $sheetIndex => $sheetName) {
if ($sheetIndex !== $this->index && $sheetName === $name) {
return false;
}
}
return true;
}
}
+22 -3
View File
@@ -4,6 +4,7 @@ namespace Box\Spout\Writer\XLSX;
use Box\Spout\Writer\AbstractWriter;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\Style\StyleBuilder;
use Box\Spout\Writer\XLSX\Internal\Workbook;
/**
@@ -14,6 +15,10 @@ use Box\Spout\Writer\XLSX\Internal\Workbook;
*/
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';
@@ -74,7 +79,7 @@ class Writer 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();
}
}
@@ -161,14 +166,28 @@ class Writer 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();
}
/**
+16 -2
View File
@@ -115,11 +115,25 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
}
/**
* @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()
public function testReadShouldSkipEmptyLines($fileName)
{
$allRows = $this->getAllRowsForFile('csv_with_empty_line.csv');
$allRows = $this->getAllRowsForFile($fileName);
$expectedRows = [
['csv--11', 'csv--12', 'csv--13'],
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace Box\Spout\Writer\Style;
/**
* Class ColorTest
*
* @package Box\Spout\Writer\Style
*/
class ColorTest extends \PHPUnit_Framework_TestCase
{
/**
* @return array
*/
public function dataProviderForTestRGB()
{
return [
[0, 0, 0, Color::BLACK],
[255, 255, 255, Color::WHITE],
[255, 0, 0, Color::RED],
[192, 0, 0, Color::DARK_RED],
[255, 192, 0, Color::ORANGE],
[255, 255, 0, Color::YELLOW],
[146, 208, 64, Color::LIGHT_GREEN],
[0, 176, 80, Color::GREEN],
[0, 176, 224, Color::LIGHT_BLUE],
[0, 112, 192, Color::BLUE],
[0, 32, 96, Color::DARK_BLUE],
[112, 48, 160, Color::PURPLE],
[0, 0, 0, 'FF000000'],
[255, 255, 255, 'FFFFFFFF'],
[255, 0, 0, 'FFFF0000'],
[0, 128, 0, 'FF008000'],
[0, 255, 0, 'FF00FF00'],
[0, 0, 255, 'FF0000FF'],
[128, 22, 43, 'FF80162B'],
];
}
/**
* @dataProvider dataProviderForTestRGB
*
* @param int $red
* @param int $green
* @param int $blue
* @param string $expectedColor
* @return void
*/
public function testRGB($red, $green, $blue, $expectedColor)
{
$color = Color::rgb($red, $green, $blue);
$this->assertEquals($expectedColor, $color);
}
/**
* @return array
*/
public function dataProviderForTestRGBAInvalidColorComponents()
{
return [
[-1, 0, 0],
[0, -1, 0],
[0, 0, -1],
[999, 0, 0],
[0, 999, 0],
[0, 0, 999],
[null, 0, 0],
[0, null, 0],
[0, 0, null],
['1', 0, 0],
[0, '1', 0],
[0, 0, '1'],
[true, 0, 0],
[0, true, 0],
[0, 0, true],
];
}
/**
* @dataProvider dataProviderForTestRGBAInvalidColorComponents
* @expectedException \Box\Spout\Writer\Exception\InvalidColorException
*
* @param int $red
* @param int $green
* @param int $blue
* @return void
*/
public function testRGBInvalidColorComponents($red, $green, $blue)
{
Color::rgb($red, $green, $blue);
}
}
+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());
}
}
@@ -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());
}
}
+99 -7
View File
@@ -20,7 +20,7 @@ class SheetTest extends \PHPUnit_Framework_TestCase
*/
public function testGetSheetIndex()
{
$sheets = $this->writeDataAndReturnSheets('test_get_sheet_index.xlsx');
$sheets = $this->writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_index.xlsx');
$this->assertEquals(2, count($sheets), '2 sheets should have been created');
$this->assertEquals(0, $sheets[0]->getIndex(), 'The first sheet should be index 0');
@@ -32,7 +32,7 @@ class SheetTest extends \PHPUnit_Framework_TestCase
*/
public function testGetSheetName()
{
$sheets = $this->writeDataAndReturnSheets('test_get_sheet_name.xlsx');
$sheets = $this->writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_name.xlsx');
$this->assertEquals(2, count($sheets), '2 sheets should have been created');
$this->assertEquals('Sheet1', $sheets[0]->getName(), 'Invalid name for the first sheet');
@@ -45,27 +45,118 @@ class SheetTest extends \PHPUnit_Framework_TestCase
public function testSetSheetNameShouldCreateSheetWithCustomName()
{
$fileName = 'test_set_name_should_create_sheet_with_custom_name.xlsx';
$customSheetName = 'CustomName';
$this->writeDataAndReturnSheetWithCustomName($fileName, $customSheetName);
$this->assertSheetNameEquals($customSheetName, $fileName, "The sheet name should have been changed to '$customSheetName'");
}
/**
* @return array
*/
public function dataProviderForInvalidSheetNames()
{
return [
[null],
[21],
[''],
['this title exceeds the 31 characters limit'],
['Illegal \\'],
['Illegal /'],
['Illegal ?'],
['Illegal *'],
['Illegal :'],
['Illegal ['],
['Illegal ]'],
['\'Illegal start'],
['Illegal end\''],
];
}
/**
* @dataProvider dataProviderForInvalidSheetNames
* @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException
*
* @param string $customSheetName
* @return void
*/
public function testSetSheetNameShouldThrowOnInvalidName($customSheetName)
{
$fileName = 'test_set_name_with_invalid_name_should_throw_exception.xlsx';
$this->writeDataAndReturnSheetWithCustomName($fileName, $customSheetName);
}
/**
* @return void
*/
public function testSetSheetNameShouldNotThrowWhenSettingSameNameAsCurrentOne()
{
$fileName = 'test_set_name_with_same_as_current.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
$customSheetName = 'CustomName';
$customSheetName = 'Sheet name';
$sheet = $writer->getCurrentSheet();
$sheet->setName($customSheetName);
$sheet->setName($customSheetName);
$writer->addRow(['xlsx--11', 'xlsx--12']);
$writer->close();
$this->assertSheetNameEquals($customSheetName, $resourcePath, "The sheet name should have been changed to '$customSheetName'");
$this->assertSheetNameEquals($customSheetName, $fileName, "The sheet name should have been changed to '$customSheetName'");
}
/**
* @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException
* @return void
*/
public function testSetSheetNameShouldThrowWhenNameIsAlreadyUsed()
{
$fileName = 'test_set_name_with_non_unique_name.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
$customSheetName = 'Sheet name';
$sheet = $writer->getCurrentSheet();
$sheet->setName($customSheetName);
$writer->addNewSheetAndMakeItCurrent();
$sheet = $writer->getCurrentSheet();
$sheet->setName($customSheetName);
}
/**
* @param string $fileName
* @param string $sheetName
* @return Sheet
*/
private function writeDataAndReturnSheetWithCustomName($fileName, $sheetName)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
$sheet = $writer->getCurrentSheet();
$sheet->setName($sheetName);
$writer->addRow(['xlsx--11', 'xlsx--12']);
$writer->close();
}
/**
* @param string $fileName
* @return Sheet[]
*/
private function writeDataAndReturnSheets($fileName)
private function writeDataToMulitpleSheetsAndReturnSheets($fileName)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
@@ -85,12 +176,13 @@ class SheetTest extends \PHPUnit_Framework_TestCase
/**
* @param string $expectedName
* @param string $resourcePath
* @param string $fileName
* @param string $message
* @return void
*/
private function assertSheetNameEquals($expectedName, $resourcePath, $message = '')
private function assertSheetNameEquals($expectedName, $fileName, $message = '')
{
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToWorkbookFile = $resourcePath . '#xl/workbook.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
+2 -6
View File
@@ -9,7 +9,7 @@ use Box\Spout\Writer\WriterFactory;
/**
* Class XLSXTest
*
* @package Box\Spout\Writer
* @package Box\Spout\Writer\XLSX
*/
class WriterTest extends \PHPUnit_Framework_TestCase
{
@@ -26,8 +26,6 @@ class WriterTest extends \PHPUnit_Framework_TestCase
$writer = WriterFactory::create(Type::XLSX);
@$writer->openToFile($filePath);
$writer->addRow(['xlsx--11', 'xlsx--12']);
$writer->close();
}
/**
@@ -37,17 +35,15 @@ class WriterTest 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();
}
/**
@@ -0,0 +1,362 @@
<?php
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\Style\Color;
use Box\Spout\Writer\Style\Style;
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)
->setFontColor(Color::RED)
->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(3, $defaultFontElement, 'The default font should only have 3 properties.');
$this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $defaultFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals(Style::DEFAULT_FONT_COLOR, $defaultFontElement, 'color', 'rgb');
$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(7, $secondFontElement, 'The font should only have 7 properties (4 custom styles + 3 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(Style::DEFAULT_FONT_COLOR, $secondFontElement, 'color', 'rgb');
$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(3, $thirdFontElement, 'The font should only have 3 properties.');
$this->assertFirstChildHasAttributeEquals('15', $thirdFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals(Color::RED, $thirdFontElement, 'color', 'rgb');
$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