From b54e21779913f3b1873d72b08097737b3bd2ec9b Mon Sep 17 00:00:00 2001 From: K J Kooistra Date: Thu, 14 May 2020 11:08:34 +0200 Subject: [PATCH] feat: add sheet view support to XLSX writer This makes it possible to freeze rows and columns, in addition to providing other sheet view settings like zoom options. --- docs/_pages/documentation.md | 17 + src/Spout/Writer/Common/Entity/Sheet.php | 31 ++ src/Spout/Writer/Common/Entity/Worksheet.php | 19 ++ .../Manager/WorkbookManagerAbstract.php | 9 +- src/Spout/Writer/XLSX/Entity/SheetView.php | 295 ++++++++++++++++++ .../Writer/XLSX/Manager/WorksheetManager.php | 25 ++ 6 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 src/Spout/Writer/XLSX/Entity/SheetView.php diff --git a/docs/_pages/documentation.md b/docs/_pages/documentation.md index d63ca037..339962e3 100755 --- a/docs/_pages/documentation.md +++ b/docs/_pages/documentation.md @@ -50,6 +50,23 @@ $writer->setShouldCreateNewSheetsAutomatically(true); // default value $writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached ``` +### Sheet view (XLSX writer) + +Sheet view settings must be configured before any rows are added to the sheet. + +```php +use Box\Spout\Writer\Common\Creator\WriterEntityFactory; +use Box\Spout\Writer\XLSX\Entity\SheetView; + +$sheetView = (new SheetView()) + ->setFreezeRow(2) // First row will be fixed + ->setFreezeColumn('D') // Columns A to C will be fixed + ->setZoomScale(150); // And other options + +$writer = WriterEntityFactory::createXLSXWriter(); +$writer->getCurrentSheet()->setSheetView($sheetView); +``` + ### Using a custom temporary folder Processing XLSX and ODS files requires temporary files to be created. By default, {{ site.spout_html }} will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer: diff --git a/src/Spout/Writer/Common/Entity/Sheet.php b/src/Spout/Writer/Common/Entity/Sheet.php index aabdd91f..0666de36 100644 --- a/src/Spout/Writer/Common/Entity/Sheet.php +++ b/src/Spout/Writer/Common/Entity/Sheet.php @@ -3,6 +3,7 @@ namespace Box\Spout\Writer\Common\Entity; use Box\Spout\Writer\Common\Manager\SheetManager; +use Box\Spout\Writer\XLSX\Entity\SheetView; /** * Class Sheet @@ -27,6 +28,9 @@ class Sheet /** @var SheetManager Sheet manager */ private $sheetManager; + /** @var SheetView */ + private $sheetView; + /** * @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 +112,31 @@ public function setIsVisible($isVisible) return $this; } + + /** + * @return SheetView|null + */ + public function getSheetView(): ?SheetView + { + return $this->sheetView; + } + + /** + * @param SheetView $sheetView + * @return $this + */ + public function setSheetView(SheetView $sheetView) + { + $this->sheetView = $sheetView; + + return $this; + } + + /** + * @return bool + */ + public function hasSheetView(): bool + { + return $this->sheetView instanceof SheetView; + } } diff --git a/src/Spout/Writer/Common/Entity/Worksheet.php b/src/Spout/Writer/Common/Entity/Worksheet.php index 74c4976f..cd2eb680 100644 --- a/src/Spout/Writer/Common/Entity/Worksheet.php +++ b/src/Spout/Writer/Common/Entity/Worksheet.php @@ -23,6 +23,9 @@ class Worksheet /** @var int Index of the last written row */ private $lastWrittenRowIndex; + /** @var bool */ + private $hasStarted = false; + /** * Worksheet constructor. * @@ -110,4 +113,20 @@ public function getId() // sheet index is zero-based, while ID is 1-based return $this->externalSheet->getIndex() + 1; } + + /** + * @return bool + */ + public function hasStarted(): bool + { + return $this->hasStarted; + } + + /** + * @param bool $hasStarted + */ + public function setHasStarted(bool $hasStarted = true): void + { + $this->hasStarted = $hasStarted; + } } diff --git a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php index b5135555..982b0ae9 100644 --- a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php +++ b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php @@ -131,8 +131,6 @@ private function addNewSheet() $worksheetFilePath = $this->getWorksheetFilePath($sheet); $worksheet = $this->entityFactory->createWorksheet($worksheetFilePath, $sheet); - $this->worksheetManager->startSheet($worksheet); - $worksheets[] = $worksheet; $this->workbook->setWorksheets($worksheets); @@ -223,8 +221,15 @@ public function addRowToCurrentWorksheet(Row $row) if ($hasReachedMaxRows) { // ... continue writing in a new sheet if option set if ($this->optionsManager->getOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY)) { + $previousSheet = $currentWorksheet->getExternalSheet(); $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); + // Use the same sheet view if set + $sheet = $currentWorksheet->getExternalSheet(); + if ($previousSheet->hasSheetView()) { + $sheet->setSheetView($previousSheet->getSheetView()); + } + $this->addRowToWorksheet($currentWorksheet, $row); } else { // otherwise, do nothing as the data won't be written anyways diff --git a/src/Spout/Writer/XLSX/Entity/SheetView.php b/src/Spout/Writer/XLSX/Entity/SheetView.php new file mode 100644 index 00000000..3aad0842 --- /dev/null +++ b/src/Spout/Writer/XLSX/Entity/SheetView.php @@ -0,0 +1,295 @@ +showFormulas = $showFormulas; + return $this; + } + + /** + * @param bool $showGridLines + * @return $this + */ + public function setShowGridLines(bool $showGridLines): self + { + $this->showGridLines = $showGridLines; + return $this; + } + + /** + * @param bool $showRowcolHeaders + * @return $this + */ + public function setShowRowcolHeaders(bool $showRowcolHeaders): self + { + $this->showRowcolHeaders = $showRowcolHeaders; + return $this; + } + + /** + * @param bool $showZeroes + * @return $this + */ + public function setShowZeroes(bool $showZeroes): self + { + $this->showZeroes = $showZeroes; + return $this; + } + + /** + * @param bool $rightToLeft + * @return $this + */ + public function setRightToLeft(bool $rightToLeft): self + { + $this->rightToLeft = $rightToLeft; + return $this; + } + + /** + * @param bool $tabSelected + * @return $this + */ + public function setTabSelected(bool $tabSelected): self + { + $this->tabSelected = $tabSelected; + return $this; + } + + /** + * @param bool $showOutlineSymbols + * @return $this + */ + public function setShowOutlineSymbols(bool $showOutlineSymbols): self + { + $this->showOutlineSymbols = $showOutlineSymbols; + return $this; + } + + /** + * @param bool $defaultGridColor + * @return $this + */ + public function setDefaultGridColor(bool $defaultGridColor): self + { + $this->defaultGridColor = $defaultGridColor; + return $this; + } + + /** + * @param string $view + * @return $this + */ + public function setView(string $view): self + { + $this->view = $view; + return $this; + } + + /** + * @param string $topLeftCell + * @return $this + */ + public function setTopLeftCell(string $topLeftCell): self + { + $this->topLeftCell = $topLeftCell; + return $this; + } + + /** + * @param int $colorId + * @return $this + */ + public function setColorId(int $colorId): self + { + $this->colorId = $colorId; + return $this; + } + + /** + * @param int $zoomScale + * @return $this + */ + public function setZoomScale(int $zoomScale): self + { + $this->zoomScale = $zoomScale; + return $this; + } + + /** + * @param int $zoomScaleNormal + * @return $this + */ + public function setZoomScaleNormal(int $zoomScaleNormal): self + { + $this->zoomScaleNormal = $zoomScaleNormal; + return $this; + } + + /** + * @param int $zoomScalePageLayoutView + * @return $this + */ + public function setZoomScalePageLayoutView(int $zoomScalePageLayoutView): self + { + $this->zoomScalePageLayoutView = $zoomScalePageLayoutView; + return $this; + } + + /** + * @param int $workbookViewId + * @return $this + */ + public function setWorkbookViewId(int $workbookViewId): self + { + $this->workbookViewId = $workbookViewId; + return $this; + } + + /** + * @param int $freezeRow Set to 2 to fix the first row + * @return $this + */ + public function setFreezeRow(int $freezeRow): self + { + if ($freezeRow < 1) { + throw new InvalidArgumentException('Freeze row must be a positive integer', 1589543073); + } + + $this->freezeRow = $freezeRow; + return $this; + } + + /** + * @param string $freezeColumn Set to B to fix the first column + * @return $this + */ + public function setFreezeColumn(string $freezeColumn): self + { + $this->freezeColumn = strtoupper($freezeColumn); + return $this; + } + + /** + * @return string + */ + public function getXml(): string + { + return 'getSheetViewAttributes() . '>' . + $this->getFreezeCellPaneXml() . + ''; + } + + /** + * @return string + */ + protected function getSheetViewAttributes(): string + { + // Get class properties + $propertyValues = get_object_vars($this); + unset($propertyValues['freezeRow'], $propertyValues['freezeColumn']); + + return $this->generateAttributes($propertyValues); + } + + /** + * @return string + */ + protected function getFreezeCellPaneXml(): string + { + if ($this->freezeRow < 2 && $this->freezeColumn === 'A') { + return ''; + } + + $columnIndex = CellHelper::getColumnIndexFromCellIndex($this->freezeColumn . '1'); + + return 'generateAttributes([ + 'xSplit' => $columnIndex, + 'ySplit' => $this->freezeRow - 1, + 'topLeftCell' => $this->freezeColumn . $this->freezeRow, + 'activePane' => 'bottomRight', + 'state' => 'frozen', + ]) . '/>'; + } + + /** + * @param array $data with key containing the attribute name and value containing the attribute value + * @return string + */ + protected function generateAttributes(array $data): string + { + // Create attribute for each key + $attributes = array_map(function ($key, $value) { + if (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + return $key . '="' . $value . '"'; + }, array_keys($data), $data); + + // Append all attributes + return ' ' . implode(' ', $attributes); + } +} diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 741d0aa8..7005ea4f 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -17,6 +17,7 @@ use Box\Spout\Writer\Common\Manager\RowManager; use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface; +use Box\Spout\Writer\XLSX\Entity\SheetView; use Box\Spout\Writer\XLSX\Manager\Style\StyleManager; /** @@ -107,13 +108,34 @@ public function getSharedStringsManager() */ public function startSheet(Worksheet $worksheet) { + // Only start once + if ($worksheet->hasStarted()) { + return; + } + $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w'); $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer); $worksheet->setFilePointer($sheetFilePointer); \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER); + $this->addSheetViews($sheetFilePointer, $worksheet); \fwrite($sheetFilePointer, ''); + + $worksheet->setHasStarted(); + } + + /** + * @param resource $sheetFilePointer + * @param resourceWorksheet $worksheet + * @return void + */ + private function addSheetViews($sheetFilePointer, Worksheet $worksheet): void + { + $sheet = $worksheet->getExternalSheet(); + if ($sheet->hasSheetView()) { + \fwrite($sheetFilePointer, '' . $sheet->getSheetView()->getXml() . ''); + } } /** @@ -135,6 +157,9 @@ private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer) */ public function addRow(Worksheet $worksheet, Row $row) { + // Start the sheet when the first row is added + $this->startSheet($worksheet); + if (!$this->rowManager->isEmpty($row)) { $this->addNonEmptyRow($worksheet, $row); }