| 1441 |
ariadna |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
|
|
|
4 |
|
|
|
5 |
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles as StyleReader;
|
|
|
6 |
use PhpOffice\PhpSpreadsheet\Style\Color;
|
|
|
7 |
use PhpOffice\PhpSpreadsheet\Style\Conditional;
|
|
|
8 |
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale;
|
|
|
9 |
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
|
|
|
10 |
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
|
|
|
11 |
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueObject;
|
|
|
12 |
use PhpOffice\PhpSpreadsheet\Style\Style as Style;
|
|
|
13 |
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
|
|
14 |
use SimpleXMLElement;
|
|
|
15 |
use stdClass;
|
|
|
16 |
|
|
|
17 |
class ConditionalStyles
|
|
|
18 |
{
|
|
|
19 |
private Worksheet $worksheet;
|
|
|
20 |
|
|
|
21 |
private SimpleXMLElement $worksheetXml;
|
|
|
22 |
|
|
|
23 |
private array $ns;
|
|
|
24 |
|
|
|
25 |
private array $dxfs;
|
|
|
26 |
|
|
|
27 |
private StyleReader $styleReader;
|
|
|
28 |
|
|
|
29 |
public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs, StyleReader $styleReader)
|
|
|
30 |
{
|
|
|
31 |
$this->worksheet = $workSheet;
|
|
|
32 |
$this->worksheetXml = $worksheetXml;
|
|
|
33 |
$this->dxfs = $dxfs;
|
|
|
34 |
$this->styleReader = $styleReader;
|
|
|
35 |
}
|
|
|
36 |
|
|
|
37 |
public function load(): void
|
|
|
38 |
{
|
|
|
39 |
$selectedCells = $this->worksheet->getSelectedCells();
|
|
|
40 |
|
|
|
41 |
$this->setConditionalStyles(
|
|
|
42 |
$this->worksheet,
|
|
|
43 |
$this->readConditionalStyles($this->worksheetXml),
|
|
|
44 |
$this->worksheetXml->extLst
|
|
|
45 |
);
|
|
|
46 |
|
|
|
47 |
$this->worksheet->setSelectedCells($selectedCells);
|
|
|
48 |
}
|
|
|
49 |
|
|
|
50 |
public function loadFromExt(): void
|
|
|
51 |
{
|
|
|
52 |
$selectedCells = $this->worksheet->getSelectedCells();
|
|
|
53 |
|
|
|
54 |
$this->ns = $this->worksheetXml->getNamespaces(true);
|
|
|
55 |
$this->setConditionalsFromExt(
|
|
|
56 |
$this->readConditionalsFromExt($this->worksheetXml->extLst)
|
|
|
57 |
);
|
|
|
58 |
|
|
|
59 |
$this->worksheet->setSelectedCells($selectedCells);
|
|
|
60 |
}
|
|
|
61 |
|
|
|
62 |
private function setConditionalsFromExt(array $conditionals): void
|
|
|
63 |
{
|
|
|
64 |
foreach ($conditionals as $conditionalRange => $cfRules) {
|
|
|
65 |
ksort($cfRules);
|
|
|
66 |
// Priority is used as the key for sorting; but may not start at 0,
|
|
|
67 |
// so we use array_values to reset the index after sorting.
|
|
|
68 |
$this->worksheet->getStyle($conditionalRange)
|
|
|
69 |
->setConditionalStyles(array_values($cfRules));
|
|
|
70 |
}
|
|
|
71 |
}
|
|
|
72 |
|
|
|
73 |
private function readConditionalsFromExt(SimpleXMLElement $extLst): array
|
|
|
74 |
{
|
|
|
75 |
$conditionals = [];
|
|
|
76 |
if (!isset($extLst->ext)) {
|
|
|
77 |
return $conditionals;
|
|
|
78 |
}
|
|
|
79 |
|
|
|
80 |
foreach ($extLst->ext as $extlstcond) {
|
|
|
81 |
$extAttrs = $extlstcond->attributes() ?? [];
|
|
|
82 |
$extUri = (string) ($extAttrs['uri'] ?? '');
|
|
|
83 |
if ($extUri !== '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') {
|
|
|
84 |
continue;
|
|
|
85 |
}
|
|
|
86 |
$conditionalFormattingRuleXml = $extlstcond->children($this->ns['x14']);
|
|
|
87 |
if (!$conditionalFormattingRuleXml->conditionalFormattings) {
|
|
|
88 |
return [];
|
|
|
89 |
}
|
|
|
90 |
|
|
|
91 |
foreach ($conditionalFormattingRuleXml->children($this->ns['x14']) as $extFormattingXml) {
|
|
|
92 |
$extFormattingRangeXml = $extFormattingXml->children($this->ns['xm']);
|
|
|
93 |
if (!$extFormattingRangeXml->sqref) {
|
|
|
94 |
continue;
|
|
|
95 |
}
|
|
|
96 |
|
|
|
97 |
$sqref = (string) $extFormattingRangeXml->sqref;
|
|
|
98 |
$extCfRuleXml = $extFormattingXml->cfRule;
|
|
|
99 |
|
|
|
100 |
$attributes = $extCfRuleXml->attributes();
|
|
|
101 |
if (!$attributes) {
|
|
|
102 |
continue;
|
|
|
103 |
}
|
|
|
104 |
$conditionType = (string) $attributes->type;
|
|
|
105 |
if (
|
|
|
106 |
!Conditional::isValidConditionType($conditionType)
|
|
|
107 |
|| $conditionType === Conditional::CONDITION_DATABAR
|
|
|
108 |
) {
|
|
|
109 |
continue;
|
|
|
110 |
}
|
|
|
111 |
|
|
|
112 |
$priority = (int) $attributes->priority;
|
|
|
113 |
|
|
|
114 |
$conditional = $this->readConditionalRuleFromExt($extCfRuleXml, $attributes);
|
|
|
115 |
$cfStyle = $this->readStyleFromExt($extCfRuleXml);
|
|
|
116 |
$conditional->setStyle($cfStyle);
|
|
|
117 |
$conditionals[$sqref][$priority] = $conditional;
|
|
|
118 |
}
|
|
|
119 |
}
|
|
|
120 |
|
|
|
121 |
return $conditionals;
|
|
|
122 |
}
|
|
|
123 |
|
|
|
124 |
private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleXMLElement $attributes): Conditional
|
|
|
125 |
{
|
|
|
126 |
$conditionType = (string) $attributes->type;
|
|
|
127 |
$operatorType = (string) $attributes->operator;
|
|
|
128 |
$priority = (int) (string) $attributes->priority;
|
|
|
129 |
|
|
|
130 |
$operands = [];
|
|
|
131 |
foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) {
|
|
|
132 |
$operands[] = (string) $cfRuleOperandsXml;
|
|
|
133 |
}
|
|
|
134 |
|
|
|
135 |
$conditional = new Conditional();
|
|
|
136 |
$conditional->setConditionType($conditionType);
|
|
|
137 |
$conditional->setOperatorType($operatorType);
|
|
|
138 |
$conditional->setPriority($priority);
|
|
|
139 |
if (
|
|
|
140 |
$conditionType === Conditional::CONDITION_CONTAINSTEXT
|
|
|
141 |
|| $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT
|
|
|
142 |
|| $conditionType === Conditional::CONDITION_BEGINSWITH
|
|
|
143 |
|| $conditionType === Conditional::CONDITION_ENDSWITH
|
|
|
144 |
|| $conditionType === Conditional::CONDITION_TIMEPERIOD
|
|
|
145 |
) {
|
|
|
146 |
$conditional->setText(array_pop($operands) ?? '');
|
|
|
147 |
}
|
|
|
148 |
$conditional->setConditions($operands);
|
|
|
149 |
|
|
|
150 |
return $conditional;
|
|
|
151 |
}
|
|
|
152 |
|
|
|
153 |
private function readStyleFromExt(SimpleXMLElement $extCfRuleXml): Style
|
|
|
154 |
{
|
|
|
155 |
$cfStyle = new Style(false, true);
|
|
|
156 |
if ($extCfRuleXml->dxf) {
|
|
|
157 |
$styleXML = $extCfRuleXml->dxf->children();
|
|
|
158 |
|
|
|
159 |
if ($styleXML->borders) {
|
|
|
160 |
$this->styleReader->readBorderStyle($cfStyle->getBorders(), $styleXML->borders);
|
|
|
161 |
}
|
|
|
162 |
if ($styleXML->fill) {
|
|
|
163 |
$this->styleReader->readFillStyle($cfStyle->getFill(), $styleXML->fill);
|
|
|
164 |
}
|
|
|
165 |
}
|
|
|
166 |
|
|
|
167 |
return $cfStyle;
|
|
|
168 |
}
|
|
|
169 |
|
|
|
170 |
private function readConditionalStyles(SimpleXMLElement $xmlSheet): array
|
|
|
171 |
{
|
|
|
172 |
$conditionals = [];
|
|
|
173 |
foreach ($xmlSheet->conditionalFormatting as $conditional) {
|
|
|
174 |
foreach ($conditional->cfRule as $cfRule) {
|
|
|
175 |
if (Conditional::isValidConditionType((string) $cfRule['type']) && (!isset($cfRule['dxfId']) || isset($this->dxfs[(int) ($cfRule['dxfId'])]))) {
|
|
|
176 |
$conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule;
|
|
|
177 |
} elseif ((string) $cfRule['type'] == Conditional::CONDITION_DATABAR) {
|
|
|
178 |
$conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule;
|
|
|
179 |
}
|
|
|
180 |
}
|
|
|
181 |
}
|
|
|
182 |
|
|
|
183 |
return $conditionals;
|
|
|
184 |
}
|
|
|
185 |
|
|
|
186 |
private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void
|
|
|
187 |
{
|
|
|
188 |
foreach ($conditionals as $cellRangeReference => $cfRules) {
|
|
|
189 |
ksort($cfRules); // no longer needed for Xlsx, but helps Xls
|
|
|
190 |
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);
|
|
|
191 |
|
|
|
192 |
// Extract all cell references in $cellRangeReference
|
|
|
193 |
// N.B. In Excel UI, intersection is space and union is comma.
|
|
|
194 |
// But in Xml, intersection is comma and union is space.
|
|
|
195 |
$cellRangeReference = str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], strtoupper($cellRangeReference));
|
|
|
196 |
$worksheet->getStyle($cellRangeReference)->setConditionalStyles($conditionalStyles);
|
|
|
197 |
}
|
|
|
198 |
}
|
|
|
199 |
|
|
|
200 |
private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
|
|
|
201 |
{
|
|
|
202 |
$conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst);
|
|
|
203 |
$conditionalStyles = [];
|
|
|
204 |
|
|
|
205 |
/** @var SimpleXMLElement $cfRule */
|
|
|
206 |
foreach ($cfRules as $cfRule) {
|
|
|
207 |
$objConditional = new Conditional();
|
|
|
208 |
$objConditional->setConditionType((string) $cfRule['type']);
|
|
|
209 |
$objConditional->setOperatorType((string) $cfRule['operator']);
|
|
|
210 |
$objConditional->setPriority((int) (string) $cfRule['priority']);
|
|
|
211 |
$objConditional->setNoFormatSet(!isset($cfRule['dxfId']));
|
|
|
212 |
|
|
|
213 |
if ((string) $cfRule['text'] != '') {
|
|
|
214 |
$objConditional->setText((string) $cfRule['text']);
|
|
|
215 |
} elseif ((string) $cfRule['timePeriod'] != '') {
|
|
|
216 |
$objConditional->setText((string) $cfRule['timePeriod']);
|
|
|
217 |
}
|
|
|
218 |
|
|
|
219 |
if (isset($cfRule['stopIfTrue']) && (int) $cfRule['stopIfTrue'] === 1) {
|
|
|
220 |
$objConditional->setStopIfTrue(true);
|
|
|
221 |
}
|
|
|
222 |
|
|
|
223 |
if (count($cfRule->formula) >= 1) {
|
|
|
224 |
foreach ($cfRule->formula as $formulax) {
|
|
|
225 |
$formula = (string) $formulax;
|
|
|
226 |
$formula = str_replace(['_xlfn.', '_xlws.'], '', $formula);
|
|
|
227 |
if ($formula === 'TRUE') {
|
|
|
228 |
$objConditional->addCondition(true);
|
|
|
229 |
} elseif ($formula === 'FALSE') {
|
|
|
230 |
$objConditional->addCondition(false);
|
|
|
231 |
} else {
|
|
|
232 |
$objConditional->addCondition($formula);
|
|
|
233 |
}
|
|
|
234 |
}
|
|
|
235 |
} else {
|
|
|
236 |
$objConditional->addCondition('');
|
|
|
237 |
}
|
|
|
238 |
|
|
|
239 |
if (isset($cfRule->dataBar)) {
|
|
|
240 |
$objConditional->setDataBar(
|
|
|
241 |
$this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions)
|
|
|
242 |
);
|
|
|
243 |
} elseif (isset($cfRule->colorScale)) {
|
|
|
244 |
$objConditional->setColorScale(
|
|
|
245 |
$this->readColorScale($cfRule)
|
|
|
246 |
);
|
|
|
247 |
} elseif (isset($cfRule['dxfId'])) {
|
|
|
248 |
$objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]);
|
|
|
249 |
}
|
|
|
250 |
|
|
|
251 |
$conditionalStyles[] = $objConditional;
|
|
|
252 |
}
|
|
|
253 |
|
|
|
254 |
return $conditionalStyles;
|
|
|
255 |
}
|
|
|
256 |
|
|
|
257 |
private function readDataBarOfConditionalRule(SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar
|
|
|
258 |
{
|
|
|
259 |
$dataBar = new ConditionalDataBar();
|
|
|
260 |
//dataBar attribute
|
|
|
261 |
if (isset($cfRule->dataBar['showValue'])) {
|
|
|
262 |
$dataBar->setShowValue((bool) $cfRule->dataBar['showValue']);
|
|
|
263 |
}
|
|
|
264 |
|
|
|
265 |
//dataBar children
|
|
|
266 |
//conditionalFormatValueObjects
|
|
|
267 |
$cfvoXml = $cfRule->dataBar->cfvo;
|
|
|
268 |
$cfvoIndex = 0;
|
|
|
269 |
foreach ((count($cfvoXml) > 1 ? $cfvoXml : [$cfvoXml]) as $cfvo) { //* @phpstan-ignore-line
|
|
|
270 |
if ($cfvoIndex === 0) {
|
|
|
271 |
$dataBar->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val']));
|
|
|
272 |
}
|
|
|
273 |
if ($cfvoIndex === 1) {
|
|
|
274 |
$dataBar->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val']));
|
|
|
275 |
}
|
|
|
276 |
++$cfvoIndex;
|
|
|
277 |
}
|
|
|
278 |
|
|
|
279 |
//color
|
|
|
280 |
if (isset($cfRule->dataBar->color)) {
|
|
|
281 |
$dataBar->setColor($this->styleReader->readColor($cfRule->dataBar->color));
|
|
|
282 |
}
|
|
|
283 |
//extLst
|
|
|
284 |
$this->readDataBarExtLstOfConditionalRule($dataBar, $cfRule, $conditionalFormattingRuleExtensions);
|
|
|
285 |
|
|
|
286 |
return $dataBar;
|
|
|
287 |
}
|
|
|
288 |
|
|
|
289 |
private function readColorScale(SimpleXMLElement|stdClass $cfRule): ConditionalColorScale
|
|
|
290 |
{
|
|
|
291 |
$colorScale = new ConditionalColorScale();
|
|
|
292 |
$count = count($cfRule->colorScale->cfvo);
|
|
|
293 |
$idx = 0;
|
|
|
294 |
foreach ($cfRule->colorScale->cfvo as $cfvoXml) {
|
|
|
295 |
$attr = $cfvoXml->attributes() ?? [];
|
|
|
296 |
$type = (string) ($attr['type'] ?? '');
|
|
|
297 |
$val = $attr['val'] ?? null;
|
|
|
298 |
if ($idx === 0) {
|
|
|
299 |
$method = 'setMinimumConditionalFormatValueObject';
|
|
|
300 |
} elseif ($idx === 1 && $count === 3) {
|
|
|
301 |
$method = 'setMidpointConditionalFormatValueObject';
|
|
|
302 |
} else {
|
|
|
303 |
$method = 'setMaximumConditionalFormatValueObject';
|
|
|
304 |
}
|
|
|
305 |
if ($type !== 'formula') {
|
|
|
306 |
$colorScale->$method(new ConditionalFormatValueObject($type, $val));
|
|
|
307 |
} else {
|
|
|
308 |
$colorScale->$method(new ConditionalFormatValueObject($type, null, $val));
|
|
|
309 |
}
|
|
|
310 |
++$idx;
|
|
|
311 |
}
|
|
|
312 |
$idx = 0;
|
|
|
313 |
foreach ($cfRule->colorScale->color as $color) {
|
|
|
314 |
$rgb = $this->styleReader->readColor($color);
|
|
|
315 |
if ($idx === 0) {
|
|
|
316 |
$colorScale->setMinimumColor(new Color($rgb));
|
|
|
317 |
} elseif ($idx === 1 && $count === 3) {
|
|
|
318 |
$colorScale->setMidpointColor(new Color($rgb));
|
|
|
319 |
} else {
|
|
|
320 |
$colorScale->setMaximumColor(new Color($rgb));
|
|
|
321 |
}
|
|
|
322 |
++$idx;
|
|
|
323 |
}
|
|
|
324 |
|
|
|
325 |
return $colorScale;
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): void
|
|
|
329 |
{
|
|
|
330 |
if (isset($cfRule->extLst)) {
|
|
|
331 |
$ns = $cfRule->extLst->getNamespaces(true);
|
|
|
332 |
foreach ((count($cfRule->extLst) > 0 ? $cfRule->extLst->ext : [$cfRule->extLst->ext]) as $ext) { //* @phpstan-ignore-line
|
|
|
333 |
$extId = (string) $ext->children($ns['x14'])->id;
|
|
|
334 |
if (isset($conditionalFormattingRuleExtensions[$extId]) && (string) $ext['uri'] === '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}') {
|
|
|
335 |
$dataBar->setConditionalFormattingRuleExt($conditionalFormattingRuleExtensions[$extId]);
|
|
|
336 |
}
|
|
|
337 |
}
|
|
|
338 |
}
|
|
|
339 |
}
|
|
|
340 |
}
|