Passed
Pull Request — master (#4153)
by Owen
13:53
created

Cell::setValueExplicit()   F

Complexity

Conditions 25
Paths 209

Size

Total Lines 82
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 25.0547

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 55
c 1
b 0
f 0
dl 0
loc 82
ccs 43
cts 45
cp 0.9556
rs 3.2208
cc 25
nc 209
nop 2
crap 25.0547

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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