Passed
Pull Request — master (#4396)
by Owen
14:27
created

Cell::updateIfCellIsTableHeader()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 17
rs 9.2222
ccs 10
cts 10
cp 1
cc 6
nc 5
nop 4
crap 6
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Cell;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
9
use PhpOffice\PhpSpreadsheet\Collection\Cells;
10
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
11
use PhpOffice\PhpSpreadsheet\RichText\RichText;
12
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
13
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
14
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
15
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
16
use PhpOffice\PhpSpreadsheet\Style\Protection;
17
use PhpOffice\PhpSpreadsheet\Style\Style;
18
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
19
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
20
use Stringable;
21
22
class Cell implements Stringable
23
{
24
    /**
25
     * Value binder to use.
26
     */
27
    private static ?IValueBinder $valueBinder = null;
28
29
    /**
30
     * Value of the cell.
31
     */
32
    private mixed $value;
33
34
    /**
35
     *    Calculated value of the cell (used for caching)
36
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
37
     *        create the original spreadsheet file.
38
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
39
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
40
     *        values used by the formula have changed since it was last calculated.
41
     *
42
     * @var mixed
43
     */
44
    private $calculatedValue;
45
46
    /**
47
     * Type of the cell data.
48
     */
49
    private string $dataType;
50
51
    /**
52
     * The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet).
53
     */
54
    private ?Cells $parent;
55
56
    /**
57
     * Index to the cellXf reference for the styling of this cell.
58
     */
59
    private int $xfIndex = 0;
60
61
    /**
62
     * Attributes of the formula.
63
     *
64
     * @var null|array<string, string>
65
     */
66
    private ?array $formulaAttributes = null;
67
68
    private IgnoredErrors $ignoredErrors;
69
70
    /**
71
     * Update the cell into the cell collection.
72
     *
73
     * @throws SpreadsheetException
74
     */
75 10091
    public function updateInCollection(): self
76
    {
77 10091
        $parent = $this->parent;
78 10091
        if ($parent === null) {
79 2
            throw new SpreadsheetException('Cannot update when cell is not bound to a worksheet');
80
        }
81 10089
        $parent->update($this);
82
83 10089
        return $this;
84
    }
85
86 9605
    public function detach(): void
87
    {
88 9605
        $this->parent = null;
89
    }
90
91 8849
    public function attach(Cells $parent): void
92
    {
93 8849
        $this->parent = $parent;
94
    }
95
96
    /**
97
     * Create a new Cell.
98
     *
99
     * @throws SpreadsheetException
100
     */
101 10130
    public function __construct(mixed $value, ?string $dataType, Worksheet $worksheet)
102
    {
103
        // Initialise cell value
104 10130
        $this->value = $value;
105
106
        // Set worksheet cache
107 10130
        $this->parent = $worksheet->getCellCollection();
108
109
        // Set datatype?
110 10130
        if ($dataType !== null) {
111 10130
            if ($dataType == DataType::TYPE_STRING2) {
112
                $dataType = DataType::TYPE_STRING;
113
            }
114 10130
            $this->dataType = $dataType;
115
        } else {
116
            $valueBinder = $worksheet->getParent()?->getValueBinder() ?? self::getValueBinder();
117
            if ($valueBinder->bindValue($this, $value) === false) {
118
                throw new SpreadsheetException('Value could not be bound to cell.');
119
            }
120
        }
121 10130
        $this->ignoredErrors = new IgnoredErrors();
122
    }
123
124
    /**
125
     * Get cell coordinate column.
126
     *
127
     * @throws SpreadsheetException
128
     */
129 8149
    public function getColumn(): string
130
    {
131 8149
        $parent = $this->parent;
132 8149
        if ($parent === null) {
133 1
            throw new SpreadsheetException('Cannot get column when cell is not bound to a worksheet');
134
        }
135
136 8148
        return $parent->getCurrentColumn();
137
    }
138
139
    /**
140
     * Get cell coordinate row.
141
     *
142
     * @throws SpreadsheetException
143
     */
144 644
    public function getRow(): int
145
    {
146 644
        $parent = $this->parent;
147 644
        if ($parent === null) {
148 1
            throw new SpreadsheetException('Cannot get row when cell is not bound to a worksheet');
149
        }
150
151 643
        return $parent->getCurrentRow();
152
    }
153
154
    /**
155
     * Get cell coordinate.
156
     *
157
     * @throws SpreadsheetException
158
     */
159 10102
    public function getCoordinate(): string
160
    {
161 10102
        $parent = $this->parent;
162 10102
        if ($parent !== null) {
163 10102
            $coordinate = $parent->getCurrentCoordinate();
164
        } else {
165 2
            $coordinate = null;
166
        }
167 10102
        if ($coordinate === null) {
168 2
            throw new SpreadsheetException('Coordinate no longer exists');
169
        }
170
171 10102
        return $coordinate;
172
    }
173
174
    /**
175
     * Get cell value.
176
     */
177 9431
    public function getValue(): mixed
178
    {
179 9431
        return $this->value;
180
    }
181
182 519
    public function getValueString(): string
183
    {
184 519
        return StringHelper::convertToString($this->value, false);
185
    }
186 519
187
    /**
188
     * Get cell value with formatting.
189
     */
190
    public function getFormattedValue(): string
191
    {
192 100
        $currentCalendar = SharedDate::getExcelCalendar();
193
        SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
194 100
        $formattedValue = (string) NumberFormat::toFormattedString(
195 100
            $this->getCalculatedValueString(),
196 100
            (string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
197 100
        );
198 100
        SharedDate::setExcelCalendar($currentCalendar);
199 100
200 100
        return $formattedValue;
201
    }
202 100
203
    protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void
204
    {
205 10036
        $oldValue = StringHelper::convertToString($oldValue, false);
206
        $newValue = StringHelper::convertToString($newValue, false);
207 10036
        if (StringHelper::strToLower($oldValue) === StringHelper::strToLower($newValue) || $workSheet === null) {
208 10036
            return;
209 10036
        }
210 929
211
        foreach ($workSheet->getTableCollection() as $table) {
212
            /** @var Table $table */
213 10013
            if ($cell->isInRange($table->getRange())) {
214
                $rangeRowsColumns = Coordinate::getRangeBoundaries($table->getRange());
215 10
                if ($cell->getRow() === (int) $rangeRowsColumns[0][1]) {
216 7
                    Table\Column::updateStructuredReferences($workSheet, $oldValue, $newValue);
217 7
                }
218 4
219
                return;
220
            }
221 7
        }
222
    }
223
224
    /**
225
     * Set cell value.
226
     *
227
     *    Sets the value for a cell, automatically determining the datatype using the value binder
228
     *
229
     * @param mixed $value Value
230
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
231
     *
232
     * @throws SpreadsheetException
233
     */
234
    public function setValue(mixed $value, ?IValueBinder $binder = null): self
235
    {
236 9629
        // Cells?->Worksheet?->Spreadsheet
237
        $binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder();
238
        if (!$binder->bindValue($this, $value)) {
239 9629
            throw new SpreadsheetException('Value could not be bound to cell.');
240 9629
        }
241
242
        return $this;
243
    }
244 9628
245
    /**
246
     * Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
247
     *
248
     * @param mixed $value Value
249
     * @param string $dataType Explicit data type, see DataType::TYPE_*
250
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
251
     *             method, then it is your responsibility as an end-user developer to validate that the value and
252
     *             the datatype match.
253
     *       If you do mismatch value and datatype, then the value you enter may be changed to match the datatype
254
     *          that you specify.
255
     *
256
     * @throws SpreadsheetException
257
     */
258
    public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
259
    {
260 10042
        $oldValue = $this->value;
261
        $quotePrefix = false;
262 10042
263 10042
        // set the value according to data type
264
        switch ($dataType) {
265
            case DataType::TYPE_NULL:
266
                $this->value = null;
267 262
268 614
                break;
269
            case DataType::TYPE_STRING2:
270 614
                $dataType = DataType::TYPE_STRING;
271 262
                // no break
272 4
            case DataType::TYPE_STRING:
273
                // Synonym for string
274 262
                if (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
275
                    $quotePrefix = true;
276 5141
                }
277 39
                // no break
278
            case DataType::TYPE_INLINE:
279
                // Rich text
280 241
                $value2 = StringHelper::convertToString($value, true);
281
                $this->value = DataType::checkString(($value instanceof RichText) ? $value : $value2);
282 5157
283 2
                break;
284
            case DataType::TYPE_NUMERIC:
285 5155
                if ($value !== null && !is_bool($value) && !is_numeric($value)) {
286
                    throw new SpreadsheetException('Invalid numeric value for datatype Numeric');
287 5155
                }
288 240
                $this->value = 0 + $value;
289 7171
290 2
                break;
291
            case DataType::TYPE_FORMULA:
292 7170
                $this->value = StringHelper::convertToString($value, true);
293
294 7170
                break;
295 187
            case DataType::TYPE_BOOL:
296 8416
                $this->value = (bool) $value;
297 2
298
                break;
299 8415
            case DataType::TYPE_ISO_DATE:
300
                $this->value = SharedDate::convertIsoDate($value);
301 8415
                $dataType = DataType::TYPE_NUMERIC;
302 18
303 610
                break;
304
            case DataType::TYPE_ERROR:
305 610
                $this->value = DataType::checkErrorCode($value);
306
307 6
                break;
308 5
            default:
309
                throw new SpreadsheetException('Invalid datatype: ' . $dataType);
310 5
        }
311
312 21
        // set the datatype
313
        $this->dataType = $dataType;
314 21
315
        $this->updateInCollection();
316 1
        $cellCoordinate = $this->getCoordinate();
317
        self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value);
318
        $worksheet = $this->getWorksheet();
319
        $spreadsheet = $worksheet->getParent();
320 10037
        if (isset($spreadsheet) && $spreadsheet->getIndex($worksheet, true) >= 0) {
321
            $originalSelected = $worksheet->getSelectedCells();
322 10037
            $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
323 10036
            $style = $this->getStyle();
324 10036
            $oldQuotePrefix = $style->getQuotePrefix();
325 10036
            if ($oldQuotePrefix !== $quotePrefix) {
326 10036
                $style->setQuotePrefix($quotePrefix);
327 10036
            }
328 10032
            $worksheet->setSelectedCells($originalSelected);
329 10032
            if ($activeSheetIndex >= 0) {
330 10032
                $spreadsheet->setActiveSheetIndex($activeSheetIndex);
331 10032
            }
332 10032
        }
333 39
334
        return $this->getParent()?->get($cellCoordinate) ?? $this;
335 10032
    }
336 10032
337 10032
    public const CALCULATE_DATE_TIME_ASIS = 0;
338
    public const CALCULATE_DATE_TIME_FLOAT = 1;
339
    public const CALCULATE_TIME_FLOAT = 2;
340
341 10036
    private static int $calculateDateTimeType = self::CALCULATE_DATE_TIME_ASIS;
342
343
    public static function getCalculateDateTimeType(): int
344
    {
345
        return self::$calculateDateTimeType;
346
    }
347
348
    /** @throws CalculationException */
349
    public static function setCalculateDateTimeType(int $calculateDateTimeType): void
350 45
    {
351
        self::$calculateDateTimeType = match ($calculateDateTimeType) {
352 45
            self::CALCULATE_DATE_TIME_ASIS, self::CALCULATE_DATE_TIME_FLOAT, self::CALCULATE_TIME_FLOAT => $calculateDateTimeType,
353
            default => throw new CalculationException("Invalid value $calculateDateTimeType for calculated date time type"),
354
        };
355
    }
356 45
357
    /**
358 45
     * Convert date, time, or datetime from int to float if desired.
359 45
     */
360 1
    private function convertDateTimeInt(mixed $result): mixed
361 45
    {
362
        if (is_int($result)) {
363
            if (self::$calculateDateTimeType === self::CALCULATE_TIME_FLOAT) {
364
                if (SharedDate::isDateTime($this, $result, false)) {
365
                    $result = (float) $result;
366
                }
367 8810
            } elseif (self::$calculateDateTimeType === self::CALCULATE_DATE_TIME_FLOAT) {
368
                if (SharedDate::isDateTime($this, $result, true)) {
369 8810
                    $result = (float) $result;
370 5023
                }
371 4
            }
372 2
        }
373
374 5019
        return $result;
375 4
    }
376 3
377
    /**
378
     * Get calculated cell value converted to string.
379
     */
380
    public function getCalculatedValueString(): string
381 8810
    {
382
        $value = $this->getCalculatedValue();
383
        while (is_array($value)) {
384
            $value = array_shift($value);
385
        }
386
387 791
        return StringHelper::convertToString($value, false);
388
    }
389 791
390 791
    /**
391 12
     * Get calculated cell value.
392
     *
393
     * @param bool $resetLog Whether the calculation engine logger should be reset or not
394 791
     *
395
     * @throws CalculationException
396
     */
397
    public function getCalculatedValue(bool $resetLog = true): mixed
398
    {
399
        $title = 'unknown';
400
        $oldAttributes = $this->formulaAttributes;
401
        $oldAttributesT = $oldAttributes['t'] ?? '';
402
        $coordinate = $this->getCoordinate();
403
        $oldAttributesRef = $oldAttributes['ref'] ?? $coordinate;
404 9056
        $originalValue = $this->value;
405
        $originalDataType = $this->dataType;
406 9056
        $this->formulaAttributes = [];
407 9056
        $spill = false;
408 9056
409 9056
        if ($this->dataType === DataType::TYPE_FORMULA) {
410 9056
            try {
411 9056
                $currentCalendar = SharedDate::getExcelCalendar();
412 9056
                SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
413 9056
                $thisworksheet = $this->getWorksheet();
414 9056
                $index = $thisworksheet->getParentOrThrow()->getActiveSheetIndex();
415
                $selected = $thisworksheet->getSelectedCells();
416 9056
                $title = $thisworksheet->getTitle();
417
                $calculation = Calculation::getInstance($thisworksheet->getParent());
418 8064
                $result = $calculation->calculateCellValue($this, $resetLog);
419 8064
                $result = $this->convertDateTimeInt($result);
420 8064
                $thisworksheet->setSelectedCells($selected);
421 8064
                $thisworksheet->getParentOrThrow()->setActiveSheetIndex($index);
422 8064
                if (is_array($result) && $calculation->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
423 8064
                    while (is_array($result)) {
424 8064
                        $result = array_shift($result);
425 8064
                    }
426 7816
                }
427 7816
                if (
428 7816
                    !is_array($result)
429 7816
                    && $calculation->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY
430
                    && $oldAttributesT === 'array'
431
                    && ($oldAttributesRef === $coordinate || $oldAttributesRef === "$coordinate:$coordinate")
432
                ) {
433
                    $result = [$result];
434
                }
435 7816
                // if return_as_array for formula like '=sheet!cell'
436 7816
                if (is_array($result) && count($result) === 1) {
437 7816
                    $resultKey = array_keys($result)[0];
438 7816
                    $resultValue = $result[$resultKey];
439
                    if (is_int($resultKey) && is_array($resultValue) && count($resultValue) === 1) {
440 8
                        $resultKey2 = array_keys($resultValue)[0];
441
                        $resultValue2 = $resultValue[$resultKey2];
442
                        if (is_string($resultKey2) && !is_array($resultValue2) && preg_match('/[a-zA-Z]{1,3}/', $resultKey2) === 1) {
443 7816
                            $result = $resultValue2;
444 29
                        }
445 29
                    }
446 29
                }
447 4
                $newColumn = $this->getColumn();
448 4
                if (is_array($result)) {
449 4
                    $this->formulaAttributes['t'] = 'array';
450
                    $this->formulaAttributes['ref'] = $maxCoordinate = $coordinate;
451
                    $newRow = $row = $this->getRow();
452
                    $column = $this->getColumn();
453
                    foreach ($result as $resultRow) {
454 7816
                        if (is_array($resultRow)) {
455 7816
                            $newColumn = $column;
456 109
                            foreach ($resultRow as $resultValue) {
457 109
                                if ($row !== $newRow || $column !== $newColumn) {
458 109
                                    $maxCoordinate = $newColumn . $newRow;
459 109
                                    if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) {
460 109
                                        if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) {
461 109
                                            $spill = true;
462 108
463 108
                                            break;
464 108
                                        }
465 104
                                    }
466 104
                                }
467 42
                                ++$newColumn;
468 3
                            }
469
                            ++$newRow;
470 3
                        } else {
471
                            if ($row !== $newRow || $column !== $newColumn) {
472
                                $maxCoordinate = $newColumn . $newRow;
473
                                if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) {
474 108
                                    if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) {
475
                                        $spill = true;
476 108
                                    }
477
                                }
478 11
                            }
479 3
                            ++$newColumn;
480 3
                        }
481 3
                        if ($spill) {
482
                            break;
483
                        }
484
                    }
485
                    if (!$spill) {
486 11
                        $this->formulaAttributes['ref'] .= ":$maxCoordinate";
487
                    }
488 109
                    $thisworksheet->getCell($column . $row);
489 3
                }
490
                if (is_array($result)) {
491
                    if ($oldAttributes !== null && $calculation->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
492 109
                        if (($oldAttributesT) === 'array') {
493 107
                            $thisworksheet = $this->getWorksheet();
494
                            $coordinate = $this->getCoordinate();
495 109
                            $ref = $oldAttributesRef;
496
                            if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) === 1) {
497 7816
                                if (isset($matches[3])) {
498 109
                                    $minCol = $matches[1];
499 44
                                    $minRow = (int) $matches[2];
500 43
                                    // https://github.com/phpstan/phpstan/issues/11602
501 43
                                    $maxCol = $matches[4]; // @phpstan-ignore-line
502 43
                                    ++$maxCol;
503 43
                                    $maxRow = (int) $matches[5]; // @phpstan-ignore-line
504 43
                                    for ($row = $minRow; $row <= $maxRow; ++$row) {
505 42
                                        for ($col = $minCol; $col !== $maxCol; ++$col) {
506 42
                                            if ("$col$row" !== $coordinate) {
507
                                                $thisworksheet->getCell("$col$row")->setValue(null);
508 42
                                            }
509 42
                                        }
510 42
                                    }
511 42
                                }
512 42
                            }
513 42
                            $thisworksheet->getCell($coordinate);
514 41
                        }
515
                    }
516
                }
517
                if ($spill) {
518
                    $result = ExcelError::SPILL();
519
                }
520 43
                if (is_array($result)) {
521
                    $newRow = $row = $this->getRow();
522
                    $newColumn = $column = $this->getColumn();
523
                    foreach ($result as $resultRow) {
524 7816
                        if (is_array($resultRow)) {
525 3
                            $newColumn = $column;
526
                            foreach ($resultRow as $resultValue) {
527 7816
                                if ($row !== $newRow || $column !== $newColumn) {
528 107
                                    $thisworksheet->getCell($newColumn . $newRow)->setValue($resultValue);
529 107
                                }
530 107
                                ++$newColumn;
531 107
                            }
532 106
                            ++$newRow;
533 106
                        } else {
534 106
                            if ($row !== $newRow || $column !== $newColumn) {
535 102
                                $thisworksheet->getCell($newColumn . $newRow)->setValue($resultRow);
536
                            }
537 106
                            ++$newColumn;
538
                        }
539 106
                    }
540
                    $thisworksheet->getCell($column . $row);
541 11
                    $this->value = $originalValue;
542 3
                    $this->dataType = $originalDataType;
543
                }
544 11
            } catch (SpreadsheetException $ex) {
545
                SharedDate::setExcelCalendar($currentCalendar);
546
                if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
547 107
                    return $this->calculatedValue; // Fallback for calculations referencing external files.
548 107
                } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) {
549 7816
                    return ExcelError::NAME();
550
                }
551 269
552 269
                throw new CalculationException(
553 269
                    $title . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(),
554 1
                    $ex->getCode(),
555 269
                    $ex
556 31
                );
557
            }
558
            SharedDate::setExcelCalendar($currentCalendar);
559 238
560 238
            if ($result === Functions::NOT_YET_IMPLEMENTED) {
561 238
                $this->formulaAttributes = $oldAttributes;
562 238
563 238
                return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
564
            }
565 7816
566
            return $result;
567 7816
        } elseif ($this->value instanceof RichText) {
568 14
            return $this->value->getPlainText();
569
        }
570 14
571
        return $this->convertDateTimeInt($this->value);
572
    }
573 7807
574 7698
    /**
575 20
     * Set old calculated value (cached).
576
     *
577
     * @param mixed $originalValue Value
578 7693
     */
579
    public function setCalculatedValue(mixed $originalValue, bool $tryNumeric = true): self
580
    {
581
        if ($originalValue !== null) {
582
            $this->calculatedValue = ($tryNumeric && is_numeric($originalValue)) ? (0 + $originalValue) : $originalValue;
583
        }
584
585
        return $this->updateInCollection();
586 438
    }
587
588 438
    /**
589 438
     *    Get old calculated value (cached)
590
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
591
     *        create the original spreadsheet file.
592 438
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
593
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
594
     *        values used by the formula have changed since it was last calculated.
595
     */
596
    public function getOldCalculatedValue(): mixed
597
    {
598
        return $this->calculatedValue;
599
    }
600
601
    /**
602
     * Get cell data type.
603 20
     */
604
    public function getDataType(): string
605 20
    {
606
        return $this->dataType;
607
    }
608
609
    /**
610
     * Set cell data type.
611 9056
     *
612
     * @param string $dataType see DataType::TYPE_*
613 9056
     */
614
    public function setDataType(string $dataType): self
615
    {
616
        $this->setValueExplicit($this->value, $dataType);
617
618
        return $this;
619
    }
620
621 2
    /**
622
     * Identify if the cell contains a formula.
623 2
     */
624
    public function isFormula(): bool
625 2
    {
626
        return $this->dataType === DataType::TYPE_FORMULA && $this->getStyle()->getQuotePrefix() === false;
627
    }
628
629
    /**
630
     *    Does this cell contain Data validation rules?
631 73
     *
632
     * @throws SpreadsheetException
633 73
     */
634
    public function hasDataValidation(): bool
635
    {
636
        if (!isset($this->parent)) {
637
            throw new SpreadsheetException('Cannot check for data validation when cell is not bound to a worksheet');
638
        }
639
640
        return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
641 24
    }
642
643 24
    /**
644 1
     * Get Data validation rules.
645
     *
646
     * @throws SpreadsheetException
647 23
     */
648
    public function getDataValidation(): DataValidation
649
    {
650
        if (!isset($this->parent)) {
651
            throw new SpreadsheetException('Cannot get data validation for cell that is not bound to a worksheet');
652
        }
653
654
        return $this->getWorksheet()->getDataValidation($this->getCoordinate());
655 29
    }
656
657 29
    /**
658 1
     * Set Data validation rules.
659
     *
660
     * @throws SpreadsheetException
661 28
     */
662
    public function setDataValidation(?DataValidation $dataValidation = null): self
663
    {
664
        if (!isset($this->parent)) {
665
            throw new SpreadsheetException('Cannot set data validation for cell that is not bound to a worksheet');
666
        }
667
668
        $this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation);
669 3
670
        return $this->updateInCollection();
671 3
    }
672 1
673
    /**
674
     * Does this cell contain valid value?
675 2
     */
676
    public function hasValidValue(): bool
677 2
    {
678
        $validator = new DataValidator();
679
680
        return $validator->isValid($this);
681
    }
682
683 12
    /**
684
     * Does this cell contain a Hyperlink?
685 12
     *
686
     * @throws SpreadsheetException
687 12
     */
688
    public function hasHyperlink(): bool
689
    {
690
        if (!isset($this->parent)) {
691
            throw new SpreadsheetException('Cannot check for hyperlink when cell is not bound to a worksheet');
692
        }
693
694
        return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
695 1
    }
696
697 1
    /**
698 1
     * Get Hyperlink.
699
     *
700
     * @throws SpreadsheetException
701
     */
702
    public function getHyperlink(): Hyperlink
703
    {
704
        if (!isset($this->parent)) {
705
            throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet');
706
        }
707
708
        return $this->getWorksheet()->getHyperlink($this->getCoordinate());
709 91
    }
710
711 91
    /**
712 1
     * Set Hyperlink.
713
     *
714
     * @throws SpreadsheetException
715 90
     */
716
    public function setHyperlink(?Hyperlink $hyperlink = null): self
717
    {
718
        if (!isset($this->parent)) {
719
            throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet');
720
        }
721
722
        $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
723 2
724
        return $this->updateInCollection();
725 2
    }
726 1
727
    /**
728
     * Get cell collection.
729 1
     */
730
    public function getParent(): ?Cells
731 1
    {
732
        return $this->parent;
733
    }
734
735
    /**
736
     * Get parent worksheet.
737 10039
     *
738
     * @throws SpreadsheetException
739 10039
     */
740
    public function getWorksheet(): Worksheet
741
    {
742
        $parent = $this->parent;
743
        if ($parent !== null) {
744
            $worksheet = $parent->getParent();
745
        } else {
746
            $worksheet = null;
747 10049
        }
748
749 10049
        if ($worksheet === null) {
750 10049
            throw new SpreadsheetException('Worksheet no longer exists');
751 10049
        }
752
753 1
        return $worksheet;
754
    }
755
756 10049
    public function getWorksheetOrNull(): ?Worksheet
757 1
    {
758
        $parent = $this->parent;
759
        if ($parent !== null) {
760 10049
            $worksheet = $parent->getParent();
761
        } else {
762
            $worksheet = null;
763 9
        }
764
765 9
        return $worksheet;
766 9
    }
767 9
768
    /**
769
     * Is this cell in a merge range.
770
     */
771
    public function isInMergeRange(): bool
772 9
    {
773
        return (bool) $this->getMergeRange();
774
    }
775
776
    /**
777
     * Is this cell the master (top left cell) in a merge range (that holds the actual data value).
778 7
     */
779
    public function isMergeRangeValueCell(): bool
780 7
    {
781
        if ($mergeRange = $this->getMergeRange()) {
782
            $mergeRange = Coordinate::splitRange($mergeRange);
783
            [$startCell] = $mergeRange[0];
784
785
            return $this->getCoordinate() === $startCell;
786 56
        }
787
788 56
        return false;
789 22
    }
790 22
791
    /**
792 22
     * If this cell is in a merge range, then return the range.
793
     *
794
     * @return false|string
795 36
     */
796
    public function getMergeRange()
797
    {
798
        foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
799
            if ($this->isInRange($mergeRange)) {
800
                return $mergeRange;
801
            }
802
        }
803 59
804
        return false;
805 59
    }
806 22
807 22
    /**
808
     * Get cell style.
809
     */
810
    public function getStyle(): Style
811 39
    {
812
        return $this->getWorksheet()->getStyle($this->getCoordinate());
813
    }
814
815
    /**
816
     * Get cell style.
817 10041
     */
818
    public function getAppliedStyle(): Style
819 10041
    {
820
        if ($this->getWorksheet()->conditionalStylesExists($this->getCoordinate()) === false) {
821
            return $this->getStyle();
822
        }
823
        $range = $this->getWorksheet()->getConditionalRange($this->getCoordinate());
824
        if ($range === null) {
825 6
            return $this->getStyle();
826
        }
827 6
828 2
        $matcher = new CellStyleAssessor($this, $range);
829
830 4
        return $matcher->matchConditions($this->getWorksheet()->getConditionalStyles($this->getCoordinate()));
831 4
    }
832
833
    /**
834
     * Re-bind parent.
835 4
     */
836
    public function rebindParent(Worksheet $parent): self
837 4
    {
838
        $this->parent = $parent->getCellCollection();
839
840
        return $this->updateInCollection();
841
    }
842
843
    /**
844
     *    Is cell in a specific range?
845
     *
846
     * @param string $range Cell range (e.g. A1:A1)
847
     */
848
    public function isInRange(string $range): bool
849
    {
850
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
851
852
        // Translate properties
853
        $myColumn = Coordinate::columnIndexFromString($this->getColumn());
854
        $myRow = $this->getRow();
855 220
856
        // Verify if cell is in range
857 220
        return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn)
858
            && ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
859
    }
860 220
861 220
    /**
862
     * Compare 2 cells.
863
     *
864 220
     * @param Cell $a Cell a
865 220
     * @param Cell $b Cell b
866
     *
867
     * @return int Result of comparison (always -1 or 1, never zero!)
868
     */
869
    public static function compareCells(self $a, self $b): int
870
    {
871
        if ($a->getRow() < $b->getRow()) {
872
            return -1;
873
        } elseif ($a->getRow() > $b->getRow()) {
874
            return 1;
875
        } elseif (Coordinate::columnIndexFromString($a->getColumn()) < Coordinate::columnIndexFromString($b->getColumn())) {
876
            return -1;
877
        }
878
879
        return 1;
880
    }
881
882
    /**
883
     * Get value binder to use.
884
     */
885
    public static function getValueBinder(): IValueBinder
886
    {
887
        if (self::$valueBinder === null) {
888
            self::$valueBinder = new DefaultValueBinder();
889
        }
890
891
        return self::$valueBinder;
892 9630
    }
893
894 9630
    /**
895 252
     * Set value binder to use.
896
     */
897
    public static function setValueBinder(IValueBinder $binder): void
898 9630
    {
899
        self::$valueBinder = $binder;
900
    }
901
902
    /**
903
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
904 160
     */
905
    public function __clone()
906 160
    {
907
        $vars = get_object_vars($this);
908
        foreach ($vars as $propertyName => $propertyValue) {
909
            if ((is_object($propertyValue)) && ($propertyName !== 'parent')) {
910
                $this->$propertyName = clone $propertyValue;
911
            } else {
912 17
                $this->$propertyName = $propertyValue;
913
            }
914 17
        }
915 17
    }
916 17
917 17
    /**
918
     * Get index to cellXf.
919 17
     */
920
    public function getXfIndex(): int
921
    {
922
        return $this->xfIndex;
923
    }
924
925
    /**
926
     * Set index to cellXf.
927 10086
     */
928
    public function setXfIndex(int $indexValue): self
929 10086
    {
930
        $this->xfIndex = $indexValue;
931
932
        return $this->updateInCollection();
933
    }
934
935 1781
    /**
936
     * Set the formula attributes.
937 1781
     *
938
     * @param $attributes null|array<string, string>
939 1781
     *
940
     * @return $this
941
     */
942
    public function setFormulaAttributes(?array $attributes): self
943
    {
944
        $this->formulaAttributes = $attributes;
945
946
        return $this;
947
    }
948
949 284
    /**
950
     * Get the formula attributes.
951 284
     *
952
     * @return null|array<string, string>
953 284
     */
954
    public function getFormulaAttributes(): mixed
955
    {
956
        return $this->formulaAttributes;
957
    }
958
959
    /**
960
     * Convert to string.
961 168
     */
962
    public function __toString(): string
963 168
    {
964
        $retVal = $this->value;
965
966
        return StringHelper::convertToString($retVal, false);
967
    }
968
969 2
    public function getIgnoredErrors(): IgnoredErrors
970
    {
971 2
        return $this->ignoredErrors;
972
    }
973 2
974
    public function isLocked(): bool
975
    {
976 348
        $protected = $this->parent?->getParent()?->getProtection()?->getSheet();
977
        if ($protected !== true) {
978 348
            return false;
979
        }
980
        $locked = $this->getStyle()->getProtection()->getLocked();
981 3
982
        return $locked !== Protection::PROTECTION_UNPROTECTED;
983 3
    }
984 3
985 1
    public function isHiddenOnFormulaBar(): bool
986
    {
987 3
        if ($this->getDataType() !== DataType::TYPE_FORMULA) {
988
            return false;
989 3
        }
990
        $protected = $this->parent?->getParent()?->getProtection()?->getSheet();
991
        if ($protected !== true) {
992 2
            return false;
993
        }
994 2
        $hidden = $this->getStyle()->getProtection()->getHidden();
995 2
996
        return $hidden !== Protection::PROTECTION_UNPROTECTED;
997 2
    }
998
}
999