Completed
Branch feature/pre-split (1b5228)
by Anton
03:14
created

AbstractColumn   D

Complexity

Total Complexity 81

Size/Duplication

Total Lines 708
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 708
rs 4.4444
c 1
b 0
f 0
wmc 81
lcom 1
cbo 7

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getType() 0 4 1
A phpType() 0 11 3
A getSize() 0 4 1
A getPrecision() 0 4 1
A getScale() 0 4 1
A isNullable() 0 4 1
A hasDefaultValue() 0 4 1
C getDefaultValue() 0 29 8
A getConstraints() 0 4 1
A getEnumValues() 0 4 1
D abstractType() 0 32 9
B setType() 0 27 5
A nullable() 0 6 1
A defaultValue() 0 11 2
A enum() 0 7 2
A string() 0 16 3
A decimal() 0 13 2
A __call() 0 4 1
B compare() 0 30 6
C sqlStatement() 0 23 7
B __debugInfo() 0 34 6
A prepareEnum() 0 13 3
C prepareDefault() 0 25 7
C normalizeDatetime() 0 37 8

How to fix   Complexity   

Complex Class

Complex classes like AbstractColumn often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractColumn, and based on these observations, apply Extract Interface, too.

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_CURRENT = '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
    public function getDefaultValue()
294
    {
295
        if (!$this->hasDefaultValue()) {
296
            return null;
297
        }
298
299
        if ($this->defaultValue instanceof FragmentInterface) {
300
            return $this->defaultValue;
301
        }
302
303
        if (in_array($this->abstractType(), ['time', 'date', 'datetime', 'timestamp'])) {
304
            return $this->normalizeDatetime($this->abstractType(), $this->defaultValue);
305
        }
306
307
        switch ($this->phpType()) {
308
            case 'int':
309
                return (int)$this->defaultValue;
310
            case 'float':
311
                return (float)$this->defaultValue;
312
            case 'bool':
313
                if (strtolower($this->defaultValue) == 'false') {
314
                    return false;
315
                }
316
317
                return (bool)$this->defaultValue;
318
        }
319
320
        return (string)$this->defaultValue;
321
    }
322
323
    /**
324
     * Get every associated column constraint names.
325
     *
326
     * @return array
327
     */
328
    public function getConstraints(): array
329
    {
330
        return [];
331
    }
332
333
    /**
334
     * Get allowed enum values.
335
     *
336
     * @return array
337
     */
338
    public function getEnumValues(): array
339
    {
340
        return $this->enumValues;
341
    }
342
343
    /**
344
     * DBMS specific reverse mapping must map database specific type into limited set of abstract
345
     * types.
346
     *
347
     * @return string
348
     */
349
    public function abstractType(): string
350
    {
351
        foreach ($this->reverseMapping as $type => $candidates) {
352
            foreach ($candidates as $candidate) {
353
                if (is_string($candidate)) {
354
                    if (strtolower($candidate) == strtolower($this->type)) {
355
                        return $type;
356
                    }
357
358
                    continue;
359
                }
360
361
                if (strtolower($candidate['type']) != strtolower($this->type)) {
362
                    continue;
363
                }
364
365
                foreach ($candidate as $option => $required) {
366
                    if ($option == 'type') {
367
                        continue;
368
                    }
369
370
                    if ($this->{$option} != $required) {
371
                        continue 2;
372
                    }
373
                }
374
375
                return $type;
376
            }
377
        }
378
379
        return 'unknown';
380
    }
381
382
    /**
383
     * Give column new abstract type. DBMS specific implementation must map provided type into one
384
     * of internal database values.
385
     *
386
     * Attention, changing type of existed columns in some databases has a lot of restrictions like
387
     * cross type conversions and etc. Try do not change column type without a reason.
388
     *
389
     * @param string $abstract Abstract or virtual type declared in mapping.
390
     *
391
     * @return self|$this
392
     *
393
     * @throws SchemaException
394
     */
395
    public function setType(string $abstract): AbstractColumn
396
    {
397
        if (isset($this->aliases[$abstract])) {
398
            $abstract = $this->aliases[$abstract];
399
        }
400
401
        if (!isset($this->mapping[$abstract])) {
402
            throw new SchemaException("Undefined abstract/virtual type '{$abstract}'");
403
        }
404
405
        //Resetting all values to default state.
406
        $this->size = $this->precision = $this->scale = 0;
407
        $this->enumValues = [];
408
409
        if (is_string($this->mapping[$abstract])) {
410
            $this->type = $this->mapping[$abstract];
411
412
            return $this;
413
        }
414
415
        //Additional type options
416
        foreach ($this->mapping[$abstract] as $property => $value) {
417
            $this->{$property} = $value;
418
        }
419
420
        return $this;
421
    }
422
423
    /**
424
     * Set column nullable/not nullable.
425
     *
426
     * @param bool $nullable
427
     *
428
     * @return self|$this
429
     */
430
    public function nullable(bool $nullable = true): AbstractColumn
431
    {
432
        $this->nullable = $nullable;
433
434
        return $this;
435
    }
436
437
    /**
438
     * Change column default value (can be forbidden for some column types).
439
     * Use Database::TIMESTAMP_NOW to use driver specific NOW() function.
440
     *
441
     * @param mixed $value
442
     *
443
     * @return self|$this
444
     */
445
    public function defaultValue($value): AbstractColumn
446
    {
447
        //Forcing driver specific values
448
        if ($value === self::DATETIME_CURRENT) {
449
            $value = static::DATETIME_CURRENT;
450
        }
451
452
        $this->defaultValue = $value;
453
454
        return $this;
455
    }
456
457
    /**
458
     * Set column as enum type and specify set of allowed values. Most of drivers will emulate enums
459
     * using column constraints.
460
     *
461
     * Examples:
462
     * $table->status->enum(['active', 'disabled']);
463
     * $table->status->enum('active', 'disabled');
464
     *
465
     * @param string|array $values Enum values (array or comma separated). String values only.
466
     *
467
     * @return self
468
     */
469
    public function enum($values): AbstractColumn
470
    {
471
        $this->setType('enum');
472
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
473
474
        return $this;
475
    }
476
477
    /**
478
     * Set column type as string with limited size. Maximum allowed size is 255 bytes, use "text"
479
     * abstract types for longer strings.
480
     *
481
     * Strings are perfect type to store email addresses as it big enough to store valid address
482
     * and
483
     * can be covered with unique index.
484
     *
485
     * @link http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
486
     *
487
     * @param int $size Max string length.
488
     *
489
     * @return self|$this
490
     *
491
     * @throws SchemaException
492
     */
493
    public function string(int $size = 255): AbstractColumn
494
    {
495
        $this->setType('string');
496
497
        if ($size > 255) {
498
            throw new SchemaException("String size can't exceed 255 characters. Use text instead");
499
        }
500
501
        if ($size < 0) {
502
            throw new SchemaException('Invalid string length value');
503
        }
504
505
        $this->size = (int)$size;
506
507
        return $this;
508
    }
509
510
    /**
511
     * Set column type as decimal with specific precision and scale.
512
     *
513
     * @param int $precision
514
     * @param int $scale
515
     *
516
     * @return self|$this
517
     *
518
     * @throws SchemaException
519
     */
520
    public function decimal(int $precision, int $scale = 0): AbstractColumn
521
    {
522
        $this->setType('decimal');
523
524
        if (empty($precision)) {
525
            throw new SchemaException('Invalid precision value');
526
        }
527
528
        $this->precision = (int)$precision;
529
        $this->scale = (int)$scale;
530
531
        return $this;
532
    }
533
534
    /**
535
     * Shortcut for AbstractColumn->type() method.
536
     *
537
     * @param string $type      Abstract type.
538
     * @param array  $arguments Not used.
539
     *
540
     * @return self
541
     */
542
    public function __call(string $type, array $arguments = []): AbstractColumn
543
    {
544
        return $this->setType($type);
545
    }
546
547
    /**
548
     * {@inheritdoc}
549
     */
550
    public function compare(ColumnInterface $initial): bool
551
    {
552
        $normalized = clone $initial;
553
554
        if ($this == $normalized) {
555
            return true;
556
        }
557
558
        $columnVars = get_object_vars($this);
559
        $dbColumnVars = get_object_vars($normalized);
560
561
        $difference = [];
562
        foreach ($columnVars as $name => $value) {
563
            if ($name == 'defaultValue') {
564
565
                //Default values has to compared using type-casted value
566
                if ($this->getDefaultValue() != $initial->getDefaultValue()) {
567
                    $difference[] = $name;
568
                }
569
570
                continue;
571
            }
572
573
            if ($value != $dbColumnVars[$name]) {
574
                $difference[] = $name;
575
            }
576
        }
577
578
        return empty($difference);
579
    }
580
581
    /**
582
     * {@inheritdoc}
583
     */
584
    public function sqlStatement(Driver $driver): string
585
    {
586
        $statement = [$driver->identifier($this->name), $this->type];
587
588
        if ($this->abstractType() == 'enum') {
589
            //Enum specific column options
590
            if (!empty($enumDefinition = $this->prepareEnum($driver))) {
591
                $statement[] = $enumDefinition;
592
            }
593
        } elseif (!empty($this->precision)) {
594
            $statement[] = "({$this->precision}, {$this->scale})";
595
        } elseif (!empty($this->size)) {
596
            $statement[] = "({$this->size})";
597
        }
598
599
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
600
601
        if ($this->defaultValue !== null) {
602
            $statement[] = "DEFAULT {$this->prepareDefault($driver)}";
603
        }
604
605
        return implode(' ', $statement);
606
    }
607
608
    /**
609
     * Simplified way to dump information.
610
     *
611
     * @return array
612
     */
613
    public function __debugInfo()
614
    {
615
        $column = [
616
            'name' => $this->name,
617
            'type' => [
618
                'database' => $this->type,
619
                'schema'   => $this->abstractType(),
620
                'php'      => $this->phpType(),
621
            ],
622
        ];
623
624
        if (!empty($this->size)) {
625
            $column['size'] = $this->size;
626
        }
627
628
        if ($this->nullable) {
629
            $column['nullable'] = true;
630
        }
631
632
        if ($this->defaultValue !== null) {
633
            $column['defaultValue'] = $this->getDefaultValue();
634
        }
635
636
        if ($this->abstractType() == 'enum') {
637
            $column['enumValues'] = $this->enumValues;
638
        }
639
640
        if ($this->abstractType() == 'decimal') {
641
            $column['precision'] = $this->precision;
642
            $column['scale'] = $this->scale;
643
        }
644
645
        return $column;
646
    }
647
648
    /**
649
     * Get database specific enum type definition options.
650
     *
651
     * @param Driver $driver
652
     *
653
     * @return string
654
     */
655
    protected function prepareEnum(Driver $driver): string
656
    {
657
        $enumValues = [];
658
        foreach ($this->enumValues as $value) {
659
            $enumValues[] = $driver->quote($value);
660
        }
661
662
        if (!empty($enumValues)) {
663
            return '(' . implode(', ', $enumValues) . ')';
664
        }
665
666
        return '';
667
    }
668
669
    /**
670
     * Must return driver specific default value.
671
     *
672
     * @param Driver $driver
673
     *
674
     * @return string
675
     */
676
    protected function prepareDefault(Driver $driver): string
677
    {
678
        $defaultValue = $this->getDefaultValue();
679
        if ($defaultValue === null) {
680
            return 'NULL';
681
        }
682
683
        if ($defaultValue instanceof FragmentInterface) {
684
            return $defaultValue->sqlStatement();
685
        }
686
687
        if ($this->phpType() == 'bool') {
688
            return $defaultValue ? 'TRUE' : 'FALSE';
689
        }
690
691
        if ($this->phpType() == 'float') {
692
            return sprintf('%F', $defaultValue);
693
        }
694
695
        if ($this->phpType() == 'int') {
696
            return $defaultValue;
697
        }
698
699
        return $driver->quote($defaultValue);
700
    }
701
702
    /**
703
     * Ensure that datetime fields are correctly formatted.
704
     *
705
     * @param string $type
706
     * @param string $value
707
     *
708
     * @return string|FragmentInterface|\DateTime
709
     *
710
     * @throws DefaultValueException
711
     */
712
    protected function normalizeDatetime(string $type, $value)
713
    {
714
        if ($value === static::DATETIME_CURRENT) {
715
            //Dynamic default value
716
            return new Fragment($value);
717
        }
718
719
        //In order to correctly normalize date or time let's convert it into DateTime object first
720
        $datetime = new \DateTime();
721
722
        if (is_numeric($value)) {
723
            $datetime->setTimestamp($value);
724
        } else {
725
            $timestamp = strtotime($value);
726
            if ($timestamp === false) {
727
                throw new DefaultValueException(
728
                    "Unable to normalize value '{$value}' for column type {$type} in " . get_class($this)
729
                );
730
            }
731
732
            $datetime->setTimestamp($timestamp);
733
        }
734
735
        switch ($type) {
736
            case 'datetime':
737
                //no break
738
            case 'timestamp':
739
                //Driver should handle conversion automatically
740
                return $datetime;
741
            case 'time':
742
                return $datetime->format(static::TIME_FORMAT);
743
            case 'date':
744
                return $datetime->format(static::DATE_FORMAT);
745
        }
746
747
        return $value;
748
    }
749
}