Completed
Branch feature/pre-split (5afa53)
by Anton
13:51
created

AbstractColumn::getDefaultValue()   D

Complexity

Conditions 9
Paths 13

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 13
nop 0
dl 0
loc 32
rs 4.909
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\SchemaException;
11
use Spiral\Database\Injections\Fragment;
12
use Spiral\Database\Injections\FragmentInterface;
13
use Spiral\Database\Schemas\ColumnInterface;
14
15
/**
16
 * Abstract column schema with read (see ColumnInterface) and write abilities. Must be implemented
17
 * by driver to support DBMS specific syntax and creation rules.
18
 *
19
 * Shortcuts for various column types:
20
 *
21
 * @method AbstractColumn|$this primary()
22
 * @method AbstractColumn|$this bigPrimary()
23
 * @method AbstractColumn|$this boolean()
24
 * @method AbstractColumn|$this integer()
25
 * @method AbstractColumn|$this tinyInteger()
26
 * @method AbstractColumn|$this bigInteger()
27
 * @method AbstractColumn|$this text()
28
 * @method AbstractColumn|$this tinyText()
29
 * @method AbstractColumn|$this longText()
30
 * @method AbstractColumn|$this double()
31
 * @method AbstractColumn|$this float()
32
 * @method AbstractColumn|$this datetime()
33
 * @method AbstractColumn|$this date()
34
 * @method AbstractColumn|$this time()
35
 * @method AbstractColumn|$this timestamp()
36
 * @method AbstractColumn|$this binary()
37
 * @method AbstractColumn|$this tinyBinary()
38
 * @method AbstractColumn|$this longBinary()
39
 * @method AbstractColumn|$this json()
40
 */
41
abstract class AbstractColumn extends AbstractElement implements ColumnInterface
42
{
43
    /**
44
     * Default datetime value.
45
     */
46
    const DATETIME_DEFAULT = '1970-01-01 00:00:00';
47
48
    /**
49
     * Default timestamp expression (driver specific).
50
     */
51
    const DATETIME_CURRENT = 'CURRENT_TIMESTAMP';
52
53
    /**
54
     * Abstract type aliases (for consistency).
55
     *
56
     * @var array
57
     */
58
    private $aliases = [
59
        'int'            => 'integer',
60
        'bigint'         => 'bigInteger',
61
        'incremental'    => 'primary',
62
        'bigIncremental' => 'bigPrimary',
63
        'bool'           => 'boolean',
64
        'blob'           => 'binary',
65
    ];
66
67
    /**
68
     * Association list between abstract types and native PHP types. Every non listed type will be
69
     * converted into string.
70
     *
71
     * @invisible
72
     *
73
     * @var array
74
     */
75
    private $phpMapping = [
76
        self::INT   => ['primary', 'bigPrimary', 'integer', 'tinyInteger', 'bigInteger'],
77
        self::BOOL  => ['boolean'],
78
        self::FLOAT => ['double', 'float', 'decimal'],
79
    ];
80
81
    /**
82
     * Mapping between abstract type and internal database type with it's options. Multiple abstract
83
     * types can map into one database type, this implementation allows us to equalize two columns
84
     * if they have different abstract types but same database one. Must be declared by DBMS
85
     * specific implementation.
86
     *
87
     * Example:
88
     * integer => array('type' => 'int', 'size' => 1),
89
     * boolean => array('type' => 'tinyint', 'size' => 1)
90
     *
91
     * @invisible
92
     *
93
     * @var array
94
     */
95
    protected $mapping = [
96
        //Primary sequences
97
        'primary'     => null,
98
        'bigPrimary'  => null,
99
100
        //Enum type (mapped via method)
101
        'enum'        => null,
102
103
        //Logical types
104
        'boolean'     => null,
105
106
        //Integer types (size can always be changed with size method), longInteger has method alias
107
        //bigInteger
108
        'integer'     => null,
109
        'tinyInteger' => null,
110
        'bigInteger'  => null,
111
112
        //String with specified length (mapped via method)
113
        'string'      => null,
114
115
        //Generic types
116
        'text'        => null,
117
        'tinyText'    => null,
118
        'longText'    => null,
119
120
        //Real types
121
        'double'      => null,
122
        'float'       => null,
123
124
        //Decimal type (mapped via method)
125
        'decimal'     => null,
126
127
        //Date and Time types
128
        'datetime'    => null,
129
        'date'        => null,
130
        'time'        => null,
131
        'timestamp'   => null,
132
133
        //Binary types
134
        'binary'      => null,
135
        'tinyBinary'  => null,
136
        'longBinary'  => null,
137
138
        //Additional types
139
        'json'        => null,
140
    ];
141
142
    /**
143
     * Reverse mapping is responsible for generating abstact type based on database type and it's
144
     * options. Multiple database types can be mapped into one abstract type.
145
     *
146
     * @invisible
147
     *
148
     * @var array
149
     */
150
    protected $reverseMapping = [
151
        'primary'     => [],
152
        'bigPrimary'  => [],
153
        'enum'        => [],
154
        'boolean'     => [],
155
        'integer'     => [],
156
        'tinyInteger' => [],
157
        'bigInteger'  => [],
158
        'string'      => [],
159
        'text'        => [],
160
        'tinyText'    => [],
161
        'longText'    => [],
162
        'double'      => [],
163
        'float'       => [],
164
        'decimal'     => [],
165
        'datetime'    => [],
166
        'date'        => [],
167
        'time'        => [],
168
        'timestamp'   => [],
169
        'binary'      => [],
170
        'tinyBinary'  => [],
171
        'longBinary'  => [],
172
        'json'        => [],
173
    ];
174
175
    /**
176
     * DBMS specific column type.
177
     *
178
     * @var string
179
     */
180
    protected $type = '';
181
182
    /**
183
     * Indicates that column can contain null values.
184
     *
185
     * @var bool
186
     */
187
    protected $nullable = true;
188
189
    /**
190
     * Default column value, may not be applied to some datatypes (for example to primary keys),
191
     * should follow type size and other options.
192
     *
193
     * @var mixed
194
     */
195
    protected $defaultValue = null;
196
197
    /**
198
     * Column type size, can have different meanings for different datatypes.
199
     *
200
     * @var int
201
     */
202
    protected $size = 0;
203
204
    /**
205
     * Precision of column, applied only for "decimal" type.
206
     *
207
     * @var int
208
     */
209
    protected $precision = 0;
210
211
    /**
212
     * Scale of column, applied only for "decimal" type.
213
     *
214
     * @var int
215
     */
216
    protected $scale = 0;
217
218
    /**
219
     * List of allowed enum values.
220
     *
221
     * @var array
222
     */
223
    protected $enumValues = [];
224
225
    /**
226
     * {@inheritdoc}
227
     */
228
    public function getType(): string
229
    {
230
        return $this->type;
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236
    public function phpType(): string
237
    {
238
        $schemaType = $this->abstractType();
239
        foreach ($this->phpMapping as $phpType => $candidates) {
240
            if (in_array($schemaType, $candidates)) {
241
                return $phpType;
242
            }
243
        }
244
245
        return self::STRING;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function getSize(): int
252
    {
253
        return $this->size;
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function getPrecision(): int
260
    {
261
        return $this->precision;
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    public function getScale(): int
268
    {
269
        return $this->scale;
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function isNullable(): bool
276
    {
277
        return $this->nullable;
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function hasDefaultValue(): bool
284
    {
285
        return !is_null($this->defaultValue);
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function getDefaultValue()
292
    {
293
        if (!$this->hasDefaultValue()) {
294
            return null;
295
        }
296
297
        if ($this->defaultValue instanceof FragmentInterface) {
298
            return $this->defaultValue;
299
        }
300
301
        if (in_array($this->abstractType(), ['time', 'date', 'datetime', 'timestamp'])) {
302
            //Driver specific now expression
303
            if (strtolower($this->defaultValue) == strtolower(static::DATETIME_CURRENT)) {
304
                return new Fragment($this->defaultValue);
305
            }
306
        }
307
308
        switch ($this->phpType()) {
309
            case 'int':
310
                return (int)$this->defaultValue;
311
            case 'float':
312
                return (float)$this->defaultValue;
313
            case 'bool':
314
                if (strtolower($this->defaultValue) == 'false') {
315
                    return false;
316
                }
317
318
                return (bool)$this->defaultValue;
319
        }
320
321
        return (string)$this->defaultValue;
322
    }
323
324
    /**
325
     * Get every associated column constraint names.
326
     *
327
     * @return array
328
     */
329
    public function getConstraints(): array
330
    {
331
        return [];
332
    }
333
334
    /**
335
     * Get allowed enum values.
336
     *
337
     * @return array
338
     */
339
    public function getEnumValues(): array
340
    {
341
        return $this->enumValues;
342
    }
343
344
    /**
345
     * DBMS specific reverse mapping must map database specific type into limited set of abstract
346
     * types.
347
     *
348
     * @return string
349
     */
350
    public function abstractType(): string
351
    {
352
        foreach ($this->reverseMapping as $type => $candidates) {
353
            foreach ($candidates as $candidate) {
354
                if (is_string($candidate)) {
355
                    if (strtolower($candidate) == strtolower($this->type)) {
356
                        return $type;
357
                    }
358
359
                    continue;
360
                }
361
362
                if (strtolower($candidate['type']) != strtolower($this->type)) {
363
                    continue;
364
                }
365
366
                foreach ($candidate as $option => $required) {
367
                    if ($option == 'type') {
368
                        continue;
369
                    }
370
371
                    if ($this->{$option} != $required) {
372
                        continue 2;
373
                    }
374
                }
375
376
                return $type;
377
            }
378
        }
379
380
        return 'unknown';
381
    }
382
383
    //--- MODIFICATIONS IS HERE
384
385
386
    /**
387
     * Give column new abstract type. DBMS specific implementation must map provided type into one
388
     * of internal database values.
389
     *
390
     * Attention, changing type of existed columns in some databases has a lot of restrictions like
391
     * cross type conversions and etc. Try do not change column type without a reason.
392
     *
393
     * @param string $abstract Abstract or virtual type declared in mapping.
394
     *
395
     * @return self|$this
396
     *
397
     * @throws SchemaException
398
     */
399
    public function setType(string $abstract): AbstractColumn
400
    {
401
        if (isset($this->aliases[$abstract])) {
402
            $abstract = $this->aliases[$abstract];
403
        }
404
405
        if (!isset($this->mapping[$abstract])) {
406
            throw new SchemaException("Undefined abstract/virtual type '{$abstract}'");
407
        }
408
409
        //Resetting all values to default state.
410
        $this->size = $this->precision = $this->scale = 0;
411
        $this->enumValues = [];
412
413
        if (is_string($this->mapping[$abstract])) {
414
            $this->type = $this->mapping[$abstract];
415
416
            return $this;
417
        }
418
419
        //Additional type options
420
        foreach ($this->mapping[$abstract] as $property => $value) {
421
            $this->{$property} = $value;
422
        }
423
424
        return $this;
425
    }
426
427
    /**
428
     * Set column nullable/not nullable.
429
     *
430
     * @param bool $nullable
431
     *
432
     * @return self|$this
433
     */
434
    public function nullable(bool $nullable = true): AbstractColumn
435
    {
436
        $this->nullable = $nullable;
437
438
        return $this;
439
    }
440
441
    /**
442
     * Change column default value (can be forbidden for some column types).
443
     * Use Database::TIMESTAMP_NOW to use driver specific NOW() function.
444
     *
445
     * @param mixed $value
446
     *
447
     * @return self|$this
448
     */
449
    public function defaultValue($value): AbstractColumn
450
    {
451
        $this->defaultValue = $value;
452
        if (
453
            $this->abstractType() == 'timestamp'
454
            && strtolower($value) == strtolower(Driver::TIMESTAMP_NOW)
455
        ) {
456
            $this->defaultValue = $this->table->getDriver()->nowExpression();
0 ignored issues
show
Bug introduced by
The method getDriver cannot be called on $this->table (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
457
        }
458
459
        return $this;
460
    }
461
462
    /**
463
     * Set column as enum type and specify set of allowed values. Most of drivers will emulate enums
464
     * using column constraints.
465
     *
466
     * Examples:
467
     * $table->status->enum(['active', 'disabled']);
468
     * $table->status->enum('active', 'disabled');
469
     *
470
     * @param string|array $values Enum values (array or comma separated). String values only.
471
     *
472
     * @return self
473
     */
474
    public function enum($values): AbstractColumn
475
    {
476
        $this->setType('enum');
477
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
478
479
        return $this;
480
    }
481
482
    /**
483
     * Set column type as string with limited size. Maximum allowed size is 255 bytes, use "text"
484
     * abstract types for longer strings.
485
     *
486
     * Strings are perfect type to store email addresses as it big enough to store valid address
487
     * and
488
     * can be covered with unique index.
489
     *
490
     * @link http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
491
     *
492
     * @param int $size Max string length.
493
     *
494
     * @return self|$this
495
     *
496
     * @throws SchemaException
497
     */
498
    public function string(int $size = 255): AbstractColumn
499
    {
500
        $this->setType('string');
501
502
        if ($size > 255) {
503
            throw new SchemaException("String size can't exceed 255 characters. Use text instead");
504
        }
505
506
        if ($size < 0) {
507
            throw new SchemaException('Invalid string length value');
508
        }
509
510
        $this->size = (int)$size;
511
512
        return $this;
513
    }
514
515
    /**
516
     * Set column type as decimal with specific precision and scale.
517
     *
518
     * @param int $precision
519
     * @param int $scale
520
     *
521
     * @return self|$this
522
     *
523
     * @throws SchemaException
524
     */
525
    public function decimal(int $precision, int $scale = 0): AbstractColumn
526
    {
527
        $this->setType('decimal');
528
529
        if (empty($precision)) {
530
            throw new SchemaException('Invalid precision value');
531
        }
532
533
        $this->precision = (int)$precision;
534
        $this->scale = (int)$scale;
535
536
        return $this;
537
    }
538
539
    /**
540
     * Shortcut for AbstractColumn->type() method.
541
     *
542
     * @param string $type      Abstract type.
543
     * @param array  $arguments Not used.
544
     *
545
     * @return self
546
     */
547
    public function __call(string $type, array $arguments = []): AbstractColumn
548
    {
549
        return $this->setType($type);
550
    }
551
552
    /**
553
     * {@inheritdoc}
554
     */
555
    public function compare(ColumnInterface $initial): bool
556
    {
557
        $normalized = clone $initial;
558
559
        if ($this == $normalized) {
560
            return true;
561
        }
562
563
        $columnVars = get_object_vars($this);
564
        $dbColumnVars = get_object_vars($normalized);
565
566
        $difference = [];
567
        foreach ($columnVars as $name => $value) {
568
            if ($name == 'defaultValue') {
569
570
                //Default values has to compared using type-casted value
571
                if ($this->getDefaultValue() != $initial->getDefaultValue()) {
572
                    $difference[] = $name;
573
                }
574
575
                continue;
576
            }
577
578
            if ($value != $dbColumnVars[$name]) {
579
                $difference[] = $name;
580
            }
581
        }
582
583
        return empty($difference);
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     */
589
    public function sqlStatement(Driver $driver): string
590
    {
591
        $statement = [$driver->identifier($this->name), $this->type];
592
593
        if ($this->abstractType() == 'enum') {
594
            //Enum specific column options
595
            if (!empty($enumDefinition = $this->prepareEnum($driver))) {
596
                $statement[] = $enumDefinition;
597
            }
598
        } elseif (!empty($this->precision)) {
599
            $statement[] = "({$this->precision}, {$this->scale})";
600
        } elseif (!empty($this->size)) {
601
            $statement[] = "({$this->size})";
602
        }
603
604
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
605
606
        if ($this->defaultValue !== null) {
607
            $statement[] = "DEFAULT {$this->prepareDefault($driver)}";
608
        }
609
610
        return implode(' ', $statement);
611
    }
612
613
614
    /**
615
     * Simplified way to dump information.
616
     *
617
     * @return array
618
     */
619
    public function __debugInfo()
620
    {
621
        $column = [
622
            'name' => $this->name,
623
            'type' => [
624
                'database' => $this->type,
625
                'schema'   => $this->abstractType(),
626
                'php'      => $this->phpType(),
627
            ],
628
        ];
629
630
        if (!empty($this->size)) {
631
            $column['size'] = $this->size;
632
        }
633
634
        if ($this->nullable) {
635
            $column['nullable'] = true;
636
        }
637
638
        if ($this->defaultValue !== null) {
639
            $column['defaultValue'] = $this->getDefaultValue();
640
        }
641
642
        if ($this->abstractType() == 'enum') {
643
            $column['enumValues'] = $this->enumValues;
644
        }
645
646
        if ($this->abstractType() == 'decimal') {
647
            $column['precision'] = $this->precision;
648
            $column['scale'] = $this->scale;
649
        }
650
651
        return $column;
652
    }
653
654
    /**
655
     * Get database specific enum type definition options.
656
     *
657
     * @param Driver $driver
658
     *
659
     * @return string.
0 ignored issues
show
Documentation introduced by
The doc-type string. could not be parsed: Unknown type name "string." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
660
     */
661
    protected function prepareEnum(Driver $driver): string
662
    {
663
        $enumValues = [];
664
        foreach ($this->enumValues as $value) {
665
            $enumValues[] = $driver->quote($value);
666
        }
667
668
        if (!empty($enumValues)) {
669
            return '(' . implode(', ', $enumValues) . ')';
670
        }
671
672
        return '';
673
    }
674
675
    /**
676
     * Must return driver specific default value.
677
     *
678
     * @param Driver $driver
679
     *
680
     * @return string
681
     */
682
    protected function prepareDefault(Driver $driver): string
683
    {
684
        if (($defaultValue = $this->getDefaultValue()) === null) {
685
            return 'NULL';
686
        }
687
688
        if ($defaultValue instanceof FragmentInterface) {
689
            return $defaultValue->sqlStatement();
690
        }
691
692
        if ($this->phpType() == 'bool') {
693
            return $defaultValue ? 'TRUE' : 'FALSE';
694
        }
695
696
        if ($this->phpType() == 'float') {
697
            return sprintf('%F', $defaultValue);
698
        }
699
700
        if ($this->phpType() == 'int') {
701
            return $defaultValue;
702
        }
703
704
        return $driver->quote($defaultValue);
705
    }
706
}