Merge branch 'master' into custom-column-widths

# Conflicts:
#	src/Spout/Writer/ODS/Manager/WorksheetManager.php
#	src/Spout/Writer/XLSX/Manager/WorksheetManager.php
This commit is contained in:
Alexander Hofstede 2021-04-05 11:30:34 +02:00
commit 615c07649c
40 changed files with 323 additions and 105 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
/vendor /vendor
/composer.lock /composer.lock
/.php_cs.cache /.php_cs.cache
/.phpunit.result.cache

View File

@ -5,16 +5,16 @@ language: php
matrix: matrix:
include: include:
- php: 7.1
env: WITH_CS=true
- php: 7.1
env: WITH_PHPUNIT=true WITH_COVERAGE=true
- php: 7.2 - php: 7.2
env: WITH_PHPUNIT=true env: WITH_PHPUNIT=true
- php: 7.3 - php: 7.3
env: WITH_PHPUNIT=true env: WITH_PHPUNIT=true
- php: 7.3
env: WITH_PHPUNIT=true WITH_COVERAGE=true
- php: 7.4 - php: 7.4
env: WITH_PHPUNIT=true env: WITH_PHPUNIT=true
- php: 8.0
env: WITH_PHPUNIT=true
cache: cache:
@ -42,7 +42,7 @@ before_script:
script: script:
- | - |
if [[ "$WITH_CS" == "true" ]]; then if [[ "$WITH_CS" == "true" ]]; then
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff
fi fi
- | - |
if [[ "$WITH_PHPUNIT" == "true" ]]; then if [[ "$WITH_PHPUNIT" == "true" ]]; then

View File

@ -68,7 +68,21 @@ This will add your changes on top of what's already in upstream, minimizing merg
Make sure that all tests are passing before submitting a pull request. Make sure that all tests are passing before submitting a pull request.
### Step 8: Send the pull request ### Step 8: Fix code style
Run the following command to check the code style of your changes:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist --verbose --diff --dry-run --diff-format=udiff
```
This will print a diff of proposed code style changes. To apply these suggestions, run the following command:
```
vendor/bin/php-cs-fixer fix --config=.php_cs.dist
```
### Step 9: Send the pull request
Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did.

View File

@ -20,7 +20,7 @@ Full documentation can be found at [https://opensource.box.com/spout/](https://o
## Requirements ## Requirements
* PHP version 7.1 or higher * PHP version 7.2 or higher
* PHP extension `php_zip` enabled * PHP extension `php_zip` enabled
* PHP extension `php_xmlreader` enabled * PHP extension `php_xmlreader` enabled

View File

@ -11,7 +11,7 @@ With the 3.0 version, this is now possible: each cell can have its own style.
Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value. Spout 3.0 tries to enforce better typing. For instance, instead of using/returning generic arrays, Spout now makes use of specific `Row` and `Cell` objects that can encapsulate more data such as type, style, value.
Finally, **_Spout 3.0 only supports PHP 7.1 and above_**, as other PHP versions are no longer supported by the community. Finally, **_Spout 3.2 only supports PHP 7.2 and above_**, as other PHP versions are no longer supported by the community.
Reader changes Reader changes
-------------- --------------

View File

@ -12,12 +12,12 @@
} }
], ],
"require": { "require": {
"php": ">=7.1.0", "php": ">=7.2.0",
"ext-zip": "*", "ext-zip": "*",
"ext-xmlreader" : "*" "ext-xmlreader" : "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^7", "phpunit/phpunit": "^8",
"friendsofphp/php-cs-fixer": "^2" "friendsofphp/php-cs-fixer": "^2"
}, },
"suggest": { "suggest": {
@ -36,7 +36,7 @@
}, },
"config": { "config": {
"platform": { "platform": {
"php": "7.1" "php": "7.2"
} }
} }
} }

View File

@ -10,7 +10,7 @@ This guide will help you install {{ site.spout_html }} and teach you how to use
## Requirements ## Requirements
* PHP version 7.1 or higher * PHP version 7.2 or higher
* PHP extension `ext-zip` enabled * PHP extension `ext-zip` enabled
* PHP extension `ext-xmlreader` enabled * PHP extension `ext-xmlreader` enabled

View File

@ -94,7 +94,7 @@ class MyStreamController extends Controller
$i++; $i++;
// Flushing the buffer every N rows to stream echo'ed content. // Flushing the buffer every N rows to stream echo'ed content.
if ($i % FLUSH_THRESHOLD === 0) { if ($i % self::FLUSH_THRESHOLD === 0) {
flush(); flush();
} }
} }

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
bootstrap="tests/bootstrap.php" bootstrap="tests/bootstrap.php"
colors="true" colors="true"
convertErrorsToExceptions="false" convertErrorsToExceptions="false"

View File

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

@ -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. * Typically, set "wrap text" if a cell contains a new line.
* *
* @param Cell $cell * @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 $this->applyWrapTextIfCellContainsNewLine($cell);
return $updatedStyle;
} }
/** /**
@ -69,21 +67,19 @@ class StyleManager implements StyleManagerInterface
* on the Windows version of Excel... * on the Windows version of Excel...
* *
* @param Cell $cell The cell the style should be applied to * @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(); $cellStyle = $cell->getStyle();
// if the "wrap text" option is already set, no-op // if the "wrap text" option is already set, no-op
if ($cellStyle->hasSetWrapText()) { if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) {
return $cellStyle;
}
if ($cell->isString() && \strpos($cell->getValue(), "\n") !== false) {
$cellStyle->setShouldWrapText(); $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. * Typically, set "wrap text" if a cell contains a new line.
* *
* @param Cell $cell * @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); $serializedStyle = $this->serialize($style);
if (!$this->hasStyleAlreadyBeenRegistered($style)) { if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) {
$nextStyleId = \count($this->serializedStyleToStyleIdMappingTable); $nextStyleId = \count($this->serializedStyleToStyleIdMappingTable);
$style->setId($nextStyleId); $style->markAsRegistered($nextStyleId);
$this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
$this->styleIdToStyleMappingTable[$nextStyleId] = $style; $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 * @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... // Using isset here because it is way faster than array_key_exists...
return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]);
} }
@ -101,13 +99,13 @@ class StyleRegistry
*/ */
public function serialize(Style $style) 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(); $currentId = $style->getId();
$style->setId(0); $style->unmarkAsRegistered();
$serializedStyle = \serialize($style); $serializedStyle = \serialize($style);
$style->setId($currentId); $style->markAsRegistered($currentId);
return $serializedStyle; return $serializedStyle;
} }

View File

@ -22,6 +22,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
*/ */
public function registerStyle(Style $style) public function registerStyle(Style $style)
{ {
if ($style->isRegistered()) {
return $style;
}
$registeredStyle = parent::registerStyle($style); $registeredStyle = parent::registerStyle($style);
$this->usedFontsSet[$style->getFontName()] = true; $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\Escaper\ODS as ODSEscaper;
use Box\Spout\Common\Helper\StringHelper; use Box\Spout\Common\Helper\StringHelper;
use Box\Spout\Writer\Common\Entity\Worksheet; 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\Style\StyleMerger;
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface; use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
use Box\Spout\Writer\ODS\Manager\Style\StyleManager; use Box\Spout\Writer\ODS\Manager\Style\StyleManager;
@ -104,8 +105,8 @@ class WorksheetManager implements WorksheetManagerInterface
* *
* @param Worksheet $worksheet The worksheet to add the row to * @param Worksheet $worksheet The worksheet to add the row to
* @param Row $row The row to be added * @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 InvalidArgumentException If a cell value's type is not supported
* @throws IOException If the data cannot be written
* @return void * @return void
*/ */
public function addRow(Worksheet $worksheet, Row $row) public function addRow(Worksheet $worksheet, Row $row)
@ -125,7 +126,13 @@ class WorksheetManager implements WorksheetManagerInterface
$nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null; $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) { 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; $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 * 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 Cell $cell
* @param Style $rowStyle * @param Style $rowStyle
* @param int $currentCellIndex
* @param int $nextCellIndex
* @throws InvalidArgumentException If a cell value's type is not supported * @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 $isMatchingRowStyle = false;
if ($cell->getStyle()->isEmpty()) {
$cell->setStyle($rowStyle);
$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); $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle); $cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$newCellStyle = $possiblyUpdatedStyle->getStyle();
} else {
$newCellStyle = $mergedCellAndRowStyle;
}
$registeredStyle = $this->styleManager->registerStyle($newCellStyle); $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
$styleIndex = $registeredStyle->getId() + 1; // 1-based }
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); $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
@ -197,7 +226,8 @@ class WorksheetManager implements WorksheetManagerInterface
$data .= '</table:table-cell>'; $data .= '</table:table-cell>';
} elseif ($cell->isBoolean()) { } 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 .= '<text:p>' . $cell->getValue() . '</text:p>';
$data .= '</table:table-cell>'; $data .= '</table:table-cell>';
} elseif ($cell->isNumeric()) { } elseif ($cell->isNumeric()) {

View File

@ -123,9 +123,26 @@ abstract class WriterAbstract implements WriterInterface
// @see https://github.com/box/spout/issues/241 // @see https://github.com/box/spout/issues/241
$this->globalFunctionsHelper->ob_end_clean(); $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-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 * When forcing the download of a file over SSL,IE8 and lower browsers fail

View File

@ -119,6 +119,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
*/ */
public function registerStyle(Style $style) public function registerStyle(Style $style)
{ {
if ($style->isRegistered()) {
return $style;
}
$registeredStyle = parent::registerStyle($style); $registeredStyle = parent::registerStyle($style);
$this->registerFill($registeredStyle); $this->registerFill($registeredStyle);
$this->registerFormat($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\Options;
use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\Common\Entity\Worksheet;
use Box\Spout\Writer\Common\Helper\CellHelper; use Box\Spout\Writer\Common\Helper\CellHelper;
use Box\Spout\Writer\Common\Manager\RegisteredStyle;
use Box\Spout\Writer\Common\Manager\ManagesCellSize; use Box\Spout\Writer\Common\Manager\ManagesCellSize;
use Box\Spout\Writer\Common\Manager\RowManager; use Box\Spout\Writer\Common\Manager\RowManager;
use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
@ -185,7 +186,12 @@ EOD;
$rowXML = "<row r=\"{$rowIndexOneBased}\" spans=\"1:{$numCells}\" customHeight=\"{$hasCustomHeight}\">"; $rowXML = "<row r=\"{$rowIndexOneBased}\" spans=\"1:{$numCells}\" customHeight=\"{$hasCustomHeight}\">";
foreach ($row->getCells() as $columnIndexZeroBased => $cell) { 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>'; $rowXML .= '</row>';
@ -198,26 +204,43 @@ EOD;
/** /**
* Applies styles to the given style, merging the cell's style with its row's style * Applies styles to the given style, merging the cell's style with its row's style
* Then builds and returns xml for the cell.
* *
* @param Cell $cell * @param Cell $cell
* @param Style $rowStyle * @param Style $rowStyle
* @param int $rowIndexOneBased
* @param int $columnIndexZeroBased
* *
* @throws InvalidArgumentException If the given value cannot be processed * @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 $isMatchingRowStyle = false;
if ($cell->getStyle()->isEmpty()) {
$cell->setStyle($rowStyle);
$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); $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle); $cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$newCellStyle = $possiblyUpdatedStyle->getStyle();
} else {
$newCellStyle = $mergedCellAndRowStyle;
}
$registeredStyle = $this->styleManager->registerStyle($newCellStyle); $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
}
return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId()); return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
} }
/** /**

View File

@ -122,6 +122,6 @@ class RowTest extends \PHPUnit\Framework\TestCase
->setStyle($this->getStyleMock()) ->setStyle($this->getStyleMock())
->setCells([]); ->setCells([]);
$this->assertInternalType('object', $row); $this->assertInstanceOf(Row::class, $row);
} }
} }

View File

@ -16,7 +16,7 @@ class FileSystemHelperTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function setUp() public function setUp() : void
{ {
$baseFolder = \sys_get_temp_dir(); $baseFolder = \sys_get_temp_dir();
$this->fileSystemHelper = new FileSystemHelper($baseFolder); $this->fileSystemHelper = new FileSystemHelper($baseFolder);

View File

@ -14,7 +14,7 @@ class OptionsManagerTest extends TestCase
*/ */
protected $optionsManager; protected $optionsManager;
protected function setUp() protected function setUp() : void
{ {
$this->optionsManager = new class() extends OptionsManagerAbstract { $this->optionsManager = new class() extends OptionsManagerAbstract {
protected function getSupportedOptions() protected function getSupportedOptions()

View File

@ -295,6 +295,10 @@ class ReaderTest extends TestCase
*/ */
public function testReadShouldBeProtectedAgainstBillionLaughsAttack() public function testReadShouldBeProtectedAgainstBillionLaughsAttack()
{ {
if (function_exists('xdebug_code_coverage_started') && xdebug_code_coverage_started()) {
$this->markTestSkipped('test not compatible with code coverage');
}
$startTime = microtime(true); $startTime = microtime(true);
$fileName = 'attack_billion_laughs.ods'; $fileName = 'attack_billion_laughs.ods';
@ -318,6 +322,10 @@ class ReaderTest extends TestCase
*/ */
public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack() public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack()
{ {
if (function_exists('xdebug_code_coverage_started') && xdebug_code_coverage_started()) {
$this->markTestSkipped('test not compatible with code coverage');
}
$startTime = microtime(true); $startTime = microtime(true);
$fileName = 'attack_quadratic_blowup.ods'; $fileName = 'attack_quadratic_blowup.ods';

View File

@ -25,7 +25,7 @@ class SharedStringsManagerTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function setUp() public function setUp() : void
{ {
$this->sharedStringsManager = null; $this->sharedStringsManager = null;
} }
@ -33,7 +33,7 @@ class SharedStringsManagerTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function tearDown() public function tearDown() : void
{ {
if ($this->sharedStringsManager !== null) { if ($this->sharedStringsManager !== null) {
$this->sharedStringsManager->cleanup(); $this->sharedStringsManager->cleanup();

View File

@ -539,6 +539,10 @@ class ReaderTest extends TestCase
*/ */
public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack() public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack()
{ {
if (function_exists('xdebug_code_coverage_started') && xdebug_code_coverage_started()) {
$this->markTestSkipped('test not compatible with code coverage');
}
$startTime = microtime(true); $startTime = microtime(true);
$this->getAllRowsForFile('attack_quadratic_blowup.xlsx'); $this->getAllRowsForFile('attack_quadratic_blowup.xlsx');

View File

@ -61,7 +61,7 @@ class WriterPerfTest extends TestCase
*/ */
private function getNumWrittenRows($resourcePath) private function getNumWrittenRows($resourcePath)
{ {
$lineCountResult = `wc -l $resourcePath`; $lineCountResult = shell_exec("wc -l $resourcePath");
return (int) $lineCountResult; return (int) $lineCountResult;
} }

View File

@ -101,7 +101,7 @@ class WriterTest extends TestCase
]); ]);
$writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_utf8_bom.csv'); $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_utf8_bom.csv');
$this->assertContains(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should contain a UTF-8 BOM'); $this->assertStringStartsWith(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should contain a UTF-8 BOM');
} }
/** /**
@ -114,7 +114,7 @@ class WriterTest extends TestCase
]); ]);
$writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_no_bom.csv', ',', '"', false); $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_no_bom.csv', ',', '"', false);
$this->assertNotContains(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should not contain a UTF-8 BOM'); $this->assertStringNotContainsString(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should not contain a UTF-8 BOM');
} }
/** /**

View File

@ -18,7 +18,7 @@ class SheetTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function setUp() public function setUp() : void
{ {
$this->sheetManager = new SheetManager(new StringHelper()); $this->sheetManager = new SheetManager(new StringHelper());
} }

View File

@ -14,7 +14,7 @@ class StyleManagerTest extends TestCase
/** /**
* @return StyleManager * @return StyleManager
*/ */
private function getStyleManager() private function getStyleManager() : StyleManager
{ {
$style = (new StyleBuilder())->build(); $style = (new StyleBuilder())->build();
$styleRegistry = new StyleRegistry($style); $styleRegistry = new StyleRegistry($style);
@ -22,31 +22,37 @@ class StyleManagerTest extends TestCase
return new StyleManager($styleRegistry); return new StyleManager($styleRegistry);
} }
/** public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine() : void
* @return void
*/
public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine()
{ {
$style = (new StyleBuilder())->build(); $style = (new StyleBuilder())->build();
$this->assertFalse($style->shouldWrapText()); $this->assertFalse($style->shouldWrapText());
$styleManager = $this->getStyleManager(); $styleManager = $this->getStyleManager();
$updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); $possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style));
$this->assertTrue($updatedStyle->shouldWrapText()); $this->assertTrue($possiblyUpdatedStyle->isUpdated());
$this->assertTrue($possiblyUpdatedStyle->getStyle()->shouldWrapText());
} }
/** public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextNotNeeded() : void
* @return void {
*/ $style = (new StyleBuilder())->build();
public function testApplyExtraStylesIfNeededShouldDoNothingIfWrapTextAlreadyApplied() $this->assertFalse($style->shouldWrapText());
$styleManager = $this->getStyleManager();
$possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell('oneline', $style));
$this->assertFalse($possiblyUpdatedStyle->isUpdated());
}
public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextAlreadyApplied() : void
{ {
$style = (new StyleBuilder())->setShouldWrapText()->build(); $style = (new StyleBuilder())->setShouldWrapText()->build();
$this->assertTrue($style->shouldWrapText()); $this->assertTrue($style->shouldWrapText());
$styleManager = $this->getStyleManager(); $styleManager = $this->getStyleManager();
$updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style)); $possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style));
$this->assertTrue($updatedStyle->shouldWrapText()); $this->assertFalse($possiblyUpdatedStyle->isUpdated());
} }
} }

View File

@ -18,7 +18,7 @@ class StyleMergerTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function setUp() public function setUp() : void
{ {
$this->styleMerger = new StyleMerger(); $this->styleMerger = new StyleMerger();
} }

View File

@ -20,7 +20,7 @@ class StyleRegistryTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function setUp() public function setUp() : void
{ {
$this->defaultStyle = (new StyleBuilder())->build(); $this->defaultStyle = (new StyleBuilder())->build();
$this->styleRegistry = new StyleRegistry($this->defaultStyle); $this->styleRegistry = new StyleRegistry($this->defaultStyle);

View File

@ -90,7 +90,7 @@ class SheetTest extends TestCase
$pathToContentFile = $resourcePath . '#content.xml'; $pathToContentFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToContentFile); $xmlContents = file_get_contents('zip://' . $pathToContentFile);
$this->assertContains(' table:display="false"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); $this->assertStringContainsString(' table:display="false"', $xmlContents, 'The sheet visibility should have been changed to "hidden"');
} }
public function testThrowsIfWorkbookIsNotInitialized() public function testThrowsIfWorkbookIsNotInitialized()
@ -285,6 +285,6 @@ class SheetTest extends TestCase
$pathToWorkbookFile = $resourcePath . '#content.xml'; $pathToWorkbookFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains("table:name=\"$expectedName\"", $xmlContents, $message); $this->assertStringContainsString("table:name=\"$expectedName\"", $xmlContents, $message);
} }
} }

View File

@ -88,7 +88,7 @@ class WriterPerfTest extends TestCase
copy($pathToContentXmlFile, $tmpFile); copy($pathToContentXmlFile, $tmpFile);
// Get the last 200 characters // Get the last 200 characters
$lastCharacters = `tail -c 200 $tmpFile`; $lastCharacters = shell_exec("tail -c 200 $tmpFile");
// remove the temporary file // remove the temporary file
unlink($tmpFile); unlink($tmpFile);

View File

@ -547,7 +547,7 @@ class WriterTest extends TestCase
$pathToContentFile = $resourcePath . '#content.xml'; $pathToContentFile = $resourcePath . '#content.xml';
$xmlContents = file_get_contents('zip://' . $pathToContentFile); $xmlContents = file_get_contents('zip://' . $pathToContentFile);
$this->assertContains($value, $xmlContents, $message); $this->assertStringContainsString($value, $xmlContents, $message);
} }
/** /**
@ -562,7 +562,7 @@ class WriterTest extends TestCase
$sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex);
$valueAsXmlString = "<text:p>$value</text:p>"; $valueAsXmlString = "<text:p>$value</text:p>";
$this->assertContains($valueAsXmlString, $sheetXmlAsString, $message); $this->assertStringContainsString($valueAsXmlString, $sheetXmlAsString, $message);
} }
/** /**
@ -577,7 +577,7 @@ class WriterTest extends TestCase
$sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex);
$valueAsXmlString = "<text:p>$value</text:p>"; $valueAsXmlString = "<text:p>$value</text:p>";
$this->assertNotContains($valueAsXmlString, $sheetXmlAsString, $message); $this->assertStringNotContainsString($valueAsXmlString, $sheetXmlAsString, $message);
} }
/** /**

View File

@ -30,7 +30,7 @@ class WriterWithStyleTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function setUp() public function setUp() : void
{ {
$this->defaultStyle = (new StyleBuilder())->build(); $this->defaultStyle = (new StyleBuilder())->build();
} }

View File

@ -90,7 +90,7 @@ class SheetTest extends TestCase
$pathToWorkbookFile = $resourcePath . '#xl/workbook.xml'; $pathToWorkbookFile = $resourcePath . '#xl/workbook.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains(' state="hidden"', $xmlContents, 'The sheet visibility should have been changed to "hidden"'); $this->assertStringContainsString(' state="hidden"', $xmlContents, 'The sheet visibility should have been changed to "hidden"');
} }
public function testThrowsIfWorkbookIsNotInitialized() public function testThrowsIfWorkbookIsNotInitialized()
@ -299,6 +299,6 @@ class SheetTest extends TestCase
$pathToWorkbookFile = $resourcePath . '#xl/workbook.xml'; $pathToWorkbookFile = $resourcePath . '#xl/workbook.xml';
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
$this->assertContains("<sheet name=\"$expectedName\"", $xmlContents, $message); $this->assertStringContainsString("<sheet name=\"$expectedName\"", $xmlContents, $message);
} }
} }

View File

@ -135,7 +135,7 @@ class WriterPerfTest extends TestCase
copy($filePath, $tmpFile); copy($filePath, $tmpFile);
// Get the last 200 characters // Get the last 200 characters
$lastCharacters = `tail -c $numCharacters $tmpFile`; $lastCharacters = shell_exec("tail -c $numCharacters $tmpFile");
// remove the temporary file // remove the temporary file
unlink($tmpFile); unlink($tmpFile);

View File

@ -608,7 +608,7 @@ class WriterTest extends TestCase
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml'; $pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml';
$xmlContents = file_get_contents('zip://' . $pathToSheetFile); $xmlContents = file_get_contents('zip://' . $pathToSheetFile);
$this->assertContains((string) $inlineData, $xmlContents, $message); $this->assertStringContainsString((string) $inlineData, $xmlContents, $message);
} }
/** /**
@ -624,7 +624,7 @@ class WriterTest extends TestCase
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml'; $pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml';
$xmlContents = file_get_contents('zip://' . $pathToSheetFile); $xmlContents = file_get_contents('zip://' . $pathToSheetFile);
$this->assertNotContains((string) $inlineData, $xmlContents, $message); $this->assertStringNotContainsString((string) $inlineData, $xmlContents, $message);
} }
/** /**
@ -639,6 +639,6 @@ class WriterTest extends TestCase
$pathToSharedStringsFile = $resourcePath . '#xl/sharedStrings.xml'; $pathToSharedStringsFile = $resourcePath . '#xl/sharedStrings.xml';
$xmlContents = file_get_contents('zip://' . $pathToSharedStringsFile); $xmlContents = file_get_contents('zip://' . $pathToSharedStringsFile);
$this->assertContains($sharedString, $xmlContents, $message); $this->assertStringContainsString($sharedString, $xmlContents, $message);
} }
} }

View File

@ -32,7 +32,7 @@ class WriterWithStyleTest extends TestCase
/** /**
* @return void * @return void
*/ */
public function setUp() public function setUp() : void
{ {
$this->defaultStyle = (new StyleBuilder())->build(); $this->defaultStyle = (new StyleBuilder())->build();
} }