Completed
Branch feature/pre-split (7b42f5)
by Anton
03:44
created

AbstractColumn::nullable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
468
    {
469
        if (!in_array($this->name, $this->table->getPrimaryKeys())) {
470
            $this->table->setPrimaryKeys([$this->name]);
471
        }
472
473
        return $this->setType('primary');
474
    }
475
476
    /**
477
     * Set column as big primary key and register it in parent table primary key list.
478
     *
479
     * @see TableSchema::setPrimaryKeys()
480
     *
481
     * @return $this
482
     */
483 View Code Duplication
    public function bigPrimary()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
484
    {
485
        if (!in_array($this->name, $this->table->getPrimaryKeys())) {
486
            $this->table->setPrimaryKeys([$this->name]);
487
        }
488
489
        return $this->setType('bigPrimary');
490
    }
491
492
    /**
493
     * Set column as enum type and specify set of allowed values. Most of drivers will emulate enums
494
     * using column constraints.
495
     *
496
     * Examples:
497
     * $table->status->enum(['active', 'disabled']);
498
     * $table->status->enum('active', 'disabled');
499
     *
500
     * @param string|array $values Enum values (array or comma separated). String values only.
501
     *
502
     * @return $this
503
     */
504
    public function enum($values)
505
    {
506
        $this->setType('enum');
507
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
508
509
        return $this;
510
    }
511
512
    /**
513
     * Set column type as string with limited size. Maximum allowed size is 255 bytes, use "text"
514
     * abstract types for longer strings.
515
     *
516
     * Strings are perfect type to store email addresses as it big enough to store valid address
517
     * and
518
     * can be covered with unique index.
519
     *
520
     * @link http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
521
     *
522
     * @param int $size Max string length.
523
     *
524
     * @return $this
525
     *
526
     * @throws SchemaException
527
     */
528
    public function string($size = 255)
529
    {
530
        $this->setType('string');
531
532
        if ($size > 255) {
533
            throw new SchemaException(
534
                "String size can't exceed 255 characters. Use text instead"
535
            );
536
        }
537
538
        if ($size < 0) {
539
            throw new SchemaException('Invalid string length value');
540
        }
541
542
        $this->size = (int)$size;
543
544
        return $this;
545
    }
546
547
    /**
548
     * Set column type as decimal with specific precision and scale.
549
     *
550
     * @param int $precision
551
     * @param int $scale
552
     *
553
     * @return $this
554
     *
555
     * @throws SchemaException
556
     */
557
    public function decimal($precision, $scale = 0)
558
    {
559
        $this->setType('decimal');
560
561
        if (empty($precision)) {
562
            throw new SchemaException('Invalid precision value');
563
        }
564
565
        $this->precision = (int)$precision;
566
        $this->scale = (int)$scale;
567
568
        return $this;
569
    }
570
571
    /**
572
     * Create/get table index associated with this column.
573
     *
574
     * @return AbstractIndex
575
     *
576
     * @throws SchemaException
577
     */
578
    public function index()
579
    {
580
        return $this->table->index($this->name);
581
    }
582
583
    /**
584
     * Create/get table index associated with this column. Index type will be forced as UNIQUE.
585
     *
586
     * @return AbstractIndex
587
     *
588
     * @throws SchemaException
589
     */
590
    public function unique()
591
    {
592
        return $this->table->unique($this->name);
593
    }
594
595
    /**
596
     * Create/get foreign key schema associated with column and referenced foreign table and column.
597
     * Make sure local and outer column types are identical.
598
     *
599
     * @param string $table  Foreign table name.
600
     * @param string $column Foreign column name (id by default).
601
     *
602
     * @return AbstractReference
603
     *
604
     * @throws SchemaException
605
     */
606
    public function references($table, $column = 'id')
607
    {
608
        if ($this->phpType() != self::INT) {
609
            throw new SchemaException(
610
                'Only numeric types can be defined with foreign key constraint.'
611
            );
612
        }
613
614
        return $this->table->foreign($this->name)->references($table, $column);
615
    }
616
617
    /**
618
     * Compile column create statement.
619
     *
620
     * @return string
621
     */
622
    public function sqlStatement()
623
    {
624
        $statement = [$this->getName(true), $this->type];
625
626
        if ($this->abstractType() == 'enum') {
627
            //Enum specific column options
628
            if (!empty($enumDefinition = $this->prepareEnum())) {
629
                $statement[] = $enumDefinition;
630
            }
631
        } elseif (!empty($this->precision)) {
632
            $statement[] = "({$this->precision}, {$this->scale})";
633
        } elseif (!empty($this->size)) {
634
            $statement[] = "({$this->size})";
635
        }
636
637
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
638
639
        if ($this->defaultValue !== null) {
640
            $statement[] = "DEFAULT {$this->prepareDefault()}";
641
        }
642
643
        return implode(' ', $statement);
644
    }
645
646
    /**
647
     * Must compare two instances of AbstractColumn.
648
     *
649
     * @param self $initial
650
     *
651
     * @return bool
652
     */
653
    public function compare(self $initial)
654
    {
655
        $normalized = clone $initial;
656
        $normalized->declared = $this->declared;
657
658
        if ($this == $normalized) {
659
            return true;
660
        }
661
662
        $columnVars = get_object_vars($this);
663
        $dbColumnVars = get_object_vars($normalized);
664
665
        $difference = [];
666
        foreach ($columnVars as $name => $value) {
667
            if ($name == 'defaultValue') {
668
                //Default values has to compared using type-casted value
669
                if ($this->getDefaultValue() != $initial->getDefaultValue()) {
670
                    $difference[] = $name;
671
                }
672
673
                continue;
674
            }
675
676
            if ($value != $dbColumnVars[$name]) {
677
                $difference[] = $name;
678
            }
679
        }
680
681
        return empty($difference);
682
    }
683
684
    /**
685
     * Shortcut for AbstractColumn->type() method.
686
     *
687
     * @param string $type      Abstract type.
688
     * @param array  $arguments Not used.
689
     *
690
     * @return $this
691
     */
692
    public function __call($type, array $arguments = [])
693
    {
694
        return $this->setType($type);
695
    }
696
697
    /**
698
     * Simplified way to dump information.
699
     *
700
     * @return array
701
     */
702
    public function __debugInfo()
703
    {
704
        $column = [
705
            'name'     => $this->name,
706
            'declared' => $this->declared,
707
            'type'     => [
708
                'database' => $this->type,
709
                'schema'   => $this->abstractType(),
710
                'php'      => $this->phpType(),
711
            ],
712
        ];
713
714
        if (!empty($this->size)) {
715
            $column['size'] = $this->size;
716
        }
717
718
        if ($this->nullable) {
719
            $column['nullable'] = true;
720
        }
721
722
        if ($this->defaultValue !== null) {
723
            $column['defaultValue'] = $this->getDefaultValue();
724
        }
725
726
        if ($this->abstractType() == 'enum') {
727
            $column['enumValues'] = $this->enumValues;
728
        }
729
730
        if ($this->abstractType() == 'decimal') {
731
            $column['precision'] = $this->precision;
732
            $column['scale'] = $this->scale;
733
        }
734
735
        return $column;
736
    }
737
738
    /**
739
     * Get database specific enum type definition options.
740
     *
741
     * @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...
742
     */
743
    protected function prepareEnum()
744
    {
745
        $enumValues = [];
746
        foreach ($this->enumValues as $value) {
747
            $enumValues[] = $this->table->driver()->getPDO()->quote($value);
748
        }
749
750
        if (!empty($enumValues)) {
751
            return '(' . implode(', ', $enumValues) . ')';
752
        }
753
754
        return '';
755
    }
756
757
    /**
758
     * Must return driver specific default value.
759
     *
760
     * @return string
761
     */
762
    protected function prepareDefault()
763
    {
764
        if (($defaultValue = $this->getDefaultValue()) === null) {
765
            return 'NULL';
766
        }
767
768
        if ($defaultValue instanceof FragmentInterface) {
769
            return $defaultValue->sqlStatement();
770
        }
771
772
        if ($this->phpType() == 'bool') {
773
            return $defaultValue ? 'TRUE' : 'FALSE';
774
        }
775
776
        if ($this->phpType() == 'float') {
777
            return sprintf('%F', $defaultValue);
778
        }
779
780
        if ($this->phpType() == 'int') {
781
            return $defaultValue;
782
        }
783
784
        return $this->table->driver()->getPDO()->quote($defaultValue);
785
    }
786
}
787