This commit is contained in:
Andrew Nicols 2021-10-06 09:15:31 +08:00
commit 7c357c22a1
26 changed files with 338 additions and 97 deletions

View File

@ -1,26 +1,26 @@
# Spout
[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges)
[![Project Status](https://opensource.box.com/badges/active.svg)](https://opensource.box.com/badges)
[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/box/spout/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB).
Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Documentation
Full documentation can be found at [http://opensource.box.com/spout/](http://opensource.box.com/spout/).
Full documentation can be found at [https://opensource.box.com/spout/](https://opensource.box.com/spout/).
## Requirements
* PHP version 7.1 or higher
* PHP version 7.2 or higher
* PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled
@ -43,7 +43,7 @@ For information, the performance tests take about 10 minutes to run (processing
## Support
You can ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
You can ask questions, submit new features ideas or discuss Spout in the chat room:<br>
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Copyright and License

View File

@ -4,6 +4,11 @@ Description of Spout library import
* Only include the src/Spout directory.
* Update lib/thirdpartylibs.xml with the latest version.
2021/09/01
----------
Update to v3.3.0 (MDL-71707)
by Paul Holden <paulh@moodle.com>
2020/12/07
----------
Update to v3.1.0 (MDL-70302)

View File

@ -65,7 +65,7 @@ class Cell
protected $style;
/**
* @param $value mixed
* @param mixed|null $value
* @param Style|null $style
*/
public function __construct($value, Style $style = null)

View File

@ -84,6 +84,12 @@ class Style
/** @var bool */
private $hasSetFormat = false;
/** @var bool */
private $isRegistered = false;
/** @var bool */
private $isEmpty = true;
/**
* @return int|null
*/
@ -119,6 +125,7 @@ class Style
{
$this->shouldApplyBorder = true;
$this->border = $border;
$this->isEmpty = false;
return $this;
}
@ -147,6 +154,7 @@ class Style
$this->fontBold = true;
$this->hasSetFontBold = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@ -175,6 +183,7 @@ class Style
$this->fontItalic = true;
$this->hasSetFontItalic = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@ -203,6 +212,7 @@ class Style
$this->fontUnderline = true;
$this->hasSetFontUnderline = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@ -231,6 +241,7 @@ class Style
$this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@ -260,6 +271,7 @@ class Style
$this->fontSize = $fontSize;
$this->hasSetFontSize = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@ -291,6 +303,7 @@ class Style
$this->fontColor = $fontColor;
$this->hasSetFontColor = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@ -320,6 +333,7 @@ class Style
$this->fontName = $fontName;
$this->hasSetFontName = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@ -350,6 +364,7 @@ class Style
$this->cellAlignment = $cellAlignment;
$this->hasSetCellAlignment = true;
$this->shouldApplyCellAlignment = true;
$this->isEmpty = false;
return $this;
}
@ -386,6 +401,7 @@ class Style
{
$this->shouldWrapText = $shouldWrap;
$this->hasSetWrapText = true;
$this->isEmpty = false;
return $this;
}
@ -415,6 +431,7 @@ class Style
{
$this->hasSetBackgroundColor = true;
$this->backgroundColor = $color;
$this->isEmpty = false;
return $this;
}
@ -444,6 +461,7 @@ class Style
{
$this->hasSetFormat = true;
$this->format = $format;
$this->isEmpty = false;
return $this;
}
@ -463,4 +481,29 @@ class Style
{
return $this->hasSetFormat;
}
/**
* @return bool
*/
public function isRegistered() : bool
{
return $this->isRegistered;
}
public function markAsRegistered(?int $id) : void
{
$this->setId($id);
$this->isRegistered = true;
}
public function unmarkAsRegistered() : void
{
$this->setId(0);
$this->isRegistered = false;
}
public function isEmpty() : bool
{
return $this->isEmpty;
}
}

View File

@ -17,7 +17,7 @@ class FileSystemHelper implements FileSystemHelperInterface
/**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
*/
public function __construct($baseFolderPath)
public function __construct(string $baseFolderPath)
{
$this->baseFolderRealPath = \realpath($baseFolderPath);
}
@ -117,12 +117,16 @@ class FileSystemHelper implements FileSystemHelperInterface
* should occur is not inside the base folder.
*
* @param string $operationFolderPath The path of the folder where the I/O operation should occur
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder
* @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur
* is not inside the base folder or the base folder does not exist
* @return void
*/
protected function throwIfOperationNotInBaseFolder($operationFolderPath)
protected function throwIfOperationNotInBaseFolder(string $operationFolderPath)
{
$operationFolderRealPath = \realpath($operationFolderPath);
if (!$this->baseFolderRealPath) {
throw new IOException("The base folder path is invalid: {$this->baseFolderRealPath}");
}
$isInBaseFolder = (\strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
if (!$isInBaseFolder) {
throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");

View File

@ -13,12 +13,20 @@ class StringHelper
/** @var bool Whether the mbstring extension is loaded */
protected $hasMbstringSupport;
/** @var bool Whether the code is running with PHP7 or older versions */
private $isRunningPhp7OrOlder;
/** @var array Locale info, used for number formatting */
private $localeInfo;
/**
*
*/
public function __construct()
{
$this->hasMbstringSupport = \extension_loaded('mbstring');
$this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0;
$this->localeInfo = \localeconv();
}
/**
@ -68,4 +76,30 @@ class StringHelper
return ($position !== false) ? $position : -1;
}
/**
* Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format.
*
* Formatting of float values is locale dependent in PHP < 8.
* Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34).
* However, float values must be formatted with no thousands separator and a "." as decimal point
* to work properly. This method can be used to convert the value to the correct format before storing it.
*
* @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8.
*
* @param int|float $numericValue
* @return string
*/
public function formatNumericValue($numericValue)
{
if ($this->isRunningPhp7OrOlder && is_float($numericValue)) {
return str_replace(
[$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
['', '.'],
$numericValue
);
}
return $numericValue;
}
}

View File

@ -56,12 +56,25 @@ class RowManager
$rowCells = $row->getCells();
$maxCellIndex = $numCells;
// If the row has empty cells, calling "setCellAtIndex" will add the cell
// but in the wrong place (the new cell is added at the end of the array).
// Therefore, we need to sort the array using keys to have proper order.
// @see https://github.com/box/spout/issues/740
$needsSorting = false;
for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) {
if (!isset($rowCells[$cellIndex])) {
$row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
$needsSorting = true;
}
}
if ($needsSorting) {
$rowCells = $row->getCells();
ksort($rowCells);
$row->setCells($rowCells);
}
return $row;
}
}

View File

@ -28,13 +28,13 @@ class SheetIterator implements IteratorInterface
const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name';
const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display';
/** @var string $filePath Path of the file to be read */
/** @var string Path of the file to be read */
protected $filePath;
/** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */
protected $optionsManager;
/** @var InternalEntityFactory $entityFactory Factory to create entities */
/** @var InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var XMLReader The XMLReader object that will help read sheet's XML data */

View File

@ -31,14 +31,6 @@ class CellValueFormatter
/** Constants used for date formatting */
const NUM_SECONDS_IN_ONE_DAY = 86400;
const NUM_SECONDS_IN_ONE_HOUR = 3600;
const NUM_SECONDS_IN_ONE_MINUTE = 60;
/**
* February 29th, 1900 is NOT a leap year but Excel thinks it is...
* @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
*/
const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
/** @var SharedStringsManager Manages shared strings */
protected $sharedStringsManager;
@ -130,10 +122,15 @@ class CellValueFormatter
*/
protected function formatInlineStringCellValue($node)
{
// inline strings are formatted this way:
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
$cellValue = $this->escaper->unescape($tNode->nodeValue);
// inline strings are formatted this way (they can contain any number of <t> nodes):
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t><t>[INLINE_STRING_2]</t></is></c>
$tNodes = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE);
$cellValue = '';
for ($i = 0; $i < $tNodes->count(); $i++) {
$tNode = $tNodes->item($i);
$cellValue .= $this->escaper->unescape($tNode->nodeValue);
}
return $cellValue;
}

View File

@ -16,9 +16,6 @@ use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface;
*/
class SharedStringsManager
{
/** Main namespace for the sharedStrings.xml file */
const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
/** Definition of XML nodes names used to parse data */
const XML_NODE_SST = 'sst';
const XML_NODE_SI = 'si';
@ -43,7 +40,7 @@ class SharedStringsManager
/** @var InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var HelperFactory $helperFactory Factory to create helpers */
/** @var HelperFactory Factory to create helpers */
protected $helperFactory;
/** @var CachingStrategyFactory Factory to create shared strings caching strategies */

View File

@ -17,10 +17,11 @@ class WorkbookRelationshipsManager
/** Path of workbook relationships XML file inside the XLSX file */
const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels';
/** Relationships types */
/** Relationships types - For Transitional and Strict OOXML */
const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
const RELATIONSHIP_TYPE_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
const RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings';
const RELATIONSHIP_TYPE_STYLES_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/styles';
/** Nodes and attributes used to find relevant information in the workbook relationships XML file */
const XML_NODE_RELATIONSHIP = 'Relationship';
@ -52,7 +53,8 @@ class WorkbookRelationshipsManager
public function getSharedStringsXMLFilePath()
{
$workbookRelationships = $this->getWorkbookRelationships();
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS];
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]
?? $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (\strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false);
@ -71,7 +73,8 @@ class WorkbookRelationshipsManager
{
$workbookRelationships = $this->getWorkbookRelationships();
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]);
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS])
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]);
}
/**
@ -81,7 +84,8 @@ class WorkbookRelationshipsManager
{
$workbookRelationships = $this->getWorkbookRelationships();
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]);
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES])
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]);
}
/**
@ -90,7 +94,8 @@ class WorkbookRelationshipsManager
public function getStylesXMLFilePath()
{
$workbookRelationships = $this->getWorkbookRelationships();
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES];
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]
?? $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (\strpos($stylesXMLFilePath, self::BASE_PATH) !== false);

View File

@ -35,7 +35,7 @@ class RowIterator implements IteratorInterface
/** @var string Path of the XLSX file being read */
protected $filePath;
/** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */
/** @var string Path of the sheet data XML file as in [Content_Types].xml */
protected $sheetDataXMLFilePath;
/** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */

View File

@ -0,0 +1,38 @@
<?php
namespace Box\Spout\Writer\Common\Manager;
use Box\Spout\Common\Entity\Style\Style;
/**
* Class RegisteredStyle
* Allow to know if this style must replace actual row style.
*/
class RegisteredStyle
{
/**
* @var Style
*/
private $style;
/**
* @var bool
*/
private $isMatchingRowStyle;
public function __construct(Style $style, bool $isMatchingRowStyle)
{
$this->style = $style;
$this->isMatchingRowStyle = $isMatchingRowStyle;
}
public function getStyle() : Style
{
return $this->style;
}
public function isMatchingRowStyle() : bool
{
return $this->isMatchingRowStyle;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Box\Spout\Writer\Common\Manager\Style;
use Box\Spout\Common\Entity\Style\Style;
/**
* Class PossiblyUpdatedStyle
* Indicates if style is updated.
* It allow to know if style registration must be done.
*/
class PossiblyUpdatedStyle
{
private $style;
private $isUpdated;
public function __construct(Style $style, bool $isUpdated)
{
$this->style = $style;
$this->isUpdated = $isUpdated;
}
public function getStyle() : Style
{
return $this->style;
}
public function isUpdated() : bool
{
return $this->isUpdated;
}
}

View File

@ -50,13 +50,11 @@ class StyleManager implements StyleManagerInterface
* Typically, set "wrap text" if a cell contains a new line.
*
* @param Cell $cell
* @return Style
* @return PossiblyUpdatedStyle The eventually updated style
*/
public function applyExtraStylesIfNeeded(Cell $cell)
public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle
{
$updatedStyle = $this->applyWrapTextIfCellContainsNewLine($cell);
return $updatedStyle;
return $this->applyWrapTextIfCellContainsNewLine($cell);
}
/**
@ -69,21 +67,19 @@ class StyleManager implements StyleManagerInterface
* on the Windows version of Excel...
*
* @param Cell $cell The cell the style should be applied to
* @return \Box\Spout\Common\Entity\Style\Style The eventually updated style
* @return PossiblyUpdatedStyle The eventually updated style
*/
protected function applyWrapTextIfCellContainsNewLine(Cell $cell)
protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : PossiblyUpdatedStyle
{
$cellStyle = $cell->getStyle();
// if the "wrap text" option is already set, no-op
if ($cellStyle->hasSetWrapText()) {
return $cellStyle;
}
if ($cell->isString() && \strpos($cell->getValue(), "\n") !== false) {
if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) {
$cellStyle->setShouldWrapText();
return new PossiblyUpdatedStyle($cellStyle, true);
}
return $cellStyle;
return new PossiblyUpdatedStyle($cellStyle, false);
}
}

View File

@ -24,7 +24,7 @@ interface StyleManagerInterface
* Typically, set "wrap text" if a cell contains a new line.
*
* @param Cell $cell
* @return Style The updated style
* @return PossiblyUpdatedStyle The eventually updated style
*/
public function applyExtraStylesIfNeeded(Cell $cell);
public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle;
}

View File

@ -36,9 +36,9 @@ class StyleRegistry
{
$serializedStyle = $this->serialize($style);
if (!$this->hasStyleAlreadyBeenRegistered($style)) {
if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) {
$nextStyleId = \count($this->serializedStyleToStyleIdMappingTable);
$style->setId($nextStyleId);
$style->markAsRegistered($nextStyleId);
$this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
$this->styleIdToStyleMappingTable[$nextStyleId] = $style;
@ -48,15 +48,13 @@ class StyleRegistry
}
/**
* Returns whether the given style has already been registered.
* Returns whether the serialized style has already been registered.
*
* @param Style $style
* @param string $serializedStyle The serialized style
* @return bool
*/
protected function hasStyleAlreadyBeenRegistered(Style $style)
protected function hasSerializedStyleAlreadyBeenRegistered(string $serializedStyle)
{
$serializedStyle = $this->serialize($style);
// Using isset here because it is way faster than array_key_exists...
return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]);
}
@ -101,13 +99,13 @@ class StyleRegistry
*/
public function serialize(Style $style)
{
// In order to be able to properly compare style, set static ID value
// In order to be able to properly compare style, set static ID value and reset registration
$currentId = $style->getId();
$style->setId(0);
$style->unmarkAsRegistered();
$serializedStyle = \serialize($style);
$style->setId($currentId);
$style->markAsRegistered($currentId);
return $serializedStyle;
}

View File

@ -44,7 +44,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
/** @var InternalEntityFactory Factory to create entities */
protected $entityFactory;
/** @var ManagerFactoryInterface $managerFactory Factory to create managers */
/** @var ManagerFactoryInterface Factory to create managers */
protected $managerFactory;
/** @var Worksheet The worksheet where data will be written to */

View File

@ -22,7 +22,7 @@ class ManagerFactory implements ManagerFactoryInterface
/** @var InternalEntityFactory */
protected $entityFactory;
/** @var HelperFactory $helperFactory */
/** @var HelperFactory */
protected $helperFactory;
/**

View File

@ -22,6 +22,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
*/
public function registerStyle(Style $style)
{
if ($style->isRegistered()) {
return $style;
}
$registeredStyle = parent::registerStyle($style);
$this->usedFontsSet[$style->getFontName()] = true;

View File

@ -10,6 +10,7 @@ use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\Escaper\ODS as ODSEscaper;
use Box\Spout\Common\Helper\StringHelper;
use Box\Spout\Writer\Common\Entity\Worksheet;
use Box\Spout\Writer\Common\Manager\RegisteredStyle;
use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
use Box\Spout\Writer\ODS\Manager\Style\StyleManager;
@ -93,7 +94,7 @@ class WorksheetManager implements WorksheetManagerInterface
$escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
$tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
$tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
$tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
$tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $worksheet->getMaxNumColumns() . '"/>';
return $tableElement;
@ -104,8 +105,8 @@ class WorksheetManager implements WorksheetManagerInterface
*
* @param Worksheet $worksheet The worksheet to add the row to
* @param Row $row The row to be added
* @throws IOException If the data cannot be written
* @throws InvalidArgumentException If a cell value's type is not supported
* @throws IOException If the data cannot be written
* @return void
*/
public function addRow(Worksheet $worksheet, Row $row)
@ -125,7 +126,13 @@ class WorksheetManager implements WorksheetManagerInterface
$nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) {
$data .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $currentCellIndex, $nextCellIndex);
$registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
$cellStyle = $registeredStyle->getStyle();
if ($registeredStyle->isMatchingRowStyle()) {
$rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
}
$data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex);
$currentCellIndex = $nextCellIndex;
}
@ -146,24 +153,46 @@ class WorksheetManager implements WorksheetManagerInterface
/**
* Applies styles to the given style, merging the cell's style with its row's style
* Then builds and returns xml for the cell.
*
* @param Cell $cell
* @param Style $rowStyle
* @param int $currentCellIndex
* @param int $nextCellIndex
* @throws InvalidArgumentException If a cell value's type is not supported
* @return string
* @return RegisteredStyle
*/
private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $currentCellIndex, $nextCellIndex)
private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
{
// Apply row and extra styles
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$isMatchingRowStyle = false;
if ($cell->getStyle()->isEmpty()) {
$cell->setStyle($rowStyle);
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
$styleIndex = $registeredStyle->getId() + 1; // 1-based
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
} else {
$registeredStyle = $this->styleManager->registerStyle($rowStyle);
$isMatchingRowStyle = true;
}
} else {
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$newCellStyle = $possiblyUpdatedStyle->getStyle();
} else {
$newCellStyle = $mergedCellAndRowStyle;
}
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
}
return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
}
private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string
{
$styleIndex = $style->getId() + 1; // 1-based
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
@ -197,12 +226,14 @@ class WorksheetManager implements WorksheetManagerInterface
$data .= '</table:table-cell>';
} elseif ($cell->isBoolean()) {
$data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cell->getValue() . '">';
$value = $cell->getValue() ? 'true' : 'false'; // boolean-value spec: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean
$data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $value . '">';
$data .= '<text:p>' . $cell->getValue() . '</text:p>';
$data .= '</table:table-cell>';
} elseif ($cell->isNumeric()) {
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">';
$data .= '<text:p>' . $cell->getValue() . '</text:p>';
$cellValue = $this->stringHelper->formatNumericValue($cell->getValue());
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
$data .= '<text:p>' . $cellValue . '</text:p>';
$data .= '</table:table-cell>';
} elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
// only writes the error value if it's a string

View File

@ -33,7 +33,7 @@ abstract class WriterAbstract implements WriterInterface
/** @var GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/** @var HelperFactory $helperFactory */
/** @var HelperFactory */
protected $helperFactory;
/** @var OptionsManagerInterface Writer options manager */
@ -123,9 +123,26 @@ abstract class WriterAbstract implements WriterInterface
// @see https://github.com/box/spout/issues/241
$this->globalFunctionsHelper->ob_end_clean();
// Set headers
/*
* Set headers
*
* For newer browsers such as Firefox, Chrome, Opera, Safari, etc., they all support and use `filename*`
* specified by the new standard, even if they do not automatically decode filename; it does not matter;
* and for older versions of Internet Explorer, they are not recognized `filename*`, will automatically
* ignore it and use the old `filename` (the only minor flaw is that there must be an English suffix name).
* In this way, the multi-browser multi-language compatibility problem is perfectly solved, which does not
* require UA judgment and is more in line with the standard.
*
* @see https://github.com/box/spout/issues/745
* @see https://tools.ietf.org/html/rfc6266
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
*/
$this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType);
$this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"');
$this->globalFunctionsHelper->header(
'Content-Disposition: attachment; ' .
'filename="' . rawurldecode($this->outputFilePath) . '"; ' .
'filename*=UTF-8\'\'' . rawurldecode($this->outputFilePath)
);
/*
* When forcing the download of a file over SSL,IE8 and lower browsers fail

View File

@ -24,7 +24,7 @@ class ManagerFactory implements ManagerFactoryInterface
/** @var InternalEntityFactory */
protected $entityFactory;
/** @var HelperFactory $helperFactory */
/** @var HelperFactory */
protected $helperFactory;
/**

View File

@ -119,6 +119,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
*/
public function registerStyle(Style $style)
{
if ($style->isRegistered()) {
return $style;
}
$registeredStyle = parent::registerStyle($style);
$this->registerFill($registeredStyle);
$this->registerFormat($registeredStyle);

View File

@ -14,6 +14,7 @@ use Box\Spout\Writer\Common\Creator\InternalEntityFactory;
use Box\Spout\Writer\Common\Entity\Options;
use Box\Spout\Writer\Common\Entity\Worksheet;
use Box\Spout\Writer\Common\Helper\CellHelper;
use Box\Spout\Writer\Common\Manager\RegisteredStyle;
use Box\Spout\Writer\Common\Manager\RowManager;
use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
@ -160,7 +161,12 @@ EOD;
$rowXML = '<row r="' . $rowIndexOneBased . '" spans="1:' . $numCells . '">';
foreach ($row->getCells() as $columnIndexZeroBased => $cell) {
$rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndexOneBased, $columnIndexZeroBased);
$registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
$cellStyle = $registeredStyle->getStyle();
if ($registeredStyle->isMatchingRowStyle()) {
$rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
}
$rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId());
}
$rowXML .= '</row>';
@ -173,26 +179,43 @@ EOD;
/**
* Applies styles to the given style, merging the cell's style with its row's style
* Then builds and returns xml for the cell.
*
* @param Cell $cell
* @param Style $rowStyle
* @param int $rowIndexOneBased
* @param int $columnIndexZeroBased
*
* @throws InvalidArgumentException If the given value cannot be processed
* @return string
* @return RegisteredStyle
*/
private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $rowIndexOneBased, $columnIndexZeroBased)
private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
{
// Apply row and extra styles
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$isMatchingRowStyle = false;
if ($cell->getStyle()->isEmpty()) {
$cell->setStyle($rowStyle);
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId());
if ($possiblyUpdatedStyle->isUpdated()) {
$registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
} else {
$registeredStyle = $this->styleManager->registerStyle($rowStyle);
$isMatchingRowStyle = true;
}
} else {
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$newCellStyle = $possiblyUpdatedStyle->getStyle();
} else {
$newCellStyle = $mergedCellAndRowStyle;
}
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
}
return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
}
/**
@ -217,7 +240,7 @@ EOD;
} elseif ($cell->isBoolean()) {
$cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
} elseif ($cell->isNumeric()) {
$cellXML .= '><v>' . $cell->getValue() . '</v></c>';
$cellXML .= '><v>' . $this->stringHelper->formatNumericValue($cell->getValue()) . '</v></c>';
} elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
// only writes the error value if it's a string
$cellXML .= ' t="e"><v>' . $cell->getValueEvenIfError() . '</v></c>';

View File

@ -221,7 +221,7 @@
<location>spout</location>
<name>Spout</name>
<license>Apache</license>
<version>3.1.0</version>
<version>3.3.0</version>
<licenseversion>2.0</licenseversion>
</library>
<library>