Cell::getDataType()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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