From 488bc371a532dfa8bdbd978e60a2df09fd01e07e Mon Sep 17 00:00:00 2001 From: Lucian Sirbu Date: Mon, 4 May 2020 16:48:18 +0300 Subject: [PATCH 1/5] Added support for mergeCells, cell height, shrink to fit Added support for mergeCells: // mergeCells (B2:G2), you may use CellHelper::getColumnLettersFromColumnIndex() to convert from "B2" to "[1,2]" $writer->mergeCells([1,2], [6, 2]); cell height: $row->setHeight(30); shouldShrinkToFit: $style->setShouldShrinkToFit(); These changes are implemented for XLSX as that's what I need and test spout on. --- src/Spout/Common/Entity/Row.php | 26 ++++++++++++ src/Spout/Common/Entity/Style/Style.php | 23 ++++++++++ .../Common/Manager/OptionsManagerAbstract.php | 15 +++++++ .../Common/Creator/Style/StyleBuilder.php | 11 +++++ src/Spout/Writer/Common/Entity/Options.php | 6 +++ src/Spout/Writer/Common/Entity/Sheet.php | 22 ++++++++++ .../Writer/WriterMultiSheetsAbstract.php | 23 ++++++++++ .../Writer/XLSX/Manager/OptionsManager.php | 4 ++ .../XLSX/Manager/Style/StyleManager.php | 4 ++ .../Writer/XLSX/Manager/WorksheetManager.php | 42 ++++++++++++++++++- 10 files changed, 174 insertions(+), 2 deletions(-) diff --git a/src/Spout/Common/Entity/Row.php b/src/Spout/Common/Entity/Row.php index 0cc49c7..4fba9b4 100644 --- a/src/Spout/Common/Entity/Row.php +++ b/src/Spout/Common/Entity/Row.php @@ -18,6 +18,12 @@ class Row */ protected $style; + /** + * Row height (default is 15) + * @var string + */ + protected $height = "15"; + /** * Row constructor. * @param Cell[] $cells @@ -126,4 +132,24 @@ class Row return $cell->getValue(); }, $this->cells); } + + /** + * Set row height + * @param String $height + * @return Row + */ + public function setHeight($height) + { + $this->height = $height; + return $this; + } + + /** + * Returns row height + * @return String + */ + public function getHeight() + { + return $this->height; + } } diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index 7e989a4..40c9f20 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -66,6 +66,9 @@ class Style /** @var bool Whether the wrap text property was set */ private $hasSetWrapText = false; + private $shrinkToFit = false; + private $shouldShrinkToFit = false; + /** @var Border */ private $border; @@ -463,4 +466,24 @@ class Style { return $this->hasSetFormat; } + + /** + * Sets should shrink to fit + * @param bool $shrinkToFit + * @return Style + */ + public function setShouldShrinkToFit($shrinkToFit = true) + { + $this->shrinkToFit = $shrinkToFit; + $this->shouldShrinkToFit = $shrinkToFit; + return $this; + } + + /** + * @return bool Whether format should be applied + */ + public function shouldShrinkToFit() + { + return $this->shouldShrinkToFit; + } } diff --git a/src/Spout/Common/Manager/OptionsManagerAbstract.php b/src/Spout/Common/Manager/OptionsManagerAbstract.php index 5670b95..672f6fd 100644 --- a/src/Spout/Common/Manager/OptionsManagerAbstract.php +++ b/src/Spout/Common/Manager/OptionsManagerAbstract.php @@ -51,6 +51,21 @@ abstract class OptionsManagerAbstract implements OptionsManagerInterface } } + /** + * Add an option to the internal list of options + * Used only for mergeCells() for now + * @return void + */ + public function addOption($optionName, $optionValue) + { + if (\in_array($optionName, $this->supportedOptions)) { + if (!isset($this->options[$optionName])) { + $this->options[$optionName] = []; + } + $this->options[$optionName][] = $optionValue; + } + } + /** * @param string $optionName * @return mixed|null The set option or NULL if no option with given name found diff --git a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php index bc2d406..8c48848 100644 --- a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php +++ b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php @@ -183,6 +183,17 @@ class StyleBuilder return $this; } + /** + * Set should shrink to fit + * @param boolean $shrinkToFit + * @return void + */ + public function setShouldShrinkToFit($shrinkToFit = true) + { + $this->style->setShouldShrinkToFit($shrinkToFit); + return $this; + } + /** * Returns the configured style. The style is cached and can be reused. * diff --git a/src/Spout/Writer/Common/Entity/Options.php b/src/Spout/Writer/Common/Entity/Options.php index d7152bb..0baa628 100644 --- a/src/Spout/Writer/Common/Entity/Options.php +++ b/src/Spout/Writer/Common/Entity/Options.php @@ -20,4 +20,10 @@ abstract class Options // XLSX specific options const SHOULD_USE_INLINE_STRINGS = 'shouldUseInlineStrings'; + + // XLSX column widths + const COLUMN_WIDTHS = 'columnWidths'; + + // XLSX merge cells + const MERGE_CELLS = 'mergeCells'; } diff --git a/src/Spout/Writer/Common/Entity/Sheet.php b/src/Spout/Writer/Common/Entity/Sheet.php index aabdd91..6391571 100644 --- a/src/Spout/Writer/Common/Entity/Sheet.php +++ b/src/Spout/Writer/Common/Entity/Sheet.php @@ -27,6 +27,9 @@ class Sheet /** @var SheetManager Sheet manager */ private $sheetManager; + /** @var bool Sheet is started */ + private $isSheetStarted = false; + /** * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $associatedWorkbookId ID of the sheet's associated workbook @@ -108,4 +111,23 @@ class Sheet return $this; } + + /** + * @return bool isSheetStarted Sheet was started + */ + public function isSheetStarted() + { + return $this->isSheetStarted; + } + + /** + * @param bool $isSheetStarted Set if sheet was started + * @return Sheet + */ + public function setIsSheetStarted($isSheetStarted) + { + $this->isSheetStarted = $isSheetStarted; + + return $this; + } } diff --git a/src/Spout/Writer/WriterMultiSheetsAbstract.php b/src/Spout/Writer/WriterMultiSheetsAbstract.php index 8170b67..ec84e44 100644 --- a/src/Spout/Writer/WriterMultiSheetsAbstract.php +++ b/src/Spout/Writer/WriterMultiSheetsAbstract.php @@ -61,6 +61,29 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract return $this; } + /** + * Set columns widths as list. If value is null will set column with default width (8.43) + * @param array $columnWidths + * @return WriterMultiSheetsAbstract + */ + public function setColumnWidths(array $columnWidths) + { + $this->optionsManager->setOption(Options::COLUMN_WIDTHS, $columnWidths); + return $this; + } + + /** + * Undocumented function + * + * @param array $range + * @return void + */ + public function mergeCells(array $range1, array $range2) + { + $this->optionsManager->addOption(Options::MERGE_CELLS, [$range1, $range2]); + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/Spout/Writer/XLSX/Manager/OptionsManager.php b/src/Spout/Writer/XLSX/Manager/OptionsManager.php index d3b5cd4..40e299e 100644 --- a/src/Spout/Writer/XLSX/Manager/OptionsManager.php +++ b/src/Spout/Writer/XLSX/Manager/OptionsManager.php @@ -39,6 +39,8 @@ class OptionsManager extends OptionsManagerAbstract Options::DEFAULT_ROW_STYLE, Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, Options::SHOULD_USE_INLINE_STRINGS, + Options::COLUMN_WIDTHS, + Options::MERGE_CELLS, ]; } @@ -56,5 +58,7 @@ class OptionsManager extends OptionsManagerAbstract $this->setOption(Options::DEFAULT_ROW_STYLE, $defaultRowStyle); $this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true); $this->setOption(Options::SHOULD_USE_INLINE_STRINGS, true); + $this->setOption(Options::COLUMN_WIDTHS, []); + $this->setOption(Options::MERGE_CELLS, []); } } diff --git a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php index f0ca9d9..b79b1da 100644 --- a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php @@ -258,6 +258,10 @@ EOD; if ($style->shouldWrapText()) { $content .= ' wrapText="1"'; } + if ($style->shouldShrinkToFit()) { + $content .= ' shrinkToFit="true"'; + } + $content .= '/>'; $content .= ''; } else { diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 741d0aa..2cac24b 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -37,6 +37,8 @@ class WorksheetManager implements WorksheetManagerInterface EOD; + /** @var OptionsManagerInterface */ + private $optionsManager; /** @var bool Whether inline or shared strings should be used */ protected $shouldUseInlineStrings; @@ -84,6 +86,7 @@ EOD; StringHelper $stringHelper, InternalEntityFactory $entityFactory ) { + $this->optionsManager = $optionsManager; $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS); $this->rowManager = $rowManager; $this->styleManager = $styleManager; @@ -113,7 +116,6 @@ EOD; $worksheet->setFilePointer($sheetFilePointer); \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER); - \fwrite($sheetFilePointer, ''); } /** @@ -153,11 +155,28 @@ EOD; */ private function addNonEmptyRow(Worksheet $worksheet, Row $row) { + if (!$worksheet->getExternalSheet()->isSheetStarted()) { + // create nodes for columns widths + if ($this->optionsManager->getOption(Options::COLUMN_WIDTHS)) { + $colsString = ''; + foreach ($this->optionsManager->getOption(Options::COLUMN_WIDTHS) as $index => $width) { + $index++; + $colsString.= ''; + } + $colsString.=""; + \fwrite($worksheet->getFilePointer(), $colsString); + } + + \fwrite($worksheet->getFilePointer(), ''); + $worksheet->getExternalSheet()->setIsSheetStarted(true); + } + $rowStyle = $row->getStyle(); $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1; $numCells = $row->getNumCells(); + $rowHeight = $row->getHeight(); - $rowXML = ''; + $rowXML = ''; foreach ($row->getCells() as $columnIndexZeroBased => $cell) { $rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndexOneBased, $columnIndexZeroBased); @@ -270,7 +289,26 @@ EOD; return; } + if (!$worksheet->getExternalSheet()->isSheetStarted()) { + \fwrite($worksheetFilePointer, ''); + $worksheet->getExternalSheet()->setIsSheetStarted(true); + } + \fwrite($worksheetFilePointer, ''); + + // create nodes for merge cells + if ($this->optionsManager->getOption(Options::MERGE_CELLS)) { + $mergeCellString = ''; + foreach ($this->optionsManager->getOption(Options::MERGE_CELLS) as $values) { + $output = array_map(function($value){ + return CellHelper::getColumnLettersFromColumnIndex($value[0]) . $value[1]; + }, $values); + $mergeCellString.= ''; + } + $mergeCellString.= ''; + \fwrite($worksheet->getFilePointer(), $mergeCellString); + } + \fwrite($worksheetFilePointer, ''); \fclose($worksheetFilePointer); } From e290f458803575ead7ae90df6097a71075ba855c Mon Sep 17 00:00:00 2001 From: Lucian Sirbu Date: Mon, 4 May 2020 17:39:28 +0300 Subject: [PATCH 2/5] cleanup, make sure we call the base functions --- src/Spout/Writer/XLSX/Manager/WorksheetManager.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 2cac24b..59ee04a 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -298,12 +298,12 @@ EOD; // create nodes for merge cells if ($this->optionsManager->getOption(Options::MERGE_CELLS)) { - $mergeCellString = ''; + $mergeCellString = ''; foreach ($this->optionsManager->getOption(Options::MERGE_CELLS) as $values) { - $output = array_map(function($value){ + $output = \array_map(function($value){ return CellHelper::getColumnLettersFromColumnIndex($value[0]) . $value[1]; }, $values); - $mergeCellString.= ''; + $mergeCellString.= ''; } $mergeCellString.= ''; \fwrite($worksheet->getFilePointer(), $mergeCellString); From f702127d3a7035e1000fca879c087dec1808bed5 Mon Sep 17 00:00:00 2001 From: Lucian Sirbu Date: Fri, 31 Jul 2020 16:06:05 +0300 Subject: [PATCH 3/5] cc, as sugggested in https://github.com/box/spout/pull/738#issuecomment-635877104 --- src/Spout/Common/Entity/Row.php | 7 ++++--- src/Spout/Common/Entity/Style/Style.php | 3 ++- src/Spout/Common/Manager/OptionsManagerAbstract.php | 2 ++ src/Spout/Reader/ODS/SheetIterator.php | 4 ++-- src/Spout/Reader/XLSX/Manager/SharedStringsManager.php | 2 +- src/Spout/Reader/XLSX/RowIterator.php | 2 +- src/Spout/Writer/Common/Creator/Style/StyleBuilder.php | 3 ++- .../Writer/Common/Manager/WorkbookManagerAbstract.php | 2 +- src/Spout/Writer/ODS/Creator/ManagerFactory.php | 2 +- src/Spout/Writer/WriterAbstract.php | 2 +- src/Spout/Writer/WriterMultiSheetsAbstract.php | 2 ++ src/Spout/Writer/XLSX/Creator/ManagerFactory.php | 2 +- src/Spout/Writer/XLSX/Manager/WorksheetManager.php | 8 ++++---- tests/Spout/Reader/CSV/SpoutTestStream.php | 4 ++-- 14 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Spout/Common/Entity/Row.php b/src/Spout/Common/Entity/Row.php index 4fba9b4..eece8ac 100644 --- a/src/Spout/Common/Entity/Row.php +++ b/src/Spout/Common/Entity/Row.php @@ -22,7 +22,7 @@ class Row * Row height (default is 15) * @var string */ - protected $height = "15"; + protected $height = '15'; /** * Row constructor. @@ -135,18 +135,19 @@ class Row /** * Set row height - * @param String $height + * @param string $height * @return Row */ public function setHeight($height) { $this->height = $height; + return $this; } /** * Returns row height - * @return String + * @return string */ public function getHeight() { diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index 40c9f20..7a49728 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -476,10 +476,11 @@ class Style { $this->shrinkToFit = $shrinkToFit; $this->shouldShrinkToFit = $shrinkToFit; + return $this; } - /** + /** * @return bool Whether format should be applied */ public function shouldShrinkToFit() diff --git a/src/Spout/Common/Manager/OptionsManagerAbstract.php b/src/Spout/Common/Manager/OptionsManagerAbstract.php index 672f6fd..2381c52 100644 --- a/src/Spout/Common/Manager/OptionsManagerAbstract.php +++ b/src/Spout/Common/Manager/OptionsManagerAbstract.php @@ -54,6 +54,8 @@ abstract class OptionsManagerAbstract implements OptionsManagerInterface /** * Add an option to the internal list of options * Used only for mergeCells() for now + * @param mixed $optionName + * @param mixed $optionValue * @return void */ public function addOption($optionName, $optionValue) diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php index f35b852..c7b8cd9 100644 --- a/src/Spout/Reader/ODS/SheetIterator.php +++ b/src/Spout/Reader/ODS/SheetIterator.php @@ -28,13 +28,13 @@ class SheetIterator implements IteratorInterface const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name'; const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display'; - /** @var string $filePath Path of the file to be read */ + /** @var string Path of the file to be read */ protected $filePath; /** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */ protected $optionsManager; - /** @var InternalEntityFactory $entityFactory Factory to create entities */ + /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; /** @var XMLReader The XMLReader object that will help read sheet's XML data */ diff --git a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php index caaeed7..8850a69 100644 --- a/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php +++ b/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php @@ -43,7 +43,7 @@ class SharedStringsManager /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; - /** @var HelperFactory $helperFactory Factory to create helpers */ + /** @var HelperFactory Factory to create helpers */ protected $helperFactory; /** @var CachingStrategyFactory Factory to create shared strings caching strategies */ diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php index 4af4530..a54b8b1 100644 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ b/src/Spout/Reader/XLSX/RowIterator.php @@ -35,7 +35,7 @@ class RowIterator implements IteratorInterface /** @var string Path of the XLSX file being read */ protected $filePath; - /** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */ + /** @var string Path of the sheet data XML file as in [Content_Types].xml */ protected $sheetDataXMLFilePath; /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ diff --git a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php index 8c48848..1341233 100644 --- a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php +++ b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php @@ -185,12 +185,13 @@ class StyleBuilder /** * Set should shrink to fit - * @param boolean $shrinkToFit + * @param bool $shrinkToFit * @return void */ public function setShouldShrinkToFit($shrinkToFit = true) { $this->style->setShouldShrinkToFit($shrinkToFit); + return $this; } diff --git a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php index b513555..653778c 100644 --- a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php +++ b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php @@ -44,7 +44,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; - /** @var ManagerFactoryInterface $managerFactory Factory to create managers */ + /** @var ManagerFactoryInterface Factory to create managers */ protected $managerFactory; /** @var Worksheet The worksheet where data will be written to */ diff --git a/src/Spout/Writer/ODS/Creator/ManagerFactory.php b/src/Spout/Writer/ODS/Creator/ManagerFactory.php index f38c500..a5b77ee 100644 --- a/src/Spout/Writer/ODS/Creator/ManagerFactory.php +++ b/src/Spout/Writer/ODS/Creator/ManagerFactory.php @@ -22,7 +22,7 @@ class ManagerFactory implements ManagerFactoryInterface /** @var InternalEntityFactory */ protected $entityFactory; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** diff --git a/src/Spout/Writer/WriterAbstract.php b/src/Spout/Writer/WriterAbstract.php index d96a628..bbaa735 100644 --- a/src/Spout/Writer/WriterAbstract.php +++ b/src/Spout/Writer/WriterAbstract.php @@ -33,7 +33,7 @@ abstract class WriterAbstract implements WriterInterface /** @var GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** @var OptionsManagerInterface Writer options manager */ diff --git a/src/Spout/Writer/WriterMultiSheetsAbstract.php b/src/Spout/Writer/WriterMultiSheetsAbstract.php index ec84e44..57efae4 100644 --- a/src/Spout/Writer/WriterMultiSheetsAbstract.php +++ b/src/Spout/Writer/WriterMultiSheetsAbstract.php @@ -69,6 +69,7 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract public function setColumnWidths(array $columnWidths) { $this->optionsManager->setOption(Options::COLUMN_WIDTHS, $columnWidths); + return $this; } @@ -81,6 +82,7 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract public function mergeCells(array $range1, array $range2) { $this->optionsManager->addOption(Options::MERGE_CELLS, [$range1, $range2]); + return $this; } diff --git a/src/Spout/Writer/XLSX/Creator/ManagerFactory.php b/src/Spout/Writer/XLSX/Creator/ManagerFactory.php index f27a2f2..aa3bcd5 100644 --- a/src/Spout/Writer/XLSX/Creator/ManagerFactory.php +++ b/src/Spout/Writer/XLSX/Creator/ManagerFactory.php @@ -24,7 +24,7 @@ class ManagerFactory implements ManagerFactoryInterface /** @var InternalEntityFactory */ protected $entityFactory; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 59ee04a..4a71603 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -161,9 +161,9 @@ EOD; $colsString = ''; foreach ($this->optionsManager->getOption(Options::COLUMN_WIDTHS) as $index => $width) { $index++; - $colsString.= ''; + $colsString.= ''; } - $colsString.=""; + $colsString.=''; \fwrite($worksheet->getFilePointer(), $colsString); } @@ -176,7 +176,7 @@ EOD; $numCells = $row->getNumCells(); $rowHeight = $row->getHeight(); - $rowXML = ''; + $rowXML = ''; foreach ($row->getCells() as $columnIndexZeroBased => $cell) { $rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndexOneBased, $columnIndexZeroBased); @@ -300,7 +300,7 @@ EOD; if ($this->optionsManager->getOption(Options::MERGE_CELLS)) { $mergeCellString = ''; foreach ($this->optionsManager->getOption(Options::MERGE_CELLS) as $values) { - $output = \array_map(function($value){ + $output = \array_map(function ($value) { return CellHelper::getColumnLettersFromColumnIndex($value[0]) . $value[1]; }, $values); $mergeCellString.= ''; diff --git a/tests/Spout/Reader/CSV/SpoutTestStream.php b/tests/Spout/Reader/CSV/SpoutTestStream.php index 3bfd06e..d66e811 100644 --- a/tests/Spout/Reader/CSV/SpoutTestStream.php +++ b/tests/Spout/Reader/CSV/SpoutTestStream.php @@ -14,10 +14,10 @@ class SpoutTestStream const PATH_TO_CSV_RESOURCES = 'tests/resources/csv/'; const CSV_EXTENSION = '.csv'; - /** @var int $position */ + /** @var int */ private $position; - /** @var resource $fileHandle */ + /** @var resource */ private $fileHandle; /** From 817e4f1f01dfe98ac6642ab329111bfe44a51e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Ign=C3=A1cz?= Date: Fri, 14 Aug 2020 16:31:07 +0200 Subject: [PATCH 4/5] #729 - fix shrink to fit + add test shrinkToFit was not handled in StyleMerger so it was overwritten by the default cell style StyleManager didn't add the property to the xml if shrinkToFit was used without alignment or text wrap. Unit test --- src/Spout/Common/Entity/Style/Style.php | 14 ++++++++++++-- .../Common/Creator/Style/StyleBuilder.php | 3 ++- .../Common/Manager/Style/StyleMerger.php | 3 +++ .../Writer/XLSX/Manager/Style/StyleManager.php | 2 +- .../Spout/Writer/XLSX/WriterWithStyleTest.php | 18 ++++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index 7a49728..97688e9 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -66,8 +66,10 @@ class Style /** @var bool Whether the wrap text property was set */ private $hasSetWrapText = false; - private $shrinkToFit = false; + /** @var bool Whether the cell should shrink to fit to content */ private $shouldShrinkToFit = false; + /** @var bool Whether the shouldShrinkToFit text property was set */ + private $hasSetShrinkToFit = false; /** @var Border */ private $border; @@ -474,7 +476,7 @@ class Style */ public function setShouldShrinkToFit($shrinkToFit = true) { - $this->shrinkToFit = $shrinkToFit; + $this->hasSetShrinkToFit = true; $this->shouldShrinkToFit = $shrinkToFit; return $this; @@ -487,4 +489,12 @@ class Style { return $this->shouldShrinkToFit; } + + /** + * @return bool + */ + public function hasSetShrinkToFit() + { + return $this->hasSetShrinkToFit; + } } diff --git a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php index 1341233..72fdcf0 100644 --- a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php +++ b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php @@ -186,7 +186,8 @@ class StyleBuilder /** * Set should shrink to fit * @param bool $shrinkToFit - * @return void + * @return StyleBuilder + * @api */ public function setShouldShrinkToFit($shrinkToFit = true) { diff --git a/src/Spout/Writer/Common/Manager/Style/StyleMerger.php b/src/Spout/Writer/Common/Manager/Style/StyleMerger.php index 806c8d5..1adeb18 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->hasSetShrinkToFit() && $baseStyle->shouldShrinkToFit()) { + $styleToUpdate->setShouldShrinkToFit(); + } if (!$style->hasSetCellAlignment() && $baseStyle->shouldApplyCellAlignment()) { $styleToUpdate->setCellAlignment($baseStyle->getCellAlignment()); } diff --git a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php index b79b1da..437b433 100644 --- a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php @@ -249,7 +249,7 @@ EOD; $content .= \sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0); - if ($style->shouldApplyCellAlignment() || $style->shouldWrapText()) { + if ($style->shouldApplyCellAlignment() || $style->shouldWrapText() || $style->shouldShrinkToFit()) { $content .= ' applyAlignment="1">'; $content .= 'shouldApplyCellAlignment()) { diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index da0aec2..761ca85 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -306,6 +306,24 @@ class WriterWithStyleTest extends TestCase $this->assertFirstChildHasAttributeEquals(CellAlignment::RIGHT, $xfElement, 'alignment', 'horizontal'); } + /** + * @return void + */ + public function testAddRowShouldApplyShrinkToFit() + { + $fileName = 'test_add_row_should_apply_shrink_to_fit.xlsx'; + + $shrinkToFitStyle = (new StyleBuilder())->setShouldShrinkToFit()->build(); + $dataRows = $this->createStyledRowsFromValues([['xlsx--11']], $shrinkToFitStyle); + + $this->writeToXLSXFile($dataRows, $fileName); + + $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); + $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); + $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); + $this->assertFirstChildHasAttributeEquals("true", $xfElement, 'alignment', 'shrinkToFit'); + } + /** * @return void */ From 41a9ae89ee50ada9c8d31658f7cbb4f80c401472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Ign=C3=A1cz?= Date: Fri, 14 Aug 2020 18:24:26 +0200 Subject: [PATCH 5/5] #529 - tests for column width, row height and cell merge + minor refact Tests Added 'addOption' to OptionsManagerInterface Moved 'setColumnWidths' and 'mergeCells' methods to Xlsx Writer implementation since the actual feature only works for Xlsx at the moment --- .../Manager/OptionsManagerInterface.php | 9 ++ .../Writer/WriterMultiSheetsAbstract.php | 27 +--- src/Spout/Writer/XLSX/Writer.php | 30 ++++ .../Common/Manager/OptionsManagerTest.php | 24 +++ tests/Spout/Writer/XLSX/WriterTest.php | 138 ++++++++++++++++++ .../Spout/Writer/XLSX/WriterWithStyleTest.php | 2 +- 6 files changed, 204 insertions(+), 26 deletions(-) diff --git a/src/Spout/Common/Manager/OptionsManagerInterface.php b/src/Spout/Common/Manager/OptionsManagerInterface.php index 21fea0c..bccb1c3 100644 --- a/src/Spout/Common/Manager/OptionsManagerInterface.php +++ b/src/Spout/Common/Manager/OptionsManagerInterface.php @@ -19,4 +19,13 @@ interface OptionsManagerInterface * @return mixed|null The set option or NULL if no option with given name found */ public function getOption($optionName); + + /** + * Add an option to the internal list of options + * Used only for mergeCells() for now + * @param mixed $optionName + * @param mixed $optionValue + * @return void + */ + public function addOption($optionName, $optionValue); } diff --git a/src/Spout/Writer/WriterMultiSheetsAbstract.php b/src/Spout/Writer/WriterMultiSheetsAbstract.php index 57efae4..58f22b8 100644 --- a/src/Spout/Writer/WriterMultiSheetsAbstract.php +++ b/src/Spout/Writer/WriterMultiSheetsAbstract.php @@ -4,6 +4,7 @@ namespace Box\Spout\Writer; use Box\Spout\Common\Creator\HelperFactory; use Box\Spout\Common\Entity\Row; +use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\GlobalFunctionsHelper; use Box\Spout\Common\Manager\OptionsManagerInterface; use Box\Spout\Writer\Common\Creator\ManagerFactoryInterface; @@ -61,31 +62,6 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract return $this; } - /** - * Set columns widths as list. If value is null will set column with default width (8.43) - * @param array $columnWidths - * @return WriterMultiSheetsAbstract - */ - public function setColumnWidths(array $columnWidths) - { - $this->optionsManager->setOption(Options::COLUMN_WIDTHS, $columnWidths); - - return $this; - } - - /** - * Undocumented function - * - * @param array $range - * @return void - */ - public function mergeCells(array $range1, array $range2) - { - $this->optionsManager->addOption(Options::MERGE_CELLS, [$range1, $range2]); - - return $this; - } - /** * {@inheritdoc} */ @@ -122,6 +98,7 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. * * @throws WriterNotOpenedException If the writer has not been opened yet + * @throws IOException If unable to open the sheet for writing * @return Sheet The created sheet */ public function addNewSheetAndMakeItCurrent() diff --git a/src/Spout/Writer/XLSX/Writer.php b/src/Spout/Writer/XLSX/Writer.php index 9d928fc..41eb03a 100644 --- a/src/Spout/Writer/XLSX/Writer.php +++ b/src/Spout/Writer/XLSX/Writer.php @@ -47,4 +47,34 @@ class Writer extends WriterMultiSheetsAbstract return $this; } + + /** + * Set columns widths as list. If value is null will set column with default width (8.43) + * @param array $columnWidths + * @return WriterMultiSheetsAbstract + */ + public function setColumnWidths(array $columnWidths) + { + $this->optionsManager->setOption(Options::COLUMN_WIDTHS, $columnWidths); + + return $this; + } + + /** + * Merge cells. + * Row coordinates are indexed from 1, columns from 0 (A = 0), + * so a merge B2:G2 looks like $writer->mergeCells([1,2], [6, 2]); + * + * You may use CellHelper::getColumnLettersFromColumnIndex() to convert from "B2" to "[1,2]" + * + * @param int[] $range1 - top left cell's coordinate [column, row] + * @param int[] $range2 - bottom right cell's coordinate [column, row] + * @return $this + */ + public function mergeCells(array $range1, array $range2) + { + $this->optionsManager->addOption(Options::MERGE_CELLS, [$range1, $range2]); + + return $this; + } } diff --git a/tests/Spout/Common/Manager/OptionsManagerTest.php b/tests/Spout/Common/Manager/OptionsManagerTest.php index e2f0c5c..66da195 100644 --- a/tests/Spout/Common/Manager/OptionsManagerTest.php +++ b/tests/Spout/Common/Manager/OptionsManagerTest.php @@ -73,4 +73,28 @@ class OptionsManagerTest extends TestCase $optionsManager->setOption('not-supported', 'something'); $this->assertNull($optionsManager->getOption('not-supported')); } + + /** + * @return void + */ + public function testOptionManagerShouldReturnArrayIfListOptionsAdded() + { + $optionsManager = $this->optionsManager; + $optionsManager->addOption('bar', 'something'); + $optionsManager->addOption('bar', 'something-else'); + $this->assertIsArray($optionsManager->getOption('bar')); + $this->assertCount(2, $optionsManager->getOption('bar')); + $this->assertEquals('something', $optionsManager->getOption('bar')[0]); + $this->assertEquals('something-else', $optionsManager->getOption('bar')[1]); + } + + /** + * @return void + */ + public function testOptionsManagerShouldReturnNullIfListOptionNotSupported() + { + $optionsManager = $this->optionsManager; + $optionsManager->addOption('not-supported', 'something'); + $this->assertNull($optionsManager->getOption('not-supported')); + } } diff --git a/tests/Spout/Writer/XLSX/WriterTest.php b/tests/Spout/Writer/XLSX/WriterTest.php index 15cc4e4..16b852c 100644 --- a/tests/Spout/Writer/XLSX/WriterTest.php +++ b/tests/Spout/Writer/XLSX/WriterTest.php @@ -7,6 +7,7 @@ use Box\Spout\Common\Entity\Row; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\SpoutException; +use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\TestUsingResource; use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Exception\WriterAlreadyOpenedException; @@ -527,6 +528,105 @@ class WriterTest extends TestCase $this->assertInlineDataWasWrittenToSheet($fileName, 1, 'control _x0015_ character'); } + /** + * @return void + */ + public function testAddRowShouldSupportRowHeights() + { + $fileName = 'test_add_row_should_support_row_heights.xlsx'; + $dataRows = $this->createRowsFromValues([ + ['First row with default height'], + ['Second row with custom height'], + ]); + + $dataRows[1]->setHeight('23'); + + $this->writeToXLSXFile($dataRows, $fileName); + $firstRow = $this->getXmlRowFromXmlFile($fileName, 1, 1); + $secondRow = $this->getXmlRowFromXmlFile($fileName, 1, 2); + $this->assertEquals('15', $firstRow->getAttribute('ht'), '1st row does not have default height.'); + $this->assertEquals('23', $secondRow->getAttribute('ht'), '2nd row does not have custom height.'); + } + + /** + * @return void + */ + public function testAddRowShouldSupportColumnWidths() + { + $fileName = 'test_add_row_should_support_column_widths.xlsx'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->setShouldUseInlineStrings(true); + $writer->openToFile($resourcePath); + + $columnWidths = [1, 2, 3, 4]; + $writer->setColumnWidths($columnWidths); + $writer->addRows($this->createRowsFromValues([ + ['Test cell'], + ])); + $writer->close(); + + $xmlReader = $this->getXmlReaderForSheetFromXmlFile($fileName, 1); + $xmlReader->readUntilNodeFound('cols'); + $this->assertEquals('cols', $xmlReader->getCurrentNodeName(), 'Sheet does not have cols tag'); + $this->assertEquals(count($columnWidths), $xmlReader->expand()->childNodes->length, 'Sheet does not have the specified number of column definitions'); + foreach ($columnWidths as $index => $columnWidth) { + $xmlReader->readUntilNodeFound('col'); + $this->assertEquals($index + 1, $xmlReader->expand()->getAttribute('min')); + $this->assertEquals($index + 1, $xmlReader->expand()->getAttribute('max')); + $this->assertEquals($columnWidth, $xmlReader->expand()->getAttribute('width')); + } + } + + /** + * @return void + */ + public function testCloseShouldAddMergeCellTags() + { + $fileName = 'test_add_row_should_support_column_widths.xlsx'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->setShouldUseInlineStrings(true); + $writer->openToFile($resourcePath); + + $writer->mergeCells([0, 1], [3, 1]); + $writer->mergeCells([2, 3], [10, 3]); + $writer->close(); + + $xmlReader = $this->getXmlReaderForSheetFromXmlFile($fileName, 1); + $xmlReader->readUntilNodeFound('mergeCells'); + $this->assertEquals('mergeCells', $xmlReader->getCurrentNodeName(), 'Sheet does not have mergeCells tag'); + $this->assertEquals(2, $xmlReader->expand()->childNodes->length, 'Sheet does not have the specified number of mergeCell definitions'); + $xmlReader->readUntilNodeFound('mergeCell'); + $this->assertEquals('A1:D1', $xmlReader->expand()->getAttribute('ref'), 'Merge ref for first range is not valid.'); + $xmlReader->readUntilNodeFound('mergeCell'); + $this->assertEquals('C3:K3', $xmlReader->expand()->getAttribute('ref'), 'Merge ref for second range is not valid.'); + } + + /** + * @return void + */ + public function testGeneratedFileShouldBeValidForEmptySheets() + { + $fileName = 'test_empty_sheet.xlsx'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile($resourcePath); + + $writer->addNewSheetAndMakeItCurrent(); + $writer->close(); + + $xmlReader = $this->getXmlReaderForSheetFromXmlFile($fileName, 1); + $xmlReader->setParserProperty(XMLReader::VALIDATE, true); + $this->assertTrue($xmlReader->isValid(), 'worksheet xml is not valid'); + $xmlReader->setParserProperty(XMLReader::VALIDATE, false); + $xmlReader->readUntilNodeFound('sheetData'); + $this->assertEquals('sheetData', $xmlReader->getCurrentNodeName(), 'worksheet xml does not have sheetData'); + } + /** * @return void */ @@ -641,4 +741,42 @@ class WriterTest extends TestCase $this->assertContains($sharedString, $xmlContents, $message); } + + /** + * @param $fileName + * @param $sheetIndex - 1 based + * @return XMLReader + */ + private function getXmlReaderForSheetFromXmlFile($fileName, $sheetIndex) + { + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $xmlReader = new XMLReader(); + $xmlReader->openFileInZip($resourcePath, 'xl/worksheets/sheet' . $sheetIndex . '.xml'); + + return $xmlReader; + } + + /** + * @param $fileName + * @param $sheetIndex - 1 based + * @param $rowIndex - 1 based + * @throws \Box\Spout\Reader\Exception\XMLProcessingException + * @return \DOMNode|null + */ + private function getXmlRowFromXmlFile($fileName, $sheetIndex, $rowIndex) + { + $xmlReader = $this->getXmlReaderForSheetFromXmlFile($fileName, $sheetIndex); + $xmlReader->readUntilNodeFound('sheetData'); + + for ($i = 0; $i < $rowIndex; $i++) { + $xmlReader->readUntilNodeFound('row'); + } + + $row = $xmlReader->expand(); + + $xmlReader->close(); + + return $row; + } } diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index 761ca85..3baa665 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -321,7 +321,7 @@ class WriterWithStyleTest extends TestCase $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); - $this->assertFirstChildHasAttributeEquals("true", $xfElement, 'alignment', 'shrinkToFit'); + $this->assertFirstChildHasAttributeEquals('true', $xfElement, 'alignment', 'shrinkToFit'); } /**