Skip to content

Commit 2f0733d

Browse files
committed
Conditional and table formatting support for html writer
1 parent 3cb7b36 commit 2f0733d

26 files changed

+1163
-37
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1212
- Add ability to add custom functions to Calculation. [PR #4390](https://github.com/PHPOffice/PhpSpreadsheet/pull/4390)
1313
- Add FormulaRange to IgnoredErrors. [PR #4393](https://github.com/PHPOffice/PhpSpreadsheet/pull/4393)
1414
- Permit read to class which extends Spreadsheet. [Discussion #4402](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4402) [PR #4404](https://github.com/PHPOffice/PhpSpreadsheet/pull/4404)
15+
- Conditional and table formatting support for html writer [PR #4412](https://github.com/PHPOffice/PhpSpreadsheet/pull/4412)
1516

1617
### Removed
1718

docs/topics/conditional-formatting.md

+22-15
Original file line numberDiff line numberDiff line change
@@ -143,20 +143,28 @@ Currently, the following Conditional Types are supported for the following Reade
143143

144144
MS Excel | Conditional Type | Readers | Writers
145145
---|---|---|---
146-
| Cell Value | Conditional::CONDITION_CELLIS | Xlsx | Xlsx, Xls
147-
Specific Text | Conditional::CONDITION_CONTAINSTEXT | Xlsx | Xlsx
148-
| Conditional::CONDITION_NOTCONTAINSTEXT | Xlsx | Xlsx
149-
| Conditional::CONDITION_BEGINSWITH | Xlsx | Xlsx
150-
| Conditional::CONDITION_ENDSWITH | Xlsx | Xlsx
151-
Dates Occurring | Conditional::CONDITION_TIMEPERIOD | Xlsx | Xlsx
152-
Blanks | Conditional::CONDITION_CONTAINSBLANKS | Xlsx | Xlsx
153-
No Blanks | Conditional::CONDITION_NOTCONTAINSBLANKS | Xlsx | Xlsx
154-
Errors | Conditional::CONDITION_CONTAINSERRORS | Xlsx | Xlsx
155-
No Errors | Conditional::CONDITION_NOTCONTAINSERRORS | Xlsx | Xlsx
156-
Duplicates/Unique | Conditional::CONDITION_DUPLICATES | Xlsx | Xlsx
157-
| Conditional::CONDITION_UNIQUE | Xlsx | Xlsx
158-
Use a formula | Conditional::CONDITION_EXPRESSION | Xlsx | Xlsx, Xls
159-
Data Bars | Conditional::CONDITION_DATABAR | Xlsx | Xlsx
146+
| Cell Value | Conditional::CONDITION_CELLIS | Xlsx | Xlsx, Xls, Html
147+
Specific Text | Conditional::CONDITION_CONTAINSTEXT | Xlsx | Xlsx, Html
148+
| Conditional::CONDITION_NOTCONTAINSTEXT | Xlsx | Xlsx, Html
149+
| Conditional::CONDITION_BEGINSWITH | Xlsx | Xlsx, Html
150+
| Conditional::CONDITION_ENDSWITH | Xlsx | Xlsx, Html
151+
Dates Occurring | Conditional::CONDITION_TIMEPERIOD | Xlsx | Xlsx, Html
152+
Blanks | Conditional::CONDITION_CONTAINSBLANKS | Xlsx | Xlsx, Html
153+
No Blanks | Conditional::CONDITION_NOTCONTAINSBLANKS | Xlsx | Xlsx, Html
154+
Errors | Conditional::CONDITION_CONTAINSERRORS | Xlsx | Xlsx, Html
155+
No Errors | Conditional::CONDITION_NOTCONTAINSERRORS | Xlsx | Xlsx, Html
156+
Duplicates/Unique | Conditional::CONDITION_DUPLICATES | Xlsx | Xlsx, Html
157+
| Conditional::CONDITION_UNIQUE | Xlsx | Xlsx, Html
158+
Use a formula | Conditional::CONDITION_EXPRESSION | Xlsx | Xlsx, Xls, Html
159+
Data Bars | Conditional::CONDITION_DATABAR | Xlsx | Xlsx, Html
160+
Colour Scales | Conditional::COLORSCALE | Xlsx | Html
161+
162+
To enable conditional formatting for Html writer, use:
163+
164+
```php
165+
$writer = new HtmlWriter($spreadsheet);
166+
$writer->setConditionalFormatting(true);
167+
```
160168

161169
The following Conditional Types are currently not supported by any Readers or Writers:
162170

@@ -165,7 +173,6 @@ MS Excel | Conditional Type
165173
Above/Below Average | ?
166174
Top/Bottom Items | ?
167175
Top/Bottom %age | ?
168-
Colour Scales |?
169176
Icon Sets | ?
170177

171178
Unsupported types will by ignored by the Readers, and cannot be created through PHPSpreadsheet.

docs/topics/tables.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Tables
2+
3+
## Introduction
4+
5+
To make managing and analyzing a group of related data easier, you can turn a range of cells into an Excel table (previously known as an Excel list).
6+
7+
## Support
8+
9+
Currently tables are supported in Xlsx reader and Html Writer
10+
11+
To enable table formatting for Html writer, use:
12+
13+
```php
14+
$writer = new HtmlWriter($spreadsheet);
15+
$writer->setConditionalFormatting(true);
16+
```

src/PhpSpreadsheet/Reader/Xlsx.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
692692
$this->styleReader->setNamespace($mainNS);
693693
$this->styleReader->setStyleBaseData($theme, $styles, $cellStyles);
694694
$dxfs = $this->styleReader->dxfs($this->readDataOnly);
695+
$tableStyles = $this->styleReader->tableStyles($this->readDataOnly);
695696
$styles = $this->styleReader->styles();
696697

697698
// Read content after setting the styles
@@ -1000,7 +1001,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
10001001
$this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels');
10011002
}
10021003

1003-
$this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS);
1004+
$this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs);
10041005

10051006
if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) {
10061007
foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) {
@@ -2299,12 +2300,14 @@ private function readTables(
22992300
string $dir,
23002301
string $fileWorksheet,
23012302
ZipArchive $zip,
2302-
string $namespaceTable
2303+
string $namespaceTable,
2304+
array $tableStyles,
2305+
array $dxfs
23032306
): void {
23042307
if ($xmlSheet && $xmlSheet->tableParts) {
23052308
$attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0];
23062309
if (((int) $attributes['count']) > 0) {
2307-
$this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable);
2310+
$this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs);
23082311
}
23092312
}
23102313
}
@@ -2315,7 +2318,9 @@ private function readTablesInTablesFile(
23152318
string $fileWorksheet,
23162319
ZipArchive $zip,
23172320
Worksheet $docSheet,
2318-
string $namespaceTable
2321+
string $namespaceTable,
2322+
array $tableStyles,
2323+
array $dxfs
23192324
): void {
23202325
foreach ($xmlSheet->tableParts->tablePart as $tablePart) {
23212326
$relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT);
@@ -2334,7 +2339,7 @@ private function readTablesInTablesFile(
23342339

23352340
if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) {
23362341
$tableXml = $this->loadZip($relationshipFilePath, $namespaceTable);
2337-
(new TableReader($docSheet, $tableXml))->load();
2342+
(new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs);
23382343
}
23392344
}
23402345
}

src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php

+7
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ private function setConditionalStyles(Worksheet $worksheet, array $conditionals,
193193
// N.B. In Excel UI, intersection is space and union is comma.
194194
// But in Xml, intersection is comma and union is space.
195195
$cellRangeReference = str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], strtoupper($cellRangeReference));
196+
197+
foreach ($conditionalStyles as $cs) {
198+
$scale = $cs->getColorScale();
199+
if ($scale !== null) {
200+
$scale->setSqRef($cellRangeReference, $worksheet);
201+
}
202+
}
196203
$worksheet->getStyle($cellRangeReference)->setConditionalStyles($conditionalStyles);
197204
}
198205
}

src/PhpSpreadsheet/Reader/Xlsx/Styles.php

+41
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
1313
use PhpOffice\PhpSpreadsheet\Style\Protection;
1414
use PhpOffice\PhpSpreadsheet\Style\Style;
15+
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableDxfsStyle;
1516
use SimpleXMLElement;
1617
use stdClass;
1718

@@ -447,6 +448,46 @@ public function dxfs(bool $readDataOnly = false): array
447448
return $dxfs;
448449
}
449450

451+
// get TableStyles
452+
public function tableStyles(bool $readDataOnly = false): array
453+
{
454+
$tableStyles = [];
455+
if (!$readDataOnly && $this->styleXml) {
456+
// Conditional Styles
457+
if ($this->styleXml->tableStyles) {
458+
foreach ($this->styleXml->tableStyles->tableStyle as $s) {
459+
$attrs = Xlsx::getAttributes($s);
460+
if (isset($attrs['name'][0])) {
461+
$style = new TableDxfsStyle((string) ($attrs['name'][0]));
462+
foreach ($s->tableStyleElement as $e) {
463+
$a = Xlsx::getAttributes($e);
464+
if (isset($a['dxfId'][0], $a['type'][0])) {
465+
switch ($a['type'][0]) {
466+
case 'headerRow':
467+
$style->setHeaderRow((int) ($a['dxfId'][0]));
468+
469+
break;
470+
case 'firstRowStripe':
471+
$style->setFirstRowStripe((int) ($a['dxfId'][0]));
472+
473+
break;
474+
case 'secondRowStripe':
475+
$style->setSecondRowStripe((int) ($a['dxfId'][0]));
476+
477+
break;
478+
default:
479+
}
480+
}
481+
}
482+
$tableStyles[] = $style;
483+
}
484+
}
485+
}
486+
}
487+
488+
return $tableStyles;
489+
}
490+
450491
public function styles(): array
451492
{
452493
return $this->styles;

src/PhpSpreadsheet/Reader/Xlsx/TableReader.php

+11-5
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ public function __construct(Worksheet $workSheet, SimpleXMLElement $tableXml)
2525
/**
2626
* Loads Table into the Worksheet.
2727
*/
28-
public function load(): void
28+
public function load(array $tableStyles, array $dxfs): void
2929
{
3030
$this->tableAttributes = $this->tableXml->attributes() ?? [];
3131
// Remove all "$" in the table range
3232
$tableRange = (string) preg_replace('/\$/', '', $this->tableAttributes['ref'] ?? '');
3333
if (str_contains($tableRange, ':')) {
34-
$this->readTable($tableRange);
34+
$this->readTable($tableRange, $tableStyles, $dxfs);
3535
}
3636
}
3737

3838
/**
3939
* Read Table from xml.
4040
*/
41-
private function readTable(string $tableRange): void
41+
private function readTable(string $tableRange, array $tableStyles, array $dxfs): void
4242
{
4343
$table = new Table($tableRange);
4444
$table->setName((string) ($this->tableAttributes['displayName'] ?? ''));
@@ -47,7 +47,7 @@ private function readTable(string $tableRange): void
4747

4848
$this->readTableAutoFilter($table, $this->tableXml->autoFilter);
4949
$this->readTableColumns($table, $this->tableXml->tableColumns);
50-
$this->readTableStyle($table, $this->tableXml->tableStyleInfo);
50+
$this->readTableStyle($table, $this->tableXml->tableStyleInfo, $tableStyles, $dxfs);
5151

5252
(new AutoFilter($table, $this->tableXml))->load();
5353
$this->worksheet->addTable($table);
@@ -100,7 +100,7 @@ private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXm
100100
/**
101101
* Reads TableStyle from xml.
102102
*/
103-
private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void
103+
private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml, array $tableStyles, array $dxfs): void
104104
{
105105
$tableStyle = new TableStyle();
106106
$attributes = $tableStyleInfoXml->attributes();
@@ -110,6 +110,12 @@ private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXm
110110
$tableStyle->setShowColumnStripes((string) $attributes['showColumnStripes'] === '1');
111111
$tableStyle->setShowFirstColumn((string) $attributes['showFirstColumn'] === '1');
112112
$tableStyle->setShowLastColumn((string) $attributes['showLastColumn'] === '1');
113+
114+
foreach ($tableStyles as $style) {
115+
if ($style->getName() === (string) $attributes['name']) {
116+
$tableStyle->setTableDxfsStyle($style, $dxfs);
117+
}
118+
}
113119
}
114120
$table->setStyle($tableStyle);
115121
}

src/PhpSpreadsheet/Style/Conditional.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,16 @@ public function addCondition($condition): static
269269
/**
270270
* Get Style.
271271
*/
272-
public function getStyle(): Style
272+
public function getStyle(mixed $cellData = null): Style
273273
{
274+
if ($this->conditionType === self::CONDITION_COLORSCALE && $cellData !== null && $this->colorScale !== null && is_numeric($cellData)) {
275+
$style = new Style();
276+
$style->getFill()->setFillType(Fill::FILL_SOLID);
277+
$style->getFill()->getStartColor()->setARGB($this->colorScale->getColorForValue((float) $cellData));
278+
279+
return $style;
280+
}
281+
274282
return $this->style;
275283
}
276284

src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public function evaluateConditional(Conditional $conditional): bool
111111
// Last 7 Days AND(TODAY()-FLOOR(<Cell Reference>,1)<=6,FLOOR(<Cell Reference>,1)<=TODAY())
112112
Conditional::CONDITION_TIMEPERIOD,
113113
Conditional::CONDITION_EXPRESSION => $this->processExpression($conditional),
114+
Conditional::CONDITION_COLORSCALE => $this->processColorScale($conditional),
114115
default => false,
115116
};
116117
}
@@ -141,8 +142,8 @@ protected function conditionCellAdjustment(array $matches): float|int|string
141142
{
142143
$column = $matches[6];
143144
$row = $matches[7];
144-
145145
if (!str_contains($column, '$')) {
146+
// $column = Coordinate::stringFromColumnIndex($this->cellColumn);
146147
$column = Coordinate::columnIndexFromString($column);
147148
$column += $this->cellColumn - $this->referenceColumn;
148149
$column = Coordinate::stringFromColumnIndex($column);
@@ -214,6 +215,15 @@ protected function processOperatorComparison(Conditional $conditional): bool
214215
return $this->evaluateExpression($expression);
215216
}
216217

218+
protected function processColorScale(Conditional $conditional): bool
219+
{
220+
if ($this->wrapCellValue() !== '' && is_float($this->wrapCellValue()) && $conditional->getColorScale() !== null && $conditional->getColorScale()->colorScaleReadyForUse()) {
221+
return true;
222+
}
223+
224+
return false;
225+
}
226+
217227
protected function processRangeOperator(Conditional $conditional): bool
218228
{
219229
$conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());

src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php

+28-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ class CellStyleAssessor
1212

1313
protected StyleMerger $styleMerger;
1414

15+
protected Cell $cell;
16+
1517
public function __construct(Cell $cell, string $conditionalRange)
1618
{
19+
$this->cell = $cell;
1720
$this->cellMatcher = new CellMatcher($cell, $conditionalRange);
1821
$this->styleMerger = new StyleMerger($cell->getStyle());
1922
}
@@ -26,7 +29,7 @@ public function matchConditions(array $conditionalStyles = []): Style
2629
foreach ($conditionalStyles as $conditional) {
2730
if ($this->cellMatcher->evaluateConditional($conditional) === true) {
2831
// Merging the conditional style into the base style goes in here
29-
$this->styleMerger->mergeStyle($conditional->getStyle());
32+
$this->styleMerger->mergeStyle($conditional->getStyle($this->cell->getValue()));
3033
if ($conditional->getStopIfTrue() === true) {
3134
break;
3235
}
@@ -35,4 +38,28 @@ public function matchConditions(array $conditionalStyles = []): Style
3538

3639
return $this->styleMerger->getStyle();
3740
}
41+
42+
/**
43+
* @param Conditional[] $conditionalStyles
44+
*/
45+
public function matchConditionsReturnNullIfNoneMatched(array $conditionalStyles, string $cellData): ?Style
46+
{
47+
$matched = false;
48+
$value = (float) $cellData;
49+
foreach ($conditionalStyles as $conditional) {
50+
if ($this->cellMatcher->evaluateConditional($conditional) === true) {
51+
$matched = true;
52+
// Merging the conditional style into the base style goes in here
53+
$this->styleMerger->mergeStyle($conditional->getStyle($value));
54+
if ($conditional->getStopIfTrue() === true) {
55+
break;
56+
}
57+
}
58+
}
59+
if ($matched) {
60+
return $this->styleMerger->getStyle();
61+
}
62+
63+
return null;
64+
}
3865
}

0 commit comments

Comments
 (0)