From 5949cb24421826d56f8549872bcfc21b4b3f8c87 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Fri, 28 Aug 2015 00:11:01 -0700 Subject: [PATCH] ODS writer Added ODS writer Refactored XLSX writer to abstract some pieces into an abstract multi-sheets writer Created an abstract style helper Moved shared components around --- src/Spout/Common/Escaper/ODS.php | 34 ++ src/Spout/Common/Type.php | 5 +- src/Spout/Reader/Wrapper/XMLReader.php | 16 + .../XLSX/Helper/SharedStringsHelper.php | 5 +- src/Spout/Reader/XLSX/Helper/SheetHelper.php | 1 + src/Spout/Reader/XLSX/RowIterator.php | 1 + .../Writer/AbstractMultiSheetsWriter.php | 114 +++++ src/Spout/Writer/AbstractWriter.php | 16 + .../Common/Helper/AbstractStyleHelper.php | 136 ++++++ .../{XLSX => Common}/Helper/CellHelper.php | 4 +- .../{XLSX => Common}/Helper/ZipHelper.php | 43 +- .../Common/Internal/AbstractWorkbook.php | 188 ++++++++ .../Common/Internal/WorkbookInterface.php | 74 +++ .../Common/Internal/WorksheetInterface.php | 40 ++ src/Spout/Writer/{XLSX => Common}/Sheet.php | 6 +- .../Writer/ODS/Helper/FileSystemHelper.php | 283 +++++++++++ src/Spout/Writer/ODS/Helper/StyleHelper.php | 270 +++++++++++ src/Spout/Writer/ODS/Internal/Workbook.php | 119 +++++ src/Spout/Writer/ODS/Internal/Worksheet.php | 210 +++++++++ src/Spout/Writer/ODS/Writer.php | 92 ++++ src/Spout/Writer/Style/Color.php | 44 +- src/Spout/Writer/WriterFactory.php | 5 +- .../Writer/XLSX/Helper/FileSystemHelper.php | 40 +- .../XLSX/Helper/SharedStringsHelper.php | 1 + src/Spout/Writer/XLSX/Helper/StyleHelper.php | 126 +---- src/Spout/Writer/XLSX/Internal/Workbook.php | 172 ++----- src/Spout/Writer/XLSX/Internal/Worksheet.php | 38 +- src/Spout/Writer/XLSX/Writer.php | 102 +--- tests/Spout/Common/Escaper/XLSXTest.php | 2 + .../Helper/CellHelperTest.php | 4 +- tests/Spout/Writer/Common/SheetTest.php | 94 ++++ .../Writer/ODS/Helper/StyleHelperTest.php | 89 ++++ tests/Spout/Writer/ODS/SheetTest.php | 134 ++++++ tests/Spout/Writer/ODS/WriterTest.php | 446 ++++++++++++++++++ .../Spout/Writer/ODS/WriterWithStyleTest.php | 364 ++++++++++++++ tests/Spout/Writer/Style/ColorTest.php | 14 +- tests/Spout/Writer/XLSX/SheetTest.php | 61 +-- tests/Spout/Writer/XLSX/WriterTest.php | 6 +- .../Spout/Writer/XLSX/WriterWithStyleTest.php | 58 ++- 39 files changed, 2924 insertions(+), 533 deletions(-) create mode 100644 src/Spout/Common/Escaper/ODS.php create mode 100644 src/Spout/Writer/AbstractMultiSheetsWriter.php create mode 100644 src/Spout/Writer/Common/Helper/AbstractStyleHelper.php rename src/Spout/Writer/{XLSX => Common}/Helper/CellHelper.php (95%) rename src/Spout/Writer/{XLSX => Common}/Helper/ZipHelper.php (57%) create mode 100644 src/Spout/Writer/Common/Internal/AbstractWorkbook.php create mode 100644 src/Spout/Writer/Common/Internal/WorkbookInterface.php create mode 100644 src/Spout/Writer/Common/Internal/WorksheetInterface.php rename src/Spout/Writer/{XLSX => Common}/Sheet.php (97%) create mode 100644 src/Spout/Writer/ODS/Helper/FileSystemHelper.php create mode 100644 src/Spout/Writer/ODS/Helper/StyleHelper.php create mode 100644 src/Spout/Writer/ODS/Internal/Workbook.php create mode 100644 src/Spout/Writer/ODS/Internal/Worksheet.php create mode 100644 src/Spout/Writer/ODS/Writer.php rename tests/Spout/Writer/{XLSX => Common}/Helper/CellHelperTest.php (97%) create mode 100644 tests/Spout/Writer/Common/SheetTest.php create mode 100644 tests/Spout/Writer/ODS/Helper/StyleHelperTest.php create mode 100644 tests/Spout/Writer/ODS/SheetTest.php create mode 100644 tests/Spout/Writer/ODS/WriterTest.php create mode 100644 tests/Spout/Writer/ODS/WriterWithStyleTest.php diff --git a/src/Spout/Common/Escaper/ODS.php b/src/Spout/Common/Escaper/ODS.php new file mode 100644 index 0000000..3e252a7 --- /dev/null +++ b/src/Spout/Common/Escaper/ODS.php @@ -0,0 +1,34 @@ +read()) && ($this->nodeType !== \XMLReader::ELEMENT || $this->name !== $nodeName)) { + // do nothing + } + + return $wasReadSuccessful; + } + /** * Move cursor to next node skipping all subtrees * @see \XMLReader::next diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php index f01150c..9c4f746 100644 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php @@ -79,6 +79,7 @@ class SharedStringsHelper { $xmlReader = new XMLReader(); $sharedStringIndex = 0; + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = new \Box\Spout\Common\Escaper\XLSX(); $sharedStringsFilePath = $this->getSharedStringsFilePath(); @@ -90,9 +91,7 @@ class SharedStringsHelper $sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader); $this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount); - while ($xmlReader->read() && $xmlReader->name !== 'si') { - // do nothing until a 'si' tag is reached - } + $xmlReader->readUntilNodeFound('si'); while ($xmlReader->name === 'si') { $node = $this->getSimpleXmlElementNodeFromXMLReader($xmlReader); diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php index 308e94e..577d58d 100644 --- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SheetHelper.php @@ -126,6 +126,7 @@ class SheetHelper $sheetId = (int) $sheetNode->getAttribute('sheetId'); $escapedSheetName = $sheetNode->getAttribute('name'); + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = new \Box\Spout\Common\Escaper\XLSX(); $sheetName = $escaper->unescape($escapedSheetName); } diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php index 37937f5..ed9db60 100644 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ b/src/Spout/Reader/XLSX/RowIterator.php @@ -77,6 +77,7 @@ class RowIterator implements IteratorInterface $this->sharedStringsHelper = $sharedStringsHelper; $this->xmlReader = new XMLReader(); + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = new \Box\Spout\Common\Escaper\XLSX(); } diff --git a/src/Spout/Writer/AbstractMultiSheetsWriter.php b/src/Spout/Writer/AbstractMultiSheetsWriter.php new file mode 100644 index 0000000..9e145dd --- /dev/null +++ b/src/Spout/Writer/AbstractMultiSheetsWriter.php @@ -0,0 +1,114 @@ +throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; + return $this; + } + + /** + * Returns all the workbook's sheets + * + * @return Common\Sheet[] All the workbook's sheets + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + */ + public function getSheets() + { + $this->throwIfBookIsNotAvailable(); + + $externalSheets = []; + $worksheets = $this->getWorkbook()->getWorksheets(); + + /** @var Common\WorksheetInterface $worksheet */ + foreach ($worksheets as $worksheet) { + $externalSheets[] = $worksheet->getExternalSheet(); + } + + return $externalSheets; + } + + /** + * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. + * + * @return Common\Sheet The created sheet + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + */ + public function addNewSheetAndMakeItCurrent() + { + $this->throwIfBookIsNotAvailable(); + $worksheet = $this->getWorkbook()->addNewSheetAndMakeItCurrent(); + + return $worksheet->getExternalSheet(); + } + + /** + * Returns the current sheet + * + * @return Common\Sheet The current sheet + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + */ + public function getCurrentSheet() + { + $this->throwIfBookIsNotAvailable(); + return $this->getWorkbook()->getCurrentWorksheet()->getExternalSheet(); + } + + /** + * 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). + * + * @param Common\Sheet $sheet The sheet to set as current + * @return void + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook + */ + public function setCurrentSheet($sheet) + { + $this->throwIfBookIsNotAvailable(); + $this->getWorkbook()->setCurrentSheet($sheet); + } + + /** + * Checks if the book has been created. Throws an exception if not created yet. + * + * @return void + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet + */ + protected function throwIfBookIsNotAvailable() + { + if (!$this->getWorkbook()) { + throw new WriterNotOpenedException('The writer must be opened before performing this action.'); + } + } +} + diff --git a/src/Spout/Writer/AbstractWriter.php b/src/Spout/Writer/AbstractWriter.php index 00adce7..99c2232 100644 --- a/src/Spout/Writer/AbstractWriter.php +++ b/src/Spout/Writer/AbstractWriter.php @@ -4,6 +4,7 @@ namespace Box\Spout\Writer; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\InvalidArgumentException; +use Box\Spout\Writer\Exception\WriterAlreadyOpenedException; use Box\Spout\Writer\Exception\WriterNotOpenedException; use Box\Spout\Writer\Style\StyleBuilder; @@ -152,6 +153,21 @@ abstract class AbstractWriter implements WriterInterface } } + /** + * Checks if the writer has already been opened, since some actions must be done before it gets opened. + * Throws an exception if already opened. + * + * @param string $message Error message + * @return void + * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be. + */ + protected function throwIfWriterAlreadyOpened($message) + { + if ($this->isWriterOpened) { + throw new WriterAlreadyOpenedException($message); + } + } + /** * Write given data to the output. New data will be appended to end of stream. * diff --git a/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php b/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php new file mode 100644 index 0000000..70ee8c9 --- /dev/null +++ b/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php @@ -0,0 +1,136 @@ + [STYLE_ID] mapping table, keeping track of the registered styles */ + protected $serializedStyleToStyleIdMappingTable = []; + + /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ + protected $styleIdToStyleMappingTable = []; + + /** + * @param \Box\Spout\Writer\Style\Style $defaultStyle + */ + public function __construct($defaultStyle) + { + // This ensures that the default style is the first one to be registered + $this->registerStyle($defaultStyle); + } + + /** + * Registers the given style as a used style. + * Duplicate styles won't be registered more than once. + * + * @param \Box\Spout\Writer\Style\Style $style The style to be registered + * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. + */ + public function registerStyle($style) + { + $serializedStyle = $style->serialize(); + + if (!$this->hasStyleAlreadyBeenRegistered($style)) { + $nextStyleId = count($this->serializedStyleToStyleIdMappingTable); + $style->setId($nextStyleId); + + $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; + $this->styleIdToStyleMappingTable[$nextStyleId] = $style; + } + + return $this->getStyleFromSerializedStyle($serializedStyle); + } + + /** + * Returns whether the given style has already been registered. + * + * @param \Box\Spout\Writer\Style\Style $style + * @return bool + */ + protected function hasStyleAlreadyBeenRegistered($style) + { + $serializedStyle = $style->serialize(); + return array_key_exists($serializedStyle, $this->serializedStyleToStyleIdMappingTable); + } + + /** + * Returns the registered style associated to the given serialization. + * + * @param string $serializedStyle The serialized style from which the actual style should be fetched from + * @return \Box\Spout\Writer\Style\Style + */ + protected function getStyleFromSerializedStyle($serializedStyle) + { + $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle]; + return $this->styleIdToStyleMappingTable[$styleId]; + } + + /** + * @return \Box\Spout\Writer\Style\Style[] List of registered styles + */ + protected function getRegisteredStyles() + { + return array_values($this->styleIdToStyleMappingTable); + } + + /** + * Returns the default style + * + * @return \Box\Spout\Writer\Style\Style Default style + */ + protected function getDefaultStyle() + { + // By construction, the default style has ID 0 + return $this->styleIdToStyleMappingTable[0]; + } + + /** + * Apply additional styles if the given row needs it. + * Typically, set "wrap text" if a cell contains a new line. + * + * @param \Box\Spout\Writer\Style\Style $style The original style + * @param array $dataRow The row the style will be applied to + * @return \Box\Spout\Writer\Style\Style The updated style + */ + public function applyExtraStylesIfNeeded($style, $dataRow) + { + $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow); + return $updatedStyle; + } + + /** + * Set the "wrap text" option if a cell of the given row contains a new line. + * + * @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines + * are ignored even when the "wrap text" option is set. This only occurs with + * inline strings (shared strings do work fine). + * A workaround would be to encode "\n" as "_x000D_" but it does not work + * on the Windows version of Excel... + * + * @param \Box\Spout\Writer\Style\Style $style The original style + * @param array $dataRow The row the style will be applied to + * @return \Box\Spout\Writer\Style\Style The eventually updated style + */ + protected function applyWrapTextIfCellContainsNewLine($style, $dataRow) + { + // if the "wrap text" option is already set, no-op + if ($style->shouldWrapText()) { + return $style; + } + + foreach ($dataRow as $cell) { + if (is_string($cell) && strpos($cell, "\n") !== false) { + $style->setShouldWrapText(); + break; + } + } + + return $style; + } +} diff --git a/src/Spout/Writer/XLSX/Helper/CellHelper.php b/src/Spout/Writer/Common/Helper/CellHelper.php similarity index 95% rename from src/Spout/Writer/XLSX/Helper/CellHelper.php rename to src/Spout/Writer/Common/Helper/CellHelper.php index 0f5dc60..2349437 100644 --- a/src/Spout/Writer/XLSX/Helper/CellHelper.php +++ b/src/Spout/Writer/Common/Helper/CellHelper.php @@ -1,12 +1,12 @@ getZipFilePath($folderPath); + $this->zipFolder($folderPath, $zipFilePath); + $this->copyZipToStream($zipFilePath, $streamPointer); + } + + /** + * @param string $folderPathToZip Path to the folder to be zipped + * @return string Path where the zip file of the given folder will be created + */ + public function getZipFilePath($folderPathToZip) + { + return $folderPathToZip . self::ZIP_EXTENSION; + } + /** * Zips the given folder * @@ -59,4 +84,18 @@ class ZipHelper $realPath = realpath($path); return str_replace(DIRECTORY_SEPARATOR, '/', $realPath); } + + /** + * Streams the contents of the zip file into the given stream + * + * @param string $zipFilePath Path to the zip file + * @param resource $pointer Pointer to the stream to copy the zip + * @return void + */ + protected function copyZipToStream($zipFilePath, $pointer) + { + $zipFilePointer = fopen($zipFilePath, 'r'); + stream_copy_to_stream($zipFilePointer, $pointer); + fclose($zipFilePointer); + } } diff --git a/src/Spout/Writer/Common/Internal/AbstractWorkbook.php b/src/Spout/Writer/Common/Internal/AbstractWorkbook.php new file mode 100644 index 0000000..2d65dad --- /dev/null +++ b/src/Spout/Writer/Common/Internal/AbstractWorkbook.php @@ -0,0 +1,188 @@ +shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; + } + + /** + * @return \Box\Spout\Writer\Common\Helper\AbstractStyleHelper The specific style helper + */ + abstract protected function getStyleHelper(); + + /** + * @return int Maximum number of rows/columns a sheet can contain + */ + abstract protected function getMaxRowsPerWorksheet(); + + /** + * Creates a new sheet in the workbook. The current sheet remains unchanged. + * + * @return WorksheetInterface The created sheet + * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing + */ + abstract public function addNewSheet(); + + /** + * 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). + * + * @return WorksheetInterface The created sheet + * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing + */ + public function addNewSheetAndMakeItCurrent() + { + $worksheet = $this->addNewSheet(); + $this->setCurrentWorksheet($worksheet); + + return $worksheet; + } + + /** + * @return WorksheetInterface[] All the workbook's sheets + */ + public function getWorksheets() + { + return $this->worksheets; + } + + /** + * Returns the current sheet + * + * @return WorksheetInterface The current sheet + */ + public function getCurrentWorksheet() + { + return $this->currentWorksheet; + } + + /** + * 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). + * + * @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current + * @return void + * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook + */ + public function setCurrentSheet($sheet) + { + $worksheet = $this->getWorksheetFromExternalSheet($sheet); + if ($worksheet !== null) { + $this->currentWorksheet = $worksheet; + } else { + throw new SheetNotFoundException('The given sheet does not exist in the workbook.'); + } + } + + /** + * @param WorksheetInterface $worksheet + * @return void + */ + protected function setCurrentWorksheet($worksheet) + { + $this->currentWorksheet = $worksheet; + } + + /** + * Returns the worksheet associated to the given external sheet. + * + * @param \Box\Spout\Writer\Common\Sheet $sheet + * @return WorksheetInterface|null The worksheet associated to the given external sheet or null if not found. + */ + protected function getWorksheetFromExternalSheet($sheet) + { + $worksheetFound = null; + + foreach ($this->worksheets as $worksheet) { + if ($worksheet->getExternalSheet() === $sheet) { + $worksheetFound = $worksheet; + break; + } + } + + return $worksheetFound; + } + + /** + * Adds data to the current sheet. + * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination + * with the creation of new worksheets if one worksheet has reached its maximum capicity. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. + * @return void + * @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing + * @throws \Box\Spout\Writer\Exception\WriterException If unable to write data + */ + public function addRowToCurrentWorksheet($dataRow, $style) + { + $currentWorksheet = $this->getCurrentWorksheet(); + $hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows(); + $styleHelper = $this->getStyleHelper(); + + // if we reached the maximum number of rows for the current sheet... + if ($hasReachedMaxRows) { + // ... continue writing in a new sheet if option set + if ($this->shouldCreateNewSheetsAutomatically) { + $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); + + $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); + $registeredStyle = $styleHelper->registerStyle($updatedStyle); + $currentWorksheet->addRow($dataRow, $registeredStyle); + } else { + // otherwise, do nothing as the data won't be read anyways + } + } else { + $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); + $registeredStyle = $styleHelper->registerStyle($updatedStyle); + $currentWorksheet->addRow($dataRow, $registeredStyle); + } + } + + /** + * @return bool Whether the current worksheet has reached the maximum number of rows per sheet. + */ + protected function hasCurrentWorkseetReachedMaxRows() + { + $currentWorksheet = $this->getCurrentWorksheet(); + return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet()); + } + + /** + * Closes the workbook and all its associated sheets. + * All the necessary files are written to disk and zipped together to create the ODS file. + * All the temporary files are then deleted. + * + * @param resource $finalFilePointer Pointer to the ODS that will be created + * @return void + */ + abstract public function close($finalFilePointer); +} diff --git a/src/Spout/Writer/Common/Internal/WorkbookInterface.php b/src/Spout/Writer/Common/Internal/WorkbookInterface.php new file mode 100644 index 0000000..fda0793 --- /dev/null +++ b/src/Spout/Writer/Common/Internal/WorkbookInterface.php @@ -0,0 +1,74 @@ +rootFolder; + } + + /** + * @return string + */ + public function getSheetsContentTempFolder() + { + return $this->sheetsContentTempFolder; + } + + /** + * Creates all the folders needed to create a ODS file, as well as the files that won't change. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders + */ + public function createBaseFilesAndFolders() + { + $this + ->createRootFolder() + ->createMetaInfoFolderAndFile() + ->createSheetsContentTempFolder() + ->createMetaFile() + ->createMimetypeFile(); + } + + /** + * Creates the folder that will be used as root + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createRootFolder() + { + $this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('ods')); + return $this; + } + + /** + * Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file + */ + protected function createMetaInfoFolderAndFile() + { + $this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME); + + $this->createManifestFile(); + + return $this; + } + + /** + * Creates the "manifest.xml" file under the "META-INF" folder (under root) + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createManifestFile() + { + $manifestXmlFileContents = << + + + + + + + + +EOD; + + $this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents); + + return $this; + } + + /** + * Creates the temp folder where specific sheets content will be written to. + * This folder is not part of the final ODS file and is only used to be able to jump between sheets. + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createSheetsContentTempFolder() + { + $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME); + return $this; + } + + /** + * Creates the "meta.xml" file under the root folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createMetaFile() + { + $appName = self::APP_NAME; + $createdDate = (new \DateTime())->format(\DateTime::W3C); + + $metaXmlFileContents = << + + + $appName + $createdDate + $createdDate + + +EOD; + + $this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents); + + return $this; + } + + /** + * Creates the "mimetype" file under the root folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createMimetypeFile() + { + $this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE); + return $this; + } + + /** + * Creates the "content.xml" file under the root folder + * + * @param Worksheet[] $worksheets + * @param StyleHelper $styleHelper + * @return FileSystemHelper + */ + public function createContentFile($worksheets, $styleHelper) + { + $contentXmlFileContents = << + + +EOD; + + $contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent(); + $contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets)); + + $contentXmlFileContents .= << + + +EOD; + + $this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents); + + // Append sheets content to "content.xml" + $contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME; + $contentXmlHandle = fopen($contentXmlFilePath, 'a'); + + foreach ($worksheets as $worksheet) { + // write the "" node, with the final sheet's name + fwrite($contentXmlHandle, $worksheet->getTableRootNodeAsString() . PHP_EOL); + + $worksheetFilePath = $worksheet->getWorksheetFilePath(); + $this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle); + + fwrite($contentXmlHandle, '' . PHP_EOL); + } + + $contentXmlFileContents = << + + +EOD; + + fwrite($contentXmlHandle, $contentXmlFileContents); + fclose($contentXmlHandle); + + return $this; + } + + /** + * Streams the content of the file at the given path into the target resource. + * Depending on which mode the target resource was created with, it will truncate then copy + * or append the content to the target file. + * + * @param string $sourceFilePath Path of the file whose content will be copied + * @param resource $targetResource Target resource that will receive the content + * @return void + */ + protected function copyFileContentsToTarget($sourceFilePath, $targetResource) + { + $sourceHandle = fopen($sourceFilePath, 'r'); + stream_copy_to_stream($sourceHandle, $targetResource); + fclose($sourceHandle); + } + + /** + * Deletes the temporary folder where sheets content was stored. + * + * @return FileSystemHelper + */ + public function deleteWorksheetTempFolder() + { + $this->deleteFolderRecursively($this->sheetsContentTempFolder); + return $this; + } + + + /** + * Creates the "styles.xml" file under the root folder + * + * @param StyleHelper $styleHelper + * @param int $numWorksheets Number of created worksheets + * @return FileSystemHelper + */ + public function createStylesFile($styleHelper, $numWorksheets) + { + $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets); + $this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); + + return $this; + } + + /** + * Zips the root folder and streams the contents of the zip into the given stream + * + * @param resource $streamPointer Pointer to the stream to copy the zip + * @return void + */ + public function zipRootFolderAndCopyToStream($streamPointer) + { + $zipHelper = new ZipHelper(); + $zipHelper->zipFolderAndCopyToStream($this->rootFolder, $streamPointer); + + // once the zip is copied, remove it + $this->deleteFile($zipHelper->getZipFilePath($this->rootFolder)); + } +} diff --git a/src/Spout/Writer/ODS/Helper/StyleHelper.php b/src/Spout/Writer/ODS/Helper/StyleHelper.php new file mode 100644 index 0000000..2752ad9 --- /dev/null +++ b/src/Spout/Writer/ODS/Helper/StyleHelper.php @@ -0,0 +1,270 @@ + [] Map whose keys contain all the fonts used */ + protected $usedFontsSet = []; + + /** + * Registers the given style as a used style. + * Duplicate styles won't be registered more than once. + * + * @param \Box\Spout\Writer\Style\Style $style The style to be registered + * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. + */ + public function registerStyle($style) + { + $this->usedFontsSet[$style->getFontName()] = true; + return parent::registerStyle($style); + } + + /** + * @return string[] List of used fonts name + */ + protected function getUsedFonts() + { + return array_keys($this->usedFontsSet); + } + + /** + * Returns the content of the "styles.xml" file, given a list of styles. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + public function getStylesXMLFileContent($numWorksheets) + { + $content = << + + +EOD; + + $content .= $this->getFontFaceSectionContent(); + $content .= $this->getStylesSectionContent(); + $content .= $this->getAutomaticStylesSectionContent($numWorksheets); + $content .= $this->getMasterStylesSectionContent($numWorksheets); + + $content .= << +EOD; + + return $content; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @return string + */ + protected function getFontFaceSectionContent() + { + $content = '' . PHP_EOL; + foreach ($this->getUsedFonts() as $fontName) { + $content .= ' ' . PHP_EOL; + } + $content .= '' . PHP_EOL; + + return $content; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @return string + */ + protected function getStylesSectionContent() + { + $defaultStyle = $this->getDefaultStyle(); + + return << + + + + + + + + + +EOD; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + protected function getAutomaticStylesSectionContent($numWorksheets) + { + $content = '' . PHP_EOL; + + for ($i = 1; $i <= $numWorksheets; $i++) { + $content .= << + + + + + +EOD; + } + + $content .= '' . PHP_EOL; + + return $content; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + protected function getMasterStylesSectionContent($numWorksheets) + { + $content = '' . PHP_EOL; + + for ($i = 1; $i <= $numWorksheets; $i++) { + $content .= << + + + + + + +EOD; + } + + $content .= '' . PHP_EOL; + + return $content; + } + + + /** + * Returns the contents of the "" section, inside "content.xml" file. + * + * @return string + */ + public function getContentXmlFontFaceSectionContent() + { + $content = '' . PHP_EOL; + foreach ($this->getUsedFonts() as $fontName) { + $content .= ' ' . PHP_EOL; + } + $content .= '' . PHP_EOL; + + return $content; + } + + /** + * Returns the contents of the "" section, inside "content.xml" file. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + public function getContentXmlAutomaticStylesSectionContent($numWorksheets) + { + $content = '' . PHP_EOL; + + foreach ($this->getRegisteredStyles() as $style) { + $content .= $this->getStyleSectionContent($style); + } + + $content .= << + + + + + + +EOD; + + for ($i = 1; $i <= $numWorksheets; $i++) { + $content .= << + + + +EOD; + } + + $content .= '' . PHP_EOL; + + return $content; + } + + /** + * Returns the contents of the "" section, inside "" section + * + * @param \Box\Spout\Writer\Style\Style $style + * @return string + */ + protected function getStyleSectionContent($style) + { + $defaultStyle = $this->getDefaultStyle(); + $styleIndex = $style->getId() + 1; // 1-based + + $content = ' ' . PHP_EOL; + + if ($style->shouldApplyFont()) { + $content .= ' getFontColor(); + if ($fontColor !== $defaultStyle->getFontColor()) { + $content .= ' fo:color="#' . $fontColor . '"'; + } + + $fontName = $style->getFontName(); + if ($fontName !== $defaultStyle->getFontName()) { + $content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"'; + } + + $fontSize = $style->getFontSize(); + if ($fontSize !== $defaultStyle->getFontSize()) { + $content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"'; + } + + if ($style->isFontBold()) { + $content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"'; + } + if ($style->isFontItalic()) { + $content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"'; + } + if ($style->isFontUnderline()) { + $content .= ' style:text-underline-style="solid" style:text-underline-type="single"'; + } + if ($style->isFontStrikethrough()) { + $content .= ' style:text-line-through-style="solid"'; + } + + $content .= '/>' . PHP_EOL; + } + + if ($style->shouldWrapText()) { + $content .= ' ' . PHP_EOL; + } + + $content .= ' ' . PHP_EOL; + + return $content; + } + +} diff --git a/src/Spout/Writer/ODS/Internal/Workbook.php b/src/Spout/Writer/ODS/Internal/Workbook.php new file mode 100644 index 0000000..5d4a9bf --- /dev/null +++ b/src/Spout/Writer/ODS/Internal/Workbook.php @@ -0,0 +1,119 @@ +fileSystemHelper = new FileSystemHelper($tempFolder); + $this->fileSystemHelper->createBaseFilesAndFolders(); + + $this->styleHelper = new StyleHelper($defaultRowStyle); + } + + /** + * @return \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles to ODS files + */ + protected function getStyleHelper() + { + return $this->styleHelper; + } + + /** + * @return int Maximum number of rows/columns a sheet can contain + */ + protected function getMaxRowsPerWorksheet() + { + return self::$maxRowsPerWorksheet; + } + + /** + * Creates a new sheet in the workbook. The current sheet remains unchanged. + * + * @return Worksheet The created sheet + * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing + */ + public function addNewSheet() + { + $newSheetIndex = count($this->worksheets); + $sheet = new Sheet($newSheetIndex); + + $sheetsContentTempFolder = $this->fileSystemHelper->getSheetsContentTempFolder(); + $worksheet = new Worksheet($sheet, $sheetsContentTempFolder); + $this->worksheets[] = $worksheet; + + return $worksheet; + } + + /** + * Closes the workbook and all its associated sheets. + * All the necessary files are written to disk and zipped together to create the ODS file. + * All the temporary files are then deleted. + * + * @param resource $finalFilePointer Pointer to the ODS that will be created + * @return void + */ + public function close($finalFilePointer) + { + /** @var Worksheet[] $worksheets */ + $worksheets = $this->worksheets; + $numWorksheets = count($worksheets); + + foreach ($worksheets as $worksheet) { + $worksheet->close(); + } + + // Finish creating all the necessary files before zipping everything together + $this->fileSystemHelper + ->createContentFile($worksheets, $this->styleHelper) + ->deleteWorksheetTempFolder() + ->createStylesFile($this->styleHelper, $numWorksheets) + ->zipRootFolderAndCopyToStream($finalFilePointer); + + $this->cleanupTempFolder(); + } + + /** + * Deletes the root folder created in the temp folder and all its contents. + * + * @return void + */ + protected function cleanupTempFolder() + { + $xlsxRootFolder = $this->fileSystemHelper->getRootFolder(); + $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder); + } +} diff --git a/src/Spout/Writer/ODS/Internal/Worksheet.php b/src/Spout/Writer/ODS/Internal/Worksheet.php new file mode 100644 index 0000000..b2b1222 --- /dev/null +++ b/src/Spout/Writer/ODS/Internal/Worksheet.php @@ -0,0 +1,210 @@ +externalSheet = $externalSheet; + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->stringsEscaper = new \Box\Spout\Common\Escaper\ODS(); + $this->worksheetFilePath = $worksheetFilesFolder . '/sheet' . $externalSheet->getIndex() . '.xml'; + + $this->stringHelper = new StringHelper(); + + $this->startSheet(); + } + + /** + * Prepares the worksheet to accept data + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function startSheet() + { + $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w'); + $this->throwIfSheetFilePointerIsNotAvailable(); + + // The XML file does not contain the "" node as it contains the sheet's name + // which may change during the execution of the program. It will be added at the end. + $content = ' ' . PHP_EOL; + fwrite($this->sheetFilePointer, $content); + } + + /** + * Checks if the book has been created. Throws an exception if not created yet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function throwIfSheetFilePointerIsNotAvailable() + { + if (!$this->sheetFilePointer) { + throw new IOException('Unable to open sheet for writing.'); + } + } + + /** + * @return string Path to the temporary sheet content XML file + */ + public function getWorksheetFilePath() + { + return $this->worksheetFilePath; + } + + /** + * Returns the table XML root node as string. + * + * @return string node as string + */ + public function getTableRootNodeAsString() + { + $escapedSheetName = $this->stringsEscaper->escape($this->externalSheet->getName()); + $tableStyleName = 'ta' . ($this->externalSheet->getIndex() + 1); + + return ''; + } + + /** + * @return \Box\Spout\Writer\Common\Sheet The "external" sheet + */ + public function getExternalSheet() + { + return $this->externalSheet; + } + + /** + * @return int The index of the last written row + */ + public function getLastWrittenRowIndex() + { + return $this->lastWrittenRowIndex; + } + + /** + * Adds data to the worksheet. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written + * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported + */ + public function addRow($dataRow, $style) + { + $numColumnsRepeated = self::MAX_NUM_COLUMNS_REPEATED; + $styleIndex = ($style->getId() + 1); // 1-based + + $data = ' ' . PHP_EOL; + + foreach($dataRow as $cellValue) { + $data .= ' stringsEscaper->escape($cellValueLine) . '' . PHP_EOL; + } + + $data .= ' ' . PHP_EOL; + } else if (CellHelper::isBoolean($cellValue)) { + $data .= ' office:value-type="boolean" office:value="' . $cellValue . '">' . PHP_EOL; + $data .= ' ' . $cellValue . '' . PHP_EOL; + $data .= ' ' . PHP_EOL; + } else if (CellHelper::isNumeric($cellValue)) { + $data .= ' office:value-type="float" office:value="' . $cellValue . '">' . PHP_EOL; + $data .= ' ' . $cellValue . '' . PHP_EOL; + $data .= ' ' . PHP_EOL; + } else if (empty($cellValue)) { + $data .= '/>' . PHP_EOL; + } else { + throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); + } + + $numColumnsRepeated--; + } + + if ($numColumnsRepeated > 0) { + $data .= ' ' . PHP_EOL; + } + + $data .= ' ' . PHP_EOL; + + $wasWriteSuccessful = fwrite($this->sheetFilePointer, $data); + if ($wasWriteSuccessful === false) { + throw new IOException("Unable to write data in {$this->worksheetFilePath}"); + } + + // only update the count if the write worked + $this->lastWrittenRowIndex++; + } + + /** + * Closes the worksheet + * + * @return void + */ + public function close() + { + $remainingRepeatedRows = self::MAX_NUM_ROWS_REPEATED - $this->lastWrittenRowIndex; + + if ($remainingRepeatedRows > 0) { + $data = ' ' . PHP_EOL; + $data .= ' ' . PHP_EOL; + $data .= ' ' . PHP_EOL; + + fwrite($this->sheetFilePointer, $data); + } + + fclose($this->sheetFilePointer); + } +} diff --git a/src/Spout/Writer/ODS/Writer.php b/src/Spout/Writer/ODS/Writer.php new file mode 100644 index 0000000..edb2c89 --- /dev/null +++ b/src/Spout/Writer/ODS/Writer.php @@ -0,0 +1,92 @@ +throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->tempFolder = $tempFolder; + return $this; + } + + /** + * Configures the write and sets the current sheet pointer to a new sheet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing + */ + protected function openWriter() + { + $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); + $this->book = new Workbook($tempFolder, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); + $this->book->addNewSheetAndMakeItCurrent(); + } + + /** + * @return Internal\Workbook The workbook representing the file to be written + */ + protected function getWorkbook() + { + return $this->book; + } + + /** + * Adds data to the currently opened writer. + * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination + * with the creation of new worksheets if one worksheet has reached its maximum capicity. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. + * @return void + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + protected function addRowToWriter(array $dataRow, $style) + { + $this->throwIfBookIsNotAvailable(); + $this->book->addRowToCurrentWorksheet($dataRow, $style); + } + + /** + * Closes the writer, preventing any additional writing. + * + * @return void + */ + protected function closeWriter() + { + if ($this->book) { + $this->book->close($this->filePointer); + } + } +} diff --git a/src/Spout/Writer/Style/Color.php b/src/Spout/Writer/Style/Color.php index 3f7f7e8..346bfc2 100644 --- a/src/Spout/Writer/Style/Color.php +++ b/src/Spout/Writer/Style/Color.php @@ -13,27 +13,26 @@ use Box\Spout\Writer\Exception\InvalidColorException; class Color { /** Standard colors - based on Office Online */ - const BLACK = 'FF000000'; - const WHITE = 'FFFFFFFF'; - const RED = 'FFFF0000'; - const DARK_RED = 'FFC00000'; - const ORANGE = 'FFFFC000'; - const YELLOW = 'FFFFFF00'; - const LIGHT_GREEN = 'FF92D040'; - const GREEN = 'FF00B050'; - const LIGHT_BLUE = 'FF00B0E0'; - const BLUE = 'FF0070C0'; - const DARK_BLUE = 'FF002060'; - const PURPLE = 'FF7030A0'; + const BLACK = '000000'; + const WHITE = 'FFFFFF'; + const RED = 'FF0000'; + const DARK_RED = 'C00000'; + const ORANGE = 'FFC000'; + const YELLOW = 'FFFF00'; + const LIGHT_GREEN = '92D040'; + const GREEN = '00B050'; + const LIGHT_BLUE = '00B0E0'; + const BLUE = '0070C0'; + const DARK_BLUE = '002060'; + const PURPLE = '7030A0'; /** - * Returns an ARGB color from R, G and B values - * Alpha is assumed to always be 1 + * Returns an RGB color from R, G and B values * * @param int $red Red component, 0 - 255 * @param int $green Green component, 0 - 255 * @param int $blue Blue component, 0 - 255 - * @return string ARGB color + * @return string RGB color */ public static function rgb($red, $green, $blue) { @@ -42,7 +41,6 @@ class Color self::throwIfInvalidColorComponentValue($blue); return strtoupper( - 'FF' . self::convertColorComponentToHex($red) . self::convertColorComponentToHex($green) . self::convertColorComponentToHex($blue) @@ -71,6 +69,18 @@ class Color */ protected static function convertColorComponentToHex($colorComponent) { - return str_pad(dechex($colorComponent), 2, '0', 0); + return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT); + } + + /** + * Returns the ARGB color of the given RGB color, + * assuming that alpha value is always 1. + * + * @param string $rgbColor RGB color like "FF08B2" + * @return string ARGB color + */ + public static function toARGB($rgbColor) + { + return 'FF' . $rgbColor; } } diff --git a/src/Spout/Writer/WriterFactory.php b/src/Spout/Writer/WriterFactory.php index ee93cd7..9d468a6 100644 --- a/src/Spout/Writer/WriterFactory.php +++ b/src/Spout/Writer/WriterFactory.php @@ -9,7 +9,7 @@ use Box\Spout\Common\Type; /** * Class WriterFactory * This factory is used to create writers, based on the type of the file to be read. - * It supports CSV and XLSX formats. + * It supports CSV, XLSX and ODS formats. * * @package Box\Spout\Writer */ @@ -33,6 +33,9 @@ class WriterFactory case Type::XLSX: $writer = new XLSX\Writer(); break; + case Type::ODS: + $writer = new ODS\Writer(); + break; default: throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType); } diff --git a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php b/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php index 3144d5e..e84b68e 100644 --- a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php +++ b/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php @@ -2,6 +2,7 @@ namespace Box\Spout\Writer\XLSX\Helper; +use Box\Spout\Writer\Common\Helper\ZipHelper; use Box\Spout\Writer\XLSX\Internal\Worksheet; /** @@ -284,6 +285,7 @@ EOD; EOD; + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = new \Box\Spout\Common\Escaper\XLSX(); /** @var Worksheet $worksheet */ @@ -354,42 +356,10 @@ EOD; */ public function zipRootFolderAndCopyToStream($streamPointer) { - $this->zipRootFolder(); - $this->copyZipToStream($streamPointer); + $zipHelper = new ZipHelper(); + $zipHelper->zipFolderAndCopyToStream($this->rootFolder, $streamPointer); // once the zip is copied, remove it - $this->deleteFile($this->getZipFilePath()); - } - - /** - * Zips the root folder - * - * @return void - */ - protected function zipRootFolder() - { - $zipHelper = new ZipHelper(); - $zipHelper->zipFolder($this->rootFolder, $this->getZipFilePath()); - } - - /** - * @return string Path of the zip file created from the root folder - */ - protected function getZipFilePath() - { - return $this->rootFolder . '.zip'; - } - - /** - * Streams the contents of the zip into the given stream - * - * @param resource $pointer Pointer to the stream to copy the zip - * @return void - */ - protected function copyZipToStream($pointer) - { - $zipFilePointer = fopen($this->getZipFilePath(), 'r'); - stream_copy_to_stream($zipFilePointer, $pointer); - fclose($zipFilePointer); + $this->deleteFile($zipHelper->getZipFilePath($this->rootFolder)); } } diff --git a/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php index 8a544f9..cc9573f 100644 --- a/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php +++ b/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php @@ -48,6 +48,7 @@ EOD; $header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>' . PHP_EOL; fwrite($this->sharedStringsFilePointer, $header); + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX(); } diff --git a/src/Spout/Writer/XLSX/Helper/StyleHelper.php b/src/Spout/Writer/XLSX/Helper/StyleHelper.php index 077b9af..06bbfc3 100644 --- a/src/Spout/Writer/XLSX/Helper/StyleHelper.php +++ b/src/Spout/Writer/XLSX/Helper/StyleHelper.php @@ -2,122 +2,20 @@ namespace Box\Spout\Writer\XLSX\Helper; +use Box\Spout\Writer\Common\Helper\AbstractStyleHelper; +use Box\Spout\Writer\Style\Color; + /** * Class StyleHelper * This class provides helper functions to manage styles * * @package Box\Spout\Writer\XLSX\Helper */ -class StyleHelper +class StyleHelper extends AbstractStyleHelper { - /** @var array [SERIALIZED_STYLE] => [STYLE_ID] mapping table, keeping track of the registered styles */ - protected $serializedStyleToStyleIdMappingTable = []; - - /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ - protected $styleIdToStyleMappingTable = []; - - /** - * @param \Box\Spout\Writer\Style\Style $defaultStyle - */ - public function __construct($defaultStyle) - { - // This ensures that the default style is the first one to be registered - $this->registerStyle($defaultStyle); - } - - /** - * Registers the given style as a used style. - * Duplicate styles won't be registered more than once. - * - * @param \Box\Spout\Writer\Style\Style $style The style to be registered - * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. - */ - public function registerStyle($style) - { - $serializedStyle = $style->serialize(); - - if (!$this->hasStyleAlreadyBeenRegistered($style)) { - $nextStyleId = count($this->serializedStyleToStyleIdMappingTable); - $style->setId($nextStyleId); - - $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; - $this->styleIdToStyleMappingTable[$nextStyleId] = $style; - } - - return $this->getStyleFromSerializedStyle($serializedStyle); - } - - /** - * Returns whether the given style has already been registered. - * - * @param \Box\Spout\Writer\Style\Style $style - * @return bool - */ - protected function hasStyleAlreadyBeenRegistered($style) - { - $serializedStyle = $style->serialize(); - return array_key_exists($serializedStyle, $this->serializedStyleToStyleIdMappingTable); - } - - /** - * Returns the registered style associated to the given serialization. - * - * @param string $serializedStyle The serialized style from which the actual style should be fetched from - * @return \Box\Spout\Writer\Style\Style - */ - protected function getStyleFromSerializedStyle($serializedStyle) - { - $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle]; - return $this->styleIdToStyleMappingTable[$styleId]; - } - - /** - * Apply additional styles if the given row needs it. - * Typically, set "wrap text" if a cell contains a new line. - * - * @param \Box\Spout\Writer\Style\Style $style The original style - * @param array $dataRow The row the style will be applied to - * @return \Box\Spout\Writer\Style\Style The updated style - */ - public function applyExtraStylesIfNeeded($style, $dataRow) - { - $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow); - - return $updatedStyle; - } - - /** - * Set the "wrap text" option if a cell of the given row contains a new line. - * - * @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines - * are ignored even when the "wrap text" option is set. This only occurs with - * inline strings (shared strings do work fine). - * A workaround would be to encode "\n" as "_x000D_" but it does not work - * on the Windows version of Excel... - * - * @param \Box\Spout\Writer\Style\Style $style The original style - * @param array $dataRow The row the style will be applied to - * @return \Box\Spout\Writer\Style\Style The eventually updated style - */ - protected function applyWrapTextIfCellContainsNewLine($style, $dataRow) - { - // if the "wrap text" option is already set, no-op - if ($style->shouldWrapText()) { - return $style; - } - - foreach ($dataRow as $cell) { - if (is_string($cell) && strpos($cell, "\n") !== false) { - $style->setShouldWrapText(); - break; - } - } - - return $style; - } - /** * Returns the content of the "styles.xml" file, given a list of styles. + * * @return string */ public function getStylesXMLFileContent() @@ -144,6 +42,7 @@ EOD; /** * Returns the content of the "" section. + * * @return string */ protected function getFontsSectionContent() @@ -151,11 +50,11 @@ EOD; $content = ' ' . PHP_EOL; /** @var \Box\Spout\Writer\Style\Style $style */ - foreach ($this->styleIdToStyleMappingTable as $style) { + foreach ($this->getRegisteredStyles() as $style) { $content .= ' ' . PHP_EOL; $content .= ' ' . PHP_EOL; - $content .= ' ' . PHP_EOL; + $content .= ' ' . PHP_EOL; $content .= ' ' . PHP_EOL; if ($style->isFontBold()) { @@ -234,14 +133,17 @@ EOD; /** * Returns the content of the "" section. + * * @return string */ protected function getCellXfsSectionContent() { - $content = ' ' . PHP_EOL; + $registeredStyles = $this->getRegisteredStyles(); - foreach ($this->styleIdToStyleMappingTable as $styleId => $style) { - $content .= ' ' . PHP_EOL; + + foreach ($registeredStyles as $style) { + $content .= ' shouldApplyFont()) { $content .= ' applyFont="1"'; diff --git a/src/Spout/Writer/XLSX/Internal/Workbook.php b/src/Spout/Writer/XLSX/Internal/Workbook.php index e1c1f08..5208d4f 100644 --- a/src/Spout/Writer/XLSX/Internal/Workbook.php +++ b/src/Spout/Writer/XLSX/Internal/Workbook.php @@ -2,20 +2,20 @@ namespace Box\Spout\Writer\XLSX\Internal; -use Box\Spout\Writer\Exception\SheetNotFoundException; +use Box\Spout\Writer\Common\Internal\AbstractWorkbook; use Box\Spout\Writer\XLSX\Helper\FileSystemHelper; use Box\Spout\Writer\XLSX\Helper\SharedStringsHelper; use Box\Spout\Writer\XLSX\Helper\StyleHelper; -use Box\Spout\Writer\XLSX\Sheet; +use Box\Spout\Writer\Common\Sheet; /** - * Class Book + * Class Workbook * Represents a workbook within a XLSX file. * It provides the functions to work with worksheets. * * @package Box\Spout\Writer\XLSX\Internal */ -class Workbook +class Workbook extends AbstractWorkbook { /** * Maximum number of rows a XLSX sheet can contain @@ -26,9 +26,6 @@ class Workbook /** @var bool Whether inline or shared strings should be used */ protected $shouldUseInlineStrings; - /** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */ - protected $shouldCreateNewSheetsAutomatically; - /** @var \Box\Spout\Writer\XLSX\Helper\FileSystemHelper Helper to perform file system operations */ protected $fileSystemHelper; @@ -38,12 +35,6 @@ class Workbook /** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles */ protected $styleHelper; - /** @var Worksheet[] Array containing the workbook's sheets */ - protected $worksheets = []; - - /** @var Worksheet The worksheet where data will be written to */ - protected $currentWorksheet; - /** * @param string $tempFolder * @param bool $shouldUseInlineStrings @@ -53,8 +44,9 @@ class Workbook */ public function __construct($tempFolder, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically, $defaultRowStyle) { + parent::__construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle); + $this->shouldUseInlineStrings = $shouldUseInlineStrings; - $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; $this->fileSystemHelper = new FileSystemHelper($tempFolder); $this->fileSystemHelper->createBaseFilesAndFolders(); @@ -66,6 +58,22 @@ class Workbook $this->sharedStringsHelper = new SharedStringsHelper($xlFolder); } + /** + * @return \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles to XLSX files + */ + protected function getStyleHelper() + { + return $this->styleHelper; + } + + /** + * @return int Maximum number of rows/columns a sheet can contain + */ + protected function getMaxRowsPerWorksheet() + { + return self::$maxRowsPerWorksheet; + } + /** * Creates a new sheet in the workbook. The current sheet remains unchanged. * @@ -84,131 +92,6 @@ class Workbook return $worksheet; } - /** - * 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). - * - * @return Worksheet The created sheet - * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing - */ - public function addNewSheetAndMakeItCurrent() - { - $worksheet = $this->addNewSheet(); - $this->setCurrentWorksheet($worksheet); - - return $worksheet; - } - - /** - * @return Worksheet[] All the workbook's sheets - */ - public function getWorksheets() - { - return $this->worksheets; - } - - /** - * Returns the current sheet - * - * @return Worksheet The current sheet - */ - public function getCurrentWorksheet() - { - return $this->currentWorksheet; - } - - /** - * 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). - * - * @param \Box\Spout\Writer\XLSX\Sheet $sheet The "external" sheet to set as current - * @return void - * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook - */ - public function setCurrentSheet($sheet) - { - $worksheet = $this->getWorksheetFromExternalSheet($sheet); - if ($worksheet !== null) { - $this->currentWorksheet = $worksheet; - } else { - throw new SheetNotFoundException('The given sheet does not exist in the workbook.'); - } - } - - /** - * @param Worksheet $worksheet - * @return void - */ - protected function setCurrentWorksheet($worksheet) - { - $this->currentWorksheet = $worksheet; - } - - /** - * Returns the worksheet associated to the given external sheet. - * - * @param \Box\Spout\Writer\XLSX\Sheet $sheet - * @return Worksheet|null The worksheet associated to the given external sheet or null if not found. - */ - protected function getWorksheetFromExternalSheet($sheet) - { - $worksheetFound = null; - - foreach ($this->worksheets as $worksheet) { - if ($worksheet->getExternalSheet() === $sheet) { - $worksheetFound = $worksheet; - break; - } - } - - return $worksheetFound; - } - - /** - * Adds data to the current sheet. - * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination - * with the creation of new worksheets if one worksheet has reached its maximum capicity. - * - * @param array $dataRow Array containing data to be written. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. - * @return void - * @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing - * @throws \Box\Spout\Writer\Exception\WriterException If unable to write data - */ - public function addRowToCurrentWorksheet($dataRow, $style) - { - $currentWorksheet = $this->getCurrentWorksheet(); - $hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows(); - - // if we reached the maximum number of rows for the current sheet... - if ($hasReachedMaxRows) { - // ... continue writing in a new sheet if option set - if ($this->shouldCreateNewSheetsAutomatically) { - $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); - - $updatedStyle = $this->styleHelper->applyExtraStylesIfNeeded($style, $dataRow); - $registeredStyle = $this->styleHelper->registerStyle($updatedStyle); - $currentWorksheet->addRow($dataRow, $registeredStyle); - } else { - // otherwise, do nothing as the data won't be read anyways - } - } else { - $updatedStyle = $this->styleHelper->applyExtraStylesIfNeeded($style, $dataRow); - $registeredStyle = $this->styleHelper->registerStyle($updatedStyle); - $currentWorksheet->addRow($dataRow, $registeredStyle); - } - } - - /** - * @return bool Whether the current worksheet has reached the maximum number of rows per sheet. - */ - protected function hasCurrentWorkseetReachedMaxRows() - { - $currentWorksheet = $this->getCurrentWorksheet(); - return ($currentWorksheet->getLastWrittenRowIndex() >= self::$maxRowsPerWorksheet); - } - /** * Closes the workbook and all its associated sheets. * All the necessary files are written to disk and zipped together to create the XLSX file. @@ -219,7 +102,10 @@ class Workbook */ public function close($finalFilePointer) { - foreach ($this->worksheets as $worksheet) { + /** @var Worksheet[] $worksheets */ + $worksheets = $this->worksheets; + + foreach ($worksheets as $worksheet) { $worksheet->close(); } @@ -227,9 +113,9 @@ class Workbook // Finish creating all the necessary files before zipping everything together $this->fileSystemHelper - ->createContentTypesFile($this->worksheets) - ->createWorkbookFile($this->worksheets) - ->createWorkbookRelsFile($this->worksheets) + ->createContentTypesFile($worksheets) + ->createWorkbookFile($worksheets) + ->createWorkbookRelsFile($worksheets) ->createStylesFile($this->styleHelper) ->zipRootFolderAndCopyToStream($finalFilePointer); diff --git a/src/Spout/Writer/XLSX/Internal/Worksheet.php b/src/Spout/Writer/XLSX/Internal/Worksheet.php index 04996bc..15a78d5 100644 --- a/src/Spout/Writer/XLSX/Internal/Worksheet.php +++ b/src/Spout/Writer/XLSX/Internal/Worksheet.php @@ -4,7 +4,8 @@ namespace Box\Spout\Writer\XLSX\Internal; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; -use Box\Spout\Writer\XLSX\Helper\CellHelper; +use Box\Spout\Writer\Common\Helper\CellHelper; +use Box\Spout\Writer\Common\Internal\WorksheetInterface; /** * Class Worksheet @@ -13,14 +14,14 @@ use Box\Spout\Writer\XLSX\Helper\CellHelper; * * @package Box\Spout\Writer\XLSX\Internal */ -class Worksheet +class Worksheet implements WorksheetInterface { const SHEET_XML_FILE_HEADER = << EOD; - /** @var \Box\Spout\Writer\XLSX\Sheet The "external" sheet */ + /** @var \Box\Spout\Writer\Common\Sheet The "external" sheet */ protected $externalSheet; /** @var string Path to the XML file that will contain the sheet data */ @@ -42,7 +43,7 @@ EOD; protected $lastWrittenRowIndex = 0; /** - * @param \Box\Spout\Writer\XLSX\Sheet $externalSheet The associated "external" sheet + * @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet * @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored * @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used @@ -54,6 +55,7 @@ EOD; $this->sharedStringsHelper = $sharedStringsHelper; $this->shouldUseInlineStrings = $shouldUseInlineStrings; + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX(); $this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml'; @@ -76,7 +78,20 @@ EOD; } /** - * @return \Box\Spout\Writer\XLSX\Sheet The "external" sheet + * Checks if the book has been created. Throws an exception if not created yet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function throwIfSheetFilePointerIsNotAvailable() + { + if (!$this->sheetFilePointer) { + throw new IOException('Unable to open sheet for writing.'); + } + } + + /** + * @return \Box\Spout\Writer\Common\Sheet The "external" sheet */ public function getExternalSheet() { @@ -100,19 +115,6 @@ EOD; return $this->externalSheet->getIndex() + 1; } - /** - * Checks if the book has been created. Throws an exception if not created yet. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - protected function throwIfSheetFilePointerIsNotAvailable() - { - if (!$this->sheetFilePointer) { - throw new IOException('Unable to open sheet for writing.'); - } - } - /** * Adds data to the worksheet. * diff --git a/src/Spout/Writer/XLSX/Writer.php b/src/Spout/Writer/XLSX/Writer.php index 1885ff7..b75628f 100644 --- a/src/Spout/Writer/XLSX/Writer.php +++ b/src/Spout/Writer/XLSX/Writer.php @@ -2,9 +2,7 @@ namespace Box\Spout\Writer\XLSX; -use Box\Spout\Writer\AbstractWriter; -use Box\Spout\Writer\Exception\WriterAlreadyOpenedException; -use Box\Spout\Writer\Exception\WriterNotOpenedException; +use Box\Spout\Writer\AbstractMultiSheetsWriter; use Box\Spout\Writer\Style\StyleBuilder; use Box\Spout\Writer\XLSX\Internal\Workbook; @@ -14,7 +12,7 @@ use Box\Spout\Writer\XLSX\Internal\Workbook; * * @package Box\Spout\Writer\XLSX */ -class Writer extends AbstractWriter +class Writer extends AbstractMultiSheetsWriter { /** Default style font values */ const DEFAULT_FONT_SIZE = 12; @@ -35,9 +33,6 @@ class Writer extends AbstractWriter /** @var Internal\Workbook The workbook for the XLSX file */ protected $book; - /** @var int */ - protected $highestRowIndex = 0; - /** * Sets a custom temporary folder for creating intermediate files/folders. * This must be set before opening the writer. @@ -48,7 +43,7 @@ class Writer extends AbstractWriter */ public function setTempFolder($tempFolder) { - $this->throwIfWriterAlreadyOpened(); + $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->tempFolder = $tempFolder; return $this; @@ -64,7 +59,7 @@ class Writer extends AbstractWriter */ public function setShouldUseInlineStrings($shouldUseInlineStrings) { - $this->throwIfWriterAlreadyOpened(); + $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->shouldUseInlineStrings = $shouldUseInlineStrings; return $this; @@ -80,26 +75,12 @@ class Writer extends AbstractWriter */ public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically) { - $this->throwIfWriterAlreadyOpened(); + $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; return $this; } - /** - * Checks if the writer has already been opened, since some actions must be done before it gets opened. - * Throws an exception if already opened. - * - * @return void - * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be. - */ - protected function throwIfWriterAlreadyOpened() - { - if ($this->isWriterOpened) { - throw new WriterAlreadyOpenedException('Writer must be configured before opening it.'); - } - } - /** * Configures the write and sets the current sheet pointer to a new sheet. * @@ -116,78 +97,11 @@ class Writer extends AbstractWriter } /** - * Returns all the workbook's sheets - * - * @return Sheet[] All the workbook's sheets - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + * @return Internal\Workbook The workbook representing the file to be written */ - public function getSheets() + protected function getWorkbook() { - $this->throwIfBookIsNotAvailable(); - - $externalSheets = []; - $worksheets = $this->book->getWorksheets(); - - /** @var Internal\Worksheet $worksheet */ - foreach ($worksheets as $worksheet) { - $externalSheets[] = $worksheet->getExternalSheet(); - } - - return $externalSheets; - } - - /** - * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. - * - * @return Sheet The created sheet - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet - */ - public function addNewSheetAndMakeItCurrent() - { - $this->throwIfBookIsNotAvailable(); - $worksheet = $this->book->addNewSheetAndMakeItCurrent(); - - return $worksheet->getExternalSheet(); - } - - /** - * Returns the current sheet - * - * @return Sheet The current sheet - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet - */ - public function getCurrentSheet() - { - $this->throwIfBookIsNotAvailable(); - return $this->book->getCurrentWorksheet()->getExternalSheet(); - } - - /** - * 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). - * - * @param Sheet $sheet The sheet to set as current - * @return void - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet - * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook - */ - public function setCurrentSheet($sheet) - { - $this->throwIfBookIsNotAvailable(); - $this->book->setCurrentSheet($sheet); - } - - /** - * Checks if the book has been created. Throws an exception if not created yet. - * - * @return void - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet - */ - protected function throwIfBookIsNotAvailable() - { - if (!$this->book) { - throw new WriterNotOpenedException('The writer must be opened before performing this action.'); - } + return $this->book; } /** diff --git a/tests/Spout/Common/Escaper/XLSXTest.php b/tests/Spout/Common/Escaper/XLSXTest.php index ba3982a..7c0b2db 100644 --- a/tests/Spout/Common/Escaper/XLSXTest.php +++ b/tests/Spout/Common/Escaper/XLSXTest.php @@ -34,6 +34,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase */ public function testEscape($stringToEscape, $expectedEscapedString) { + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = new \Box\Spout\Common\Escaper\XLSX(); $escapedString = $escaper->escape($stringToEscape); @@ -65,6 +66,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase */ public function testUnescape($stringToUnescape, $expectedUnescapedString) { + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = new \Box\Spout\Common\Escaper\XLSX(); $unescapedString = $escaper->unescape($stringToUnescape); diff --git a/tests/Spout/Writer/XLSX/Helper/CellHelperTest.php b/tests/Spout/Writer/Common/Helper/CellHelperTest.php similarity index 97% rename from tests/Spout/Writer/XLSX/Helper/CellHelperTest.php rename to tests/Spout/Writer/Common/Helper/CellHelperTest.php index f46b1c6..2abb4cc 100644 --- a/tests/Spout/Writer/XLSX/Helper/CellHelperTest.php +++ b/tests/Spout/Writer/Common/Helper/CellHelperTest.php @@ -1,11 +1,11 @@ assertEquals('Sheet1', $sheets[0]->getName(), 'Invalid name for the first sheet'); + $this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet'); + } + + /** + * @return void + */ + public function testSetSheetNameShouldCreateSheetWithCustomName() + { + $customSheetName = 'CustomName'; + $sheet = new Sheet(0); + $sheet->setName($customSheetName); + + $this->assertEquals($customSheetName, $sheet->getName(), "The sheet name should have been changed to '$customSheetName'"); + } + + /** + * @return array + */ + public function dataProviderForInvalidSheetNames() + { + return [ + [null], + [21], + [''], + ['this title exceeds the 31 characters limit'], + ['Illegal \\'], + ['Illegal /'], + ['Illegal ?'], + ['Illegal *'], + ['Illegal :'], + ['Illegal ['], + ['Illegal ]'], + ['\'Illegal start'], + ['Illegal end\''], + ]; + } + + /** + * @dataProvider dataProviderForInvalidSheetNames + * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException + * + * @param string $customSheetName + * @return void + */ + public function testSetSheetNameShouldThrowOnInvalidName($customSheetName) + { + (new Sheet(0))->setName($customSheetName); + } + + /** + * @return void + */ + public function testSetSheetNameShouldNotThrowWhenSettingSameNameAsCurrentOne() + { + $customSheetName = 'Sheet name'; + $sheet = new Sheet(0); + $sheet->setName($customSheetName); + $sheet->setName($customSheetName); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException + * @return void + */ + public function testSetSheetNameShouldThrowWhenNameIsAlreadyUsed() + { + $customSheetName = 'Sheet name'; + + $sheet = new Sheet(0); + $sheet->setName($customSheetName); + + $sheet = new Sheet(1); + $sheet->setName($customSheetName); + } +} diff --git a/tests/Spout/Writer/ODS/Helper/StyleHelperTest.php b/tests/Spout/Writer/ODS/Helper/StyleHelperTest.php new file mode 100644 index 0000000..763f904 --- /dev/null +++ b/tests/Spout/Writer/ODS/Helper/StyleHelperTest.php @@ -0,0 +1,89 @@ +defaultStyle = (new StyleBuilder())->build(); + } + + /** + * @return void + */ + public function testRegisterStyleShouldUpdateId() + { + $style1 = (new StyleBuilder())->setFontBold()->build(); + $style2 = (new StyleBuilder())->setFontUnderline()->build(); + + $this->assertEquals(0, $this->defaultStyle->getId(), 'Default style ID should be 0'); + $this->assertNull($style1->getId()); + $this->assertNull($style2->getId()); + + $styleHelper = new StyleHelper($this->defaultStyle); + $registeredStyle1 = $styleHelper->registerStyle($style1); + $registeredStyle2 = $styleHelper->registerStyle($style2); + + $this->assertEquals(1, $registeredStyle1->getId()); + $this->assertEquals(2, $registeredStyle2->getId()); + } + + /** + * @return void + */ + public function testRegisterStyleShouldReuseAlreadyRegisteredStyles() + { + $style = (new StyleBuilder())->setFontBold()->build(); + + $styleHelper = new StyleHelper($this->defaultStyle); + $registeredStyle1 = $styleHelper->registerStyle($style); + $registeredStyle2 = $styleHelper->registerStyle($style); + + $this->assertEquals(1, $registeredStyle1->getId()); + $this->assertEquals(1, $registeredStyle2->getId()); + } + + /** + * @return void + */ + public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine() + { + $style = clone $this->defaultStyle; + $styleHelper = new StyleHelper($this->defaultStyle); + + $this->assertFalse($style->shouldWrapText()); + + $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, [12, 'single line', "multi\nlines", null]); + + $this->assertTrue($updatedStyle->shouldWrapText()); + } + + /** + * @return void + */ + public function testApplyExtraStylesIfNeededShouldDoNothingIfWrapTextAlreadyApplied() + { + $style = (new StyleBuilder())->setShouldWrapText()->build(); + $styleHelper = new StyleHelper($this->defaultStyle); + + $this->assertTrue($style->shouldWrapText()); + + $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, ["multi\nlines"]); + + $this->assertTrue($updatedStyle->shouldWrapText()); + } +} diff --git a/tests/Spout/Writer/ODS/SheetTest.php b/tests/Spout/Writer/ODS/SheetTest.php new file mode 100644 index 0000000..ee59501 --- /dev/null +++ b/tests/Spout/Writer/ODS/SheetTest.php @@ -0,0 +1,134 @@ +writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_index.ods'); + + $this->assertEquals(2, count($sheets), '2 sheets should have been created'); + $this->assertEquals(0, $sheets[0]->getIndex(), 'The first sheet should be index 0'); + $this->assertEquals(1, $sheets[1]->getIndex(), 'The second sheet should be index 1'); + } + + /** + * @return void + */ + public function testGetSheetName() + { + $sheets = $this->writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_name.ods'); + + $this->assertEquals(2, count($sheets), '2 sheets should have been created'); + $this->assertEquals('Sheet1', $sheets[0]->getName(), 'Invalid name for the first sheet'); + $this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet'); + } + + /** + * @return void + */ + public function testSetSheetNameShouldCreateSheetWithCustomName() + { + $fileName = 'test_set_name_should_create_sheet_with_custom_name.ods'; + $customSheetName = 'CustomName'; + $this->writeDataAndReturnSheetWithCustomName($fileName, $customSheetName); + + $this->assertSheetNameEquals($customSheetName, $fileName, "The sheet name should have been changed to '$customSheetName'"); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException + * @return void + */ + public function testSetSheetNameShouldThrowWhenNameIsAlreadyUsed() + { + $fileName = 'test_set_name_with_non_unique_name.ods'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + + $customSheetName = 'Sheet name'; + + $sheet = $writer->getCurrentSheet(); + $sheet->setName($customSheetName); + + $writer->addNewSheetAndMakeItCurrent(); + $sheet = $writer->getCurrentSheet(); + $sheet->setName($customSheetName); + } + + /** + * @param string $fileName + * @param string $sheetName + * @return Sheet + */ + private function writeDataAndReturnSheetWithCustomName($fileName, $sheetName) + { + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + + $sheet = $writer->getCurrentSheet(); + $sheet->setName($sheetName); + + $writer->addRow(['ods--11', 'ods--12']); + $writer->close(); + } + + /** + * @param string $fileName + * @return Sheet[] + */ + private function writeDataToMulitpleSheetsAndReturnSheets($fileName) + { + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + + $writer->addRow(['ods--sheet1--11', 'ods--sheet1--12']); + $writer->addNewSheetAndMakeItCurrent(); + $writer->addRow(['ods--sheet2--11', 'ods--sheet2--12', 'ods--sheet2--13']); + + $writer->close(); + + return $writer->getSheets(); + } + + /** + * @param string $expectedName + * @param string $fileName + * @param string $message + * @return void + */ + private function assertSheetNameEquals($expectedName, $fileName, $message = '') + { + $resourcePath = $this->getGeneratedResourcePath($fileName); + $pathToWorkbookFile = $resourcePath . '#content.xml'; + $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); + + $this->assertContains("table:name=\"$expectedName\"", $xmlContents, $message); + } +} diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php new file mode 100644 index 0000000..15d8d09 --- /dev/null +++ b/tests/Spout/Writer/ODS/WriterTest.php @@ -0,0 +1,446 @@ +createUnwritableFolderIfNeeded($fileName); + $filePath = $this->getGeneratedUnwritableResourcePath($fileName); + + $writer = WriterFactory::create(Type::ODS); + @$writer->openToFile($filePath); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException + */ + public function testAddRowShouldThrowExceptionIfCallAddRowBeforeOpeningWriter() + { + $writer = WriterFactory::create(Type::ODS); + $writer->addRow(['ods--11', 'ods--12']); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException + */ + public function testAddRowShouldThrowExceptionIfCalledBeforeOpeningWriter() + { + $writer = WriterFactory::create(Type::ODS); + $writer->addRows([['ods--11', 'ods--12']]); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException + */ + public function testSetTempFolderShouldThrowExceptionIfCalledAfterOpeningWriter() + { + $fileName = 'file_that_wont_be_written.ods'; + $filePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($filePath); + + $writer->setTempFolder(''); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException + */ + public function testsetShouldCreateNewSheetsAutomaticallyShouldThrowExceptionIfCalledAfterOpeningWriter() + { + $fileName = 'file_that_wont_be_written.ods'; + $filePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($filePath); + + $writer->setShouldCreateNewSheetsAutomatically(true); + } + + /** + * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException + */ + public function testAddRowShouldThrowExceptionIfUnsupportedDataTypePassedIn() + { + $fileName = 'test_add_row_should_throw_exception_if_unsupported_data_type_passed_in.ods'; + $dataRows = [ + [new \stdClass()], + ]; + + $this->writeToODSFile($dataRows, $fileName); + } + + /** + * @return void + */ + public function testAddNewSheetAndMakeItCurrent() + { + $fileName = 'test_add_new_sheet_and_make_it_current.ods'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + $writer->addNewSheetAndMakeItCurrent(); + $writer->close(); + + $sheets = $writer->getSheets(); + $this->assertEquals(2, count($sheets), 'There should be 2 sheets'); + $this->assertEquals($sheets[1], $writer->getCurrentSheet(), 'The current sheet should be the second one.'); + } + + /** + * @return void + */ + public function testSetCurrentSheet() + { + $fileName = 'test_set_current_sheet.ods'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + + $writer->addNewSheetAndMakeItCurrent(); + $writer->addNewSheetAndMakeItCurrent(); + + $firstSheet = $writer->getSheets()[0]; + $writer->setCurrentSheet($firstSheet); + + $writer->close(); + + $this->assertEquals($firstSheet, $writer->getCurrentSheet(), 'The current sheet should be the first one.'); + } + + /** + * @return void + */ + public function testAddRowShouldWriteGivenDataToSheet() + { + $fileName = 'test_add_row_should_write_given_data_to_sheet.ods'; + $dataRows = [ + ['ods--11', 'ods--12'], + ['ods--21', 'ods--22', 'ods--23'], + ]; + + $this->writeToODSFile($dataRows, $fileName); + + foreach ($dataRows as $dataRow) { + foreach ($dataRow as $cellValue) { + $this->assertValueWasWritten($fileName, $cellValue); + } + } + } + + /** + * @return void + */ + public function testAddRowShouldWriteGivenDataToTwoSheets() + { + $fileName = 'test_add_row_should_write_given_data_to_two_sheets.ods'; + $dataRows = [ + ['ods--11', 'ods--12'], + ['ods--21', 'ods--22', 'ods--23'], + ]; + + $numSheets = 2; + $this->writeToMultipleSheetsInODSFile($dataRows, $numSheets, $fileName); + + for ($i = 1; $i <= $numSheets; $i++) { + foreach ($dataRows as $dataRow) { + foreach ($dataRow as $cellValue) { + $this->assertValueWasWritten($fileName, $cellValue); + } + } + } + } + + /** + * @return void + */ + public function testAddRowShouldSupportMultipleTypesOfData() + { + $fileName = 'test_add_row_should_support_multiple_types_of_data.ods'; + $dataRows = [ + ['ods--11', true, '', 0, 10.2, null], + ]; + + $this->writeToODSFile($dataRows, $fileName); + + $this->assertValueWasWritten($fileName, 'ods--11'); + $this->assertValueWasWrittenToSheet($fileName, 1, 1); // true is converted to 1 + $this->assertValueWasWrittenToSheet($fileName, 1, 0); + $this->assertValueWasWrittenToSheet($fileName, 1, 10.2); + } + + /** + * @return void + */ + public function testAddRowShouldWriteGivenDataToTheCorrectSheet() + { + $fileName = 'test_add_row_should_write_given_data_to_the_correct_sheet.ods'; + $dataRowsSheet1 = [ + ['ods--sheet1--11', 'ods--sheet1--12'], + ['ods--sheet1--21', 'ods--sheet1--22', 'ods--sheet1--23'], + ]; + $dataRowsSheet2 = [ + ['ods--sheet2--11', 'ods--sheet2--12'], + ['ods--sheet2--21', 'ods--sheet2--22', 'ods--sheet2--23'], + ]; + $dataRowsSheet1Again = [ + ['ods--sheet1--31', 'ods--sheet1--32'], + ['ods--sheet1--41', 'ods--sheet1--42', 'ods--sheet1--43'], + ]; + + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + + $writer->addRows($dataRowsSheet1); + + $writer->addNewSheetAndMakeItCurrent(); + $writer->addRows($dataRowsSheet2); + + $firstSheet = $writer->getSheets()[0]; + $writer->setCurrentSheet($firstSheet); + + $writer->addRows($dataRowsSheet1Again); + + $writer->close(); + + foreach ($dataRowsSheet1 as $dataRow) { + foreach ($dataRow as $cellValue) { + $this->assertValueWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1'); + } + } + foreach ($dataRowsSheet2 as $dataRow) { + foreach ($dataRow as $cellValue) { + $this->assertValueWasWrittenToSheet($fileName, 2, $cellValue, 'Data should have been written in Sheet 2'); + } + } + foreach ($dataRowsSheet1Again as $dataRow) { + foreach ($dataRow as $cellValue) { + $this->assertValueWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1'); + } + } + } + + /** + * @return void + */ + public function testAddRowShouldAutomaticallyCreateNewSheetsIfMaxRowsReachedAndOptionTurnedOn() + { + $fileName = 'test_add_row_should_automatically_create_new_sheets_if_max_rows_reached_and_option_turned_on.ods'; + $dataRows = [ + ['ods--sheet1--11', 'ods--sheet1--12'], + ['ods--sheet1--21', 'ods--sheet1--22', 'ods--sheet1--23'], + ['ods--sheet2--11', 'ods--sheet2--12'], // this should be written in a new sheet + ]; + + // set the maxRowsPerSheet limit to 2 + \ReflectionHelper::setStaticValue('\Box\Spout\Writer\ODS\Internal\Workbook', 'maxRowsPerWorksheet', 2); + + $writer = $this->writeToODSFile($dataRows, $fileName, $shouldCreateSheetsAutomatically = true); + $this->assertEquals(2, count($writer->getSheets()), '2 sheets should have been created.'); + + $this->assertValueWasNotWrittenToSheet($fileName, 1, 'ods--sheet2--11'); + $this->assertValueWasWrittenToSheet($fileName, 2, 'ods--sheet2--11'); + + \ReflectionHelper::reset(); + } + + /** + * @return void + */ + public function testAddRowShouldNotCreateNewSheetsIfMaxRowsReachedAndOptionTurnedOff() + { + $fileName = 'test_add_row_should_not_create_new_sheets_if_max_rows_reached_and_option_turned_off.ods'; + $dataRows = [ + ['ods--sheet1--11', 'ods--sheet1--12'], + ['ods--sheet1--21', 'ods--sheet1--22', 'ods--sheet1--23'], + ['ods--sheet1--31', 'ods--sheet1--32'], // this should NOT be written in a new sheet + ]; + + // set the maxRowsPerSheet limit to 2 + \ReflectionHelper::setStaticValue('\Box\Spout\Writer\ODS\Internal\Workbook', 'maxRowsPerWorksheet', 2); + + $writer = $this->writeToODSFile($dataRows, $fileName, $shouldCreateSheetsAutomatically = false); + $this->assertEquals(1, count($writer->getSheets()), 'Only 1 sheet should have been created.'); + + $this->assertValueWasNotWrittenToSheet($fileName, 1, 'ods--sheet1--31'); + + \ReflectionHelper::reset(); + } + + /** + * @return void + */ + public function testAddRowShouldEscapeHtmlSpecialCharacters() + { + $fileName = 'test_add_row_should_escape_html_special_characters.ods'; + $dataRows = [ + ['I\'m in "great" mood', 'This be escaped & tested'], + ]; + + $this->writeToODSFile($dataRows, $fileName); + + $this->assertValueWasWritten($fileName, 'I'm in "great" mood', 'Quotes should be escaped'); + $this->assertValueWasWritten($fileName, 'This <must> be escaped & tested', '<, > and & should be escaped'); + } + + /** + * @return void + */ + public function testAddRowShouldKeepNewLines() + { + $fileName = 'test_add_row_should_keep_new_lines.ods'; + $dataRow = ["I have\na dream"]; + + $this->writeToODSFile([$dataRow], $fileName); + + $this->assertValueWasWrittenToSheet($fileName, 1, 'I have'); + $this->assertValueWasWrittenToSheet($fileName, 1, 'a dream'); + } + + /** + * @param array $allRows + * @param string $fileName + * @param bool $shouldCreateSheetsAutomatically + * @return Writer + */ + private function writeToODSFile($allRows, $fileName, $shouldCreateSheetsAutomatically = true) + { + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + $writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically); + + $writer->openToFile($resourcePath); + $writer->addRows($allRows); + $writer->close(); + + return $writer; + } + + /** + * @param array $allRows + * @param int $numSheets + * @param string $fileName + * @param bool $shouldCreateSheetsAutomatically + * @return Writer + */ + private function writeToMultipleSheetsInODSFile($allRows, $numSheets, $fileName, $shouldCreateSheetsAutomatically = true) + { + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + $writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically); + + $writer->openToFile($resourcePath); + $writer->addRows($allRows); + + for ($i = 1; $i < $numSheets; $i++) { + $writer->addNewSheetAndMakeItCurrent(); + $writer->addRows($allRows); + } + + $writer->close(); + + return $writer; + } + + /** + * @param string $fileName + * @param string $value + * @param string $message + * @return void + */ + private function assertValueWasWritten($fileName, $value, $message = '') + { + $resourcePath = $this->getGeneratedResourcePath($fileName); + $pathToContentFile = $resourcePath . '#content.xml'; + $xmlContents = file_get_contents('zip://' . $pathToContentFile); + + $this->assertContains($value, $xmlContents, $message); + } + + /** + * @param string $fileName + * @param int $sheetIndex + * @param mixed $value + * @param string $message + * @return void + */ + private function assertValueWasWrittenToSheet($fileName, $sheetIndex, $value, $message = '') + { + $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); + $valueAsXmlString = "$value"; + + $this->assertContains($valueAsXmlString, $sheetXmlAsString, $message); + } + + /** + * @param string $fileName + * @param int $sheetIndex + * @param mixed $value + * @param string $message + * @return void + */ + private function assertValueWasNotWrittenToSheet($fileName, $sheetIndex, $value, $message = '') + { + $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); + $valueAsXmlString = "$value"; + + $this->assertNotContains($valueAsXmlString, $sheetXmlAsString, $message); + } + + /** + * @param string $fileName + * @param int $sheetIndex + * @return string + */ + private function getSheetXmlNodeAsString($fileName, $sheetIndex) + { + $resourcePath = $this->getGeneratedResourcePath($fileName); + $pathToSheetFile = $resourcePath . '#content.xml'; + + $xmlReader = new XMLReader(); + $xmlReader->open('zip://' . $pathToSheetFile); + $xmlReader->readUntilNodeFound('table:table'); + + for ($i = 1; $i < $sheetIndex; $i++) { + $xmlReader->readUntilNodeFound('table:table'); + } + + return $xmlReader->readOuterXml(); + } +} diff --git a/tests/Spout/Writer/ODS/WriterWithStyleTest.php b/tests/Spout/Writer/ODS/WriterWithStyleTest.php new file mode 100644 index 0000000..1cfca7a --- /dev/null +++ b/tests/Spout/Writer/ODS/WriterWithStyleTest.php @@ -0,0 +1,364 @@ +defaultStyle = (new StyleBuilder())->build(); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException + */ + public function testAddRowWithStyleShouldThrowExceptionIfCallAddRowBeforeOpeningWriter() + { + $writer = WriterFactory::create(Type::ODS); + $writer->addRowWithStyle(['ods--11', 'ods--12'], $this->defaultStyle); + } + + /** + * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException + */ + public function testAddRowWithStyleShouldThrowExceptionIfCalledBeforeOpeningWriter() + { + $writer = WriterFactory::create(Type::ODS); + $writer->addRowWithStyle(['ods--11', 'ods--12'], $this->defaultStyle); + } + + /** + * @return array + */ + public function dataProviderForInvalidStyle() + { + return [ + ['style'], + [new \stdClass()], + [null], + ]; + } + + /** + * @dataProvider dataProviderForInvalidStyle + * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException + * + * @param \Box\Spout\Writer\Style\Style $style + */ + public function testAddRowWithStyleShouldThrowExceptionIfInvalidStyleGiven($style) + { + $fileName = 'test_add_row_with_style_should_throw_exception.ods'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + $writer->addRowWithStyle(['ods--11', 'ods--12'], $style); + } + + /** + * @dataProvider dataProviderForInvalidStyle + * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException + * + * @param \Box\Spout\Writer\Style\Style $style + */ + public function testAddRowsWithStyleShouldThrowExceptionIfInvalidStyleGiven($style) + { + $fileName = 'test_add_row_with_style_should_throw_exception.ods'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $writer = WriterFactory::create(Type::ODS); + $writer->openToFile($resourcePath); + $writer->addRowsWithStyle([['ods--11', 'ods--12']], $style); + } + + /** + * @return void + */ + public function testAddRowWithStyleShouldListAllUsedStylesInCreatedContentXmlFile() + { + $fileName = 'test_add_row_with_style_should_list_all_used_fonts.ods'; + $dataRows = [ + ['ods--11', 'ods--12'], + ['ods--21', 'ods--22'], + ]; + + $style = (new StyleBuilder()) + ->setFontBold() + ->setFontItalic() + ->setFontUnderline() + ->setFontStrikethrough() + ->build(); + $style2 = (new StyleBuilder()) + ->setFontSize(15) + ->setFontColor(Color::RED) + ->setFontName('Font') + ->build(); + + $this->writeToODSFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]); + + $cellStyleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); + $this->assertEquals(3, count($cellStyleElements), 'There should be 3 separate cell styles, including the default one.'); + + // Second font should contain data from the first created style + $customFont1Element = $cellStyleElements[1]; + $this->assertFirstChildHasAttributeEquals('bold', $customFont1Element, 'text-properties', 'fo:font-weight'); + $this->assertFirstChildHasAttributeEquals('italic', $customFont1Element, 'text-properties', 'fo:font-style'); + $this->assertFirstChildHasAttributeEquals('solid', $customFont1Element, 'text-properties', 'style:text-underline-style'); + $this->assertFirstChildHasAttributeEquals('solid', $customFont1Element, 'text-properties', 'style:text-line-through-style'); + + // Third font should contain data from the second created style + $customFont2Element = $cellStyleElements[2]; + $this->assertFirstChildHasAttributeEquals('15pt', $customFont2Element, 'text-properties', 'fo:font-size'); + $this->assertFirstChildHasAttributeEquals('#' . Color::RED, $customFont2Element, 'text-properties', 'fo:color'); + $this->assertFirstChildHasAttributeEquals('Font', $customFont2Element, 'text-properties', 'style:font-name'); + } + + /** + * @return void + */ + public function testAddRowWithStyleShouldWriteDefaultStyleSettings() + { + $fileName = 'test_add_row_with_style_should_write_default_style_settings.ods'; + $dataRow = ['ods--11', 'ods--12']; + + $this->writeToODSFile([$dataRow], $fileName, $this->defaultStyle); + + $textPropertiesElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'style:text-properties'); + $this->assertEquals(Style::DEFAULT_FONT_SIZE . 'pt', $textPropertiesElement->getAttribute('fo:font-size')); + $this->assertEquals('#' . Style::DEFAULT_FONT_COLOR, $textPropertiesElement->getAttribute('fo:color')); + $this->assertEquals(Style::DEFAULT_FONT_NAME, $textPropertiesElement->getAttribute('style:font-name')); + } + + /** + * @return void + */ + public function testAddRowWithStyleShouldApplyStyleToCells() + { + $fileName = 'test_add_row_with_style_should_apply_style_to_cells.ods'; + $dataRows = [ + ['ods--11'], + ['ods--21'], + ['ods--31'], + ]; + $style = (new StyleBuilder())->setFontBold()->build(); + $style2 = (new StyleBuilder())->setFontSize(15)->build(); + + $this->writeToODSFileWithMultipleStyles($dataRows, $fileName, [$style, $style2, null]); + + $cellDomElements = $this->getCellElementsFromContentXmlFile($fileName); + $this->assertEquals(3, count($cellDomElements), 'There should be 3 cells with content'); + + $this->assertEquals('ce2', $cellDomElements[0]->getAttribute('table:style-name')); + $this->assertEquals('ce3', $cellDomElements[1]->getAttribute('table:style-name')); + $this->assertEquals('ce1', $cellDomElements[2]->getAttribute('table:style-name')); + } + + /** + * @return void + */ + public function testAddRowWithStyleShouldReuseDuplicateStyles() + { + $fileName = 'test_add_row_with_style_should_reuse_duplicate_styles.ods'; + $dataRows = [ + ['ods--11'], + ['ods--21'], + ]; + $style = (new StyleBuilder())->setFontBold()->build(); + + $this->writeToODSFile($dataRows, $fileName, $style); + + $cellDomElements = $this->getCellElementsFromContentXmlFile($fileName); + $this->assertEquals(2, count($cellDomElements), 'There should be 2 cells with content'); + + $this->assertEquals('ce2', $cellDomElements[0]->getAttribute('table:style-name')); + $this->assertEquals('ce2', $cellDomElements[1]->getAttribute('table:style-name')); + } + + /** + * @return void + */ + public function testAddRowWithStyleShouldAddWrapTextAlignmentInfoInStylesXmlFileIfSpecified() + { + $fileName = 'test_add_row_with_style_should_add_wrap_text_alignment.ods'; + $dataRows = [ + ['ods--11', 'ods--12'], + ]; + $style = (new StyleBuilder())->setShouldWrapText()->build(); + + $this->writeToODSFile($dataRows, $fileName,$style); + + $styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); + $this->assertEquals(2, count($styleElements), 'There should be 2 styles (default and custom)'); + + $customStyleElement = $styleElements[1]; + $this->assertFirstChildHasAttributeEquals('wrap', $customStyleElement, 'table-cell-properties', 'fo:wrap-option'); + } + + /** + * @return void + */ + public function testAddRowWithStyleShouldApplyWrapTextIfCellContainsNewLine() + { + $fileName = 'test_add_row_with_style_should_apply_wrap_text_if_new_lines.ods'; + $dataRows = [ + ["ods--11\nods--11"], + ]; + + $this->writeToODSFile($dataRows, $fileName, $this->defaultStyle); + + $styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); + $this->assertEquals(2, count($styleElements), 'There should be 2 styles (default and custom)'); + + $customStyleElement = $styleElements[1]; + $this->assertFirstChildHasAttributeEquals('wrap', $customStyleElement, 'table-cell-properties', 'fo:wrap-option'); + } + + /** + * @param array $allRows + * @param string $fileName + * @param \Box\Spout\Writer\Style\Style $style + * @return Writer + */ + private function writeToODSFile($allRows, $fileName, $style) + { + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + + $writer->openToFile($resourcePath); + $writer->addRowsWithStyle($allRows, $style); + $writer->close(); + + return $writer; + } + + /** + * @param array $allRows + * @param string $fileName + * @param \Box\Spout\Writer\Style\Style|null[] $styles + * @return Writer + */ + private function writeToODSFileWithMultipleStyles($allRows, $fileName, $styles) + { + // there should be as many rows as there are styles passed in + $this->assertEquals(count($allRows), count($styles)); + + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\ODS\Writer $writer */ + $writer = WriterFactory::create(Type::ODS); + + $writer->openToFile($resourcePath); + for ($i = 0; $i < count($allRows); $i++) { + if ($styles[$i] === null) { + $writer->addRow($allRows[$i]); + } else { + $writer->addRowWithStyle($allRows[$i], $styles[$i]); + } + } + $writer->close(); + + return $writer; + } + + /** + * @param string $fileName + * @return \DOMNode[] + */ + private function getCellElementsFromContentXmlFile($fileName) + { + $cellElements = []; + + $resourcePath = $this->getGeneratedResourcePath($fileName); + $pathToStylesXmlFile = $resourcePath . '#content.xml'; + + $xmlReader = new \XMLReader(); + $xmlReader->open('zip://' . $pathToStylesXmlFile); + + while ($xmlReader->read()) { + if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'table:table-cell' && $xmlReader->getAttribute('office:value-type') !== null) { + $cellElements[] = $xmlReader->expand(); + } + } + + return $cellElements; + } + + /** + * @param string $fileName + * @return \DOMNode[] + */ + private function getCellStyleElementsFromContentXmlFile($fileName) + { + $cellStyleElements = []; + + $resourcePath = $this->getGeneratedResourcePath($fileName); + $pathToStylesXmlFile = $resourcePath . '#content.xml'; + + $xmlReader = new \XMLReader(); + $xmlReader->open('zip://' . $pathToStylesXmlFile); + + while ($xmlReader->read()) { + if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'style:style' && $xmlReader->getAttribute('style:family') === 'table-cell') { + $cellStyleElements[] = $xmlReader->expand(); + } + } + + return $cellStyleElements; + } + + /** + * @param string $fileName + * @param string $section + * @return \DomElement + */ + private function getXmlSectionFromStylesXmlFile($fileName, $section) + { + $resourcePath = $this->getGeneratedResourcePath($fileName); + $pathToStylesXmlFile = $resourcePath . '#styles.xml'; + + $xmlReader = new XMLReader(); + $xmlReader->open('zip://' . $pathToStylesXmlFile); + $xmlReader->readUntilNodeFound($section); + + return $xmlReader->expand(); + } + + /** + * @param string $expectedValue + * @param \DOMNode $parentElement + * @param string $childTagName + * @param string $attributeName + * @return void + */ + private function assertFirstChildHasAttributeEquals($expectedValue, $parentElement, $childTagName, $attributeName) + { + $this->assertEquals($expectedValue, $parentElement->getElementsByTagName($childTagName)->item(0)->getAttribute($attributeName)); + } +} diff --git a/tests/Spout/Writer/Style/ColorTest.php b/tests/Spout/Writer/Style/ColorTest.php index 6824d8d..7dd4587 100644 --- a/tests/Spout/Writer/Style/ColorTest.php +++ b/tests/Spout/Writer/Style/ColorTest.php @@ -28,13 +28,13 @@ class ColorTest extends \PHPUnit_Framework_TestCase [0, 112, 192, Color::BLUE], [0, 32, 96, Color::DARK_BLUE], [112, 48, 160, Color::PURPLE], - [0, 0, 0, 'FF000000'], - [255, 255, 255, 'FFFFFFFF'], - [255, 0, 0, 'FFFF0000'], - [0, 128, 0, 'FF008000'], - [0, 255, 0, 'FF00FF00'], - [0, 0, 255, 'FF0000FF'], - [128, 22, 43, 'FF80162B'], + [0, 0, 0, '000000'], + [255, 255, 255, 'FFFFFF'], + [255, 0, 0, 'FF0000'], + [0, 128, 0, '008000'], + [0, 255, 0, '00FF00'], + [0, 0, 255, '0000FF'], + [128, 22, 43, '80162B'], ]; } diff --git a/tests/Spout/Writer/XLSX/SheetTest.php b/tests/Spout/Writer/XLSX/SheetTest.php index 9be8254..58c4b05 100644 --- a/tests/Spout/Writer/XLSX/SheetTest.php +++ b/tests/Spout/Writer/XLSX/SheetTest.php @@ -4,6 +4,7 @@ namespace Box\Spout\Writer\XLSX; use Box\Spout\Common\Type; use Box\Spout\TestUsingResource; +use Box\Spout\Writer\Common\Sheet; use Box\Spout\Writer\WriterFactory; /** @@ -51,64 +52,6 @@ class SheetTest extends \PHPUnit_Framework_TestCase $this->assertSheetNameEquals($customSheetName, $fileName, "The sheet name should have been changed to '$customSheetName'"); } - /** - * @return array - */ - public function dataProviderForInvalidSheetNames() - { - return [ - [null], - [21], - [''], - ['this title exceeds the 31 characters limit'], - ['Illegal \\'], - ['Illegal /'], - ['Illegal ?'], - ['Illegal *'], - ['Illegal :'], - ['Illegal ['], - ['Illegal ]'], - ['\'Illegal start'], - ['Illegal end\''], - ]; - } - - /** - * @dataProvider dataProviderForInvalidSheetNames - * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException - * - * @param string $customSheetName - * @return void - */ - public function testSetSheetNameShouldThrowOnInvalidName($customSheetName) - { - $fileName = 'test_set_name_with_invalid_name_should_throw_exception.xlsx'; - $this->writeDataAndReturnSheetWithCustomName($fileName, $customSheetName); - } - - /** - * @return void - */ - public function testSetSheetNameShouldNotThrowWhenSettingSameNameAsCurrentOne() - { - $fileName = 'test_set_name_with_same_as_current.xlsx'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - - $customSheetName = 'Sheet name'; - $sheet = $writer->getCurrentSheet(); - $sheet->setName($customSheetName); - $sheet->setName($customSheetName); - - $writer->addRow(['xlsx--11', 'xlsx--12']); - $writer->close(); - - $this->assertSheetNameEquals($customSheetName, $fileName, "The sheet name should have been changed to '$customSheetName'"); - } - /** * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException * @return void @@ -186,6 +129,6 @@ class SheetTest extends \PHPUnit_Framework_TestCase $pathToWorkbookFile = $resourcePath . '#xl/workbook.xml'; $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); - $this->assertContains('assertContains("setFontSize(15) ->setFontColor(Color::RED) - ->setFontName('Arial') + ->setFontName('Font') ->build(); $this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]); @@ -128,7 +129,7 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase $defaultFontElement = $fontElements->item(0); $this->assertChildrenNumEquals(3, $defaultFontElement, 'The default font should only have 3 properties.'); $this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $defaultFontElement, 'sz', 'val'); - $this->assertFirstChildHasAttributeEquals(Style::DEFAULT_FONT_COLOR, $defaultFontElement, 'color', 'rgb'); + $this->assertFirstChildHasAttributeEquals(Color::toARGB(Style::DEFAULT_FONT_COLOR), $defaultFontElement, 'color', 'rgb'); $this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $defaultFontElement, 'name', 'val'); // Second font should contain data from the first created style @@ -139,34 +140,15 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase $this->assertChildExists($secondFontElement, 'u'); $this->assertChildExists($secondFontElement, 'strike'); $this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $secondFontElement, 'sz', 'val'); - $this->assertFirstChildHasAttributeEquals(Style::DEFAULT_FONT_COLOR, $secondFontElement, 'color', 'rgb'); + $this->assertFirstChildHasAttributeEquals(Color::toARGB(Style::DEFAULT_FONT_COLOR), $secondFontElement, 'color', 'rgb'); $this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $secondFontElement, 'name', 'val'); // Third font should contain data from the second created style $thirdFontElement = $fontElements->item(2); $this->assertChildrenNumEquals(3, $thirdFontElement, 'The font should only have 3 properties.'); $this->assertFirstChildHasAttributeEquals('15', $thirdFontElement, 'sz', 'val'); - $this->assertFirstChildHasAttributeEquals(Color::RED, $thirdFontElement, 'color', 'rgb'); - $this->assertFirstChildHasAttributeEquals('Arial', $thirdFontElement, 'name', 'val'); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldAddWrapTextAlignmentInfoInStylesXmlFileIfSpecified() - { - $fileName = 'test_add_row_with_style_should_add_wrap_text_alignment.xlsx'; - $dataRows = [ - ['xlsx--11', 'xlsx--12'], - ]; - $style = (new StyleBuilder())->setShouldWrapText()->build(); - - $this->writeToXLSXFile($dataRows, $fileName,$style); - - $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); - $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); - $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); - $this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText'); + $this->assertFirstChildHasAttributeEquals(Color::toARGB(Color::RED), $thirdFontElement, 'color', 'rgb'); + $this->assertFirstChildHasAttributeEquals('Font', $thirdFontElement, 'name', 'val'); } /** @@ -212,6 +194,25 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase $this->assertEquals('1', $cellDomElements[1]->getAttribute('s')); } + /** + * @return void + */ + public function testAddRowWithStyleShouldAddWrapTextAlignmentInfoInStylesXmlFileIfSpecified() + { + $fileName = 'test_add_row_with_style_should_add_wrap_text_alignment.xlsx'; + $dataRows = [ + ['xlsx--11', 'xlsx--12'], + ]; + $style = (new StyleBuilder())->setShouldWrapText()->build(); + + $this->writeToXLSXFile($dataRows, $fileName,$style); + + $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); + $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); + $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); + $this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText'); + } + /** * @return void */ @@ -294,12 +295,9 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase $resourcePath = $this->getGeneratedResourcePath($fileName); $pathToStylesXmlFile = $resourcePath . '#xl/styles.xml'; - $xmlReader = new \XMLReader(); + $xmlReader = new XMLReader(); $xmlReader->open('zip://' . $pathToStylesXmlFile); - - while ($xmlReader->read() && ($xmlReader->nodeType !== \XMLReader::ELEMENT || $xmlReader->name !== $section)) { - // do nothing - } + $xmlReader->readUntilNodeFound($section); return $xmlReader->expand(); }