Compare commits

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

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

For now, the style properties supported are:
 - bold
 - italic
 - underline
 - strikethrough
 - font size
 - font name
 - wrap text (alignment)
2015-08-07 20:39:17 -07:00
Adrien Loison 0104714cbd Merge pull request #79 from jtreminio/master
getRowIterator() is on $sheet variable
2015-07-29 20:44:56 -07:00
Juan Treminio 611a091290 getRowIterator() is on $sheet variable 2015-07-29 22:04:47 -05:00
Adrien Loison 40a86c4b6c Merge pull request #77 from box/fix_csv_reader_empty_last_line
Fix CSV reader when last line is empty
2015-07-29 10:27:44 -07:00
Adrien Loison 8a3b895afc Fix CSV reader when last line is empty
If the last line was empty, it would create an infinite loop...
2015-07-29 10:17:51 -07:00
Adrien Loison e3f7ecfa64 Merge pull request #78 from box/fix_xml_reader_open_overriding
Fix XMLReader open() overriding
2015-07-29 10:17:34 -07:00
Adrien Loison 93d7aafe8b Fix XMLReader open() overriding
This is to avoid a warning in PHP7 (and also because that's how it should be!)
2015-07-29 09:59:33 -07:00
Adrien Loison dc53b6aa20 Update README.md
Change Travis badge from PNG to SVG
2015-07-27 22:26:36 -07:00
23 changed files with 1558 additions and 47 deletions

No files matched your search

+41 -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)
@@ -65,7 +65,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 +83,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 +145,44 @@ The writer always generate CSV files encoded in UTF-8, with a BOM.
### Configuring the XLSX writer
#### Row styling
It is possible to apply some formatting options to a row. Spout supports fonts as well as alignment styles.
```php
use Box\Spout\Common\Type;
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Writer\Style\StyleBuilder;
$style = (new StyleBuilder())
->setFontBold()
->setFontSize(15)
->setShouldWrapText()
->build();
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($filePath);
$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row
$writer->addRow($otherSingleRow); // no style will be applied
$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows
$writer->close();
```
Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
Category | Property | API
----------|---------------|---------------------------------------
Font | Bold | `StyleBuilder::setFontBold()`
| Italic | `StyleBuilder::setFontItalic()`
| Underline | `StyleBuilder::setFontUnderline()`
| Strikethrough | `StyleBuilder::setFontStrikethrough()`
| Font name | `StyleBuilder::setFontName('Arial')`
| Font size | `StyleBuilder::setFontSize(14)`
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
#### Strings storage
XLSX files support different ways to store the string values:
+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()
+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) {
+277
View File
@@ -0,0 +1,277 @@
<?php
namespace Box\Spout\Writer\Style;
/**
* Class Style
* Represents a style to be applied to a cell
*
* @package Box\Spout\Writer\Style
*/
class Style
{
/** Default font values */
const DEFAULT_FONT_SIZE = 11;
const DEFAULT_FONT_NAME = 'Arial';
/** @var int|null Style ID */
protected $id = null;
/** @var bool Whether the font should be bold */
protected $fontBold = false;
/** @var bool Whether the bold property was set */
protected $hasSetFontBold = false;
/** @var bool Whether the font should be italic */
protected $fontItalic = false;
/** @var bool Whether the italic property was set */
protected $hasSetFontItalic = false;
/** @var bool Whether the font should be underlined */
protected $fontUnderline = false;
/** @var bool Whether the underline property was set */
protected $hasSetFontUnderline = false;
/** @var bool Whether the font should be struck through */
protected $fontStrikethrough = false;
/** @var bool Whether the strikethrough property was set */
protected $hasSetFontStrikethrough = false;
/** @var int Font size */
protected $fontSize = self::DEFAULT_FONT_SIZE;
/** @var bool Whether the font size property was set */
protected $hasSetFontSize = false;
/** @var string Font name */
protected $fontName = self::DEFAULT_FONT_NAME;
/** @var bool Whether the font name property was set */
protected $hasSetFontName = false;
/** @var bool Whether specific font properties should be applied */
protected $shouldApplyFont = false;
/** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */
protected $shouldWrapText = false;
/** @var bool Whether the wrap text property was set */
protected $hasSetWrapText = false;
/**
* @return int|null
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
* @return Style
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return boolean
*/
public function isFontBold()
{
return $this->fontBold;
}
/**
* @return Style
*/
public function setFontBold()
{
$this->fontBold = true;
$this->hasSetFontBold = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function isFontItalic()
{
return $this->fontItalic;
}
/**
* @return Style
*/
public function setFontItalic()
{
$this->fontItalic = true;
$this->hasSetFontItalic = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function isFontUnderline()
{
return $this->fontUnderline;
}
/**
* @return Style
*/
public function setFontUnderline()
{
$this->fontUnderline = true;
$this->hasSetFontUnderline = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function isFontStrikethrough()
{
return $this->fontStrikethrough;
}
/**
* @return Style
*/
public function setFontStrikethrough()
{
$this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return int
*/
public function getFontSize()
{
return $this->fontSize;
}
/**
* @param int $fontSize Font size, in pixels
* @return Style
*/
public function setFontSize($fontSize)
{
$this->fontSize = $fontSize;
$this->hasSetFontSize = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return string
*/
public function getFontName()
{
return $this->fontName;
}
/**
* @param string $fontName Name of the font to use
* @return Style
*/
public function setFontName($fontName)
{
$this->fontName = $fontName;
$this->hasSetFontName = true;
$this->shouldApplyFont = true;
return $this;
}
/**
* @return boolean
*/
public function shouldWrapText()
{
return $this->shouldWrapText;
}
/**
* @return Style
*/
public function setShouldWrapText()
{
$this->shouldWrapText = true;
$this->hasSetWrapText = true;
return $this;
}
/**
* @return bool Whether specific font properties should be applied
*/
public function shouldApplyFont()
{
return $this->shouldApplyFont;
}
/**
* Serializes the style for future comparison with other styles.
* The ID is excluded from the comparison, as we only care about
* actual style properties.
*
* @return string The serialized style
*/
public function serialize()
{
// In order to be able to properly compare style, set static ID value
$currentId = $this->id;
$this->setId(0);
$serializedStyle = serialize($this);
$this->setId($currentId);
return $serializedStyle;
}
/**
* Merges the current style with the given style, using the given style as a base. This means that:
* - if current style and base style both have property A set, use current style property's value
* - if current style has property A set but base style does not, use current style property's value
* - if base style has property A set but current style does not, use base style property's value
*
* @NOTE: This function returns a new style.
*
* @param Style $baseStyle
* @return Style New style corresponding to the merge of the 2 styles
*/
public function mergeWith($baseStyle)
{
$mergedStyle = clone $this;
if (!$this->hasSetFontBold && $baseStyle->isFontBold()) {
$mergedStyle->setFontBold();
}
if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) {
$mergedStyle->setFontItalic();
}
if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) {
$mergedStyle->setFontUnderline();
}
if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) {
$mergedStyle->setFontStrikethrough();
}
if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
$mergedStyle->setFontSize($baseStyle->getFontSize());
}
if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
$mergedStyle->setFontName($baseStyle->getFontName());
}
if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) {
$mergedStyle->setShouldWrapText();
}
return $mergedStyle;
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace Box\Spout\Writer\Style;
/**
* Class StyleBuilder
* Builder to create new styles
*
* @package Box\Spout\Writer\Style
*/
class StyleBuilder
{
/** @var Style Style to be created */
protected $style;
/**
*
*/
public function __construct()
{
$this->style = new Style();
}
/**
* Makes the font bold.
*
* @return StyleBuilder
*/
public function setFontBold()
{
$this->style->setFontBold();
return $this;
}
/**
* Makes the font italic.
*
* @return StyleBuilder
*/
public function setFontItalic()
{
$this->style->setFontItalic();
return $this;
}
/**
* Makes the font underlined.
*
* @return StyleBuilder
*/
public function setFontUnderline()
{
$this->style->setFontUnderline();
return $this;
}
/**
* Makes the font struck through.
*
* @return StyleBuilder
*/
public function setFontStrikethrough()
{
$this->style->setFontStrikethrough();
return $this;
}
/**
* Sets the font size.
*
* @param int $fontSize Font size, in pixels
* @return StyleBuilder
*/
public function setFontSize($fontSize)
{
$this->style->setFontSize($fontSize);
return $this;
}
/**
* Sets the font name.
*
* @param string $fontName Name of the font to use
* @return StyleBuilder
*/
public function setFontName($fontName)
{
$this->style->setFontName($fontName);
return $this;
}
/**
* Makes the text wrap in the cell if it's too long or
* on multiple lines.
*
* @return StyleBuilder
*/
public function setShouldWrapText()
{
$this->style->setShouldWrapText();
return $this;
}
/**
* Returns the configured style. The style is cached and can be reused.
*
* @return Style
*/
public function build()
{
return $this->style;
}
}
+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;
@@ -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,275 @@
<?php
namespace Box\Spout\Writer\XLSX\Helper;
/**
* Class StyleHelper
* This class provides helper functions to manage styles
*
* @package Box\Spout\Writer\XLSX\Helper
*/
class StyleHelper
{
/** @var array [SERIALIZED_STYLE] => [STYLE_ID] mapping table, keeping track of the registered styles */
protected $serializedStyleToStyleIdMappingTable = [];
/** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */
protected $styleIdToStyleMappingTable = [];
/**
* @param \Box\Spout\Writer\Style\Style $defaultStyle
*/
public function __construct($defaultStyle)
{
// This ensures that the default style is the first one to be registered
$this->registerStyle($defaultStyle);
}
/**
* Registers the given style as a used style.
* Duplicate styles won't be registered more than once.
*
* @param \Box\Spout\Writer\Style\Style $style The style to be registered
* @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID.
*/
public function registerStyle($style)
{
$serializedStyle = $style->serialize();
if (!$this->hasStyleAlreadyBeenRegistered($style)) {
$nextStyleId = count($this->serializedStyleToStyleIdMappingTable);
$style->setId($nextStyleId);
$this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
$this->styleIdToStyleMappingTable[$nextStyleId] = $style;
}
return $this->getStyleFromSerializedStyle($serializedStyle);
}
/**
* Returns whether the given style has already been registered.
*
* @param \Box\Spout\Writer\Style\Style $style
* @return bool
*/
protected function hasStyleAlreadyBeenRegistered($style)
{
$serializedStyle = $style->serialize();
return array_key_exists($serializedStyle, $this->serializedStyleToStyleIdMappingTable);
}
/**
* Returns the registered style associated to the given serialization.
*
* @param string $serializedStyle The serialized style from which the actual style should be fetched from
* @return \Box\Spout\Writer\Style\Style
*/
protected function getStyleFromSerializedStyle($serializedStyle)
{
$styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle];
return $this->styleIdToStyleMappingTable[$styleId];
}
/**
* Apply additional styles if the given row needs it.
* Typically, set "wrap text" if a cell contains a new line.
*
* @param \Box\Spout\Writer\Style\Style $style The original style
* @param array $dataRow The row the style will be applied to
* @return \Box\Spout\Writer\Style\Style The updated style
*/
public function applyExtraStylesIfNeeded($style, $dataRow)
{
$updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow);
return $updatedStyle;
}
/**
* Set the "wrap text" option if a cell of the given row contains a new line.
*
* @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines
* are ignored even when the "wrap text" option is set. This only occurs with
* inline strings (shared strings do work fine).
* A workaround would be to encode "\n" as "_x000D_" but it does not work
* on the Windows version of Excel...
*
* @param \Box\Spout\Writer\Style\Style $style The original style
* @param array $dataRow The row the style will be applied to
* @return \Box\Spout\Writer\Style\Style The eventually updated style
*/
protected function applyWrapTextIfCellContainsNewLine($style, $dataRow)
{
// if the "wrap text" option is already set, no-op
if ($style->shouldWrapText()) {
return $style;
}
foreach ($dataRow as $cell) {
if (is_string($cell) && strpos($cell, "\n") !== false) {
$style->setShouldWrapText();
break;
}
}
return $style;
}
/**
* Returns the content of the "styles.xml" file, given a list of styles.
* @return string
*/
public function getStylesXMLFileContent()
{
$content = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
EOD;
$content .= $this->getFontsSectionContent();
$content .= $this->getFillsSectionContent();
$content .= $this->getBordersSectionContent();
$content .= $this->getCellStyleXfsSectionContent();
$content .= $this->getCellXfsSectionContent();
$content .= $this->getCellStylesSectionContent();
$content .= <<<EOD
</styleSheet>
EOD;
return $content;
}
/**
* Returns the content of the "<fonts>" section.
* @return string
*/
protected function getFontsSectionContent()
{
$content = ' <fonts count="' . count($this->styleIdToStyleMappingTable) . '">' . PHP_EOL;
foreach ($this->styleIdToStyleMappingTable as $style) {
$content .= ' <font>' . PHP_EOL;
if ($style->isFontBold()) {
$content .= ' <b/>' . PHP_EOL;
}
if ($style->isFontItalic()) {
$content .= ' <i/>' . PHP_EOL;
}
if ($style->isFontUnderline()) {
$content .= ' <u/>' . PHP_EOL;
}
if ($style->isFontStrikethrough()) {
$content .= ' <strike/>' . PHP_EOL;
}
$content .= ' <sz val="' . $style->getFontSize() . '"/>' . PHP_EOL;
$content .= ' <name val="' . $style->getFontName() . '"/>' . PHP_EOL;
$content .= ' </font>' . PHP_EOL;
}
$content .= ' </fonts>' . PHP_EOL;
return $content;
}
/**
* Returns the content of the "<fills>" section.
*
* @return string
*/
protected function getFillsSectionContent()
{
return <<<EOD
<fills count="1">
<fill>
<patternFill patternType="none"/>
</fill>
</fills>
EOD;
}
/**
* Returns the content of the "<borders>" section.
*
* @return string
*/
protected function getBordersSectionContent()
{
return <<<EOD
<borders count="1">
<border>
<left/>
<right/>
<top/>
<bottom/>
<diagonal/>
</border>
</borders>
EOD;
}
/**
* Returns the content of the "<cellStyleXfs>" section.
*
* @return string
*/
protected function getCellStyleXfsSectionContent()
{
return <<<EOD
<cellStyleXfs count="1">
<xf borderId="0" fillId="0" fontId="0" numFmtId="0"/>
</cellStyleXfs>
EOD;
}
/**
* Returns the content of the "<cellXfs>" section.
* @return string
*/
protected function getCellXfsSectionContent()
{
$content = ' <cellXfs count="' . count($this->styleIdToStyleMappingTable) . '">' . PHP_EOL;
foreach ($this->styleIdToStyleMappingTable as $styleId => $style) {
$content .= ' <xf numFmtId="0" fontId="' . $styleId . '" fillId="0" borderId="0" xfId="0"';
if ($style->shouldApplyFont()) {
$content .= ' applyFont="1"';
}
if ($style->shouldWrapText()) {
$content .= ' applyAlignment="1">' . PHP_EOL;
$content .= ' <alignment wrapText="1"/>' . PHP_EOL;
$content .= ' </xf>' . PHP_EOL;
} else {
$content .= '/>' . PHP_EOL;
}
}
$content .= ' </cellXfs>' . PHP_EOL;
return $content;
}
/**
* Returns the content of the "<cellStyles>" section.
*
* @return string
*/
protected function getCellStylesSectionContent()
{
return <<<EOD
<cellStyles count="1">
<cellStyle builtinId="0" name="Normal" xfId="0"/>
</cellStyles>
EOD;
}
}
+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) {
+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'],
+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());
}
}
+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,356 @@
<?php
namespace Box\Spout\Writer\XLSX;
use Box\Spout\Common\Type;
use Box\Spout\TestUsingResource;
use Box\Spout\Writer\Style\StyleBuilder;
use Box\Spout\Writer\WriterFactory;
/**
* Class WriterWithStyleTest
*
* @package Box\Spout\Writer\XLSX
*/
class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
{
use TestUsingResource;
/** @var \Box\Spout\Writer\Style\Style */
protected $defaultStyle;
/**
* @return void
*/
public function setUp()
{
$this->defaultStyle = (new StyleBuilder())->build();
}
/**
* @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException
*/
public function testAddRowWithStyleShouldThrowExceptionIfCallAddRowBeforeOpeningWriter()
{
$writer = WriterFactory::create(Type::XLSX);
$writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $this->defaultStyle);
}
/**
* @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException
*/
public function testAddRowWithStyleShouldThrowExceptionIfCalledBeforeOpeningWriter()
{
$writer = WriterFactory::create(Type::XLSX);
$writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $this->defaultStyle);
}
/**
* @return array
*/
public function dataProviderForInvalidStyle()
{
return [
['style'],
[new \stdClass()],
[null],
];
}
/**
* @dataProvider dataProviderForInvalidStyle
* @expectedException \Box\Spout\Common\Exception\InvalidArgumentException
*
* @param \Box\Spout\Writer\Style\Style $style
*/
public function testAddRowWithStyleShouldThrowExceptionIfInvalidStyleGiven($style)
{
$fileName = 'test_add_row_with_style_should_throw_exception.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
$writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $style);
}
/**
* @dataProvider dataProviderForInvalidStyle
* @expectedException \Box\Spout\Common\Exception\InvalidArgumentException
*
* @param \Box\Spout\Writer\Style\Style $style
*/
public function testAddRowsWithStyleShouldThrowExceptionIfInvalidStyleGiven($style)
{
$fileName = 'test_add_row_with_style_should_throw_exception.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterFactory::create(Type::XLSX);
$writer->openToFile($resourcePath);
$writer->addRowsWithStyle([['xlsx--11', 'xlsx--12']], $style);
}
/**
* @return void
*/
public function testAddRowWithStyleShouldListAllUsedFontsInCreateStylesXmlFile()
{
$fileName = 'test_add_row_with_style_should_list_all_used_fonts.xlsx';
$dataRows = [
['xlsx--11', 'xlsx--12'],
['xlsx--21', 'xlsx--22'],
];
$style = (new StyleBuilder())
->setFontBold()
->setFontItalic()
->setFontUnderline()
->setFontStrikethrough()
->build();
$style2 = (new StyleBuilder())
->setFontSize(15)
->setFontName('Arial')
->build();
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]);
$fontsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fonts');
$this->assertEquals(3, $fontsDomElement->getAttribute('count'), 'There should be 3 fonts, including the default one.');
$fontElements = $fontsDomElement->getElementsByTagName('font');
$this->assertEquals(3, $fontElements->length, 'There should be 3 associated "font" elements, including the default one.');
// First font should be the default one
$defaultFontElement = $fontElements->item(0);
$this->assertChildrenNumEquals(2, $defaultFontElement, 'The default font should only have 2 properties.');
$this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $defaultFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $defaultFontElement, 'name', 'val');
// Second font should contain data from the first created style
$secondFontElement = $fontElements->item(1);
$this->assertChildrenNumEquals(6, $secondFontElement, 'The font should only have 6 properties (4 custom styles + 2 default styles).');
$this->assertChildExists($secondFontElement, 'b');
$this->assertChildExists($secondFontElement, 'i');
$this->assertChildExists($secondFontElement, 'u');
$this->assertChildExists($secondFontElement, 'strike');
$this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $secondFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $secondFontElement, 'name', 'val');
// Third font should contain data from the second created style
$thirdFontElement = $fontElements->item(2);
$this->assertChildrenNumEquals(2, $thirdFontElement, 'The font should only have 2 properties.');
$this->assertFirstChildHasAttributeEquals('15', $thirdFontElement, 'sz', 'val');
$this->assertFirstChildHasAttributeEquals('Arial', $thirdFontElement, 'name', 'val');
}
/**
* @return void
*/
public function testAddRowWithStyleShouldAddWrapTextAlignmentInfoInStylesXmlFileIfSpecified()
{
$fileName = 'test_add_row_with_style_should_add_wrap_text_alignment.xlsx';
$dataRows = [
['xlsx--11', 'xlsx--12'],
];
$style = (new StyleBuilder())->setShouldWrapText()->build();
$this->writeToXLSXFile($dataRows, $fileName,$style);
$cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
$xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1);
$this->assertEquals(1, $xfElement->getAttribute('applyAlignment'));
$this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText');
}
/**
* @return void
*/
public function testAddRowWithStyleShouldApplyStyleToCells()
{
$fileName = 'test_add_row_with_style_should_apply_style_to_cells.xlsx';
$dataRows = [
['xlsx--11'],
['xlsx--21'],
['xlsx--31'],
];
$style = (new StyleBuilder())->setFontBold()->build();
$style2 = (new StyleBuilder())->setFontSize(15)->build();
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style, $style2, null]);
$cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName);
$this->assertEquals(3, count($cellDomElements), 'There should be 3 cells.');
$this->assertEquals('1', $cellDomElements[0]->getAttribute('s'));
$this->assertEquals('2', $cellDomElements[1]->getAttribute('s'));
$this->assertEquals('0', $cellDomElements[2]->getAttribute('s'));
}
/**
* @return void
*/
public function testAddRowWithStyleShouldReuseDuplicateStyles()
{
$fileName = 'test_add_row_with_style_should_reuse_duplicate_styles.xlsx';
$dataRows = [
['xlsx--11'],
['xlsx--21'],
];
$style = (new StyleBuilder())->setFontBold()->build();
$this->writeToXLSXFile($dataRows, $fileName, $style);
$cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName);
$this->assertEquals('1', $cellDomElements[0]->getAttribute('s'));
$this->assertEquals('1', $cellDomElements[1]->getAttribute('s'));
}
/**
* @return void
*/
public function testAddRowWithStyleShouldApplyWrapTextIfCellContainsNewLine()
{
$fileName = 'test_add_row_with_style_should_apply_wrap_text_if_new_lines.xlsx';
$dataRows = [
["xlsx--11\nxlsx--11"],
['xlsx--21'],
];
$this->writeToXLSXFile($dataRows, $fileName, $this->defaultStyle);
$cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
$xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1);
$this->assertEquals(1, $xfElement->getAttribute('applyAlignment'));
$this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText');
}
/**
* @param array $allRows
* @param string $fileName
* @param \Box\Spout\Writer\Style\Style $style
* @return Writer
*/
private function writeToXLSXFile($allRows, $fileName, $style)
{
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings(true);
$writer->openToFile($resourcePath);
$writer->addRowsWithStyle($allRows, $style);
$writer->close();
return $writer;
}
/**
* @param array $allRows
* @param string $fileName
* @param \Box\Spout\Writer\Style\Style|null[] $styles
* @return Writer
*/
private function writeToXLSXFileWithMultipleStyles($allRows, $fileName, $styles)
{
// there should be as many rows as there are styles passed in
$this->assertEquals(count($allRows), count($styles));
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
$writer->setShouldUseInlineStrings(true);
$writer->openToFile($resourcePath);
for ($i = 0; $i < count($allRows); $i++) {
if ($styles[$i] === null) {
$writer->addRow($allRows[$i]);
} else {
$writer->addRowWithStyle($allRows[$i], $styles[$i]);
}
}
$writer->close();
return $writer;
}
/**
* @param string $fileName
* @param string $section
* @return \DomElement
*/
private function getXmlSectionFromStylesXmlFile($fileName, $section)
{
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToStylesXmlFile = $resourcePath . '#xl/styles.xml';
$xmlReader = new \XMLReader();
$xmlReader->open('zip://' . $pathToStylesXmlFile);
while ($xmlReader->read() && ($xmlReader->nodeType !== \XMLReader::ELEMENT || $xmlReader->name !== $section)) {
// do nothing
}
return $xmlReader->expand();
}
/**
* @param string $fileName
* @return \DOMNode[]
*/
private function getCellElementsFromSheetXmlFile($fileName)
{
$cellElements = [];
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToStylesXmlFile = $resourcePath . '#xl/worksheets/sheet1.xml';
$xmlReader = new \XMLReader();
$xmlReader->open('zip://' . $pathToStylesXmlFile);
while ($xmlReader->read()) {
if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'c') {
$cellElements[] = $xmlReader->expand();
}
}
return $cellElements;
}
/**
* @param string $expectedValue
* @param \DOMNode $parentElement
* @param string $childTagName
* @param string $attributeName
* @return void
*/
private function assertFirstChildHasAttributeEquals($expectedValue, $parentElement, $childTagName, $attributeName)
{
$this->assertEquals($expectedValue, $parentElement->getElementsByTagName($childTagName)->item(0)->getAttribute($attributeName));
}
/**
* @param int $expectedNumber
* @param \DOMNode $parentElement
* @param string $message
* @return void
*/
private function assertChildrenNumEquals($expectedNumber, $parentElement, $message)
{
$this->assertEquals($expectedNumber, $parentElement->getElementsByTagName('*')->length, $message);
}
/**
* @param \DOMNode $parentElement
* @param string $childTagName
* @return void
*/
private function assertChildExists($parentElement, $childTagName)
{
$this->assertEquals(1, $parentElement->getElementsByTagName($childTagName)->length);
}
}
@@ -0,0 +1,2 @@
csv--11,csv--12,csv--13
csv--31,csv--32,csv--33
1 csv--11 csv--12 csv--13
2 csv--31 csv--32 csv--33