Passed
Pull Request — 2.x (#104)
by Maxim
19:05
created

AbstractColumn::compare()   C

Complexity

Conditions 13
Paths 14

Size

Total Lines 45
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 13.0245

Importance

Changes 0
Metric Value
cc 13
eloc 23
c 0
b 0
f 0
nc 14
nop 1
dl 0
loc 45
ccs 18
cts 19
cp 0.9474
crap 13.0245
rs 6.6166

How to fix   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
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Schema;
13
14
use Cycle\Database\Schema\Attribute\ColumnAttribute;
15
use Cycle\Database\Schema\Traits\ColumnAttributesTrait;
16
use DateTimeImmutable;
17
use Cycle\Database\ColumnInterface;
18
use Cycle\Database\Driver\DriverInterface;
19
use Cycle\Database\Exception\DefaultValueException;
20
use Cycle\Database\Exception\SchemaException;
21
use Cycle\Database\Injection\Fragment;
22
use Cycle\Database\Injection\FragmentInterface;
23
use Cycle\Database\Query\QueryParameters;
24
use Cycle\Database\Schema\Traits\ElementTrait;
25
26
/**
27
 * Abstract column schema with read (see ColumnInterface) and write abilities. Must be implemented
28
 * by driver to support DBMS specific syntax and creation rules.
29
 *
30
 * Shortcuts for various column types:
31
 *
32
 * @method $this|AbstractColumn primary()
33
 * @method $this|AbstractColumn bigPrimary()
34
 * @method $this|AbstractColumn boolean()
35
 * @method $this|AbstractColumn integer()
36
 * @method $this|AbstractColumn tinyInteger()
37
 * @method $this|AbstractColumn smallInteger()
38
 * @method $this|AbstractColumn bigInteger()
39
 * @method $this|AbstractColumn text()
40
 * @method $this|AbstractColumn tinyText()
41
 * @method $this|AbstractColumn longText()
42
 * @method $this|AbstractColumn double()
43
 * @method $this|AbstractColumn float()
44
 * @method $this|AbstractColumn date()
45
 * @method $this|AbstractColumn time()
46
 * @method $this|AbstractColumn timestamp()
47
 * @method $this|AbstractColumn binary()
48
 * @method $this|AbstractColumn tinyBinary()
49
 * @method $this|AbstractColumn longBinary()
50
 * @method $this|AbstractColumn json()
51
 * @method $this|AbstractColumn uuid()
52
 */
53
abstract class AbstractColumn implements ColumnInterface, ElementInterface
54
{
55
    use ColumnAttributesTrait;
56
    use ElementTrait;
57
58
    /**
59
     * Default timestamp expression (driver specific).
60
     */
61
    public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
62
63
    /**
64
     * Value to be excluded from comparision.
65
     */
66
    public const EXCLUDE_FROM_COMPARE = ['timezone', 'userType', 'attributes'];
67
68
    /**
69
     * Normalization for time and dates.
70
     */
71
    public const DATE_FORMAT = 'Y-m-d';
72
    public const TIME_FORMAT = 'H:i:s';
73
    public const DATETIME_PRECISION = 6;
74
75
    /**
76
     * Mapping between abstract type and internal database type with it's options. Multiple abstract
77
     * types can map into one database type, this implementation allows us to equalize two columns
78
     * if they have different abstract types but same database one. Must be declared by DBMS
79
     * specific implementation.
80
     *
81
     * Example:
82
     * integer => array('type' => 'int', 'size' => 1),
83
     * boolean => array('type' => 'tinyint', 'size' => 1)
84
     *
85
     * @internal
86
     */
87
    protected array $mapping = [
88
        //Primary sequences
89
        'primary'     => null,
90
        'bigPrimary'  => null,
91
92
        //Enum type (mapped via method)
93
        'enum'        => null,
94
95
        //Logical types
96
        'boolean'     => null,
97
98
        //Integer types (size can always be changed with size method), longInteger has method alias
99
        //bigInteger
100
        'integer'     => null,
101
        'tinyInteger' => null,
102
        'smallInteger'=> null,
103
        'bigInteger'  => null,
104
105
        //String with specified length (mapped via method)
106
        'string'      => null,
107
108
        //Generic types
109
        'text'        => null,
110
        'tinyText'    => null,
111
        'longText'    => null,
112
113
        //Real types
114
        'double'      => null,
115
        'float'       => null,
116
117
        //Decimal type (mapped via method)
118
        'decimal'     => null,
119
120
        //Date and Time types
121
        'datetime'    => null,
122
        'date'        => null,
123
        'time'        => null,
124
        'timestamp'   => null,
125
126
        //Binary types
127
        'binary'      => null,
128
        'tinyBinary'  => null,
129
        'longBinary'  => null,
130
131
        //Additional types
132
        'json'        => null,
133
    ];
134
135
    /**
136
     * Reverse mapping is responsible for generating abstact type based on database type and it's
137
     * options. Multiple database types can be mapped into one abstract type.
138
     *
139
     * @internal
140
     */
141
    protected array $reverseMapping = [
142
        'primary'     => [],
143
        'bigPrimary'  => [],
144
        'enum'        => [],
145
        'boolean'     => [],
146
        'integer'     => [],
147
        'tinyInteger' => [],
148
        'smallInteger'=> [],
149
        'bigInteger'  => [],
150
        'string'      => [],
151
        'text'        => [],
152
        'tinyText'    => [],
153
        'longText'    => [],
154
        'double'      => [],
155
        'float'       => [],
156
        'decimal'     => [],
157
        'datetime'    => [],
158
        'date'        => [],
159
        'time'        => [],
160
        'timestamp'   => [],
161
        'binary'      => [],
162
        'tinyBinary'  => [],
163
        'longBinary'  => [],
164
        'json'        => [],
165
    ];
166
167
    /**
168
     * User defined type. Only until actual mapping.
169
     */
170
    protected ?string $userType = null;
171
172
    /**
173
     * DBMS specific column type.
174
     */
175
    protected string $type = '';
176
177
    protected ?\DateTimeZone $timezone = null;
178
179
    /**
180
     * Indicates that column can contain null values.
181
     */
182
    #[ColumnAttribute]
183
    protected bool $nullable = true;
184
185
    /**
186
     * Default column value, may not be applied to some datatypes (for example to primary keys),
187
     * should follow type size and other options.
188
     */
189
    #[ColumnAttribute]
190
    protected mixed $defaultValue = null;
191
192
    /**
193
     * Column type size, can have different meanings for different datatypes.
194
     */
195
    #[ColumnAttribute]
196
    protected int $size = 0;
197
198
    /**
199
     * Precision of column, applied only for "decimal" type.
200
     */
201
    #[ColumnAttribute(['decimal'])]
202
    protected int $precision = 0;
203
204
    /**
205
     * Scale of column, applied only for "decimal" type.
206
     */
207
    #[ColumnAttribute(['decimal'])]
208
    protected int $scale = 0;
209
210
    /**
211
     * List of allowed enum values.
212
     */
213
    protected array $enumValues = [];
214
215
    /**
216
     * Abstract type aliases (for consistency).
217
     */
218
    protected array $aliases = [
219
        'int'            => 'integer',
220
        'smallint'       => 'smallInteger',
221
        'bigint'         => 'bigInteger',
222
        'incremental'    => 'primary',
223
        'bigIncremental' => 'bigPrimary',
224
        'bool'           => 'boolean',
225
        'blob'           => 'binary',
226
    ];
227
228
    /**
229
     * Association list between abstract types and native PHP types. Every non listed type will be
230
     * converted into string.
231
     *
232 1950
     * @internal
233
     */
234
    private array $phpMapping = [
235
        self::INT   => ['primary', 'bigPrimary', 'integer', 'tinyInteger', 'smallInteger', 'bigInteger'],
236
        self::BOOL  => ['boolean'],
237 1950
        self::FLOAT => ['double', 'float', 'decimal'],
238 1950
    ];
239
240
    /**
241
     * @psalm-param non-empty-string $table
242
     * @psalm-param non-empty-string $name
243
     */
244
    public function __construct(
245 1750
        protected string $table,
246
        protected string $name,
247 1750
        \DateTimeZone $timezone = null
248
    ) {
249
        $this->timezone = $timezone ?? new \DateTimeZone(date_default_timezone_get());
250 856
    }
251
252 856
    /**
253
     * Shortcut for AbstractColumn->type() method.
254
     *
255
     * @psalm-param non-empty-string $name
256
     */
257
    public function __call(string $name, array $arguments = []): self
258 24
    {
259
        if (\count($arguments) === 1 && \key($arguments) === 0) {
260 24
            if (\array_key_exists($name, $this->getAttributesMap())) {
261 24
                $this->fillAttributes([$name => $arguments[0]]);
262
                return $this;
263 24
            }
264 24
        }
265 24
266
        $this->type($name);
267
        $this->fillAttributes($arguments);
268
269 24
        return $this;
270 8
    }
271
272
    public function __toString(): string
273 24
    {
274 24
        return $this->table . '.' . $this->getName();
275
    }
276
277 24
    /**
278 8
     * Simplified way to dump information.
279
     */
280
    public function __debugInfo(): array
281 24
    {
282
        $column = [
283
            'name' => $this->name,
284
            'type' => [
285 24
                'database' => $this->type,
286 16
                'schema'   => $this->getAbstractType(),
287 16
                'php'      => $this->getType(),
288
            ],
289
        ];
290 24
291
        if (!empty($this->size)) {
292
            $column['size'] = $this->size;
293 8
        }
294
295 8
        if ($this->nullable) {
296
            $column['nullable'] = true;
297
        }
298 852
299
        if ($this->defaultValue !== null) {
300 852
            $column['defaultValue'] = $this->getDefaultValue();
301
        }
302
303 852
        if (static::isEnum($this)) {
304
            $column['enumValues'] = $this->enumValues;
305 852
        }
306
307
        if ($this->getAbstractType() === 'decimal') {
308 8
            $column['precision'] = $this->precision;
309
            $column['scale'] = $this->scale;
310 8
        }
311
312
        if ($this->attributes !== []) {
313 1798
            $column['attributes'] = $this->attributes;
314
        }
315 1798
316
        return $column;
317
    }
318
319
    public function getSize(): int
320
    {
321 1798
        return $this->size;
322
    }
323 1798
324 1514
    public function getPrecision(): int
325
    {
326
        return $this->precision;
327 1074
    }
328
329 368
    public function getScale(): int
330
    {
331
        return $this->scale;
332 850
    }
333 682
334
    public function isNullable(): bool
335
    {
336 698
        return $this->nullable;
337 170
    }
338 293
339 205
    public function hasDefaultValue(): bool
340 205
    {
341 698
        return $this->defaultValue !== null;
342
    }
343
344
    /**
345
     * @throws DefaultValueException
346
     */
347
    public function getDefaultValue(): mixed
348 60
    {
349
        if (!$this->hasDefaultValue()) {
350 60
            return null;
351
        }
352
353
        if ($this->defaultValue instanceof FragmentInterface) {
354
            //Defined as SQL piece
355
            return $this->defaultValue;
356 852
        }
357
358 852
        if (\in_array($this->getAbstractType(), ['time', 'date', 'datetime', 'timestamp'])) {
359
            return $this->formatDatetime($this->getAbstractType(), $this->defaultValue);
360
        }
361 856
362
        return match ($this->getType()) {
363 856
            'int' => (int) $this->defaultValue,
364
            'float' => (float) $this->defaultValue,
365
            'bool' => \is_string($this->defaultValue) && strtolower($this->defaultValue) === 'false'
366
                ? false : (bool) $this->defaultValue,
367
            default => (string)$this->defaultValue
368
        };
369 842
    }
370
371 842
    /**
372 842
     * Get every associated column constraint names.
373 842
     */
374 618
    public function getConstraints(): array
375
    {
376
        return [];
377
    }
378 754
379
    /**
380
     * Get allowed enum values.
381
     */
382
    public function getEnumValues(): array
383
    {
384
        return $this->enumValues;
385
    }
386
387
    public function getInternalType(): string
388
    {
389
        return $this->type;
390
    }
391
392
    /**
393
     * @psalm-return non-empty-string
394
     */
395
    public function getType(): string
396 1922
    {
397
        $schemaType = $this->getAbstractType();
398 1922
        foreach ($this->phpMapping as $phpType => $candidates) {
399 1922
            if (\in_array($schemaType, $candidates, true)) {
400 1922
                return $phpType;
401 1890
            }
402 1862
        }
403
404
        return self::STRING;
405 1838
    }
406
407
    /**
408 1414
     * Returns type defined by the user, only until schema sync. Attention, this value is only preserved during the
409 1360
     * declaration process. Value will become null after the schema fetched from database.
410
     *
411
     * @internal
412 1034
     */
413 1034
    public function getDeclaredType(): ?string
414 1034
    {
415
        return $this->userType;
416
    }
417 1034
418 876
    /**
419
     * DBMS specific reverse mapping must map database specific type into limited set of abstract
420
     * types.
421
     */
422 688
    public function getAbstractType(): string
423
    {
424
        foreach ($this->reverseMapping as $type => $candidates) {
425
            foreach ($candidates as $candidate) {
426 4
                if (\is_string($candidate)) {
427
                    if (strtolower($candidate) === strtolower($this->type)) {
428
                        return $type;
429
                    }
430
431
                    continue;
432
                }
433
434
                if (strtolower($candidate['type']) !== strtolower($this->type)) {
435
                    continue;
436
                }
437
438
                foreach ($candidate as $option => $required) {
439
                    if ($option === 'type') {
440
                        continue;
441
                    }
442 1932
443
                    if ($this->{$option} !== $required) {
444 1932
                        continue 2;
445
                    }
446
                }
447
448
                return $type;
449 1932
            }
450
        }
451
452 1932
        return 'unknown';
453
    }
454
455 1932
    /**
456 1932
     * Give column new abstract type. DBMS specific implementation must map provided type into one
457
     * of internal database values.
458
     *
459 1932
     * Attention, changing type of existed columns in some databases has a lot of restrictions like
460 1656
     * cross type conversions and etc. Try do not change column type without a reason.
461
     *
462 1656
     * @psalm-param non-empty-string $abstract Abstract or virtual type declared in mapping.
463
     *
464
     * @todo Support native database types (simply bypass abstractType)!
465
     */
466 1442
    public function type(string $abstract): self
467 1442
    {
468
        if (isset($this->aliases[$abstract])) {
469
            //Make recursive
470 1442
            $abstract = $this->aliases[$abstract];
471
        }
472
473
        if (!isset($this->mapping[$abstract]) && !\array_key_exists($abstract, $this->getAttributesMap())) {
474
            $this->type = $abstract;
475
            $this->userType = $abstract;
476 698
477
            return $this;
478 698
        }
479
480 698
        // Originally specified type.
481
        $this->userType = $abstract;
482
483
        // Resetting all values to default state.
484
        $this->size = $this->precision = $this->scale = 0;
485
        $this->enumValues = [];
486
487 954
        // Abstract type points to DBMS specific type
488
        if (\is_string($this->mapping[$abstract])) {
489
            $this->type = $this->mapping[$abstract];
490 954
491 578
            return $this;
492
        }
493
494 954
        // Configuring column properties based on abstractType preferences
495
        foreach ($this->mapping[$abstract] as $property => $value) {
496 954
            $this->{$property} = $value;
497
        }
498
499
        return $this;
500
    }
501
502
    /**
503
     * Set column nullable/not nullable.
504
     */
505
    public function nullable(bool $nullable = true): self
506
    {
507
        $this->nullable = $nullable;
508
509 304
        return $this;
510
    }
511 304
512 304
    /**
513 304
     * Change column default value (can be forbidden for some column types).
514 304
     * Use Database::TIMESTAMP_NOW to use driver specific NOW() function.
515
     */
516
    public function defaultValue(mixed $value): self
517 304
    {
518
        //Forcing driver specific values
519
        if ($value === self::DATETIME_NOW) {
520
            $value = static::DATETIME_NOW;
521
        }
522
523
        $this->defaultValue = $value;
524
525
        return $this;
526
    }
527
528
    /**
529
     * Set column as enum type and specify set of allowed values. Most of drivers will emulate enums
530
     * using column constraints.
531
     *
532
     * Examples:
533
     * $table->status->enum(['active', 'disabled']);
534 1008
     * $table->status->enum('active', 'disabled');
535
     *
536 1008
     * @param array|string $values Enum values (array or comma separated). String values only.
537
     */
538 1008
    public function enum(string|array $values): self
539
    {
540 1008
        $this->type('enum');
541
        $this->enumValues = array_map(
542 1008
            'strval',
543
            is_array($values) ? $values : func_get_args()
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
544
        );
545
546
        return $this;
547
    }
548
549
    /**
550 64
     * Set column type as string with limited size. Maximum allowed size is 255 bytes, use "text"
551
     * abstract types for longer strings.
552 64
     *
553
     * Strings are perfect type to store email addresses as it big enough to store valid address
554 64
     * and
555
     * can be covered with unique index.
556 56
     *
557 56
     * @link http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
558
     *
559 56
     * @param int $size Max string length.
560
     *
561
     * @throws SchemaException
562 1464
     */
563
    public function string(int $size = 255): self
564 1464
    {
565
        $this->type('string');
566 1464
567
        $size < 0 && throw new SchemaException('Invalid string length value');
568 458
569 458
        $this->size = $size;
570
571 1440
        return $this;
572 42
    }
573 1410
574 878
    public function datetime(int $size = 0, mixed ...$attributes): self
575
    {
576
        $this->type('datetime');
577 1464
        $this->fillAttributes($attributes);
578
579 1464
        ($size < 0 || $size > static::DATETIME_PRECISION) && throw new SchemaException(
580 634
            \sprintf('Invalid %s precision value.', $this->getAbstractType())
581
        );
582
        $this->size = $size;
583 1464
584
        return $this;
585
    }
586 1894
587
    /**
588 1894
     * Set column type as decimal with specific precision and scale.
589
     *
590
     * @throws SchemaException
591 1894
     */
592 1838
    public function decimal(int $precision, int $scale = 0): self
593
    {
594
        $this->type('decimal');
595 1598
596 1598
        empty($precision) && throw new SchemaException('Invalid precision value');
597
598 1598
        $this->precision = $precision;
599 1598
        $this->scale = $scale;
600 1598
601 1598
        return $this;
602
    }
603
604 1598
    public function sqlStatement(DriverInterface $driver): string
605
    {
606 1598
        $statement = [$driver->identifier($this->name), $this->type];
0 ignored issues
show
Bug introduced by
The method identifier() does not exist on Cycle\Database\Driver\DriverInterface. It seems like you code against a sub-type of Cycle\Database\Driver\DriverInterface such as Cycle\Database\Driver\Driver. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

606
        $statement = [$driver->/** @scrutinizer ignore-call */ identifier($this->name), $this->type];
Loading history...
607 380
608
        if (static::isEnum($this)) {
609 1554
            //Enum specific column options
610 1554
            if (!empty($enumDefinition = $this->quoteEnum($driver))) {
611
                $statement[] = $enumDefinition;
612 16
            }
613
        } elseif (!empty($this->precision)) {
614
            $statement[] = "({$this->precision}, {$this->scale})";
615 1598
        } elseif (!empty($this->size)) {
616
            $statement[] = "({$this->size})";
617
        }
618 1598
619 154
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
620
621
        if ($this->defaultValue !== null) {
622
            $statement[] = "DEFAULT {$this->quoteDefault($driver)}";
623 1598
        }
624
625
        return \implode(' ', $statement);
626
    }
627
628
    public function compare(self $initial): bool
629 152
    {
630
        $normalized = clone $initial;
631 152
632 152
        // soft compare, todo: improve
633 152
        if ($this == $normalized) {
634
            return true;
635
        }
636 152
637
        $columnVars = get_object_vars($this);
638
        $dbColumnVars = get_object_vars($normalized);
639
640
        $difference = [];
641
        foreach ($columnVars as $name => $value) {
642 846
            if (\in_array($name, static::EXCLUDE_FROM_COMPARE, true)) {
643
                continue;
644 846
            }
645 846
646
            if ($name === 'type') {
647
                // user defined type
648
                if (!isset($this->mapping[$this->type]) && $this->type === $this->userType) {
649 846
                    continue;
650 578
                }
651 578
            }
652 578
653
            if ($name === 'defaultValue') {
654
                //Default values has to compared using type-casted value
655
                if ($this->getDefaultValue() != $initial->getDefaultValue()) {
656
                    $difference[] = $name;
657 798
                } elseif (
658 410
                    $this->getDefaultValue() !== $initial->getDefaultValue()
659 293
                    && (!\is_object($this->getDefaultValue()) && !\is_object($initial->getDefaultValue()))
660 83
                ) {
661 798
                    $difference[] = $name;
662
                }
663
664
                continue;
665
            }
666
667
            if ($value !== $dbColumnVars[$name]) {
668
                $difference[] = $name;
669
            }
670
        }
671
672 682
        return empty($difference);
673
    }
674
675
    /**
676 682
     * Get database specific enum type definition options.
677
     */
678 578
    protected function quoteEnum(DriverInterface $driver): string
679
    {
680
        $enumValues = [];
681 634
        foreach ($this->enumValues as $value) {
682 8
            $enumValues[] = $driver->quote($value);
683
        }
684 634
685
        return !empty($enumValues) ? '(' . implode(', ', $enumValues) . ')' : '';
686 32
    }
687 32
688
    /**
689 632
     * Must return driver specific default value.
690
     */
691
    protected function quoteDefault(DriverInterface $driver): string
692
    {
693 634
        $defaultValue = $this->getDefaultValue();
694 32
        if ($defaultValue === null) {
695 301
            return 'NULL';
696
        }
697 634
698
        if ($defaultValue instanceof FragmentInterface) {
699
            return $driver->getQueryCompiler()->compile(
700
                new QueryParameters(),
701
                '',
702
                $defaultValue
703
            );
704
        }
705
706
        return match ($this->getType()) {
707
            'bool' => $defaultValue ? 'TRUE' : 'FALSE',
708
            'float' => sprintf('%F', $defaultValue),
709
            'int' => (string) $defaultValue,
710
            default => $driver->quote($defaultValue)
711
        };
712
    }
713
714
    /**
715
     * Ensure that datetime fields are correctly formatted.
716
     *
717
     * @psalm-param non-empty-string $type
718
     *
719
     * @throws DefaultValueException
720
     */
721
    protected function formatDatetime(
722
        string $type,
723
        string|int|\DateTimeInterface $value
724
    ): \DateTimeInterface|FragmentInterface|string {
725
        if ($value === static::DATETIME_NOW) {
726
            //Dynamic default value
727
            return new Fragment($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type DateTimeInterface; however, parameter $fragment of Cycle\Database\Injection\Fragment::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

727
            return new Fragment(/** @scrutinizer ignore-type */ $value);
Loading history...
728
        }
729
730
        if ($value instanceof \DateTimeInterface) {
731
            $datetime = clone $value;
732
        } else {
733
            if (is_numeric($value)) {
734
                //Presumably timestamp
735
                $datetime = new DateTimeImmutable('now', $this->timezone);
736
                $datetime = $datetime->setTimestamp($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type string; however, parameter $timestamp of DateTimeImmutable::setTimestamp() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

736
                $datetime = $datetime->setTimestamp(/** @scrutinizer ignore-type */ $value);
Loading history...
737
            } else {
738
                $datetime = new DateTimeImmutable($value, $this->timezone);
739
            }
740
        }
741
742
        return match ($type) {
743
            'datetime', 'timestamp' => $datetime,
744
            'time' => $datetime->format(static::TIME_FORMAT),
745
            'date' => $datetime->format(static::DATE_FORMAT),
746
            default => $value
747
        };
748
    }
749
750
    protected static function isEnum(self $column): bool
751
    {
752
        return $column->getAbstractType() === 'enum';
753
    }
754
}
755