Completed
Branch feature/pre-split (713d19)
by Anton
03:04
created

AbstractColumn::defaultValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Schemas\Prototypes;
8
9
use Spiral\Database\Entities\Driver;
10
use Spiral\Database\Exceptions\DefaultValueException;
11
use Spiral\Database\Exceptions\SchemaException;
12
use Spiral\Database\Injections\Fragment;
13
use Spiral\Database\Injections\FragmentInterface;
14
use Spiral\Database\Schemas\ColumnInterface;
15
16
/**
17
 * Abstract column schema with read (see ColumnInterface) and write abilities. Must be implemented
18
 * by driver to support DBMS specific syntax and creation rules.
19
 *
20
 * Shortcuts for various column types:
21
 *
22
 * @method AbstractColumn|$this primary()
23
 * @method AbstractColumn|$this bigPrimary()
24
 * @method AbstractColumn|$this boolean()
25
 * @method AbstractColumn|$this integer()
26
 * @method AbstractColumn|$this tinyInteger()
27
 * @method AbstractColumn|$this bigInteger()
28
 * @method AbstractColumn|$this text()
29
 * @method AbstractColumn|$this tinyText()
30
 * @method AbstractColumn|$this longText()
31
 * @method AbstractColumn|$this double()
32
 * @method AbstractColumn|$this float()
33
 * @method AbstractColumn|$this datetime()
34
 * @method AbstractColumn|$this date()
35
 * @method AbstractColumn|$this time()
36
 * @method AbstractColumn|$this timestamp()
37
 * @method AbstractColumn|$this binary()
38
 * @method AbstractColumn|$this tinyBinary()
39
 * @method AbstractColumn|$this longBinary()
40
 * @method AbstractColumn|$this json()
41
 */
42
abstract class AbstractColumn extends AbstractElement implements ColumnInterface
43
{
44
    /**
45
     * Default timestamp expression (driver specific).
46
     */
47
    const DATETIME_NOW = 'CURRENT_TIMESTAMP';
48
49
    /**
50
     * Normalization for time and dates.
51
     */
52
    const DATE_FORMAT = 'Y-m-d';
53
    const TIME_FORMAT = 'H:i:s';
54
55
    /**
56
     * Abstract type aliases (for consistency).
57
     *
58
     * @var array
59
     */
60
    private $aliases = [
61
        'int'            => 'integer',
62
        'bigint'         => 'bigInteger',
63
        'incremental'    => 'primary',
64
        'bigIncremental' => 'bigPrimary',
65
        'bool'           => 'boolean',
66
        'blob'           => 'binary',
67
    ];
68
69
    /**
70
     * Association list between abstract types and native PHP types. Every non listed type will be
71
     * converted into string.
72
     *
73
     * @invisible
74
     *
75
     * @var array
76
     */
77
    private $phpMapping = [
78
        self::INT   => ['primary', 'bigPrimary', 'integer', 'tinyInteger', 'bigInteger'],
79
        self::BOOL  => ['boolean'],
80
        self::FLOAT => ['double', 'float', 'decimal'],
81
    ];
82
83
    /**
84
     * Mapping between abstract type and internal database type with it's options. Multiple abstract
85
     * types can map into one database type, this implementation allows us to equalize two columns
86
     * if they have different abstract types but same database one. Must be declared by DBMS
87
     * specific implementation.
88
     *
89
     * Example:
90
     * integer => array('type' => 'int', 'size' => 1),
91
     * boolean => array('type' => 'tinyint', 'size' => 1)
92
     *
93
     * @invisible
94
     *
95
     * @var array
96
     */
97
    protected $mapping = [
98
        //Primary sequences
99
        'primary'     => null,
100
        'bigPrimary'  => null,
101
102
        //Enum type (mapped via method)
103
        'enum'        => null,
104
105
        //Logical types
106
        'boolean'     => null,
107
108
        //Integer types (size can always be changed with size method), longInteger has method alias
109
        //bigInteger
110
        'integer'     => null,
111
        'tinyInteger' => null,
112
        'bigInteger'  => null,
113
114
        //String with specified length (mapped via method)
115
        'string'      => null,
116
117
        //Generic types
118
        'text'        => null,
119
        'tinyText'    => null,
120
        'longText'    => null,
121
122
        //Real types
123
        'double'      => null,
124
        'float'       => null,
125
126
        //Decimal type (mapped via method)
127
        'decimal'     => null,
128
129
        //Date and Time types
130
        'datetime'    => null,
131
        'date'        => null,
132
        'time'        => null,
133
        'timestamp'   => null,
134
135
        //Binary types
136
        'binary'      => null,
137
        'tinyBinary'  => null,
138
        'longBinary'  => null,
139
140
        //Additional types
141
        'json'        => null,
142
    ];
143
144
    /**
145
     * Reverse mapping is responsible for generating abstact type based on database type and it's
146
     * options. Multiple database types can be mapped into one abstract type.
147
     *
148
     * @invisible
149
     *
150
     * @var array
151
     */
152
    protected $reverseMapping = [
153
        'primary'     => [],
154
        'bigPrimary'  => [],
155
        'enum'        => [],
156
        'boolean'     => [],
157
        'integer'     => [],
158
        'tinyInteger' => [],
159
        'bigInteger'  => [],
160
        'string'      => [],
161
        'text'        => [],
162
        'tinyText'    => [],
163
        'longText'    => [],
164
        'double'      => [],
165
        'float'       => [],
166
        'decimal'     => [],
167
        'datetime'    => [],
168
        'date'        => [],
169
        'time'        => [],
170
        'timestamp'   => [],
171
        'binary'      => [],
172
        'tinyBinary'  => [],
173
        'longBinary'  => [],
174
        'json'        => [],
175
    ];
176
177
    /**
178
     * DBMS specific column type.
179
     *
180
     * @var string
181
     */
182
    protected $type = '';
183
184
    /**
185
     * Indicates that column can contain null values.
186
     *
187
     * @var bool
188
     */
189
    protected $nullable = true;
190
191
    /**
192
     * Default column value, may not be applied to some datatypes (for example to primary keys),
193
     * should follow type size and other options.
194
     *
195
     * @var mixed
196
     */
197
    protected $defaultValue = null;
198
199
    /**
200
     * Column type size, can have different meanings for different datatypes.
201
     *
202
     * @var int
203
     */
204
    protected $size = 0;
205
206
    /**
207
     * Precision of column, applied only for "decimal" type.
208
     *
209
     * @var int
210
     */
211
    protected $precision = 0;
212
213
    /**
214
     * Scale of column, applied only for "decimal" type.
215
     *
216
     * @var int
217
     */
218
    protected $scale = 0;
219
220
    /**
221
     * List of allowed enum values.
222
     *
223
     * @var array
224
     */
225
    protected $enumValues = [];
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    public function getType(): string
231
    {
232
        return $this->type;
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238
    public function phpType(): string
239
    {
240
        $schemaType = $this->abstractType();
241
        foreach ($this->phpMapping as $phpType => $candidates) {
242
            if (in_array($schemaType, $candidates)) {
243
                return $phpType;
244
            }
245
        }
246
247
        return self::STRING;
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253
    public function getSize(): int
254
    {
255
        return $this->size;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function getPrecision(): int
262
    {
263
        return $this->precision;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function getScale(): int
270
    {
271
        return $this->scale;
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277
    public function isNullable(): bool
278
    {
279
        return $this->nullable;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function hasDefaultValue(): bool
286
    {
287
        return !is_null($this->defaultValue);
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     *
293
     * @throws DefaultValueException
294
     */
295
    public function getDefaultValue()
296
    {
297
        if (!$this->hasDefaultValue()) {
298
            return null;
299
        }
300
301
        if ($this->defaultValue instanceof FragmentInterface) {
302
            //Defined as SQL piece
303
            return $this->defaultValue;
304
        }
305
306
        if (in_array($this->abstractType(), ['time', 'date', 'datetime', 'timestamp'])) {
307
            return $this->normalizeDatetime($this->abstractType(), $this->defaultValue);
308
        }
309
310
        switch ($this->phpType()) {
311
            case 'int':
312
                return (int)$this->defaultValue;
313
            case 'float':
314
                return (float)$this->defaultValue;
315
            case 'bool':
316
                if (strtolower($this->defaultValue) == 'false') {
317
                    return false;
318
                }
319
320
                return (bool)$this->defaultValue;
321
        }
322
323
        return (string)$this->defaultValue;
324
    }
325
326
    /**
327
     * Get every associated column constraint names.
328
     *
329
     * @return array
330
     */
331
    public function getConstraints(): array
332
    {
333
        return [];
334
    }
335
336
    /**
337
     * Get allowed enum values.
338
     *
339
     * @return array
340
     */
341
    public function getEnumValues(): array
342
    {
343
        return $this->enumValues;
344
    }
345
346
    /**
347
     * DBMS specific reverse mapping must map database specific type into limited set of abstract
348
     * types.
349
     *
350
     * @return string
351
     */
352
    public function abstractType(): string
353
    {
354
        foreach ($this->reverseMapping as $type => $candidates) {
355
            foreach ($candidates as $candidate) {
356
                if (is_string($candidate)) {
357
                    if (strtolower($candidate) == strtolower($this->type)) {
358
                        return $type;
359
                    }
360
361
                    continue;
362
                }
363
364
                if (strtolower($candidate['type']) != strtolower($this->type)) {
365
                    continue;
366
                }
367
368
                foreach ($candidate as $option => $required) {
369
                    if ($option == 'type') {
370
                        continue;
371
                    }
372
373
                    if ($this->{$option} != $required) {
374
                        continue 2;
375
                    }
376
                }
377
378
                return $type;
379
            }
380
        }
381
382
        return 'unknown';
383
    }
384
385
    /**
386
     * Give column new abstract type. DBMS specific implementation must map provided type into one
387
     * of internal database values.
388
     *
389
     * Attention, changing type of existed columns in some databases has a lot of restrictions like
390
     * cross type conversions and etc. Try do not change column type without a reason.
391
     *
392
     * @param string $abstract Abstract or virtual type declared in mapping.
393
     *
394
     * @return self|$this
395
     *
396
     * @throws SchemaException
397
     */
398
    public function setType(string $abstract): AbstractColumn
399
    {
400
        if (isset($this->aliases[$abstract])) {
401
            $abstract = $this->aliases[$abstract];
402
        }
403
404
        if (!isset($this->mapping[$abstract])) {
405
            throw new SchemaException("Undefined abstract/virtual type '{$abstract}'");
406
        }
407
408
        //Resetting all values to default state.
409
        $this->size = $this->precision = $this->scale = 0;
410
        $this->enumValues = [];
411
412
        if (is_string($this->mapping[$abstract])) {
413
            $this->type = $this->mapping[$abstract];
414
415
            return $this;
416
        }
417
418
        //Additional type options
419
        foreach ($this->mapping[$abstract] as $property => $value) {
420
            $this->{$property} = $value;
421
        }
422
423
        return $this;
424
    }
425
426
    /**
427
     * Set column nullable/not nullable.
428
     *
429
     * @param bool $nullable
430
     *
431
     * @return self|$this
432
     */
433
    public function nullable(bool $nullable = true): AbstractColumn
434
    {
435
        $this->nullable = $nullable;
436
437
        return $this;
438
    }
439
440
    /**
441
     * Change column default value (can be forbidden for some column types).
442
     * Use Database::TIMESTAMP_NOW to use driver specific NOW() function.
443
     *
444
     * @param mixed $value
445
     *
446
     * @return self|$this
447
     */
448
    public function defaultValue($value): AbstractColumn
449
    {
450
        //Forcing driver specific values
451
        if ($value === self::DATETIME_NOW) {
452
            $value = static::DATETIME_NOW;
453
        }
454
455
        $this->defaultValue = $value;
456
457
        return $this;
458
    }
459
460
    /**
461
     * Set column as enum type and specify set of allowed values. Most of drivers will emulate enums
462
     * using column constraints.
463
     *
464
     * Examples:
465
     * $table->status->enum(['active', 'disabled']);
466
     * $table->status->enum('active', 'disabled');
467
     *
468
     * @param string|array $values Enum values (array or comma separated). String values only.
469
     *
470
     * @return self
471
     */
472
    public function enum($values): AbstractColumn
473
    {
474
        $this->setType('enum');
475
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
476
477
        return $this;
478
    }
479
480
    /**
481
     * Set column type as string with limited size. Maximum allowed size is 255 bytes, use "text"
482
     * abstract types for longer strings.
483
     *
484
     * Strings are perfect type to store email addresses as it big enough to store valid address
485
     * and
486
     * can be covered with unique index.
487
     *
488
     * @link http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
489
     *
490
     * @param int $size Max string length.
491
     *
492
     * @return self|$this
493
     *
494
     * @throws SchemaException
495
     */
496
    public function string(int $size = 255): AbstractColumn
497
    {
498
        $this->setType('string');
499
500
        if ($size > 255) {
501
            throw new SchemaException("String size can't exceed 255 characters. Use text instead");
502
        }
503
504
        if ($size < 0) {
505
            throw new SchemaException('Invalid string length value');
506
        }
507
508
        $this->size = (int)$size;
509
510
        return $this;
511
    }
512
513
    /**
514
     * Set column type as decimal with specific precision and scale.
515
     *
516
     * @param int $precision
517
     * @param int $scale
518
     *
519
     * @return self|$this
520
     *
521
     * @throws SchemaException
522
     */
523
    public function decimal(int $precision, int $scale = 0): AbstractColumn
524
    {
525
        $this->setType('decimal');
526
527
        if (empty($precision)) {
528
            throw new SchemaException('Invalid precision value');
529
        }
530
531
        $this->precision = (int)$precision;
532
        $this->scale = (int)$scale;
533
534
        return $this;
535
    }
536
537
    /**
538
     * Shortcut for AbstractColumn->type() method.
539
     *
540
     * @param string $type      Abstract type.
541
     * @param array  $arguments Not used.
542
     *
543
     * @return self
544
     */
545
    public function __call(string $type, array $arguments = []): AbstractColumn
546
    {
547
        return $this->setType($type);
548
    }
549
550
    /**
551
     * {@inheritdoc}
552
     */
553
    public function compare(ColumnInterface $initial): bool
554
    {
555
        $normalized = clone $initial;
556
557
        if ($this == $normalized) {
558
            return true;
559
        }
560
561
        $columnVars = get_object_vars($this);
562
        $dbColumnVars = get_object_vars($normalized);
563
564
        $difference = [];
565
        foreach ($columnVars as $name => $value) {
566
            if ($name == 'defaultValue') {
567
568
                //Default values has to compared using type-casted value
569
                if ($this->getDefaultValue() != $initial->getDefaultValue()) {
570
                    $difference[] = $name;
571
                }
572
573
                continue;
574
            }
575
576
            if ($value != $dbColumnVars[$name]) {
577
                $difference[] = $name;
578
            }
579
        }
580
581
        return empty($difference);
582
    }
583
584
    /**
585
     * {@inheritdoc}
586
     */
587
    public function sqlStatement(Driver $driver): string
588
    {
589
        $statement = [$driver->identifier($this->name), $this->type];
590
591
        if ($this->abstractType() == 'enum') {
592
            //Enum specific column options
593
            if (!empty($enumDefinition = $this->quoteEnum($driver))) {
594
                $statement[] = $enumDefinition;
595
            }
596
        } elseif (!empty($this->precision)) {
597
            $statement[] = "({$this->precision}, {$this->scale})";
598
        } elseif (!empty($this->size)) {
599
            $statement[] = "({$this->size})";
600
        }
601
602
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
603
604
        if ($this->defaultValue !== null) {
605
            $statement[] = "DEFAULT {$this->quoteDefault($driver)}";
606
        }
607
608
        return implode(' ', $statement);
609
    }
610
611
    /**
612
     * Simplified way to dump information.
613
     *
614
     * @return array
615
     */
616
    public function __debugInfo()
617
    {
618
        $column = [
619
            'name' => $this->name,
620
            'type' => [
621
                'database' => $this->type,
622
                'schema'   => $this->abstractType(),
623
                'php'      => $this->phpType(),
624
            ],
625
        ];
626
627
        if (!empty($this->size)) {
628
            $column['size'] = $this->size;
629
        }
630
631
        if ($this->nullable) {
632
            $column['nullable'] = true;
633
        }
634
635
        if ($this->defaultValue !== null) {
636
            $column['defaultValue'] = $this->getDefaultValue();
637
        }
638
639
        if ($this->abstractType() == 'enum') {
640
            $column['enumValues'] = $this->enumValues;
641
        }
642
643
        if ($this->abstractType() == 'decimal') {
644
            $column['precision'] = $this->precision;
645
            $column['scale'] = $this->scale;
646
        }
647
648
        return $column;
649
    }
650
651
    /**
652
     * Get database specific enum type definition options.
653
     *
654
     * @param Driver $driver
655
     *
656
     * @return string
657
     */
658
    protected function quoteEnum(Driver $driver): string
659
    {
660
        $enumValues = [];
661
        foreach ($this->enumValues as $value) {
662
            $enumValues[] = $driver->quote($value);
663
        }
664
665
        if (!empty($enumValues)) {
666
            return '(' . implode(', ', $enumValues) . ')';
667
        }
668
669
        return '';
670
    }
671
672
    /**
673
     * Must return driver specific default value.
674
     *
675
     * @param Driver $driver
676
     *
677
     * @return string
678
     */
679
    protected function quoteDefault(Driver $driver): string
680
    {
681
        $defaultValue = $this->getDefaultValue();
682
        if ($defaultValue === null) {
683
            return 'NULL';
684
        }
685
686
        if ($defaultValue instanceof FragmentInterface) {
687
            return $defaultValue->sqlStatement();
688
        }
689
690
        if ($this->phpType() == 'bool') {
691
            return $defaultValue ? 'TRUE' : 'FALSE';
692
        }
693
694
        if ($this->phpType() == 'float') {
695
            return sprintf('%F', $defaultValue);
696
        }
697
698
        if ($this->phpType() == 'int') {
699
            return $defaultValue;
700
        }
701
702
        return $driver->quote($defaultValue);
703
    }
704
705
    /**
706
     * Ensure that datetime fields are correctly formatted.
707
     *
708
     * @param string $type
709
     * @param string $value
710
     *
711
     * @return string|FragmentInterface|\DateTime
712
     *
713
     * @throws DefaultValueException
714
     */
715
    protected function normalizeDatetime(string $type, $value)
716
    {
717
        if ($value === static::DATETIME_NOW) {
718
            //Dynamic default value
719
            return new Fragment($value);
720
        }
721
722
        //In order to correctly normalize date or time let's convert it into DateTime object first
723
        $datetime = new \DateTime();
724
725
        if (is_numeric($value)) {
726
            //Presumably timestamp
727
            $datetime->setTimestamp($value);
728
        } else {
729
            $timestamp = strtotime($value);
730
            if ($timestamp === false) {
731
                throw new DefaultValueException(
732
                    "Unable to normalize timestamp '{$value}' for column type '{$type}' in " . get_class($this)
733
                );
734
            }
735
736
            $datetime->setTimestamp($timestamp);
737
        }
738
739
        switch ($type) {
740
            case 'datetime':
741
                //no break
742
            case 'timestamp':
743
                //Driver should handle conversion automatically in this case
744
                return $datetime;
745
            case 'time':
746
                return $datetime->format(static::TIME_FORMAT);
747
            case 'date':
748
                return $datetime->format(static::DATE_FORMAT);
749
        }
750
751
        return $value;
752
    }
753
}