diff --git a/src/Spout/Writer/Common/Entity/Options.php b/src/Spout/Writer/Common/Entity/Options.php index d7152bb..3918333 100644 --- a/src/Spout/Writer/Common/Entity/Options.php +++ b/src/Spout/Writer/Common/Entity/Options.php @@ -20,4 +20,9 @@ abstract class Options // XLSX specific options const SHOULD_USE_INLINE_STRINGS = 'shouldUseInlineStrings'; + + // Cell size options + const DEFAULT_COLUMN_WIDTH = 'defaultColumnWidth'; + const DEFAULT_ROW_HEIGHT = 'defaultRowHeight'; + const COLUMN_WIDTHS = 'columnWidthDefinition'; } diff --git a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php index 7be5c6e..12c6523 100644 --- a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php +++ b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php @@ -103,7 +103,6 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * Creates a new sheet in the workbook and make it the current sheet. * The writing will resume where it stopped (i.e. data won't be truncated). * - * @throws IOException If unable to open the sheet for writing * @return Worksheet The created sheet */ public function addNewSheetAndMakeItCurrent() @@ -117,8 +116,8 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** * Creates a new sheet in the workbook. The current sheet remains unchanged. * - * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing * @return Worksheet The created sheet + * @throws IOException */ private function addNewSheet() { @@ -157,6 +156,16 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface return $this->currentWorksheet; } + /** + * Starts the current sheet and opens the file pointer + * + * @throws IOException + */ + public function startCurrentSheet() + { + $this->worksheetManager->startSheet($this->getCurrentWorksheet()); + } + /** * Sets the given sheet as the current one. New data will be written to this sheet. * The writing will resume where it stopped (i.e. data won't be truncated). @@ -210,9 +219,10 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * with the creation of new worksheets if one worksheet has reached its maximum capicity. * * @param Row $row The row to be added - * @throws IOException If trying to create a new sheet and unable to open the sheet for writing - * @throws WriterException If unable to write data + * * @return void + * @throws IOException If trying to create a new sheet and unable to open the sheet for writing + * @throws \Box\Spout\Common\Exception\InvalidArgumentException */ public function addRowToCurrentWorksheet(Row $row) { @@ -249,8 +259,10 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface * * @param Worksheet $worksheet Worksheet to write the row to * @param Row $row The row to be added - * @throws WriterException If unable to write data + * * @return void + * @throws IOException + * @throws \Box\Spout\Common\Exception\InvalidArgumentException */ private function addRowToWorksheet(Worksheet $worksheet, Row $row) { @@ -276,6 +288,28 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface } } + /** + * @param float|null $width + */ + public function setDefaultColumnWidth(float $width) { + $this->worksheetManager->setDefaultColumnWidth($width); + } + + /** + * @param float|null $height + */ + public function setDefaultRowHeight(float $height) { + $this->worksheetManager->setDefaultRowHeight($height); + } + + /** + * @param float|null $width + * @param array $columns One or more columns with this width + */ + public function setColumnWidth($width, ...$columns) { + $this->worksheetManager->setColumnWidth($width, ...$columns); + } + /** * Closes the workbook and all its associated sheets. * All the necessary files are written to disk and zipped together to create the final file. diff --git a/src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php b/src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php index aed304a..7bb469e 100644 --- a/src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php +++ b/src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php @@ -42,6 +42,21 @@ interface WorkbookManagerInterface */ public function getCurrentWorksheet(); + /** + * Starts the current sheet and opens its file pointer + */ + public function startCurrentSheet(); + + /** + * @param float $width + */ + public function setDefaultColumnWidth(float $width); + + /** + * @param float $height + */ + public function setDefaultRowHeight(float $height); + /** * Sets the given sheet as the current one. New data will be written to this sheet. * The writing will resume where it stopped (i.e. data won't be truncated). diff --git a/src/Spout/Writer/WriterMultiSheetsAbstract.php b/src/Spout/Writer/WriterMultiSheetsAbstract.php index 8170b67..6148738 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; @@ -96,8 +97,9 @@ 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 * @return Sheet The created sheet + * @throws IOException + * @throws WriterNotOpenedException If the writer has not been opened yet */ public function addNewSheetAndMakeItCurrent() { @@ -135,6 +137,36 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract $this->workbookManager->setCurrentSheet($sheet); } + /** + * @param float $width + * @throws WriterNotOpenedException + */ + public function setDefaultColumnWidth(float $width) + { + $this->throwIfWorkbookIsNotAvailable(); + $this->workbookManager->setDefaultColumnWidth($width); + } + + /** + * @param float $height + * @throws WriterNotOpenedException + */ + public function setDefaultRowHeight(float $height) + { + $this->throwIfWorkbookIsNotAvailable(); + $this->workbookManager->setDefaultRowHeight($height); + } + + /** + * @param float|null $width + * @param array $columns One or more columns with this width + * @throws WriterNotOpenedException + */ + public function setColumnWidth($width, ...$columns) { + $this->throwIfWorkbookIsNotAvailable(); + $this->workbookManager->setColumnWidth($width, ...$columns); + } + /** * Checks if the workbook has been created. Throws an exception if not created yet. * @@ -143,13 +175,15 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract */ protected function throwIfWorkbookIsNotAvailable() { - if (!$this->workbookManager->getWorkbook()) { + if (empty($this->workbookManager) || !$this->workbookManager->getWorkbook()) { throw new WriterNotOpenedException('The writer must be opened before performing this action.'); } } /** * {@inheritdoc} + * + * @throws Exception\WriterException */ protected function addRowToWriter(Row $row) { diff --git a/src/Spout/Writer/XLSX/Manager/OptionsManager.php b/src/Spout/Writer/XLSX/Manager/OptionsManager.php index 53718bc..d83b8ae 100644 --- a/src/Spout/Writer/XLSX/Manager/OptionsManager.php +++ b/src/Spout/Writer/XLSX/Manager/OptionsManager.php @@ -39,6 +39,9 @@ class OptionsManager extends OptionsManagerAbstract Options::DEFAULT_ROW_STYLE, Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, Options::SHOULD_USE_INLINE_STRINGS, + Options::DEFAULT_COLUMN_WIDTH, + Options::DEFAULT_ROW_HEIGHT, + Options::COLUMN_WIDTHS, ]; } diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index b0e458f..f6ee831 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -62,6 +62,18 @@ EOD; /** @var InternalEntityFactory Factory to create entities */ private $entityFactory; + /** @var float|null The default column width to use */ + private $defaultColumnWidth; + + /** @var float|null The default row height to use */ + private $defaultRowHeight; + + /** @var bool Whether rows have been written */ + private $hasWrittenRows = false; + + /** @var array Array of min-max-width arrays */ + private $columnWidths; + /** * WorksheetManager constructor. * @@ -85,6 +97,9 @@ EOD; InternalEntityFactory $entityFactory ) { $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS); + $this->setDefaultColumnWidth($optionsManager->getOption(Options::DEFAULT_COLUMN_WIDTH)); + $this->setDefaultRowHeight($optionsManager->getOption(Options::DEFAULT_ROW_HEIGHT)); + $this->columnWidths = $optionsManager->getOption(Options::COLUMN_WIDTHS) ?? []; $this->rowManager = $rowManager; $this->styleManager = $styleManager; $this->styleMerger = $styleMerger; @@ -102,6 +117,39 @@ EOD; return $this->sharedStringsManager; } + /** + * @param float|null $width + */ + public function setDefaultColumnWidth($width) { + $this->defaultColumnWidth = $width; + } + + /** + * @param float|null $height + */ + public function setDefaultRowHeight($height) { + $this->defaultRowHeight = $height; + } + + /** + * @param float|null $width + * @param array $columns One or more columns with this width + */ + public function setColumnWidth($width, ...$columns) { + // Gather sequences + $sequence = []; + foreach ($columns as $i) { + $sequenceLength = count($sequence); + $previousValue = $sequence[$sequenceLength - 1]; + if ($sequenceLength > 0 && $i !== $previousValue + 1) { + $this->columnWidths[] = [$sequence[0], $previousValue, $width]; + $sequence = []; + } + $sequence[] = $i; + } + $this->columnWidths[] = [$sequence[0], $sequence[count($sequence) - 1], $width]; + } + /** * {@inheritdoc} */ @@ -113,7 +161,6 @@ EOD; $worksheet->setFilePointer($sheetFilePointer); fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER); - fwrite($sheetFilePointer, ''); } /** @@ -153,6 +200,12 @@ EOD; */ private function addNonEmptyRow(Worksheet $worksheet, Row $row) { + $sheetFilePointer = $worksheet->getFilePointer(); + if (!$this->hasWrittenRows) { + fwrite($sheetFilePointer, $this->getXMLFragmentForDefaultCellSizing()); + fwrite($sheetFilePointer, $this->getXMLFragmentForColumnWidths()); + fwrite($sheetFilePointer, ''); + } $cellIndex = 0; $rowStyle = $row->getStyle(); $rowIndex = $worksheet->getLastWrittenRowIndex() + 1; @@ -167,10 +220,11 @@ EOD; $rowXML .= ''; - $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML); + $wasWriteSuccessful = fwrite($sheetFilePointer, $rowXML); if ($wasWriteSuccessful === false) { throw new IOException("Unable to write data in {$worksheet->getFilePath()}"); } + $this->hasWrittenRows = true; } /** @@ -256,6 +310,41 @@ EOD; return $cellXMLFragment; } + /** + * Construct column width references xml to inject into worksheet xml file + * + * @return string + */ + public function getXMLFragmentForColumnWidths() + { + if (empty($this->columnWidths)) { + return ''; + } + $xml = ''; + foreach ($this->columnWidths as $entry) { + $xml .= ''; + } + $xml .= ''; + return $xml; + } + + /** + * Constructs default row height and width xml to inject into worksheet xml file + * + * @return string + */ + public function getXMLFragmentForDefaultCellSizing() + { + $rowHeightXml = empty($this->defaultRowHeight) ? '' : " defaultRowHeight=\"{$this->defaultRowHeight}\""; + $colWidthXml = empty($this->defaultColumnWidth) ? '' : " defaultColWidth=\"{$this->defaultColumnWidth}\""; + if (empty($colWidthXml) && empty($rowHeightXml)) { + return ''; + } + // Ensure that the required defaultRowHeight is set + $rowHeightXml = empty($rowHeightXml) ? ' defaultRowHeight="0"' : $rowHeightXml; + return ""; + } + /** * {@inheritdoc} */ @@ -267,7 +356,9 @@ EOD; return; } - fwrite($worksheetFilePointer, ''); + if ($this->hasWrittenRows) { + fwrite($worksheetFilePointer, ''); + } fwrite($worksheetFilePointer, ''); fclose($worksheetFilePointer); } diff --git a/tests/Spout/Writer/XLSX/SheetTest.php b/tests/Spout/Writer/XLSX/SheetTest.php index cebd72e..bf1bbb8 100644 --- a/tests/Spout/Writer/XLSX/SheetTest.php +++ b/tests/Spout/Writer/XLSX/SheetTest.php @@ -6,6 +6,7 @@ use Box\Spout\TestUsingResource; use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Common\Entity\Sheet; use Box\Spout\Writer\Exception\InvalidSheetNameException; +use Box\Spout\Writer\Exception\WriterNotOpenedException; use Box\Spout\Writer\RowCreationHelper; use PHPUnit\Framework\TestCase; @@ -92,6 +93,124 @@ class SheetTest extends TestCase $this->assertContains(' state="hidden"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); } + public function testThrowsIfWorkbookIsNotInitialized() + { + $this->expectException(WriterNotOpenedException::class); + $writer = WriterEntityFactory::createXLSXWriter(); + + $writer->addRow($this->createRowFromValues([])); + } + + public function testThrowsWhenTryingToSetDefaultsBeforeWorkbookLoaded() + { + $this->expectException(WriterNotOpenedException::class); + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->setDefaultColumnWidth(10.0); + } + + public function testWritesDefaultCellSizesIfSet() + { + $fileName = 'test_writes_default_cell_sizes_if_set.xlsx'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile($resourcePath); + $writer->setDefaultColumnWidth(10.0); + $writer->setDefaultRowHeight(10.0); + $writer->addRow($this->createRowFromValues(['xlsx--11', 'xlsx--12'])); + $writer->close(); + + $pathToWorkbookFile = $resourcePath . '#xl/worksheets/sheet1.xml'; + $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); + + $this->assertContains('assertContains(' defaultColWidth="10', $xmlContents, 'No default column width found in sheet'); + $this->assertContains(' defaultRowHeight="10', $xmlContents, 'No default row height found in sheet'); + } + + public function testWritesDefaultRequiredRowHeightIfOmitted() + { + $fileName = 'test_writes_default_required_row_height_if_omitted.xlsx'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile($resourcePath); + $writer->setDefaultColumnWidth(10.0); + $writer->addRow($this->createRowFromValues(['xlsx--11', 'xlsx--12'])); + $writer->close(); + + $pathToWorkbookFile = $resourcePath . '#xl/worksheets/sheet1.xml'; + $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); + + $this->assertContains('assertContains(' defaultColWidth="10', $xmlContents, 'No default column width found in sheet'); + $this->assertContains(' defaultRowHeight="0', $xmlContents, 'No default row height found in sheet'); + } + + public function testWritesColumnWidths() + { + $fileName = 'test_column_widths.xlsx'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile($resourcePath); + $writer->setColumnWidth(100.0, 1); + $writer->addRow($this->createRowFromValues(['xlsx--11', 'xlsx--12'])); + $writer->close(); + + $pathToWorkbookFile = $resourcePath . '#xl/worksheets/sheet1.xml'; + $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); + + $this->assertContains('assertContains('createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile($resourcePath); + $writer->setColumnWidth(100.0, 1, 2, 3); + $writer->addRow($this->createRowFromValues(['xlsx--11', 'xlsx--12', 'xlsx--13'])); + $writer->close(); + + $pathToWorkbookFile = $resourcePath . '#xl/worksheets/sheet1.xml'; + $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); + + $this->assertContains('assertContains('createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile($resourcePath); + $writer->setColumnWidth(50.0, 1, 3, 4, 6); + $writer->setColumnWidth(100.0, 2, 5); + $writer->addRow($this->createRowFromValues(['xlsx--11', 'xlsx--12', 'xlsx--13', 'xlsx--14', 'xlsx--15', 'xlsx--16'])); + $writer->close(); + + $pathToWorkbookFile = $resourcePath . '#xl/worksheets/sheet1.xml'; + $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); + + $this->assertContains('assertContains('assertContains('assertContains('assertContains('assertContains('