Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
444308d42c | ||
|
|
353d4e86a5 | ||
|
|
f043f8d4d0 | ||
|
|
c8ddcf5441 | ||
|
|
4a346cef0c | ||
|
|
1c8934790d | ||
|
|
2a1925bc51 | ||
|
|
2183ff6738 | ||
|
|
380ee524a5 | ||
|
|
21263a0730 | ||
|
|
0104714cbd | ||
|
|
611a091290 | ||
|
|
40a86c4b6c | ||
|
|
8a3b895afc | ||
|
|
e3f7ecfa64 | ||
|
|
93d7aafe8b | ||
|
|
dc53b6aa20 |
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
[](http://opensource.box.com/badges)
|
||||
[](http://travis-ci.org/box/spout)
|
||||
[](http://travis-ci.org/box/spout)
|
||||
[](https://scrutinizer-ci.com/g/box/spout/?branch=master)
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
[](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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
Reference in new issue
Block a user