Merge 5055121846071b6e9b9146f63746cb764f068828 into 6a10ec3586a192d77f64d71e8696a476a145fe28

This commit is contained in:
Alexander Hofstede 2022-01-11 19:29:19 +00:00 committed by GitHub
commit 195810435d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 690 additions and 34 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
/.idea /.idea
*.iml *.iml
/tests/resources/generated /tests/**/resources/generated
/tests/coverage /tests/coverage
/vendor /vendor
/composer.lock /composer.lock

View File

@ -20,4 +20,9 @@ abstract class Options
// XLSX specific options // XLSX specific options
const SHOULD_USE_INLINE_STRINGS = 'shouldUseInlineStrings'; const SHOULD_USE_INLINE_STRINGS = 'shouldUseInlineStrings';
// Cell size options
const DEFAULT_COLUMN_WIDTH = 'defaultColumnWidth';
const DEFAULT_ROW_HEIGHT = 'defaultRowHeight';
const COLUMN_WIDTHS = 'columnWidthDefinition';
} }

View File

@ -23,6 +23,9 @@ class Worksheet
/** @var int Index of the last written row */ /** @var int Index of the last written row */
private $lastWrittenRowIndex; private $lastWrittenRowIndex;
/** @var bool has the sheet data header been written */
private $sheetDataStarted = false;
/** /**
* Worksheet constructor. * Worksheet constructor.
* *
@ -36,6 +39,7 @@ class Worksheet
$this->externalSheet = $externalSheet; $this->externalSheet = $externalSheet;
$this->maxNumColumns = 0; $this->maxNumColumns = 0;
$this->lastWrittenRowIndex = 0; $this->lastWrittenRowIndex = 0;
$this->sheetDataStarted = false;
} }
/** /**
@ -110,4 +114,20 @@ class Worksheet
// sheet index is zero-based, while ID is 1-based // sheet index is zero-based, while ID is 1-based
return $this->externalSheet->getIndex() + 1; return $this->externalSheet->getIndex() + 1;
} }
/**
* @return bool
*/
public function getSheetDataStarted()
{
return $this->sheetDataStarted;
}
/**
* @param bool $sheetDataStarted
*/
public function setSheetDataStarted($sheetDataStarted)
{
$this->sheetDataStarted = $sheetDataStarted;
}
} }

View File

@ -0,0 +1,63 @@
<?php
namespace Box\Spout\Writer\Common\Manager;
trait ManagesCellSize
{
/** @var float|null The default column width to use */
private $defaultColumnWidth;
/** @var float|null The default row height to use */
private $defaultRowHeight;
/** @var array Array of min-max-width arrays */
private $columnWidths = [];
/**
* @param float|null $width
*/
public function setDefaultColumnWidth($width)
{
$this->defaultColumnWidth = $width;
}
/**
* @param float|null $height
*/
public function setDefaultRowHeight($height)
{
$this->defaultRowHeight = $height;
}
/**
* @param float $width
* @param array $columns One or more columns with this width
*/
public function setColumnWidth(float $width, ...$columns)
{
// Gather sequences
$sequence = [];
foreach ($columns as $i) {
$sequenceLength = count($sequence);
if ($sequenceLength > 0) {
$previousValue = $sequence[$sequenceLength - 1];
if ($i !== $previousValue + 1) {
$this->setColumnWidthForRange($width, $sequence[0], $previousValue);
$sequence = [];
}
}
$sequence[] = $i;
}
$this->setColumnWidthForRange($width, $sequence[0], $sequence[count($sequence) - 1]);
}
/**
* @param float $width The width to set
* @param int $start First column index of the range
* @param int $end Last column index of the range
*/
public function setColumnWidthForRange(float $width, int $start, int $end)
{
$this->columnWidths[] = [$start, $end, $width];
}
}

View File

@ -15,7 +15,6 @@ use Box\Spout\Writer\Common\Helper\FileSystemWithRootFolderHelperInterface;
use Box\Spout\Writer\Common\Manager\Style\StyleManagerInterface; use Box\Spout\Writer\Common\Manager\Style\StyleManagerInterface;
use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
use Box\Spout\Writer\Exception\SheetNotFoundException; use Box\Spout\Writer\Exception\SheetNotFoundException;
use Box\Spout\Writer\Exception\WriterException;
/** /**
* Class WorkbookManagerAbstract * Class WorkbookManagerAbstract
@ -103,7 +102,6 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
* Creates a new sheet in the workbook and make it the current sheet. * 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). * 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 * @return Worksheet The created sheet
*/ */
public function addNewSheetAndMakeItCurrent() public function addNewSheetAndMakeItCurrent()
@ -117,7 +115,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
/** /**
* Creates a new sheet in the workbook. The current sheet remains unchanged. * 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 * @throws IOException
* @return Worksheet The created sheet * @return Worksheet The created sheet
*/ */
private function addNewSheet() private function addNewSheet()
@ -157,6 +155,16 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
return $this->currentWorksheet; 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. * 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). * The writing will resume where it stopped (i.e. data won't be truncated).
@ -210,8 +218,9 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
* with the creation of new worksheets if one worksheet has reached its maximum capicity. * with the creation of new worksheets if one worksheet has reached its maximum capicity.
* *
* @param Row $row The row to be added * @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 IOException If trying to create a new sheet and unable to open the sheet for writing
* @throws WriterException If unable to write data * @throws \Box\Spout\Common\Exception\InvalidArgumentException
* @return void * @return void
*/ */
public function addRowToCurrentWorksheet(Row $row) public function addRowToCurrentWorksheet(Row $row)
@ -249,7 +258,9 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
* *
* @param Worksheet $worksheet Worksheet to write the row to * @param Worksheet $worksheet Worksheet to write the row to
* @param Row $row The row to be added * @param Row $row The row to be added
* @throws WriterException If unable to write data *
* @throws IOException
* @throws \Box\Spout\Common\Exception\InvalidArgumentException
* @return void * @return void
*/ */
private function addRowToWorksheet(Worksheet $worksheet, Row $row) private function addRowToWorksheet(Worksheet $worksheet, Row $row)
@ -276,6 +287,41 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
} }
} }
/**
* @param float $width
*/
public function setDefaultColumnWidth(float $width)
{
$this->worksheetManager->setDefaultColumnWidth($width);
}
/**
* @param float $height
*/
public function setDefaultRowHeight(float $height)
{
$this->worksheetManager->setDefaultRowHeight($height);
}
/**
* @param float $width
* @param array $columns One or more columns with this width
*/
public function setColumnWidth(float $width, ...$columns)
{
$this->worksheetManager->setColumnWidth($width, ...$columns);
}
/**
* @param float $width The width to set
* @param int $start First column index of the range
* @param int $end Last column index of the range
*/
public function setColumnWidthForRange(float $width, int $start, int $end)
{
$this->worksheetManager->setColumnWidthForRange($width, $start, $end);
}
/** /**
* Closes the workbook and all its associated sheets. * Closes the workbook and all its associated sheets.
* All the necessary files are written to disk and zipped together to create the final file. * All the necessary files are written to disk and zipped together to create the final file.

View File

@ -42,6 +42,21 @@ interface WorkbookManagerInterface
*/ */
public function getCurrentWorksheet(); 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. * 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). * The writing will resume where it stopped (i.e. data won't be truncated).

View File

@ -11,6 +11,29 @@ use Box\Spout\Writer\Common\Entity\Worksheet;
*/ */
interface WorksheetManagerInterface interface WorksheetManagerInterface
{ {
/**
* @param float|null $width
*/
public function setDefaultColumnWidth($width);
/**
* @param float|null $height
*/
public function setDefaultRowHeight($height);
/**
* @param float $width
* @param array $columns One or more columns with this width
*/
public function setColumnWidth(float $width, ...$columns);
/**
* @param float $width The width to set
* @param int $start First column index of the range
* @param int $end Last column index of the range
*/
public function setColumnWidthForRange(float $width, int $start, int $end);
/** /**
* Adds a row to the worksheet. * Adds a row to the worksheet.
* *

View File

@ -93,7 +93,7 @@ class ManagerFactory implements ManagerFactoryInterface
{ {
$styleRegistry = $this->createStyleRegistry($optionsManager); $styleRegistry = $this->createStyleRegistry($optionsManager);
return new StyleManager($styleRegistry); return new StyleManager($styleRegistry, $optionsManager);
} }
/** /**

View File

@ -34,6 +34,9 @@ class OptionsManager extends OptionsManagerAbstract
Options::TEMP_FOLDER, Options::TEMP_FOLDER,
Options::DEFAULT_ROW_STYLE, Options::DEFAULT_ROW_STYLE,
Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY,
Options::DEFAULT_COLUMN_WIDTH,
Options::DEFAULT_ROW_HEIGHT,
Options::COLUMN_WIDTHS,
]; ];
} }

View File

@ -4,7 +4,10 @@ namespace Box\Spout\Writer\ODS\Manager\Style;
use Box\Spout\Common\Entity\Style\BorderPart; use Box\Spout\Common\Entity\Style\BorderPart;
use Box\Spout\Common\Entity\Style\CellAlignment; use Box\Spout\Common\Entity\Style\CellAlignment;
use Box\Spout\Common\Manager\OptionsManagerInterface;
use Box\Spout\Writer\Common\Entity\Options;
use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\Common\Entity\Worksheet;
use Box\Spout\Writer\Common\Manager\ManagesCellSize;
use Box\Spout\Writer\ODS\Helper\BorderHelper; use Box\Spout\Writer\ODS\Helper\BorderHelper;
/** /**
@ -13,9 +16,22 @@ use Box\Spout\Writer\ODS\Helper\BorderHelper;
*/ */
class StyleManager extends \Box\Spout\Writer\Common\Manager\Style\StyleManager class StyleManager extends \Box\Spout\Writer\Common\Manager\Style\StyleManager
{ {
use ManagesCellSize;
/** @var StyleRegistry */ /** @var StyleRegistry */
protected $styleRegistry; protected $styleRegistry;
/**
* @param StyleRegistry $styleRegistry
*/
public function __construct(StyleRegistry $styleRegistry, OptionsManagerInterface $optionsManager)
{
parent::__construct($styleRegistry);
$this->setDefaultColumnWidth($optionsManager->getOption(Options::DEFAULT_COLUMN_WIDTH));
$this->setDefaultRowHeight($optionsManager->getOption(Options::DEFAULT_ROW_HEIGHT));
$this->columnWidths = $optionsManager->getOption(Options::COLUMN_WIDTHS) ?? [];
}
/** /**
* 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.
* *
@ -162,12 +178,16 @@ EOD;
$content .= $this->getStyleSectionContent($style); $content .= $this->getStyleSectionContent($style);
} }
$content .= <<<'EOD' $useOptimalRowHeight = empty($this->defaultRowHeight) ? 'true' : 'false';
<style:style style:family="table-column" style:name="co1"> $defaultRowHeight = empty($this->defaultRowHeight) ? '15pt' : "{$this->defaultRowHeight}pt";
<style:table-column-properties fo:break-before="auto"/> $defaultColumnWidth = empty($this->defaultColumnWidth) ? '' : "style:column-width=\"{$this->defaultColumnWidth}pt\"";
$content .= <<<EOD
<style:style style:family="table-column" style:name="default-column-style">
<style:table-column-properties fo:break-before="auto" {$defaultColumnWidth}/>
</style:style> </style:style>
<style:style style:family="table-row" style:name="ro1"> <style:style style:family="table-row" style:name="ro1">
<style:table-row-properties fo:break-before="auto" style:row-height="15pt" style:use-optimal-row-height="true"/> <style:table-row-properties fo:break-before="auto" style:row-height="{$defaultRowHeight}" style:use-optimal-row-height="{$useOptimalRowHeight}"/>
</style:style> </style:style>
EOD; EOD;
@ -182,6 +202,16 @@ EOD;
EOD; EOD;
} }
// Sort column widths since ODS cares about order
usort($this->columnWidths, function ($a, $b) {
if ($a[0] === $b[0]) {
return 0;
}
return ($a[0] < $b[0]) ? -1 : 1;
});
$content .= $this->getTableColumnStylesXMLContent();
$content .= '</office:automatic-styles>'; $content .= '</office:automatic-styles>';
return $content; return $content;
@ -313,9 +343,12 @@ EOD;
private function transformCellAlignment($cellAlignment) private function transformCellAlignment($cellAlignment)
{ {
switch ($cellAlignment) { switch ($cellAlignment) {
case CellAlignment::LEFT: return 'start'; case CellAlignment::LEFT:
case CellAlignment::RIGHT: return 'end'; return 'start';
default: return $cellAlignment; case CellAlignment::RIGHT:
return 'end';
default:
return $cellAlignment;
} }
} }
@ -381,4 +414,42 @@ EOD;
{ {
return \sprintf(' fo:background-color="#%s" ', $style->getBackgroundColor()); return \sprintf(' fo:background-color="#%s" ', $style->getBackgroundColor());
} }
public function getTableColumnStylesXMLContent() : string
{
if (empty($this->columnWidths)) {
return '';
}
$content = '';
foreach ($this->columnWidths as $styleIndex => $entry) {
$content .= <<<EOD
<style:style style:family="table-column" style:name="co{$styleIndex}">
<style:table-column-properties fo:break-before="auto" style:use-optimal-column-width="false" style:column-width="{$entry[2]}pt"/>
</style:style>
EOD;
}
return $content;
}
public function getStyledTableColumnXMLContent(int $maxNumColumns) : string
{
if (empty($this->columnWidths)) {
return '';
}
$content = '';
foreach ($this->columnWidths as $styleIndex => $entry) {
$numCols = $entry[1] - $entry[0] + 1;
$content .= <<<EOD
<table:table-column table:default-cell-style-name='Default' table:style-name="co{$styleIndex}" table:number-columns-repeated="{$numCols}"/>
EOD;
}
// Note: This assumes the column widths are contiguous and default width is
// only applied to columns after the last custom column with a custom width
$content .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="default-column-style" table:number-columns-repeated="' . ($maxNumColumns - $entry[1]) . '"/>';
return $content;
}
} }

View File

@ -95,7 +95,7 @@ class WorksheetManager implements WorksheetManagerInterface
$tableStyleName = 'ta' . ($externalSheet->getIndex() + 1); $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
$tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">'; $tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
$tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $worksheet->getMaxNumColumns() . '"/>'; $tableElement .= $this->styleManager->getStyledTableColumnXMLContent($worksheet->getMaxNumColumns());
return $tableElement; return $tableElement;
} }
@ -265,4 +265,39 @@ class WorksheetManager implements WorksheetManagerInterface
\fclose($worksheetFilePointer); \fclose($worksheetFilePointer);
} }
/**
* @param float|null $width
*/
public function setDefaultColumnWidth($width)
{
$this->styleManager->setDefaultColumnWidth($width);
}
/**
* @param float|null $height
*/
public function setDefaultRowHeight($height)
{
$this->styleManager->setDefaultRowHeight($height);
}
/**
* @param float $width
* @param array $columns One or more columns with this width
*/
public function setColumnWidth(float $width, ...$columns)
{
$this->styleManager->setColumnWidth($width, ...$columns);
}
/**
* @param float $width The width to set
* @param int $start First column index of the range
* @param int $end Last column index of the range
*/
public function setColumnWidthForRange(float $width, int $start, int $end)
{
$this->styleManager->setColumnWidthForRange($width, $start, $end);
}
} }

View File

@ -4,6 +4,7 @@ namespace Box\Spout\Writer;
use Box\Spout\Common\Creator\HelperFactory; use Box\Spout\Common\Creator\HelperFactory;
use Box\Spout\Common\Entity\Row; use Box\Spout\Common\Entity\Row;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\GlobalFunctionsHelper; use Box\Spout\Common\Helper\GlobalFunctionsHelper;
use Box\Spout\Common\Manager\OptionsManagerInterface; use Box\Spout\Common\Manager\OptionsManagerInterface;
use Box\Spout\Writer\Common\Creator\ManagerFactoryInterface; use Box\Spout\Writer\Common\Creator\ManagerFactoryInterface;
@ -56,7 +57,10 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract
{ {
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
$this->optionsManager->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, $shouldCreateNewSheetsAutomatically); $this->optionsManager->setOption(
Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY,
$shouldCreateNewSheetsAutomatically
);
return $this; return $this;
} }
@ -96,6 +100,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. * Creates a new sheet and make it the current sheet. The data will now be written to this sheet.
* *
* @throws IOException
* @throws WriterNotOpenedException If the writer has not been opened yet * @throws WriterNotOpenedException If the writer has not been opened yet
* @return Sheet The created sheet * @return Sheet The created sheet
*/ */
@ -125,8 +130,8 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract
* The writing will resume where it stopped (i.e. data won't be truncated). * The writing will resume where it stopped (i.e. data won't be truncated).
* *
* @param Sheet $sheet The sheet to set as current * @param Sheet $sheet The sheet to set as current
* @throws WriterNotOpenedException If the writer has not been opened yet
* @throws SheetNotFoundException If the given sheet does not exist in the workbook * @throws SheetNotFoundException If the given sheet does not exist in the workbook
* @throws WriterNotOpenedException If the writer has not been opened yet
* @return void * @return void
*/ */
public function setCurrentSheet($sheet) public function setCurrentSheet($sheet)
@ -135,6 +140,55 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract
$this->workbookManager->setCurrentSheet($sheet); $this->workbookManager->setCurrentSheet($sheet);
} }
/**
* @param float $width
* @throws WriterAlreadyOpenedException
*/
public function setDefaultColumnWidth(float $width)
{
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
$this->optionsManager->setOption(
Options::DEFAULT_COLUMN_WIDTH,
$width
);
}
/**
* @param float $height
* @throws WriterAlreadyOpenedException
*/
public function setDefaultRowHeight(float $height)
{
$this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
$this->optionsManager->setOption(
Options::DEFAULT_ROW_HEIGHT,
$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);
}
/**
* @param float $width The width to set
* @param int $start First column index of the range
* @param int $end Last column index of the range
* @throws WriterNotOpenedException
*/
public function setColumnWidthForRange(float $width, int $start, int $end)
{
$this->throwIfWorkbookIsNotAvailable();
$this->workbookManager->setColumnWidthForRange($width, $start, $end);
}
/** /**
* Checks if the workbook has been created. Throws an exception if not created yet. * Checks if the workbook has been created. Throws an exception if not created yet.
* *
@ -143,13 +197,15 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract
*/ */
protected function throwIfWorkbookIsNotAvailable() 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.'); throw new WriterNotOpenedException('The writer must be opened before performing this action.');
} }
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* @throws Exception\WriterException
*/ */
protected function addRowToWriter(Row $row) protected function addRowToWriter(Row $row)
{ {

View File

@ -39,6 +39,9 @@ class OptionsManager extends OptionsManagerAbstract
Options::DEFAULT_ROW_STYLE, Options::DEFAULT_ROW_STYLE,
Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY,
Options::SHOULD_USE_INLINE_STRINGS, Options::SHOULD_USE_INLINE_STRINGS,
Options::DEFAULT_COLUMN_WIDTH,
Options::DEFAULT_ROW_HEIGHT,
Options::COLUMN_WIDTHS,
]; ];
} }

View File

@ -15,6 +15,7 @@ use Box\Spout\Writer\Common\Entity\Options;
use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\Common\Entity\Worksheet;
use Box\Spout\Writer\Common\Helper\CellHelper; use Box\Spout\Writer\Common\Helper\CellHelper;
use Box\Spout\Writer\Common\Manager\RegisteredStyle; use Box\Spout\Writer\Common\Manager\RegisteredStyle;
use Box\Spout\Writer\Common\Manager\ManagesCellSize;
use Box\Spout\Writer\Common\Manager\RowManager; use Box\Spout\Writer\Common\Manager\RowManager;
use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface; use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
@ -26,6 +27,8 @@ use Box\Spout\Writer\XLSX\Manager\Style\StyleManager;
*/ */
class WorksheetManager implements WorksheetManagerInterface class WorksheetManager implements WorksheetManagerInterface
{ {
use ManagesCellSize;
/** /**
* Maximum number of characters a cell can contain * Maximum number of characters a cell can contain
* @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007] * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007]
@ -86,6 +89,9 @@ EOD;
InternalEntityFactory $entityFactory InternalEntityFactory $entityFactory
) { ) {
$this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS); $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->rowManager = $rowManager;
$this->styleManager = $styleManager; $this->styleManager = $styleManager;
$this->styleMerger = $styleMerger; $this->styleMerger = $styleMerger;
@ -114,7 +120,23 @@ EOD;
$worksheet->setFilePointer($sheetFilePointer); $worksheet->setFilePointer($sheetFilePointer);
\fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER); \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
\fwrite($sheetFilePointer, '<sheetData>'); }
/**
* Writes the sheet data header
*
* @param Worksheet $worksheet The worksheet to add the row to
* @return void
*/
private function ensureSheetDataStated(Worksheet $worksheet)
{
if (!$worksheet->getSheetDataStarted()) {
$worksheetFilePointer = $worksheet->getFilePointer();
\fwrite($worksheetFilePointer, $this->getXMLFragmentForDefaultCellSizing());
\fwrite($worksheetFilePointer, $this->getXMLFragmentForColumnWidths());
\fwrite($worksheetFilePointer, '<sheetData>');
$worksheet->setSheetDataStarted(true);
}
} }
/** /**
@ -148,17 +170,20 @@ EOD;
* *
* @param Worksheet $worksheet The worksheet to add the row to * @param Worksheet $worksheet The worksheet to add the row to
* @param Row $row The row to be written * @param Row $row The row to be written
* @throws IOException If the data cannot be written
* @throws InvalidArgumentException If a cell value's type is not supported * @throws InvalidArgumentException If a cell value's type is not supported
* @throws IOException If the data cannot be written
* @return void * @return void
*/ */
private function addNonEmptyRow(Worksheet $worksheet, Row $row) private function addNonEmptyRow(Worksheet $worksheet, Row $row)
{ {
$this->ensureSheetDataStated($worksheet);
$sheetFilePointer = $worksheet->getFilePointer();
$rowStyle = $row->getStyle(); $rowStyle = $row->getStyle();
$rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1; $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1;
$numCells = $row->getNumCells(); $numCells = $row->getNumCells();
$rowXML = '<row r="' . $rowIndexOneBased . '" spans="1:' . $numCells . '">'; $hasCustomHeight = $this->defaultRowHeight > 0 ? '1' : '0';
$rowXML = "<row r=\"{$rowIndexOneBased}\" spans=\"1:{$numCells}\" customHeight=\"{$hasCustomHeight}\">";
foreach ($row->getCells() as $columnIndexZeroBased => $cell) { foreach ($row->getCells() as $columnIndexZeroBased => $cell) {
$registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
@ -171,7 +196,7 @@ EOD;
$rowXML .= '</row>'; $rowXML .= '</row>';
$wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $rowXML); $wasWriteSuccessful = \fwrite($sheetFilePointer, $rowXML);
if ($wasWriteSuccessful === false) { if ($wasWriteSuccessful === false) {
throw new IOException("Unable to write data in {$worksheet->getFilePath()}"); throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
} }
@ -180,7 +205,7 @@ EOD;
/** /**
* Applies styles to the given style, merging the cell's style with its row's style * Applies styles to the given style, merging the cell's style with its row's style
* *
* @param Cell $cell * @param Cell $cell
* @param Style $rowStyle * @param Style $rowStyle
* *
* @throws InvalidArgumentException If the given value cannot be processed * @throws InvalidArgumentException If the given value cannot be processed
@ -221,10 +246,10 @@ EOD;
/** /**
* Builds and returns xml for a single cell. * Builds and returns xml for a single cell.
* *
* @param int $rowIndexOneBased * @param int $rowIndexOneBased
* @param int $columnIndexZeroBased * @param int $columnIndexZeroBased
* @param Cell $cell * @param Cell $cell
* @param int $styleId * @param int $styleId
* *
* @throws InvalidArgumentException If the given value cannot be processed * @throws InvalidArgumentException If the given value cannot be processed
* @return string * @return string
@ -282,6 +307,43 @@ EOD;
return $cellXMLFragment; return $cellXMLFragment;
} }
/**
* Construct column width references xml to inject into worksheet xml file
*
* @return string
*/
public function getXMLFragmentForColumnWidths()
{
if (empty($this->columnWidths)) {
return '';
}
$xml = '<cols>';
foreach ($this->columnWidths as $entry) {
$xml .= '<col min="' . $entry[0] . '" max="' . $entry[1] . '" width="' . $entry[2] . '" customWidth="true"/>';
}
$xml .= '</cols>';
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 "<sheetFormatPr{$colWidthXml}{$rowHeightXml}/>";
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -292,7 +354,7 @@ EOD;
if (!\is_resource($worksheetFilePointer)) { if (!\is_resource($worksheetFilePointer)) {
return; return;
} }
$this->ensureSheetDataStated($worksheet);
\fwrite($worksheetFilePointer, '</sheetData>'); \fwrite($worksheetFilePointer, '</sheetData>');
\fwrite($worksheetFilePointer, '</worksheet>'); \fwrite($worksheetFilePointer, '</worksheet>');
\fclose($worksheetFilePointer); \fclose($worksheetFilePointer);

View File

@ -6,6 +6,7 @@ use Box\Spout\TestUsingResource;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Writer\Common\Entity\Sheet; use Box\Spout\Writer\Common\Entity\Sheet;
use Box\Spout\Writer\Exception\InvalidSheetNameException; use Box\Spout\Writer\Exception\InvalidSheetNameException;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\RowCreationHelper; use Box\Spout\Writer\RowCreationHelper;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -82,7 +83,7 @@ class SheetTest extends TestCase
*/ */
public function testSetSheetVisibilityShouldCreateSheetHidden() public function testSetSheetVisibilityShouldCreateSheetHidden()
{ {
$fileName = 'test_set_visibility_should_create_sheet_hidden.xlsx'; $fileName = 'test_set_visibility_should_create_sheet_hidden.ods';
$this->writeDataToHiddenSheet($fileName); $this->writeDataToHiddenSheet($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName); $resourcePath = $this->getGeneratedResourcePath($fileName);
@ -92,12 +93,120 @@ class SheetTest extends TestCase
$this->assertStringContainsString(' table:display="false"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); $this->assertStringContainsString(' table:display="false"', $xmlContents, 'The sheet visibility should have been changed to "hidden"');
} }
/** public function testThrowsIfWorkbookIsNotInitialized()
* @param string $fileName {
* @param string $sheetName $this->expectException(WriterNotOpenedException::class);
* @return Sheet $writer = WriterEntityFactory::createODSWriter();
*/
private function writeDataAndReturnSheetWithCustomName($fileName, $sheetName) $writer->addRow($this->createRowFromValues([]));
}
public function testWritesDefaultCellSizesIfSet()
{
$fileName = 'test_writes_default_cell_sizes_if_set.ods';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterEntityFactory::createODSWriter();
$writer->setDefaultColumnWidth(100.0);
$writer->setDefaultRowHeight(20.0);
$writer->openToFile($resourcePath);
$writer->addRow($this->createRowFromValues(['ods--11', 'ods--12']));
$writer->close();
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToWorkbookFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains(' style:column-width="100pt"', $xmlContents, 'No default col width found in sheet');
$this->assertContains(' style:row-height="20pt"', $xmlContents, 'No default row height found in sheet');
$this->assertContains(' style:use-optimal-row-height="false', $xmlContents, 'No optimal row height override found in sheet');
}
public function testWritesColumnWidths()
{
$fileName = 'test_column_widths.ods';
$writer = $this->writerForFile($fileName);
$writer->setColumnWidth(100.0, 1);
$writer->addRow($this->createRowFromValues(['ods--11', 'ods--12']));
$writer->close();
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToWorkbookFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains('<style:style style:family="table-column" style:name="co0">', $xmlContents, 'No matching custom col style definition found in sheet');
$this->assertContains('<style:table-column-properties fo:break-before="auto" style:use-optimal-column-width="false" style:column-width="100pt"/>', $xmlContents, 'No matching table-column-properties found in sheet');
$this->assertContains('table:style-name="co0"', $xmlContents, 'No matching table:style-name found in sheet');
$this->assertContains('table:number-columns-repeated="1"', $xmlContents, 'No matching table:number-columns-repeated count found in sheet');
}
public function testWritesMultipleColumnWidths()
{
$fileName = 'test_multiple_column_widths.ods';
$writer = $this->writerForFile($fileName);
$writer->setColumnWidth(100.0, 1, 2, 3);
$writer->addRow($this->createRowFromValues(['ods--11', 'ods--12', 'ods--13']));
$writer->close();
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToWorkbookFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains('<style:style style:family="table-column" style:name="co0">', $xmlContents, 'No matching custom col style definition found in sheet');
$this->assertContains('<style:table-column-properties fo:break-before="auto" style:use-optimal-column-width="false" style:column-width="100pt"/>', $xmlContents, 'No matching table-column-properties found in sheet');
$this->assertContains('table:style-name="co0"', $xmlContents, 'No matching table:style-name found in sheet');
$this->assertContains('table:number-columns-repeated="3"', $xmlContents, 'No matching table:number-columns-repeated count found in sheet');
}
public function testWritesMultipleColumnWidthsInRanges()
{
$fileName = 'test_multiple_column_widths_in_ranges.ods';
$writer = $this->writerForFile($fileName);
$writer->setColumnWidth(50.0, 1, 3, 4, 6);
$writer->setColumnWidth(100.0, 2, 5);
$writer->addRow($this->createRowFromValues(['ods--11', 'ods--12', 'ods--13', 'ods--14', 'ods--15', 'ods--16']));
$writer->close();
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToWorkbookFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains('<style:style style:family="table-column" style:name="co0">', $xmlContents, 'No matching custom col style 0 definition found in sheet');
$this->assertContains('<style:style style:family="table-column" style:name="co1">', $xmlContents, 'No matching custom col style 1 definition found in sheet');
$this->assertContains('<style:style style:family="table-column" style:name="co2">', $xmlContents, 'No matching custom col style 2 definition found in sheet');
$this->assertContains('<style:style style:family="table-column" style:name="co3">', $xmlContents, 'No matching custom col style 3 definition found in sheet');
$this->assertContains('<style:style style:family="table-column" style:name="co4">', $xmlContents, 'No matching custom col style 4 definition found in sheet');
$this->assertContains('<style:table-column-properties fo:break-before="auto" style:use-optimal-column-width="false" style:column-width="100pt"/>', $xmlContents, 'No matching table-column-properties found in sheet');
$this->assertContains('<style:table-column-properties fo:break-before="auto" style:use-optimal-column-width="false" style:column-width="50pt"/>', $xmlContents, 'No matching table-column-properties found in sheet');
$this->assertContains('<table:table-column table:default-cell-style-name=\'Default\' table:style-name="co0" table:number-columns-repeated="1"/><table:table-column table:default-cell-style-name=\'Default\' table:style-name="co1" table:number-columns-repeated="1"/><table:table-column table:default-cell-style-name=\'Default\' table:style-name="co2" table:number-columns-repeated="2"/><table:table-column table:default-cell-style-name=\'Default\' table:style-name="co3" table:number-columns-repeated="1"/><table:table-column table:default-cell-style-name=\'Default\' table:style-name="co4" table:number-columns-repeated="1"/>', $xmlContents, 'No matching table:number-columns-repeated count found in sheet');
}
public function testCanTakeColumnWidthsAsRange()
{
$fileName = 'test_column_widths_as_ranges.ods';
$writer = $this->writerForFile($fileName);
$writer->setColumnWidthForRange(150.0, 1, 3);
$writer->addRow($this->createRowFromValues(['ods--11', 'ods--12', 'ods--13']));
$writer->close();
$resourcePath = $this->getGeneratedResourcePath($fileName);
$pathToWorkbookFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains('<style:style style:family="table-column" style:name="co0">', $xmlContents, 'No matching custom col style 0 definition found in sheet');
$this->assertContains('style:column-width="150pt"/>', $xmlContents, 'No matching table-column-properties found in sheet');
$this->assertContains('table:style-name="co0"', $xmlContents, 'No matching table:style-name found in sheet');
$this->assertContains('table:number-columns-repeated="3"', $xmlContents, 'No matching table:number-columns-repeated count found in sheet');
}
private function writerForFile($fileName)
{ {
$this->createGeneratedFolderIfNeeded($fileName); $this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName); $resourcePath = $this->getGeneratedResourcePath($fileName);
@ -105,6 +214,18 @@ class SheetTest extends TestCase
$writer = WriterEntityFactory::createODSWriter(); $writer = WriterEntityFactory::createODSWriter();
$writer->openToFile($resourcePath); $writer->openToFile($resourcePath);
return $writer;
}
/**
* @param string $fileName
* @param string $sheetName
* @return void
*/
private function writeDataAndReturnSheetWithCustomName($fileName, $sheetName)
{
$writer = $this->writerForFile($fileName);
$sheet = $writer->getCurrentSheet(); $sheet = $writer->getCurrentSheet();
$sheet->setName($sheetName); $sheet->setName($sheetName);

View File

@ -6,6 +6,7 @@ use Box\Spout\TestUsingResource;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Writer\Common\Entity\Sheet; use Box\Spout\Writer\Common\Entity\Sheet;
use Box\Spout\Writer\Exception\InvalidSheetNameException; use Box\Spout\Writer\Exception\InvalidSheetNameException;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\RowCreationHelper; use Box\Spout\Writer\RowCreationHelper;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -92,6 +93,138 @@ class SheetTest extends TestCase
$this->assertStringContainsString(' state="hidden"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); $this->assertStringContainsString(' 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 testWritesDefaultCellSizesIfSet()
{
$fileName = 'test_writes_default_cell_sizes_if_set.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterEntityFactory::createXLSXWriter();
$writer->setDefaultColumnWidth(10.0);
$writer->setDefaultRowHeight(20.0);
$writer->openToFile($resourcePath);
$writer->addRow($this->createRowFromValues(['xlsx--11', 'xlsx--12']));
$writer->close();
$pathToWorkbookFile = $resourcePath . '#xl/worksheets/sheet1.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains('<sheetFormatPr', $xmlContents, 'No sheetFormatPr tag found in sheet');
$this->assertContains(' defaultColWidth="10', $xmlContents, 'No default column width found in sheet');
$this->assertContains(' defaultRowHeight="20', $xmlContents, 'No default row height found in sheet');
$this->assertContains(' customHeight="1"', $xmlContents, 'No row height override flag found in row');
}
public function testWritesDefaultRequiredRowHeightIfOmitted()
{
$fileName = 'test_writes_default_required_row_height_if_omitted.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterEntityFactory::createXLSXWriter();
$writer->setDefaultColumnWidth(10.0);
$writer->openToFile($resourcePath);
$writer->addRow($this->createRowFromValues(['xlsx--11', 'xlsx--12']));
$writer->close();
$pathToWorkbookFile = $resourcePath . '#xl/worksheets/sheet1.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains('<sheetFormatPr', $xmlContents, 'No sheetFormatPr tag found in sheet');
$this->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('<cols', $xmlContents, 'No cols tag found in sheet');
$this->assertContains('<col min="1" max="1" width="100" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
}
public function testWritesMultipleColumnWidths()
{
$fileName = 'test_multiple_column_widths.xlsx';
$this->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('<cols', $xmlContents, 'No cols tag found in sheet');
$this->assertContains('<col min="1" max="3" width="100" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
}
public function testWritesMultipleColumnWidthsInRanges()
{
$fileName = 'test_multiple_column_widths_in_ranges.xlsx';
$this->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('<cols', $xmlContents, 'No cols tag found in sheet');
$this->assertContains('<col min="1" max="1" width="50" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
$this->assertContains('<col min="3" max="4" width="50" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
$this->assertContains('<col min="6" max="6" width="50" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
$this->assertContains('<col min="2" max="2" width="100" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
$this->assertContains('<col min="5" max="5" width="100" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
}
public function testCanTakeColumnWidthsAsRange()
{
$fileName = 'test_column_widths_as_ranges.xlsx';
$this->createGeneratedFolderIfNeeded($fileName);
$resourcePath = $this->getGeneratedResourcePath($fileName);
$writer = WriterEntityFactory::createXLSXWriter();
$writer->openToFile($resourcePath);
$writer->setColumnWidthForRange(50.0, 1, 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('<cols', $xmlContents, 'No cols tag found in sheet');
$this->assertContains('<col min="1" max="3" width="50" customWidth="true"', $xmlContents, 'No expected column width definition found in sheet');
}
/** /**
* @param string $fileName * @param string $fileName
* @param string $sheetName * @param string $sheetName