Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e0afd858f | ||
|
|
d4e57b1f0d | ||
|
|
435a9a016e | ||
|
|
5e7a1745ac | ||
|
|
277c353984 | ||
|
|
ff2d54cc8d | ||
|
|
c94694cb60 | ||
|
|
584121d478 | ||
|
|
b2dc0c3fa9 | ||
|
|
7f65993c87 | ||
|
|
b75a3e34fc | ||
|
|
82605ab57b | ||
|
|
7a613eed8c | ||
|
|
54a1a09e29 |
No files matched your search
@@ -133,7 +133,7 @@ $writer->setShouldAddBOM(false);
|
|||||||
|
|
||||||
#### Row styling
|
#### Row styling
|
||||||
|
|
||||||
It is possible to apply some formatting options to a row. Spout supports fonts, borders as well as alignment styles.
|
It is possible to apply some formatting options to a row. Spout supports fonts, background, borders as well as alignment styles.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Box\Spout\Common\Type;
|
use Box\Spout\Common\Type;
|
||||||
@@ -146,6 +146,7 @@ $style = (new StyleBuilder())
|
|||||||
->setFontSize(15)
|
->setFontSize(15)
|
||||||
->setFontColor(Color::BLUE)
|
->setFontColor(Color::BLUE)
|
||||||
->setShouldWrapText()
|
->setShouldWrapText()
|
||||||
|
->setBackgroundColor(Color::YELLOW)
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
$writer = WriterFactory::create(Type::XLSX);
|
$writer = WriterFactory::create(Type::XLSX);
|
||||||
@@ -179,11 +180,24 @@ $style = (new StyleBuilder())
|
|||||||
$writer = WriterFactory::create(Type::XLSX);
|
$writer = WriterFactory::create(Type::XLSX);
|
||||||
$writer->openToFile($filePath);
|
$writer->openToFile($filePath);
|
||||||
|
|
||||||
$writer->addRowWithStyle(['Border Bottom Green Thin Dashed'], $style)
|
$writer->addRowWithStyle(['Border Bottom Green Thin Dashed'], $style);
|
||||||
|
|
||||||
$writer->close();
|
$writer->close();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Spout will use a default style for all created rows. This style can be overridden this way:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$defaultStyle = (new StyleBuilder())
|
||||||
|
->setFontName('Arial')
|
||||||
|
->setFontSize(11)
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$writer = WriterFactory::create(Type::XLSX);
|
||||||
|
$writer->setDefaultRowStyle($defaultStyle)
|
||||||
|
->openToFile($filePath);
|
||||||
|
```
|
||||||
|
|
||||||
Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
|
Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
|
||||||
|
|
||||||
Category | Property | API
|
Category | Property | API
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "2.5.x-dev"
|
"dev-master": "2.6.x-dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,23 @@ class XLSX implements EscaperInterface
|
|||||||
{
|
{
|
||||||
use Singleton;
|
use Singleton;
|
||||||
|
|
||||||
/** @var string[] Control characters to be escaped */
|
/** @var string Regex pattern to detect control characters that need to be escaped */
|
||||||
|
protected $escapableControlCharactersPattern;
|
||||||
|
|
||||||
|
/** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */
|
||||||
protected $controlCharactersEscapingMap;
|
protected $controlCharactersEscapingMap;
|
||||||
|
|
||||||
|
/** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */
|
||||||
|
protected $controlCharactersEscapingReverseMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the singleton instance
|
* Initializes the singleton instance
|
||||||
*/
|
*/
|
||||||
protected function init()
|
protected function init()
|
||||||
{
|
{
|
||||||
|
$this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
|
||||||
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
|
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
|
||||||
|
$this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,6 +61,20 @@ class XLSX implements EscaperInterface
|
|||||||
return $unescapedString;
|
return $unescapedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string Regex pattern containing all escapable control characters
|
||||||
|
*/
|
||||||
|
protected function getEscapableControlCharactersPattern()
|
||||||
|
{
|
||||||
|
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||||
|
// some characters should not be escaped though: "\t", "\r" and "\n".
|
||||||
|
return '[\x00-\x08' .
|
||||||
|
// skipping "\t" (0x9) and "\n" (0xA)
|
||||||
|
'\x0B-\x0C' .
|
||||||
|
// skipping "\r" (0xD)
|
||||||
|
'\x0E-\x1F]';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the map containing control characters to be escaped
|
* Builds the map containing control characters to be escaped
|
||||||
* mapped to their escaped values.
|
* mapped to their escaped values.
|
||||||
@@ -66,14 +88,14 @@ class XLSX implements EscaperInterface
|
|||||||
protected function getControlCharactersEscapingMap()
|
protected function getControlCharactersEscapingMap()
|
||||||
{
|
{
|
||||||
$controlCharactersEscapingMap = [];
|
$controlCharactersEscapingMap = [];
|
||||||
$whitelistedControlCharacters = ["\t", "\r", "\n"];
|
|
||||||
|
|
||||||
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
// control characters values are from 0 to 1F (hex values) in the ASCII table
|
||||||
for ($charValue = 0x0; $charValue <= 0x1F; $charValue++) {
|
for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) {
|
||||||
if (!in_array(chr($charValue), $whitelistedControlCharacters)) {
|
$character = chr($charValue);
|
||||||
|
if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
|
||||||
$charHexValue = dechex($charValue);
|
$charHexValue = dechex($charValue);
|
||||||
$escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
|
$escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
|
||||||
$controlCharactersEscapingMap[$escapedChar] = chr($charValue);
|
$controlCharactersEscapingMap[$escapedChar] = $character;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +118,15 @@ class XLSX implements EscaperInterface
|
|||||||
protected function escapeControlCharacters($string)
|
protected function escapeControlCharacters($string)
|
||||||
{
|
{
|
||||||
$escapedString = $this->escapeEscapeCharacter($string);
|
$escapedString = $this->escapeEscapeCharacter($string);
|
||||||
return str_replace(array_values($this->controlCharactersEscapingMap), array_keys($this->controlCharactersEscapingMap), $escapedString);
|
|
||||||
|
// if no control characters
|
||||||
|
if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
|
||||||
|
return $escapedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function($matches) {
|
||||||
|
return $this->controlCharactersEscapingReverseMap[$matches[0]];
|
||||||
|
}, $escapedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,6 +156,7 @@ class XLSX implements EscaperInterface
|
|||||||
protected function unescapeControlCharacters($string)
|
protected function unescapeControlCharacters($string)
|
||||||
{
|
{
|
||||||
$unescapedString = $string;
|
$unescapedString = $string;
|
||||||
|
|
||||||
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
|
foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
|
||||||
// only unescape characters that don't contain the escaped escape character for now
|
// only unescape characters that don't contain the escaped escape character for now
|
||||||
$unescapedString = preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
|
$unescapedString = preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString);
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ class SimpleXMLElement
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->resetXMLInternalErrorsSetting();
|
$this->resetXMLInternalErrorsSetting();
|
||||||
|
|
||||||
return $this->simpleXMLElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -283,16 +283,16 @@ class CellValueFormatter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
|
* Returns a cell's PHP Date value, associated to the given stored nodeValue.
|
||||||
|
* @see ECMA-376 Part 1 - §18.17.4
|
||||||
*
|
*
|
||||||
* @param string $nodeValue
|
* @param string $nodeValue ISO 8601 Date string
|
||||||
* @return \DateTime|null The value associated with the cell or NULL if invalid date value
|
* @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
|
||||||
*/
|
*/
|
||||||
protected function formatDateCellValue($nodeValue)
|
protected function formatDateCellValue($nodeValue)
|
||||||
{
|
{
|
||||||
// Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
|
// Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
|
||||||
try {
|
try {
|
||||||
$cellValue = new \DateTime($nodeValue);
|
return ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue);
|
||||||
return $cellValue;
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Box\Spout\Reader\XLSX\Helper;
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
|
|
||||||
use Box\Spout\Reader\Wrapper\XMLReader;
|
use Box\Spout\Reader\Wrapper\XMLReader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,12 +80,10 @@ class StyleHelper
|
|||||||
if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) {
|
if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) {
|
||||||
while ($xmlReader->read()) {
|
while ($xmlReader->read()) {
|
||||||
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
|
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
|
||||||
$numFmtsNode = new SimpleXMLElement($xmlReader->readOuterXml());
|
$this->extractNumberFormats($xmlReader);
|
||||||
$this->extractNumberFormats($numFmtsNode);
|
|
||||||
|
|
||||||
} else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
|
} else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
|
||||||
$cellXfsNode = new SimpleXMLElement($xmlReader->readOuterXml());
|
$this->extractStyleAttributes($xmlReader);
|
||||||
$this->extractStyleAttributes($cellXfsNode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,15 +96,20 @@ class StyleHelper
|
|||||||
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
||||||
* to the reuse of formats. So 1 million cells should not use 1 million formats.
|
* to the reuse of formats. So 1 million cells should not use 1 million formats.
|
||||||
*
|
*
|
||||||
* @param SimpleXMLElement $numFmtsNode The "numFmts" node
|
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function extractNumberFormats($numFmtsNode)
|
protected function extractNumberFormats($xmlReader)
|
||||||
{
|
{
|
||||||
foreach ($numFmtsNode->children() as $numFmtNode) {
|
while ($xmlReader->read()) {
|
||||||
$numFmtId = intval($numFmtNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
|
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) {
|
||||||
$formatCode = $numFmtNode->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
|
$numFmtId = intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
|
||||||
$this->customNumberFormats[$numFmtId] = $formatCode;
|
$formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
|
||||||
|
$this->customNumberFormats[$numFmtId] = $formatCode;
|
||||||
|
} else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) {
|
||||||
|
// Once done reading "numFmts" node's children
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,16 +118,21 @@ class StyleHelper
|
|||||||
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
* For simplicity, the styles attributes are kept in memory. This is possible thanks
|
||||||
* to the reuse of styles. So 1 million cells should not use 1 million styles.
|
* to the reuse of styles. So 1 million cells should not use 1 million styles.
|
||||||
*
|
*
|
||||||
* @param SimpleXMLElement $cellXfsNode The "cellXfs" node
|
* @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function extractStyleAttributes($cellXfsNode)
|
protected function extractStyleAttributes($xmlReader)
|
||||||
{
|
{
|
||||||
foreach ($cellXfsNode->children() as $xfNode) {
|
while ($xmlReader->read()) {
|
||||||
$this->stylesAttributes[] = [
|
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) {
|
||||||
self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xfNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)),
|
$this->stylesAttributes[] = [
|
||||||
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xfNode->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)),
|
self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)),
|
||||||
];
|
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)),
|
||||||
|
];
|
||||||
|
} else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
|
||||||
|
// Once done reading "cellXfs" node's children
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,21 @@ abstract class AbstractWriter implements WriterInterface
|
|||||||
$this->resetRowStyleToDefault();
|
$this->resetRowStyleToDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default styles for all rows added with "addRow".
|
||||||
|
* Overriding the default style instead of using "addRowWithStyle" improves performance by 20%.
|
||||||
|
* @see https://github.com/box/spout/issues/272
|
||||||
|
*
|
||||||
|
* @param Style\Style $defaultStyle
|
||||||
|
* @return AbstractWriter
|
||||||
|
*/
|
||||||
|
public function setDefaultRowStyle($defaultStyle)
|
||||||
|
{
|
||||||
|
$this->defaultRowStyle = $defaultStyle;
|
||||||
|
$this->resetRowStyleToDefault();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||||
* @return AbstractWriter
|
* @return AbstractWriter
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ EOD;
|
|||||||
$content .= sprintf($borderProperty, implode(' ', $borders));
|
$content .= sprintf($borderProperty, implode(' ', $borders));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($style->shouldApplyBackgroundColor()) {
|
||||||
|
$content .= sprintf('
|
||||||
|
<style:table-cell-properties fo:background-color="#%s"/>', $style->getBackgroundColor());
|
||||||
|
}
|
||||||
|
|
||||||
$content .= '</style:style>';
|
$content .= '</style:style>';
|
||||||
|
|
||||||
return $content;
|
return $content;
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class Worksheet implements WorksheetInterface
|
|||||||
$currentCellValue !== $dataRowWithNumericIndexes[$nextCellIndex]) {
|
$currentCellValue !== $dataRowWithNumericIndexes[$nextCellIndex]) {
|
||||||
|
|
||||||
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
|
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
|
||||||
$data .= $this->getCellContent($currentCellValue, $styleIndex, $numTimesValueRepeated);
|
$data .= $this->getCellXML($currentCellValue, $styleIndex, $numTimesValueRepeated);
|
||||||
|
|
||||||
$currentCellIndex = $nextCellIndex;
|
$currentCellIndex = $nextCellIndex;
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ class Worksheet implements WorksheetInterface
|
|||||||
* @return string The cell XML content
|
* @return string The cell XML content
|
||||||
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
|
||||||
*/
|
*/
|
||||||
protected function getCellContent($cellValue, $styleIndex, $numTimesValueRepeated)
|
protected function getCellXML($cellValue, $styleIndex, $numTimesValueRepeated)
|
||||||
{
|
{
|
||||||
$data = '<table:table-cell table:style-name="ce' . $styleIndex . '"';
|
$data = '<table:table-cell table:style-name="ce' . $styleIndex . '"';
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,24 @@ class Border
|
|||||||
$this->setParts($borderParts);
|
$this->setParts($borderParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $name The name of the border part
|
||||||
|
* @return null|BorderPart
|
||||||
|
*/
|
||||||
|
public function getPart($name)
|
||||||
|
{
|
||||||
|
return $this->hasPart($name) ? $this->parts[$name] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $name The name of the border part
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasPart($name)
|
||||||
|
{
|
||||||
|
return isset($this->parts[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ class Style
|
|||||||
*/
|
*/
|
||||||
protected $shouldApplyBorder = false;
|
protected $shouldApplyBorder = false;
|
||||||
|
|
||||||
|
/** @var string Background color */
|
||||||
|
protected $backgroundColor = null;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
protected $hasSetBackgroundColor = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int|null
|
* @return int|null
|
||||||
*/
|
*/
|
||||||
@@ -279,6 +286,35 @@ class Style
|
|||||||
return $this->shouldApplyFont;
|
return $this->shouldApplyFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the background color
|
||||||
|
* @param string $color ARGB color (@see Color)
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setBackgroundColor($color)
|
||||||
|
{
|
||||||
|
$this->hasSetBackgroundColor = true;
|
||||||
|
$this->backgroundColor = $color;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBackgroundColor()
|
||||||
|
{
|
||||||
|
return $this->backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return bool Whether the background color should be applied
|
||||||
|
*/
|
||||||
|
public function shouldApplyBackgroundColor()
|
||||||
|
{
|
||||||
|
return $this->hasSetBackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the style for future comparison with other styles.
|
* Serializes the style for future comparison with other styles.
|
||||||
* The ID is excluded from the comparison, as we only care about
|
* The ID is excluded from the comparison, as we only care about
|
||||||
@@ -341,6 +377,9 @@ class Style
|
|||||||
if (!$this->getBorder() && $baseStyle->shouldApplyBorder()) {
|
if (!$this->getBorder() && $baseStyle->shouldApplyBorder()) {
|
||||||
$mergedStyle->setBorder($baseStyle->getBorder());
|
$mergedStyle->setBorder($baseStyle->getBorder());
|
||||||
}
|
}
|
||||||
|
if (!$this->hasSetBackgroundColor && $baseStyle->shouldApplyBackgroundColor()) {
|
||||||
|
$mergedStyle->setBackgroundColor($baseStyle->getBackgroundColor());
|
||||||
|
}
|
||||||
|
|
||||||
return $mergedStyle;
|
return $mergedStyle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,19 @@ class StyleBuilder
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a background color
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param string $color ARGB color (@see Color)
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setBackgroundColor($color)
|
||||||
|
{
|
||||||
|
$this->style->setBackgroundColor($color);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the configured style. The style is cached and can be reused.
|
* Returns the configured style. The style is cached and can be reused.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace Box\Spout\Writer\XLSX\Helper;
|
|||||||
|
|
||||||
use Box\Spout\Writer\Common\Helper\AbstractStyleHelper;
|
use Box\Spout\Writer\Common\Helper\AbstractStyleHelper;
|
||||||
use Box\Spout\Writer\Style\Color;
|
use Box\Spout\Writer\Style\Color;
|
||||||
|
use Box\Spout\Writer\Style\Style;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class StyleHelper
|
* Class StyleHelper
|
||||||
@@ -13,6 +14,131 @@ use Box\Spout\Writer\Style\Color;
|
|||||||
*/
|
*/
|
||||||
class StyleHelper extends AbstractStyleHelper
|
class StyleHelper extends AbstractStyleHelper
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $registeredFills = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array [STYLE_ID] => [FILL_ID] maps a style to a fill declaration
|
||||||
|
*/
|
||||||
|
protected $styleIdToFillMappingTable = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excel preserves two default fills with index 0 and 1
|
||||||
|
* Since Excel is the dominant vendor - we play along here
|
||||||
|
*
|
||||||
|
* @var int The fill index counter for custom fills.
|
||||||
|
*/
|
||||||
|
protected $fillIndex = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $registeredBorders = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration
|
||||||
|
*/
|
||||||
|
protected $styleIdToBorderMappingTable = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XLSX specific operations on the registered styles
|
||||||
|
*
|
||||||
|
* @param \Box\Spout\Writer\Style\Style $style
|
||||||
|
* @return \Box\Spout\Writer\Style\Style
|
||||||
|
*/
|
||||||
|
public function registerStyle($style)
|
||||||
|
{
|
||||||
|
$registeredStyle = parent::registerStyle($style);
|
||||||
|
$this->registerFill($registeredStyle);
|
||||||
|
$this->registerBorder($registeredStyle);
|
||||||
|
return $registeredStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a fill definition
|
||||||
|
*
|
||||||
|
* @param \Box\Spout\Writer\Style\Style $style
|
||||||
|
*/
|
||||||
|
protected function registerFill($style)
|
||||||
|
{
|
||||||
|
$styleId = $style->getId();
|
||||||
|
|
||||||
|
// Currently - only solid backgrounds are supported
|
||||||
|
// so $backgroundColor is a scalar value (RGB Color)
|
||||||
|
$backgroundColor = $style->getBackgroundColor();
|
||||||
|
|
||||||
|
if ($backgroundColor) {
|
||||||
|
$isBackgroundColorRegistered = isset($this->registeredFills[$backgroundColor]);
|
||||||
|
|
||||||
|
// We need to track the already registered background definitions
|
||||||
|
if ($isBackgroundColorRegistered) {
|
||||||
|
$registeredStyleId = $this->registeredFills[$backgroundColor];
|
||||||
|
$registeredFillId = $this->styleIdToFillMappingTable[$registeredStyleId];
|
||||||
|
$this->styleIdToFillMappingTable[$styleId] = $registeredFillId;
|
||||||
|
} else {
|
||||||
|
$this->registeredFills[$backgroundColor] = $styleId;
|
||||||
|
$this->styleIdToFillMappingTable[$styleId] = $this->fillIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// The fillId maps a style to a fill declaration
|
||||||
|
// When there is no background color definition - we default to 0
|
||||||
|
$this->styleIdToFillMappingTable[$styleId] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a border definition
|
||||||
|
*
|
||||||
|
* @param \Box\Spout\Writer\Style\Style $style
|
||||||
|
*/
|
||||||
|
protected function registerBorder($style)
|
||||||
|
{
|
||||||
|
$styleId = $style->getId();
|
||||||
|
|
||||||
|
if ($style->shouldApplyBorder()) {
|
||||||
|
$border = $style->getBorder();
|
||||||
|
$serializedBorder = serialize($border);
|
||||||
|
|
||||||
|
$isBorderAlreadyRegistered = isset($this->registeredBorders[$serializedBorder]);
|
||||||
|
|
||||||
|
if ($isBorderAlreadyRegistered) {
|
||||||
|
$registeredStyleId = $this->registeredBorders[$serializedBorder];
|
||||||
|
$registeredBorderId = $this->styleIdToBorderMappingTable[$registeredStyleId];
|
||||||
|
$this->styleIdToBorderMappingTable[$styleId] = $registeredBorderId;
|
||||||
|
} else {
|
||||||
|
$this->registeredBorders[$serializedBorder] = $styleId;
|
||||||
|
$this->styleIdToBorderMappingTable[$styleId] = count($this->registeredBorders);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If no border should be applied - the mapping is the default border: 0
|
||||||
|
$this->styleIdToBorderMappingTable[$styleId] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For empty cells, we can specify a style or not. If no style are specified,
|
||||||
|
* then the software default will be applied. But sometimes, it may be useful
|
||||||
|
* to override this default style, for instance if the cell should have a
|
||||||
|
* background color different than the default one or some borders
|
||||||
|
* (fonts property don't really matter here).
|
||||||
|
*
|
||||||
|
* @param int $styleId
|
||||||
|
* @return bool Whether the cell should define a custom style
|
||||||
|
*/
|
||||||
|
public function shouldApplyStyleOnEmptyCell($styleId)
|
||||||
|
{
|
||||||
|
$hasStyleCustomFill = (isset($this->styleIdToFillMappingTable[$styleId]) && $this->styleIdToFillMappingTable[$styleId] !== 0);
|
||||||
|
$hasStyleCustomBorders = (isset($this->styleIdToBorderMappingTable[$styleId]) && $this->styleIdToBorderMappingTable[$styleId] !== 0);
|
||||||
|
|
||||||
|
return ($hasStyleCustomFill || $hasStyleCustomBorders);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the content of the "styles.xml" file, given a list of styles.
|
* Returns the content of the "styles.xml" file, given a list of styles.
|
||||||
*
|
*
|
||||||
@@ -84,13 +210,28 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
protected function getFillsSectionContent()
|
protected function getFillsSectionContent()
|
||||||
{
|
{
|
||||||
return <<<EOD
|
// Excel reserves two default fills
|
||||||
<fills count="1">
|
$fillsCount = count($this->registeredFills) + 2;
|
||||||
<fill>
|
$content = sprintf('<fills count="%d">', $fillsCount);
|
||||||
<patternFill patternType="none"/>
|
|
||||||
</fill>
|
$content .= '<fill><patternFill patternType="none"/></fill>';
|
||||||
</fills>
|
$content .= '<fill><patternFill patternType="gray125"/></fill>';
|
||||||
EOD;
|
|
||||||
|
// The other fills are actually registered by setting a background color
|
||||||
|
foreach ($this->registeredFills as $styleId) {
|
||||||
|
/** @var Style $style */
|
||||||
|
$style = $this->styleIdToStyleMappingTable[$styleId];
|
||||||
|
|
||||||
|
$backgroundColor = $style->getBackgroundColor();
|
||||||
|
$content .= sprintf(
|
||||||
|
'<fill><patternFill patternType="solid"><fgColor rgb="%s"/></patternFill></fill>',
|
||||||
|
$backgroundColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content .= '</fills>';
|
||||||
|
|
||||||
|
return $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,24 +241,34 @@ EOD;
|
|||||||
*/
|
*/
|
||||||
protected function getBordersSectionContent()
|
protected function getBordersSectionContent()
|
||||||
{
|
{
|
||||||
$registeredStyles = $this->getRegisteredStyles();
|
|
||||||
$registeredStylesCount = count($registeredStyles);
|
|
||||||
|
|
||||||
$content = '<borders count="' . $registeredStylesCount . '">';
|
// There is one default border with index 0
|
||||||
|
$borderCount = count($this->registeredBorders) + 1;
|
||||||
|
|
||||||
/** @var \Box\Spout\Writer\Style\Style $style */
|
$content = '<borders count="' . $borderCount . '">';
|
||||||
foreach ($registeredStyles as $style) {
|
|
||||||
|
// Default border starting at index 0
|
||||||
|
$content .= '<border><left/><right/><top/><bottom/></border>';
|
||||||
|
|
||||||
|
foreach ($this->registeredBorders as $styleId) {
|
||||||
|
/** @var \Box\Spout\Writer\Style\Style $style */
|
||||||
|
$style = $this->styleIdToStyleMappingTable[$styleId];
|
||||||
$border = $style->getBorder();
|
$border = $style->getBorder();
|
||||||
if ($border) {
|
$content .= '<border>';
|
||||||
$content .= '<border>';
|
|
||||||
foreach ($border->getParts() as $part) {
|
// @link https://github.com/box/spout/issues/271
|
||||||
|
$sortOrder = ['left', 'right', 'top', 'bottom'];
|
||||||
|
|
||||||
|
foreach ($sortOrder as $partName) {
|
||||||
|
if ($border->hasPart($partName)) {
|
||||||
/** @var $part \Box\Spout\Writer\Style\BorderPart */
|
/** @var $part \Box\Spout\Writer\Style\BorderPart */
|
||||||
|
$part = $border->getPart($partName);
|
||||||
$content .= BorderHelper::serializeBorderPart($part);
|
$content .= BorderHelper::serializeBorderPart($part);
|
||||||
}
|
}
|
||||||
$content .= '</border>';
|
|
||||||
} else {
|
|
||||||
$content .= '<border><left/><right/><top/><bottom/><diagonal/></border>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$content .= '</border>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$content .= '</borders>';
|
$content .= '</borders>';
|
||||||
@@ -151,15 +302,17 @@ EOD;
|
|||||||
$content = '<cellXfs count="' . count($registeredStyles) . '">';
|
$content = '<cellXfs count="' . count($registeredStyles) . '">';
|
||||||
|
|
||||||
foreach ($registeredStyles as $style) {
|
foreach ($registeredStyles as $style) {
|
||||||
$content .= '<xf numFmtId="0" fontId="' . $style->getId() . '" fillId="0" borderId="' . $style->getId() . '" xfId="0"';
|
$styleId = $style->getId();
|
||||||
|
$fillId = $this->styleIdToFillMappingTable[$styleId];
|
||||||
|
$borderId = $this->styleIdToBorderMappingTable[$styleId];
|
||||||
|
|
||||||
|
$content .= '<xf numFmtId="0" fontId="' . $styleId . '" fillId="' . $fillId . '" borderId="' . $borderId . '" xfId="0"';
|
||||||
|
|
||||||
if ($style->shouldApplyFont()) {
|
if ($style->shouldApplyFont()) {
|
||||||
$content .= ' applyFont="1"';
|
$content .= ' applyFont="1"';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($style->shouldApplyBorder()) {
|
$content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0);
|
||||||
$content .= ' applyBorder="1"';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($style->shouldWrapText()) {
|
if ($style->shouldWrapText()) {
|
||||||
$content .= ' applyAlignment="1">';
|
$content .= ' applyAlignment="1">';
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class Workbook extends AbstractWorkbook
|
|||||||
$sheet = new Sheet($newSheetIndex);
|
$sheet = new Sheet($newSheetIndex);
|
||||||
|
|
||||||
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
|
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
|
||||||
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings);
|
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->styleHelper, $this->shouldUseInlineStrings);
|
||||||
$this->worksheets[] = $worksheet;
|
$this->worksheets[] = $worksheet;
|
||||||
|
|
||||||
return $worksheet;
|
return $worksheet;
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ EOD;
|
|||||||
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
|
/** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
|
||||||
protected $sharedStringsHelper;
|
protected $sharedStringsHelper;
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles */
|
||||||
|
protected $styleHelper;
|
||||||
|
|
||||||
/** @var bool Whether inline or shared strings should be used */
|
/** @var bool Whether inline or shared strings should be used */
|
||||||
protected $shouldUseInlineStrings;
|
protected $shouldUseInlineStrings;
|
||||||
|
|
||||||
@@ -46,13 +49,15 @@ EOD;
|
|||||||
* @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet
|
* @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet
|
||||||
* @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
|
* @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
|
||||||
* @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings
|
* @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings
|
||||||
|
* @param \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles
|
||||||
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
|
||||||
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
* @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
|
||||||
*/
|
*/
|
||||||
public function __construct($externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $shouldUseInlineStrings)
|
public function __construct($externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $styleHelper, $shouldUseInlineStrings)
|
||||||
{
|
{
|
||||||
$this->externalSheet = $externalSheet;
|
$this->externalSheet = $externalSheet;
|
||||||
$this->sharedStringsHelper = $sharedStringsHelper;
|
$this->sharedStringsHelper = $sharedStringsHelper;
|
||||||
|
$this->styleHelper = $styleHelper;
|
||||||
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
|
||||||
|
|
||||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||||
@@ -134,29 +139,7 @@ EOD;
|
|||||||
$rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
|
$rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
|
||||||
|
|
||||||
foreach($dataRow as $cellValue) {
|
foreach($dataRow as $cellValue) {
|
||||||
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
$rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style->getId());
|
||||||
$cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
|
|
||||||
$cellXML .= ' s="' . $style->getId() . '"';
|
|
||||||
|
|
||||||
if (CellHelper::isNonEmptyString($cellValue)) {
|
|
||||||
if ($this->shouldUseInlineStrings) {
|
|
||||||
$cellXML .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
|
|
||||||
} else {
|
|
||||||
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
|
||||||
$cellXML .= ' t="s"><v>' . $sharedStringId . '</v></c>';
|
|
||||||
}
|
|
||||||
} else if (CellHelper::isBoolean($cellValue)) {
|
|
||||||
$cellXML .= ' t="b"><v>' . intval($cellValue) . '</v></c>';
|
|
||||||
} else if (CellHelper::isNumeric($cellValue)) {
|
|
||||||
$cellXML .= '><v>' . $cellValue . '</v></c>';
|
|
||||||
} else if (empty($cellValue)) {
|
|
||||||
// don't write empty cells (not appending to $cellXML is the right behavior!)
|
|
||||||
$cellXML = '';
|
|
||||||
} else {
|
|
||||||
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
$rowXML .= $cellXML;
|
|
||||||
$cellNumber++;
|
$cellNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +154,48 @@ EOD;
|
|||||||
$this->lastWrittenRowIndex++;
|
$this->lastWrittenRowIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and return xml for a single cell.
|
||||||
|
*
|
||||||
|
* @param int $rowIndex
|
||||||
|
* @param int $cellNumber
|
||||||
|
* @param mixed $cellValue
|
||||||
|
* @param int $styleId
|
||||||
|
* @return string
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId)
|
||||||
|
{
|
||||||
|
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
|
||||||
|
$cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
|
||||||
|
$cellXML .= ' s="' . $styleId . '"';
|
||||||
|
|
||||||
|
if (CellHelper::isNonEmptyString($cellValue)) {
|
||||||
|
if ($this->shouldUseInlineStrings) {
|
||||||
|
$cellXML .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
|
||||||
|
} else {
|
||||||
|
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
|
||||||
|
$cellXML .= ' t="s"><v>' . $sharedStringId . '</v></c>';
|
||||||
|
}
|
||||||
|
} else if (CellHelper::isBoolean($cellValue)) {
|
||||||
|
$cellXML .= ' t="b"><v>' . intval($cellValue) . '</v></c>';
|
||||||
|
} else if (CellHelper::isNumeric($cellValue)) {
|
||||||
|
$cellXML .= '><v>' . $cellValue . '</v></c>';
|
||||||
|
} else if (empty($cellValue)) {
|
||||||
|
if ($this->styleHelper->shouldApplyStyleOnEmptyCell($styleId)) {
|
||||||
|
$cellXML .= '/>';
|
||||||
|
} else {
|
||||||
|
// don't write empty cells that do no need styling
|
||||||
|
// NOTE: not appending to $cellXML is the right behavior!!
|
||||||
|
$cellXML = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cellXML;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the worksheet
|
* Closes the worksheet
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
return [
|
return [
|
||||||
['test', 'test'],
|
['test', 'test'],
|
||||||
['adam\'s "car"', 'adam's "car"'],
|
['adam\'s "car"', 'adam's "car"'],
|
||||||
|
["\n", "\n"],
|
||||||
|
["\r", "\r"],
|
||||||
|
["\t", "\t"],
|
||||||
[chr(0), '_x0000_'],
|
[chr(0), '_x0000_'],
|
||||||
|
[chr(4), '_x0004_'],
|
||||||
['_x0000_', '_x005F_x0000_'],
|
['_x0000_', '_x005F_x0000_'],
|
||||||
[chr(21), '_x0015_'],
|
[chr(21), '_x0015_'],
|
||||||
['control '.chr(21).' character', 'control _x0015_ character'],
|
['control '.chr(21).' character', 'control _x0015_ character'],
|
||||||
@@ -49,7 +53,11 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
|||||||
return [
|
return [
|
||||||
['test', 'test'],
|
['test', 'test'],
|
||||||
['adam's "car"', 'adam\'s "car"'],
|
['adam's "car"', 'adam\'s "car"'],
|
||||||
|
["\n", "\n"],
|
||||||
|
["\r", "\r"],
|
||||||
|
["\t", "\t"],
|
||||||
['_x0000_', chr(0)],
|
['_x0000_', chr(0)],
|
||||||
|
['_x0004_', chr(4)],
|
||||||
['_x005F_x0000_', '_x0000_'],
|
['_x005F_x0000_', '_x0000_'],
|
||||||
['_x0015_', chr(21)],
|
['_x0015_', chr(21)],
|
||||||
['control _x0015_ character', 'control '.chr(21).' character'],
|
['control _x0015_ character', 'control '.chr(21).' character'],
|
||||||
|
|||||||
@@ -138,11 +138,12 @@ class XMLReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
public function dataProviderForTestGetRealPathURIForFileInZip()
|
public function dataProviderForTestGetRealPathURIForFileInZip()
|
||||||
{
|
{
|
||||||
$tempFolder = realpath(sys_get_temp_dir());
|
$tempFolder = realpath(sys_get_temp_dir());
|
||||||
|
$tempFolderName = basename($tempFolder);
|
||||||
$expectedRealPathURI = 'zip://' . $tempFolder . '/test.xlsx#test.xml';
|
$expectedRealPathURI = 'zip://' . $tempFolder . '/test.xlsx#test.xml';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[$tempFolder, "$tempFolder/test.xlsx", 'test.xml', $expectedRealPathURI],
|
[$tempFolder, "$tempFolder/test.xlsx", 'test.xml', $expectedRealPathURI],
|
||||||
[$tempFolder, "/../../../$tempFolder/test.xlsx", 'test.xml', $expectedRealPathURI],
|
[$tempFolder, "$tempFolder/../$tempFolderName/test.xlsx", 'test.xml', $expectedRealPathURI],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +163,11 @@ class XMLReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$xmlReader = new XMLReader();
|
$xmlReader = new XMLReader();
|
||||||
$realPathURI = \ReflectionHelper::callMethodOnObject($xmlReader, 'getRealPathURIForFileInZip', $zipFilePath, $fileInsideZipPath);
|
$realPathURI = \ReflectionHelper::callMethodOnObject($xmlReader, 'getRealPathURIForFileInZip', $zipFilePath, $fileInsideZipPath);
|
||||||
|
|
||||||
$this->assertEquals($expectedRealPathURI, $realPathURI);
|
// Normalizing path separators for Windows support
|
||||||
|
$normalizedRealPathURI = str_replace('\\', '/', $realPathURI);
|
||||||
|
$normalizedExpectedRealPathURI = str_replace('\\', '/', $expectedRealPathURI);
|
||||||
|
|
||||||
|
$this->assertEquals($normalizedExpectedRealPathURI, $normalizedRealPathURI);
|
||||||
|
|
||||||
unlink($tempFolder . '/test.xlsx');
|
unlink($tempFolder . '/test.xlsx');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$expectedRows = [
|
$expectedRows = [
|
||||||
['1/13/2016', '01/13/2016', '13-Jan-16', 'Wednesday January 13, 16', 'Today is 1/13/2016'],
|
['1/13/2016', '01/13/2016', '13-Jan-16', 'Wednesday January 13, 16', 'Today is 1/13/2016'],
|
||||||
['4:43:25', '04:43', '4:43', '4:43:25 AM', '4:43:25 PM'],
|
['4:43:25', '04:43', '4:43', '4:43:25 AM', '4:43:25 PM'],
|
||||||
|
['1976-11-22T08:30:00.000', '1976-11-22T08:30', '1582-10-15', '08:30:00', '08:30'],
|
||||||
];
|
];
|
||||||
$this->assertEquals($expectedRows, $allRows);
|
$this->assertEquals($expectedRows, $allRows);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ namespace Box\Spout;
|
|||||||
*
|
*
|
||||||
* @package Box\Spout
|
* @package Box\Spout
|
||||||
*/
|
*/
|
||||||
trait TestUsingResource {
|
trait TestUsingResource
|
||||||
|
{
|
||||||
/** @var string Path to the test resources folder */
|
/** @var string Path to the test resources folder */
|
||||||
private $resourcesPath = 'tests/resources';
|
private $resourcesPath = 'tests/resources';
|
||||||
|
|
||||||
@@ -70,6 +70,11 @@ trait TestUsingResource {
|
|||||||
*/
|
*/
|
||||||
protected function createUnwritableFolderIfNeeded()
|
protected function createUnwritableFolderIfNeeded()
|
||||||
{
|
{
|
||||||
|
// On Windows, chmod() or the mkdir's mode is ignored
|
||||||
|
if ($this->isWindows()) {
|
||||||
|
$this->markTestSkipped('Skipping because Windows cannot create read-only folders through PHP');
|
||||||
|
}
|
||||||
|
|
||||||
if (!file_exists($this->generatedUnwritableResourcesPath)) {
|
if (!file_exists($this->generatedUnwritableResourcesPath)) {
|
||||||
// Make sure generated folder exists first
|
// Make sure generated folder exists first
|
||||||
if (!file_exists($this->generatedResourcesPath)) {
|
if (!file_exists($this->generatedResourcesPath)) {
|
||||||
@@ -80,4 +85,12 @@ trait TestUsingResource {
|
|||||||
mkdir($this->generatedUnwritableResourcesPath, 0444, true);
|
mkdir($this->generatedUnwritableResourcesPath, 0444, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool Whether the OS on which PHP is installed is Windows
|
||||||
|
*/
|
||||||
|
protected function isWindows()
|
||||||
|
{
|
||||||
|
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ class WriterTest extends \PHPUnit_Framework_TestCase
|
|||||||
public function testWriteShouldThrowExceptionIfCannotOpenFileForWriting()
|
public function testWriteShouldThrowExceptionIfCannotOpenFileForWriting()
|
||||||
{
|
{
|
||||||
$fileName = 'file_that_wont_be_written.csv';
|
$fileName = 'file_that_wont_be_written.csv';
|
||||||
$this->createUnwritableFolderIfNeeded($fileName);
|
$this->createUnwritableFolderIfNeeded();
|
||||||
$filePath = $this->getGeneratedUnwritableResourcePath($fileName);
|
$filePath = $this->getGeneratedUnwritableResourcePath($fileName);
|
||||||
|
|
||||||
$writer = WriterFactory::create(Type::CSV);
|
$writer = WriterFactory::create(Type::CSV);
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
->setFontSize(15)
|
->setFontSize(15)
|
||||||
->setFontColor(Color::RED)
|
->setFontColor(Color::RED)
|
||||||
->setFontName('Cambria')
|
->setFontName('Cambria')
|
||||||
|
->setBackgroundColor(Color::GREEN)
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
$this->writeToODSFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]);
|
$this->writeToODSFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]);
|
||||||
@@ -137,6 +138,7 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertFirstChildHasAttributeEquals('15pt', $customFont2Element, 'text-properties', 'fo:font-size');
|
$this->assertFirstChildHasAttributeEquals('15pt', $customFont2Element, 'text-properties', 'fo:font-size');
|
||||||
$this->assertFirstChildHasAttributeEquals('#' . Color::RED, $customFont2Element, 'text-properties', 'fo:color');
|
$this->assertFirstChildHasAttributeEquals('#' . Color::RED, $customFont2Element, 'text-properties', 'fo:color');
|
||||||
$this->assertFirstChildHasAttributeEquals('Cambria', $customFont2Element, 'text-properties', 'style:font-name');
|
$this->assertFirstChildHasAttributeEquals('Cambria', $customFont2Element, 'text-properties', 'style:font-name');
|
||||||
|
$this->assertFirstChildHasAttributeEquals('#' . Color::GREEN, $customFont2Element, 'table-cell-properties', 'fo:background-color');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -239,6 +241,26 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertFirstChildHasAttributeEquals('wrap', $customStyleElement, 'table-cell-properties', 'fo:wrap-option');
|
$this->assertFirstChildHasAttributeEquals('wrap', $customStyleElement, 'table-cell-properties', 'fo:wrap-option');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAddBackgroundColor()
|
||||||
|
{
|
||||||
|
$fileName = 'test_default_background_style.ods';
|
||||||
|
$dataRows = [
|
||||||
|
['defaultBgColor'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$style = (new StyleBuilder())->setBackgroundColor(Color::WHITE)->build();
|
||||||
|
$this->writeToODSFile($dataRows, $fileName, $style);
|
||||||
|
|
||||||
|
$styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName);
|
||||||
|
$this->assertEquals(2, count($styleElements), 'There should be 2 styles (default and custom)');
|
||||||
|
|
||||||
|
$customStyleElement = $styleElements[1];
|
||||||
|
$this->assertFirstChildHasAttributeEquals('#' . Color::WHITE, $customStyleElement, 'table-cell-properties', 'fo:background-color');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -304,6 +326,22 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedThird, $actualThird);
|
$this->assertEquals($expectedThird, $actualThird);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSetDefaultRowStyle()
|
||||||
|
{
|
||||||
|
$fileName = 'test_set_default_row_style.ods';
|
||||||
|
$dataRows = [['ods--11']];
|
||||||
|
|
||||||
|
$defaultFontSize = 50;
|
||||||
|
$defaultStyle = (new StyleBuilder())->setFontSize($defaultFontSize)->build();
|
||||||
|
|
||||||
|
$this->writeToODSFileWithDefaultStyle($dataRows, $fileName, $defaultStyle);
|
||||||
|
|
||||||
|
$textPropertiesElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'style:text-properties');
|
||||||
|
$this->assertEquals($defaultFontSize . 'pt', $textPropertiesElement->getAttribute('fo:font-size'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $allRows
|
* @param array $allRows
|
||||||
@@ -326,6 +364,28 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
return $writer;
|
return $writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $allRows
|
||||||
|
* @param string $fileName
|
||||||
|
* @param \Box\Spout\Writer\Style\Style|null $defaultStyle
|
||||||
|
* @return Writer
|
||||||
|
*/
|
||||||
|
private function writeToODSFileWithDefaultStyle($allRows, $fileName, $defaultStyle)
|
||||||
|
{
|
||||||
|
$this->createGeneratedFolderIfNeeded($fileName);
|
||||||
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
|
||||||
|
$writer = WriterFactory::create(Type::ODS);
|
||||||
|
$writer->setDefaultRowStyle($defaultStyle);
|
||||||
|
|
||||||
|
$writer->openToFile($resourcePath);
|
||||||
|
$writer->addRows($allRows);
|
||||||
|
$writer->close();
|
||||||
|
|
||||||
|
return $writer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $allRows
|
* @param array $allRows
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
@@ -376,7 +436,7 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
$xmlReader->close();
|
$xmlReader->close();
|
||||||
|
|
||||||
return $cellElements;
|
return $cellElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +460,7 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
$xmlReader->close();
|
$xmlReader->close();
|
||||||
|
|
||||||
return $cellStyleElements;
|
return $cellStyleElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace Box\Spout\Writer\XLSX\Helper;
|
namespace Box\Spout\Writer\XLSX\Helper;
|
||||||
|
|
||||||
|
use Box\Spout\Writer\Style\Border;
|
||||||
|
use Box\Spout\Writer\Style\BorderBuilder;
|
||||||
|
use Box\Spout\Writer\Style\Color;
|
||||||
use Box\Spout\Writer\Style\StyleBuilder;
|
use Box\Spout\Writer\Style\StyleBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,6 +60,27 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals(1, $registeredStyle2->getId());
|
$this->assertEquals(1, $registeredStyle2->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testShouldApplyStyleOnEmptyCell()
|
||||||
|
{
|
||||||
|
$styleWithFont = (new StyleBuilder())->setFontBold()->build();
|
||||||
|
$styleWithBackground = (new StyleBuilder())->setBackgroundColor(Color::BLUE)->build();
|
||||||
|
|
||||||
|
$border = (new BorderBuilder())->setBorderBottom(Color::GREEN)->build();
|
||||||
|
$styleWithBorder = (new StyleBuilder())->setBorder($border)->build();
|
||||||
|
|
||||||
|
$styleHelper = new StyleHelper($this->defaultStyle);
|
||||||
|
$styleHelper->registerStyle($styleWithFont);
|
||||||
|
$styleHelper->registerStyle($styleWithBackground);
|
||||||
|
$styleHelper->registerStyle($styleWithBorder);
|
||||||
|
|
||||||
|
$this->assertFalse($styleHelper->shouldApplyStyleOnEmptyCell($styleWithFont->getId()));
|
||||||
|
$this->assertTrue($styleHelper->shouldApplyStyleOnEmptyCell($styleWithBackground->getId()));
|
||||||
|
$this->assertTrue($styleHelper->shouldApplyStyleOnEmptyCell($styleWithBorder->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$fontElements = $fontsDomElement->getElementsByTagName('font');
|
$fontElements = $fontsDomElement->getElementsByTagName('font');
|
||||||
$this->assertEquals(3, $fontElements->length, 'There should be 3 associated "font" elements, including the default one.');
|
$this->assertEquals(3, $fontElements->length, 'There should be 3 associated "font" elements, including the default one.');
|
||||||
|
|
||||||
// First font should be the default one
|
// First font should be the default one
|
||||||
$defaultFontElement = $fontElements->item(0);
|
$defaultFontElement = $fontElements->item(0);
|
||||||
$this->assertChildrenNumEquals(3, $defaultFontElement, 'The default font should only have 3 properties.');
|
$this->assertChildrenNumEquals(3, $defaultFontElement, 'The default font should only have 3 properties.');
|
||||||
@@ -177,6 +176,53 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals('0', $cellDomElements[2]->getAttribute('s'));
|
$this->assertEquals('0', $cellDomElements[2]->getAttribute('s'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAddRowWithStyleShouldApplyStyleToEmptyCellsIfNeeded()
|
||||||
|
{
|
||||||
|
$fileName = 'test_add_row_with_style_should_apply_style_to_empty_cells_if_needed.xlsx';
|
||||||
|
$dataRows = [
|
||||||
|
['xlsx--11', '', 'xlsx--13'],
|
||||||
|
['xlsx--21', '', 'xlsx--23'],
|
||||||
|
['xlsx--31', '', 'xlsx--33'],
|
||||||
|
['xlsx--41', '', 'xlsx--43'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$styleWithFont = (new StyleBuilder())->setFontBold()->build();
|
||||||
|
$styleWithBackground = (new StyleBuilder())->setBackgroundColor(Color::BLUE)->build();
|
||||||
|
|
||||||
|
$border = (new BorderBuilder())->setBorderBottom(Color::GREEN)->build();
|
||||||
|
$styleWithBorder = (new StyleBuilder())->setBorder($border)->build();
|
||||||
|
|
||||||
|
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [null, $styleWithFont, $styleWithBackground, $styleWithBorder]);
|
||||||
|
|
||||||
|
$cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName);
|
||||||
|
|
||||||
|
// The first and second rows should not have a reference to the empty cell
|
||||||
|
// The other rows should have the reference because style should be applied to them
|
||||||
|
// So that's: 2 + 2 + 3 + 3 = 10 cells
|
||||||
|
$this->assertEquals(10, count($cellDomElements));
|
||||||
|
|
||||||
|
// First row has 2 styled cells
|
||||||
|
$this->assertEquals('0', $cellDomElements[0]->getAttribute('s'));
|
||||||
|
$this->assertEquals('0', $cellDomElements[1]->getAttribute('s'));
|
||||||
|
|
||||||
|
// Second row has 2 styled cells
|
||||||
|
$this->assertEquals('1', $cellDomElements[2]->getAttribute('s'));
|
||||||
|
$this->assertEquals('1', $cellDomElements[3]->getAttribute('s'));
|
||||||
|
|
||||||
|
// Third row has 3 styled cells
|
||||||
|
$this->assertEquals('2', $cellDomElements[4]->getAttribute('s'));
|
||||||
|
$this->assertEquals('2', $cellDomElements[5]->getAttribute('s'));
|
||||||
|
$this->assertEquals('2', $cellDomElements[6]->getAttribute('s'));
|
||||||
|
|
||||||
|
// Third row has 3 styled cells
|
||||||
|
$this->assertEquals('3', $cellDomElements[7]->getAttribute('s'));
|
||||||
|
$this->assertEquals('3', $cellDomElements[8]->getAttribute('s'));
|
||||||
|
$this->assertEquals('3', $cellDomElements[9]->getAttribute('s'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -234,6 +280,73 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText');
|
$this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAddBackgroundColor()
|
||||||
|
{
|
||||||
|
$fileName = 'test_add_background_color.xlsx';
|
||||||
|
$dataRows = [
|
||||||
|
["BgColor"],
|
||||||
|
];
|
||||||
|
$style = (new StyleBuilder())->setBackgroundColor(Color::WHITE)->build();
|
||||||
|
$this->writeToXLSXFile($dataRows, $fileName, $style);
|
||||||
|
$fillsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fills');
|
||||||
|
$this->assertEquals(3, $fillsDomElement->getAttribute('count'), 'There should be 3 fills, including the 2 default ones');
|
||||||
|
|
||||||
|
$fillsElements = $fillsDomElement->getElementsByTagName('fill');
|
||||||
|
|
||||||
|
$thirdFillElement = $fillsElements->item(2); // Zero based
|
||||||
|
$fgColor = $thirdFillElement->getElementsByTagName('fgColor')->item(0)->getAttribute('rgb');
|
||||||
|
|
||||||
|
$this->assertEquals(Color::WHITE, $fgColor, 'The foreground color should equal white');
|
||||||
|
|
||||||
|
$styleXfsElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
|
||||||
|
$this->assertEquals(2, $styleXfsElements->getAttribute('count'), '2 cell xfs present - a default one and a custom one');
|
||||||
|
|
||||||
|
$customFillId = $styleXfsElements->lastChild->getAttribute('fillId');
|
||||||
|
$this->assertEquals(2, (int)$customFillId, 'The custom fill id should have the index 2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReuseBackgroundColorSharedDefinition()
|
||||||
|
{
|
||||||
|
$fileName = 'test_add_background_color_shared_definition.xlsx';
|
||||||
|
$dataRows = [
|
||||||
|
["row-bold-background-red"],
|
||||||
|
["row-background-red"],
|
||||||
|
];
|
||||||
|
|
||||||
|
$styles = [
|
||||||
|
(new StyleBuilder())->setBackgroundColor(Color::RED)->setFontBold()->build(),
|
||||||
|
(new StyleBuilder())->setBackgroundColor(Color::RED)->build()
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, $styles);
|
||||||
|
|
||||||
|
$fillsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fills');
|
||||||
|
$this->assertEquals(
|
||||||
|
3,
|
||||||
|
$fillsDomElement->getAttribute('count'),
|
||||||
|
'There should be 3 fills, including the 2 default ones'
|
||||||
|
);
|
||||||
|
|
||||||
|
$styleXfsElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
|
||||||
|
$this->assertEquals(
|
||||||
|
3,
|
||||||
|
$styleXfsElements->getAttribute('count'),
|
||||||
|
'3 cell xfs present - a default one and two custom ones'
|
||||||
|
);
|
||||||
|
|
||||||
|
$firstCustomId = $styleXfsElements->childNodes->item(1)->getAttribute('fillId');
|
||||||
|
$this->assertEquals(2, (int)$firstCustomId, 'The first custom fill id should have the index 2');
|
||||||
|
|
||||||
|
$secondCustomId = $styleXfsElements->childNodes->item(2)->getAttribute('fillId');
|
||||||
|
$this->assertEquals(2, (int)$secondCustomId, 'The second custom fill id should have the index 2');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -268,6 +381,138 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals(3, $styleXfsElements->getAttribute('count'), '3 cell xfs present');
|
$this->assertEquals(3, $styleXfsElements->getAttribute('count'), '3 cell xfs present');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testBordersCorrectOrder()
|
||||||
|
{
|
||||||
|
// Border should be Left, Right, Top, Bottom
|
||||||
|
$fileName = 'test_borders_correct_order.xlsx';
|
||||||
|
|
||||||
|
$dataRows = [
|
||||||
|
['I am a teapot'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$borders = (new BorderBuilder())
|
||||||
|
->setBorderRight()
|
||||||
|
->setBorderTop()
|
||||||
|
->setBorderLeft()
|
||||||
|
->setBorderBottom()
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$style = (new StyleBuilder())->setBorder($borders)->build();
|
||||||
|
$this->writeToXLSXFile($dataRows, $fileName, $style);
|
||||||
|
$borderElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'borders');
|
||||||
|
|
||||||
|
$correctOrdering = [
|
||||||
|
'left', 'right', 'top', 'bottom'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var $borderNode \DOMElement */
|
||||||
|
foreach ($borderElements->childNodes as $borderNode) {
|
||||||
|
$borderParts = $borderNode->childNodes;
|
||||||
|
$ordering = [];
|
||||||
|
|
||||||
|
/** @var $part \DOMText */
|
||||||
|
foreach ($borderParts as $part) {
|
||||||
|
if ($part instanceof \DOMElement) {
|
||||||
|
$ordering[] = $part->nodeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals($correctOrdering, $ordering, 'The border parts are in correct ordering');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSetDefaultRowStyle()
|
||||||
|
{
|
||||||
|
$fileName = 'test_set_default_row_style.xlsx';
|
||||||
|
$dataRows = [['xlsx--11']];
|
||||||
|
|
||||||
|
$defaultFontSize = 50;
|
||||||
|
$defaultStyle = (new StyleBuilder())->setFontSize($defaultFontSize)->build();
|
||||||
|
|
||||||
|
$this->writeToXLSXFileWithDefaultStyle($dataRows, $fileName, $defaultStyle);
|
||||||
|
|
||||||
|
$fontsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fonts');
|
||||||
|
$fontElements = $fontsDomElement->getElementsByTagName('font');
|
||||||
|
$this->assertEquals(1, $fontElements->length, 'There should only be the default font.');
|
||||||
|
|
||||||
|
$defaultFontElement = $fontElements->item(0);
|
||||||
|
$this->assertFirstChildHasAttributeEquals((string) $defaultFontSize, $defaultFontElement, 'sz', 'val');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReUseBorders()
|
||||||
|
{
|
||||||
|
$fileName = 'test_reuse_borders.xlsx';
|
||||||
|
|
||||||
|
$borderLeft = (new BorderBuilder())->setBorderLeft()->build();
|
||||||
|
$borderLeftStyle = (new StyleBuilder())->setBorder($borderLeft)->build();
|
||||||
|
|
||||||
|
$borderRight = (new BorderBuilder())->setBorderRight(Color::RED, Border::WIDTH_THICK)->build();
|
||||||
|
$borderRightStyle = (new StyleBuilder())->setBorder($borderRight)->build();
|
||||||
|
|
||||||
|
$fontStyle = (new StyleBuilder())->setFontBold()->build();
|
||||||
|
$emptyStyle = (new StyleBuilder())->build();
|
||||||
|
|
||||||
|
$borderRightFontBoldStyle = $borderRightStyle->mergeWith($fontStyle);
|
||||||
|
|
||||||
|
$dataRows = [
|
||||||
|
['Border-Left'],
|
||||||
|
['Empty'],
|
||||||
|
['Font-Bold'],
|
||||||
|
['Border-Right'],
|
||||||
|
['Border-Right-Font-Bold'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$styles = [
|
||||||
|
$borderLeftStyle,
|
||||||
|
$emptyStyle,
|
||||||
|
$fontStyle,
|
||||||
|
$borderRightStyle,
|
||||||
|
$borderRightFontBoldStyle
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, $styles);
|
||||||
|
$borderElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'borders');
|
||||||
|
|
||||||
|
$this->assertEquals(3, $borderElements->getAttribute('count'), '3 borders in count attribute');
|
||||||
|
$this->assertEquals(3, $borderElements->childNodes->length, '3 border childnodes present');
|
||||||
|
|
||||||
|
/** @var \DOMElement $firstBorder */
|
||||||
|
$firstBorder = $borderElements->childNodes->item(1); // 0 = default border
|
||||||
|
$leftStyle = $firstBorder->getElementsByTagName('left')->item(0)->getAttribute('style');
|
||||||
|
$this->assertEquals('medium', $leftStyle, 'Style is medium');
|
||||||
|
|
||||||
|
/** @var \DOMElement $secondBorder */
|
||||||
|
$secondBorder = $borderElements->childNodes->item(2);
|
||||||
|
$rightStyle = $secondBorder->getElementsByTagName('right')->item(0)->getAttribute('style');
|
||||||
|
$this->assertEquals('thick', $rightStyle, 'Style is thick');
|
||||||
|
|
||||||
|
$styleXfsElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
|
||||||
|
|
||||||
|
// A rather relaxed test
|
||||||
|
// Where a border is applied - the borderId attribute has to be greater than 0
|
||||||
|
$bordersApplied = 0;
|
||||||
|
/** @var \DOMElement $node */
|
||||||
|
foreach ($styleXfsElements->childNodes as $node) {
|
||||||
|
if ($node->getAttribute('applyBorder') == 1) {
|
||||||
|
$bordersApplied++;
|
||||||
|
$this->assertTrue((int)$node->getAttribute('borderId') > 0, 'BorderId is greater than 0');
|
||||||
|
} else {
|
||||||
|
$this->assertTrue((int)$node->getAttribute('borderId') === 0, 'BorderId is 0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals(3, $bordersApplied, 'Three borders have been applied');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $allRows
|
* @param array $allRows
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
@@ -290,6 +535,29 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
|
|||||||
return $writer;
|
return $writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $allRows
|
||||||
|
* @param string $fileName
|
||||||
|
* @param \Box\Spout\Writer\Style\Style|null $defaultStyle
|
||||||
|
* @return Writer
|
||||||
|
*/
|
||||||
|
private function writeToXLSXFileWithDefaultStyle($allRows, $fileName, $defaultStyle)
|
||||||
|
{
|
||||||
|
$this->createGeneratedFolderIfNeeded($fileName);
|
||||||
|
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||||
|
|
||||||
|
/** @var \Box\Spout\Writer\XLSX\Writer $writer */
|
||||||
|
$writer = WriterFactory::create(Type::XLSX);
|
||||||
|
$writer->setShouldUseInlineStrings(true);
|
||||||
|
$writer->setDefaultRowStyle($defaultStyle);
|
||||||
|
|
||||||
|
$writer->openToFile($resourcePath);
|
||||||
|
$writer->addRows($allRows);
|
||||||
|
$writer->close();
|
||||||
|
|
||||||
|
return $writer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $allRows
|
* @param array $allRows
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user