From 9f4c094fa02250761266f2b5c49653b3c8baf922 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Sun, 27 Oct 2019 18:00:09 +0100 Subject: [PATCH] Cell alignment This PR adds support for cell alignment for XLSX and ODS files. You can now align the content of the cells this way: ``` use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Writer\Common\Creator\Style\StyleBuilder; $style = (new StyleBuilder()) ->setCellAlignment(CellAlignment::RIGHT) ->build(); ... ``` Possible cell alignments are: LEFT, RIGHT, CENTER and JUSTIFY. --- docs/_pages/documentation.md | 23 ++--- .../Common/Entity/Style/CellAlignment.php | 32 +++++++ src/Spout/Common/Entity/Style/Color.php | 2 +- src/Spout/Common/Entity/Style/Style.php | 47 +++++++++- .../Common/Creator/Style/StyleBuilder.php | 21 +++++ .../Common/Manager/Style/StyleMerger.php | 3 + .../Writer/ODS/Manager/Style/StyleManager.php | 86 +++++++++++++++---- .../XLSX/Manager/Style/StyleManager.php | 11 ++- .../Common/Creator/StyleBuilderTest.php | 26 +++--- .../Common/Manager/Style/StyleMergerTest.php | 2 + .../Spout/Writer/ODS/WriterWithStyleTest.php | 20 +++++ .../Spout/Writer/XLSX/WriterWithStyleTest.php | 19 ++++ 12 files changed, 246 insertions(+), 46 deletions(-) create mode 100644 src/Spout/Common/Entity/Style/CellAlignment.php diff --git a/docs/_pages/documentation.md b/docs/_pages/documentation.md index 553cf67..d63ca03 100755 --- a/docs/_pages/documentation.md +++ b/docs/_pages/documentation.md @@ -116,16 +116,17 @@ $reader->setShouldPreserveEmptyRows(true); For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones: -| Category | Property | API -|:----------|:--------------|:-------------------------------------- -| Font | Bold | `StyleBuilder::setFontBold()` -| | Italic | `StyleBuilder::setFontItalic()` -| | Underline | `StyleBuilder::setFontUnderline()` -| | Strikethrough | `StyleBuilder::setFontStrikethrough()` -| | Font name | `StyleBuilder::setFontName('Arial')` -| | Font size | `StyleBuilder::setFontSize(14)` -| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` -| Alignment | Wrap text | `StyleBuilder::setShouldWrapText(true|false)` +| Category | Property | API +|:----------|:---------------|:-------------------------------------- +| Font | Bold | `StyleBuilder::setFontBold()` +| | Italic | `StyleBuilder::setFontItalic()` +| | Underline | `StyleBuilder::setFontUnderline()` +| | Strikethrough | `StyleBuilder::setFontStrikethrough()` +| | Font name | `StyleBuilder::setFontName('Arial')` +| | Font size | `StyleBuilder::setFontSize(14)` +| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` +| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)` +| | Wrap text | `StyleBuilder::setShouldWrapText(true)` ### Styling rows @@ -135,6 +136,7 @@ It is possible to apply some formatting options to a row. In this case, all cell ```php use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Common\Creator\Style\StyleBuilder; +use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Common\Entity\Style\Color; $writer = WriterEntityFactory::createXLSXWriter(); @@ -146,6 +148,7 @@ $style = (new StyleBuilder()) ->setFontSize(15) ->setFontColor(Color::BLUE) ->setShouldWrapText() + ->setCellAlignment(CellAlignment::RIGHT) ->setBackgroundColor(Color::YELLOW) ->build(); diff --git a/src/Spout/Common/Entity/Style/CellAlignment.php b/src/Spout/Common/Entity/Style/CellAlignment.php new file mode 100644 index 0000000..60fe833 --- /dev/null +++ b/src/Spout/Common/Entity/Style/CellAlignment.php @@ -0,0 +1,32 @@ + 1, + self::RIGHT => 1, + self::CENTER => 1, + self::JUSTIFY => 1, + ]; + + /** + * @param string $cellAlignment + * + * @return bool Whether the given cell alignment is valid + */ + public static function isValid($cellAlignment) + { + return isset(self::$VALID_ALIGNMENTS[$cellAlignment]); + } +} diff --git a/src/Spout/Common/Entity/Style/Color.php b/src/Spout/Common/Entity/Style/Color.php index 36b3d26..ce903e5 100644 --- a/src/Spout/Common/Entity/Style/Color.php +++ b/src/Spout/Common/Entity/Style/Color.php @@ -8,7 +8,7 @@ use Box\Spout\Common\Exception\InvalidColorException; * Class Color * This class provides constants and functions to work with colors */ -class Color +abstract class Color { /** Standard colors - based on Office Online */ const BLACK = '000000'; diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index 0b5c0d7..7e989a4 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -8,7 +8,7 @@ namespace Box\Spout\Common\Entity\Style; */ class Style { - /** Default font values */ + /** Default values */ const DEFAULT_FONT_SIZE = 11; const DEFAULT_FONT_COLOR = Color::BLACK; const DEFAULT_FONT_NAME = 'Arial'; @@ -54,6 +54,13 @@ class Style /** @var bool Whether specific font properties should be applied */ private $shouldApplyFont = false; + /** @var bool Whether specific cell alignment should be applied */ + private $shouldApplyCellAlignment = false; + /** @var string Cell alignment */ + private $cellAlignment; + /** @var bool Whether the cell alignment property was set */ + private $hasSetCellAlignment = false; + /** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */ private $shouldWrapText = false; /** @var bool Whether the wrap text property was set */ @@ -325,6 +332,44 @@ class Style return $this->hasSetFontName; } + /** + * @return string + */ + public function getCellAlignment() + { + return $this->cellAlignment; + } + + /** + * @param string $cellAlignment The cell alignment + * + * @return Style + */ + public function setCellAlignment($cellAlignment) + { + $this->cellAlignment = $cellAlignment; + $this->hasSetCellAlignment = true; + $this->shouldApplyCellAlignment = true; + + return $this; + } + + /** + * @return bool + */ + public function hasSetCellAlignment() + { + return $this->hasSetCellAlignment; + } + + /** + * @return bool Whether specific cell alignment should be applied + */ + public function shouldApplyCellAlignment() + { + return $this->shouldApplyCellAlignment; + } + /** * @return bool */ diff --git a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php index 65792eb..bc2d406 100644 --- a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php +++ b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php @@ -3,7 +3,9 @@ namespace Box\Spout\Writer\Common\Creator\Style; use Box\Spout\Common\Entity\Style\Border; +use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Common\Entity\Style\Style; +use Box\Spout\Common\Exception\InvalidArgumentException; /** * Class StyleBuilder @@ -122,6 +124,25 @@ class StyleBuilder return $this; } + /** + * Sets the cell alignment. + * + * @param string $cellAlignment The cell alignment + * + * @throws InvalidArgumentException If the given cell alignment is not valid + * @return StyleBuilder + */ + public function setCellAlignment($cellAlignment) + { + if (!CellAlignment::isValid($cellAlignment)) { + throw new InvalidArgumentException('Invalid cell alignment value'); + } + + $this->style->setCellAlignment($cellAlignment); + + return $this; + } + /** * Set a border * diff --git a/src/Spout/Writer/Common/Manager/Style/StyleMerger.php b/src/Spout/Writer/Common/Manager/Style/StyleMerger.php index 1628783..806c8d5 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleMerger.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleMerger.php @@ -85,6 +85,9 @@ class StyleMerger if (!$style->hasSetWrapText() && $baseStyle->shouldWrapText()) { $styleToUpdate->setShouldWrapText(); } + if (!$style->hasSetCellAlignment() && $baseStyle->shouldApplyCellAlignment()) { + $styleToUpdate->setCellAlignment($baseStyle->getCellAlignment()); + } if (!$style->getBorder() && $baseStyle->shouldApplyBorder()) { $styleToUpdate->setBorder($baseStyle->getBorder()); } diff --git a/src/Spout/Writer/ODS/Manager/Style/StyleManager.php b/src/Spout/Writer/ODS/Manager/Style/StyleManager.php index 5b3bb0d..4e163eb 100644 --- a/src/Spout/Writer/ODS/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/ODS/Manager/Style/StyleManager.php @@ -3,6 +3,7 @@ namespace Box\Spout\Writer\ODS\Manager\Style; use Box\Spout\Common\Entity\Style\BorderPart; +use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\ODS\Helper\BorderHelper; @@ -199,6 +200,7 @@ EOD; $content = ''; $content .= $this->getTextPropertiesSectionContent($style); + $content .= $this->getParagraphPropertiesSectionContent($style); $content .= $this->getTableCellPropertiesSectionContent($style); $content .= ''; @@ -214,26 +216,26 @@ EOD; */ private function getTextPropertiesSectionContent($style) { - $content = ''; - - if ($style->shouldApplyFont()) { - $content .= $this->getFontSectionContent($style); + if (!$style->shouldApplyFont()) { + return ''; } - return $content; + return 'getFontSectionContent($style) + . '/>'; } /** - * Returns the contents of the "" section, inside "" section + * Returns the contents of the fonts definition section, inside "" section * * @param \Box\Spout\Common\Entity\Style\Style $style + * * @return string */ private function getFontSectionContent($style) { $defaultStyle = $this->getDefaultStyle(); - - $content = 'getFontColor(); if ($fontColor !== $defaultStyle->getFontColor()) { @@ -263,11 +265,60 @@ EOD; $content .= ' style:text-line-through-style="solid"'; } - $content .= '/>'; - return $content; } + /** + * Returns the contents of the "" section, inside "" section + * + * @param \Box\Spout\Common\Entity\Style\Style $style + * + * @return string + */ + private function getParagraphPropertiesSectionContent($style) + { + if (!$style->shouldApplyCellAlignment()) { + return ''; + } + + return 'getCellAlignmentSectionContent($style) + . '/>'; + } + + /** + * Returns the contents of the cell alignment definition for the "" section + * + * @param \Box\Spout\Common\Entity\Style\Style $style + * + * @return string + */ + private function getCellAlignmentSectionContent($style) + { + return sprintf( + ' fo:text-align="%s" ', + $this->transformCellAlignment($style->getCellAlignment()) + ); + } + + /** + * Even though "left" and "right" alignments are part of the spec, and interpreted + * respectively as "start" and "end", using the recommended values increase compatibility + * with software that will read the created ODS file. + * + * @param string $cellAlignment + * + * @return string + */ + private function transformCellAlignment($cellAlignment) + { + switch ($cellAlignment) { + case CellAlignment::LEFT: return 'start'; + case CellAlignment::RIGHT: return 'end'; + default: return $cellAlignment; + } + } + /** * Returns the contents of the "" section, inside "" section * @@ -276,7 +327,7 @@ EOD; */ private function getTableCellPropertiesSectionContent($style) { - $content = ''; + $content = 'shouldWrapText()) { $content .= $this->getWrapTextXMLContent(); @@ -290,6 +341,8 @@ EOD; $content .= $this->getBackgroundColorXMLContent($style); } + $content .= '/>'; + return $content; } @@ -300,7 +353,7 @@ EOD; */ private function getWrapTextXMLContent() { - return ''; + return ' fo:wrap-option="wrap" style:vertical-align="automatic" '; } /** @@ -311,13 +364,11 @@ EOD; */ private function getBorderXMLContent($style) { - $borderProperty = ''; - $borders = array_map(function (BorderPart $borderPart) { return BorderHelper::serializeBorderPart($borderPart); }, $style->getBorder()->getParts()); - return sprintf($borderProperty, implode(' ', $borders)); + return sprintf(' %s ', implode(' ', $borders)); } /** @@ -328,9 +379,6 @@ EOD; */ private function getBackgroundColorXMLContent($style) { - return sprintf( - '', - $style->getBackgroundColor() - ); + return sprintf(' fo:background-color="#%s" ', $style->getBackgroundColor()); } } diff --git a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php index b7491a9..5eaa606 100644 --- a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php @@ -249,9 +249,16 @@ EOD; $content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0); - if ($style->shouldWrapText()) { + if ($style->shouldApplyCellAlignment() || $style->shouldWrapText()) { $content .= ' applyAlignment="1">'; - $content .= ''; + $content .= 'shouldApplyCellAlignment()) { + $content .= sprintf(' horizontal="%s"', $style->getCellAlignment()); + } + if ($style->shouldWrapText()) { + $content .= ' wrapText="1"'; + } + $content .= '/>'; $content .= ''; } else { $content .= '/>'; diff --git a/tests/Spout/Writer/Common/Creator/StyleBuilderTest.php b/tests/Spout/Writer/Common/Creator/StyleBuilderTest.php index 7790928..e1eb00f 100644 --- a/tests/Spout/Writer/Common/Creator/StyleBuilderTest.php +++ b/tests/Spout/Writer/Common/Creator/StyleBuilderTest.php @@ -3,7 +3,9 @@ namespace Box\Spout\Writer\Common\Creator\Style; use Box\Spout\Common\Entity\Style\Border; +use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Common\Entity\Style\Color; +use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use PHPUnit\Framework\TestCase; @@ -45,20 +47,18 @@ class StyleBuilderTest extends TestCase /** * @return void */ - public function testStyleBuilderShouldMergeFormats() + public function testStyleBuilderShouldApplyCellAlignment() { - $baseStyle = (new StyleBuilder()) - ->setFontBold() - ->setFormat('0.00') - ->build(); + $style = (new StyleBuilder())->setCellAlignment(CellAlignment::CENTER)->build(); + $this->assertTrue($style->shouldApplyCellAlignment()); + } - $currentStyle = (new StyleBuilder())->build(); - - $styleMerger = new StyleMerger(); - $mergedStyle = $styleMerger->merge($currentStyle, $baseStyle); - - $this->assertNull($currentStyle->getFormat(), 'Current style has no border'); - $this->assertEquals('0.00', $baseStyle->getFormat(), 'Base style has a format 0.00'); - $this->assertEquals('0.00', $mergedStyle->getFormat(), 'Merged style has a format 0.00'); + /** + * @return void + */ + public function testStyleBuilderShouldThrowOnInvalidCellAlignment() + { + $this->expectException(InvalidArgumentException::class); + (new StyleBuilder())->setCellAlignment('invalid_cell_alignment')->build(); } } diff --git a/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php b/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php index 125d676..46820c0 100644 --- a/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php +++ b/tests/Spout/Writer/Common/Manager/Style/StyleMergerTest.php @@ -45,6 +45,7 @@ class StyleMergerTest extends TestCase ->setFontBold() ->setFontColor(Color::YELLOW) ->setBackgroundColor(Color::BLUE) + ->setFormat('0.00') ->build(); $currentStyle = (new StyleBuilder())->setFontName('Font')->setFontUnderline()->build(); $mergedStyle = $this->styleMerger->merge($currentStyle, $baseStyle); @@ -60,6 +61,7 @@ class StyleMergerTest extends TestCase $this->assertTrue($mergedStyle->isFontUnderline()); $this->assertEquals(Color::YELLOW, $mergedStyle->getFontColor()); $this->assertEquals(Color::BLUE, $mergedStyle->getBackgroundColor()); + $this->assertEquals('0.00', $mergedStyle->getFormat()); } /** diff --git a/tests/Spout/Writer/ODS/WriterWithStyleTest.php b/tests/Spout/Writer/ODS/WriterWithStyleTest.php index 5afd73b..dcdd364 100644 --- a/tests/Spout/Writer/ODS/WriterWithStyleTest.php +++ b/tests/Spout/Writer/ODS/WriterWithStyleTest.php @@ -4,6 +4,7 @@ namespace Box\Spout\Writer\ODS; use Box\Spout\Common\Entity\Row; use Box\Spout\Common\Entity\Style\Border; +use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Common\Entity\Style\Color; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Reader\Wrapper\XMLReader; @@ -204,6 +205,25 @@ class WriterWithStyleTest extends TestCase $this->assertFirstChildHasAttributeEquals('wrap', $customStyleElement, 'table-cell-properties', 'fo:wrap-option'); } + /** + * @return void + */ + public function testAddRowShouldApplyCellAlignment() + { + $fileName = 'test_add_row_should_apply_cell_alignment.xlsx'; + + $rightAlignedStyle = (new StyleBuilder())->setCellAlignment(CellAlignment::RIGHT)->build(); + $dataRows = $this->createStyledRowsFromValues([['ods--11']], $rightAlignedStyle); + + $this->writeToODSFile($dataRows, $fileName); + + $styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); + $this->assertCount(2, $styleElements, 'There should be 2 styles (default and custom)'); + + $customStyleElement = $styleElements[1]; + $this->assertFirstChildHasAttributeEquals('end', $customStyleElement, 'paragraph-properties', 'fo:text-align'); + } + /** * @return void */ diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index d556623..da0aec2 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -4,6 +4,7 @@ namespace Box\Spout\Writer\XLSX; use Box\Spout\Common\Entity\Row; use Box\Spout\Common\Entity\Style\Border; +use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Common\Entity\Style\Color; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Reader\Wrapper\XMLReader; @@ -287,6 +288,24 @@ class WriterWithStyleTest extends TestCase $this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText'); } + /** + * @return void + */ + public function testAddRowShouldApplyCellAlignment() + { + $fileName = 'test_add_row_should_apply_cell_alignment.xlsx'; + + $rightAlignedStyle = (new StyleBuilder())->setCellAlignment(CellAlignment::RIGHT)->build(); + $dataRows = $this->createStyledRowsFromValues([['xlsx--11']], $rightAlignedStyle); + + $this->writeToXLSXFile($dataRows, $fileName); + + $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); + $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); + $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); + $this->assertFirstChildHasAttributeEquals(CellAlignment::RIGHT, $xfElement, 'alignment', 'horizontal'); + } + /** * @return void */