MySQLColumn::createInstance()   F
last analyzed

Complexity

Conditions 19
Paths 541

Size

Total Lines 89
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 26.3208

Importance

Changes 0
Metric Value
cc 19
eloc 54
nc 541
nop 3
dl 0
loc 89
ccs 8
cts 11
cp 0.7272
crap 26.3208
rs 0.9874
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver\MySQL\Schema;
13
14
use Cycle\Database\Driver\DriverInterface;
15
use Cycle\Database\Exception\DefaultValueException;
16
use Cycle\Database\Exception\SchemaException;
17
use Cycle\Database\Injection\Fragment;
18
use Cycle\Database\Injection\FragmentInterface;
19
use Cycle\Database\Schema\AbstractColumn;
0 ignored issues
show
Bug introduced by
The type Cycle\Database\Schema\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use Cycle\Database\Schema\Attribute\ColumnAttribute;
21
22
/**
23
 * Attention! You can use only one timestamp or datetime with DATETIME_NOW setting! Thought, it will
24
 * work on multiple fields with MySQL 5.6.6+ version.
25
 *
26
 * @method $this primary(int $size, bool $unsigned = false, $zerofill = false)
27
 * @method $this smallPrimary(int $size, bool $unsigned = false, $zerofill = false)
28
 * @method $this bigPrimary(int $size, bool $unsigned = false, $zerofill = false)
29
 * @method $this integer(int $size, bool $unsigned = false, $zerofill = false)
30
 * @method $this tinyInteger(int $size, bool $unsigned = false, $zerofill = false)
31
 * @method $this smallInteger(int $size, bool $unsigned = false, $zerofill = false)
32
 * @method $this bigInteger(int $size, bool $unsigned = false, $zerofill = false)
33
 * @method $this unsigned(bool $value)
34
 * @method $this zerofill(bool $value)
35
 * @method $this comment(string $value)
36
 * @method $this after(string $column)
37
 */
38
class MySQLColumn extends AbstractColumn
39
{
40
    /**
41
     * Default timestamp expression ().
42
     */
43
    public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
44
45
    public const EXCLUDE_FROM_COMPARE = ['size', 'timezone', 'userType', 'attributes', 'first', 'after', 'unknownSize'];
46
    protected const INTEGER_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'];
47
48
    protected array $mapping = [
49
        //Primary sequences
50
        'primary'     => [
51
            'type'          => 'int',
52
            'size'          => 11,
53
            'autoIncrement' => true,
54
            'nullable'      => false,
55
        ],
56
        'smallPrimary'  => [
57
            'type'          => 'smallint',
58
            'size'          => 6,
59
            'autoIncrement' => true,
60
            'nullable'      => false,
61
        ],
62
        'bigPrimary'  => [
63
            'type'          => 'bigint',
64
            'size'          => 20,
65
            'autoIncrement' => true,
66
            'nullable'      => false,
67
        ],
68
69
        //Enum type (mapped via method)
70
        'enum'        => 'enum',
71
72
        //Set type (mapped via method)
73
        'set'         => 'set',
74
75
        //Logical types
76
        'boolean'     => ['type' => 'tinyint', 'size' => 1],
77
78
        //Integer types (size can always be changed with size method), longInteger has method alias
79
        //bigInteger
80
        'integer'     => ['type' => 'int', 'size' => 11, 'unsigned' => false, 'zerofill' => false],
81
        'tinyInteger' => ['type' => 'tinyint', 'size' => 4, 'unsigned' => false, 'zerofill' => false],
82
        'smallInteger' => ['type' => 'smallint', 'size' => 6, 'unsigned' => false, 'zerofill' => false],
83
        'bigInteger'  => ['type' => 'bigint', 'size' => 20, 'unsigned' => false, 'zerofill' => false],
84
85
        //String with specified length (mapped via method)
86
        'string'      => ['type' => 'varchar', 'size' => 255],
87
88
        //Generic types
89
        'text'        => 'text',
90
        'tinyText'    => 'tinytext',
91
        'mediumText'  => 'mediumtext',
92
        'longText'    => 'longtext',
93
94
        //Real types
95
        'double'      => 'double',
96
        'float'       => 'float',
97
98
        //Decimal type (mapped via method)
99
        'decimal'     => 'decimal',
100
101
        //Date and Time types
102
        'datetime'    => 'datetime',
103
        'date'        => 'date',
104
        'time'        => 'time',
105
        'timestamp'   => ['type' => 'timestamp', 'defaultValue' => null],
106
107
        //Binary types
108
        'binary'      => 'blob',
109
        'tinyBinary'  => 'tinyblob',
110
        'longBinary'  => 'longblob',
111
        'varbinary'   => ['type' => 'varbinary', 'size' => 255],
112
113
        //Additional types
114
        'json'        => 'json',
115
        'snowflake'   => ['type' => 'bigint', 'size' => 20],
116
        'ulid'        => ['type' => 'varchar', 'size' => 26],
117
        'uuid'        => ['type' => 'varchar', 'size' => 36],
118
    ];
119
    protected array $reverseMapping = [
120
        'primary'     => [['type' => 'int', 'autoIncrement' => true]],
121
        'bigPrimary'  => ['serial', ['type' => 'bigint', 'size' => 20, 'autoIncrement' => true]],
122
        'enum'        => ['enum'],
123
        'set'         => ['set'],
124
        'boolean'     => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]],
125
        'integer'     => ['int', 'integer', 'mediumint'],
126
        'tinyInteger' => ['tinyint'],
127
        'smallInteger' => ['smallint'],
128
        'bigInteger'  => ['bigint'],
129
        'string'      => ['varchar', 'char'],
130
        'text'        => ['text'],
131
        'tinyText'    => ['tinytext'],
132
        'mediumText'  => ['mediumtext'],
133
        'longText'    => ['longtext'],
134 474
        'double'      => ['double'],
135
        'float'       => ['float', 'real'],
136 474
        'decimal'     => ['decimal'],
137
        'datetime'    => ['datetime'],
138 474
        'date'        => ['date'],
139
        'time'        => ['time'],
140 214
        'timestamp'   => ['timestamp'],
141
        'binary'      => ['blob', 'binary', 'varbinary'],
142
        'tinyBinary'  => ['tinyblob'],
143 474
        'longBinary'  => ['longblob'],
144
        'json'        => ['json'],
145 474
    ];
146 474
147 332
    /**
148
     * List of types forbids default value set.
149
     */
150 456
    protected array $forbiddenDefaults = [
151
        'text',
152
        'mediumtext',
153
        'tinytext',
154
        'longtext',
155
        'blob',
156 470
        'tinyblob',
157
        'longblob',
158 470
        'json',
159
    ];
160 470
161 470
    #[ColumnAttribute(
162 470
        ['int', 'tinyint', 'smallint', 'bigint', 'varchar', 'varbinary', 'time', 'datetime', 'timestamp'],
163 470
    )]
164
    protected int $size = 0;
165
166 470
    /**
167 470
     * True if size is not defined in DB schema.
168 470
     */
169
    protected bool $unknownSize = false;
170
171
    /**
172
     * Column is auto incremental.
173
     */
174
    #[ColumnAttribute(self::INTEGER_TYPES)]
175
    protected bool $autoIncrement = false;
176 470
177
    /**
178 470
     * Unsigned integer type. Related to {@see INTEGER_TYPES} only.
179 470
     */
180 280
    #[ColumnAttribute(self::INTEGER_TYPES)]
181
    protected bool $unsigned = false;
182 280
183 162
    /**
184 162
     * Zerofill option. Related to {@see INTEGER_TYPES} only.
185
     */
186 262
    #[ColumnAttribute(self::INTEGER_TYPES)]
187
    protected bool $zerofill = false;
188
189
    /**
190
     * Column comment.
191 470
     */
192 446
    #[ColumnAttribute]
193 446
    protected string $comment = '';
194 338
195 338
    /**
196 320
     * Column name to position after.
197 6
     */
198 6
    #[ColumnAttribute]
199 316
    protected string $after = '';
200 2
201 2
    /**
202
     * Whether the column should be positioned first.
203
     */
204
    #[ColumnAttribute]
205
    protected bool $first = false;
206
207 470
    /**
208 152
     * @psalm-param non-empty-string $table
209
     */
210 152
    public static function createInstance(string $table, array $schema, ?\DateTimeZone $timezone = null): self
211
    {
212
        $column = new self($table, $schema['Field'], $timezone);
213
214 462
        $column->type = $schema['Type'];
0 ignored issues
show
Bug Best Practice introduced by
The property type does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
215
        $column->comment = $schema['Comment'];
216
        $column->nullable = \strtolower($schema['Null']) === 'yes';
0 ignored issues
show
Bug Best Practice introduced by
The property nullable does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
217
        $column->defaultValue = $schema['Default'];
0 ignored issues
show
Bug Best Practice introduced by
The property defaultValue does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
218
        $column->autoIncrement = \stripos($schema['Extra'], 'auto_increment') !== false;
219
220 462
        if (
221 462
            !\preg_match(
222
                '/^(?P<type>[a-z]+)(?:\((?P<options>[^)]+)\))?(?: (?P<attr>[a-z ]+))?/',
223
                $column->type,
224
                $matches,
225
            )
226
        ) {
227 462
            //No extra definitions
228
            return $column;
229
        }
230
231
        $column->type = $matches['type'];
232
233
        $options = [];
234
        if (!empty($matches['options'])) {
235
            $options = \explode(',', $matches['options']);
236
237 170
            if (\count($options) > 1) {
238
                $column->precision = (int) $options[0];
0 ignored issues
show
Bug Best Practice introduced by
The property precision does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
239
                $column->scale = (int) $options[1];
0 ignored issues
show
Bug Best Practice introduced by
The property scale does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
240
            } else {
241 170
                $column->size = (int) $options[0];
242
            }
243
        }
244
245 170
        if (!empty($matches['attr'])) {
246
            if (\in_array($column->type, self::INTEGER_TYPES, true)) {
247
                $intAttr = \array_map('trim', \explode(' ', $matches['attr']));
248
                if (\in_array('unsigned', $intAttr, true)) {
249
                    $column->unsigned = true;
250
                }
251
                if (\in_array('zerofill', $intAttr, true)) {
252
                    $column->zerofill = true;
253
                }
254
                unset($intAttr);
255
            }
256
        }
257
258
        // since 8.0 database does not provide size for some columns
259
        if ($column->size === 0) {
260
            $column->unknownSize = true;
261
            switch ($column->type) {
262
                case 'int':
263
                    $column->size = 11;
264
                    break;
265
                case 'bigint':
266
                    $column->size = 20;
267
                    break;
268
                case 'tinyint':
269
                    $column->size = 4;
270
                    break;
271
                case 'smallint':
272
                    $column->size = 6;
273
                    break;
274
            }
275
        }
276
277
        //Fetching enum and set values
278
        if ($options !== [] && static::isEnum($column)) {
279
            $column->enumValues = \array_map(static fn($value) => \trim($value, $value[0]), $options);
0 ignored issues
show
Bug Best Practice introduced by
The property enumValues does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
280
281
            return $column;
282
        }
283
284
        //Default value conversions
285
        if ($column->type === 'bit' && $column->hasDefaultValue()) {
286
            //Cutting b\ and '
287
            $column->defaultValue = new Fragment($column->defaultValue);
288
        }
289
290
        if (
291
            $column->defaultValue === '0000-00-00 00:00:00'
292
            && $column->getAbstractType() === 'timestamp'
293
        ) {
294
            //Normalizing default value for timestamps
295
            $column->defaultValue = 0;
296
        }
297
298
        return $column;
299
    }
300
301
    public function size(int $value): self
302
    {
303
        $this->unknownSize = false;
304
        return parent::__call('size', [$value]);
305
    }
306
307
    /**
308
     * @psalm-return non-empty-string
309
     */
310
    public function sqlStatement(DriverInterface $driver): string
311
    {
312
        if (\in_array($this->type, self::INTEGER_TYPES, true)) {
313
            $statement = $this->sqlStatementInteger($driver);
314
        } else {
315
            $defaultValue = $this->defaultValue;
316
317
            if (\in_array($this->type, $this->forbiddenDefaults, true)) {
318
                // Flushing default value for forbidden types
319
                $this->defaultValue = null;
0 ignored issues
show
Bug Best Practice introduced by
The property defaultValue does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
320
            }
321
322
            $statement = parent::sqlStatement($driver);
323
324
            $this->defaultValue = $defaultValue;
325
        }
326
327
        $this->comment === '' or $statement .= " COMMENT {$driver->quote($this->comment)}";
328
329
        $first = $this->first;
330
        $after = $first ? '' : $this->after;
331
332
        $statement .= match (true) {
333
            $first => ' FIRST',
334
            $after !== '' => " AFTER {$driver->identifier($after)}",
0 ignored issues
show
Bug introduced by
The method identifier() does not exist on Cycle\Database\Driver\DriverInterface. It seems like you code against a sub-type of Cycle\Database\Driver\DriverInterface such as Cycle\Database\Driver\Driver. ( Ignorable by Annotation )

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

334
            $after !== '' => " AFTER {$driver->/** @scrutinizer ignore-call */ identifier($after)}",
Loading history...
335
            default => '',
336
        };
337
338
        return $statement;
339
    }
340
341
    public function compare(AbstractColumn $initial): bool
342
    {
343
        \assert($initial instanceof self);
344
        $self = $this;
345
346
        // MySQL 8.0 does not provide size for unsigned integers without zerofill
347
        // so we can get wrong results in comparison of boolean columns
348
        if ($self->unknownSize || $initial->unknownSize) {
349
            // if one of the columns is boolean, we can safely assume that size is 1
350
            if (\in_array($self->userType, ['bool', 'boolean'], true)) {
351
                $initial = clone $initial;
352
                $initial->size = 1;
353
            } elseif (\in_array($initial->userType, ['bool', 'boolean'], true)) {
354
                $self = clone $self;
355
                $self->size = 1;
356
            }
357
        }
358
359
        $result = \Closure::fromCallable([parent::class, 'compare'])->bindTo($self)($initial);
360
361
362
        if ($self->type === 'varchar' || $self->type === 'varbinary') {
363
            return $result && $self->size === $initial->size;
364
        }
365
366
        return $result;
367
    }
368
369
    public function isUnsigned(): bool
370
    {
371
        return $this->unsigned;
372
    }
373
374
    public function isZerofill(): bool
375
    {
376
        return $this->zerofill;
377
    }
378
379
    public function first(bool $value = true): self
380
    {
381
        $this->first = $value;
382
383
        return $this;
384
    }
385
386
    public function set(string|array $values): self
387
    {
388
        $this->type('set');
389
        $this->enumValues = \array_map('strval', \is_array($values) ? $values : \func_get_args());
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
Bug Best Practice introduced by
The property enumValues does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
390
391
        return $this;
392
    }
393
394
    /**
395
     * @param int<0, max> $size
396
     */
397
    public function varbinary(int $size = 255): self
398
    {
399
        $this->type('varbinary');
400
401
        $size < 0 && throw new SchemaException('Invalid varbinary size value');
402
403
        $this->size = $size;
404
405
        return $this;
406
    }
407
408
    /**
409
     * If a size is provided, a varbinary column of the specified size will be created.
410
     * Otherwise, a blob type column will be created.
411
     *
412
     * @param int<0, max> $size
413
     */
414
    public function binary(int $size = 0): self
415
    {
416
        if ($size > 0) {
417
            return $this->varbinary($size);
418
        }
419
420
        $this->type('blob');
421
422
        return $this;
423
    }
424
425
    public function getComment(): string
426
    {
427
        return $this->comment;
428
    }
429
430
    protected static function isEnum(AbstractColumn $column): bool
431
    {
432
        return $column->getAbstractType() === 'enum' || $column->getAbstractType() === 'set';
433
    }
434
435
    /**
436
     * Ensure that datetime fields are correctly formatted.
437
     *
438
     * @psalm-param non-empty-string $type
439
     *
440
     * @throws DefaultValueException
441
     */
442
    protected function formatDatetime(
443
        string $type,
444
        string|int|\DateTimeInterface $value,
445
    ): \DateTimeInterface|FragmentInterface|string {
446
        if ($value === 'current_timestamp()') {
447
            $value = self::DATETIME_NOW;
448
        }
449
450
        return parent::formatDatetime($type, $value);
451
    }
452
453
    private function sqlStatementInteger(DriverInterface $driver): string
454
    {
455
        return \sprintf(
456
            '%s %s(%s)%s%s%s%s%s',
457
            $driver->identifier($this->name),
458
            $this->type,
459
            $this->size,
460
            $this->unsigned ? ' UNSIGNED' : '',
461
            $this->zerofill ? ' ZEROFILL' : '',
462
            $this->nullable ? ' NULL' : ' NOT NULL',
463
            $this->defaultValue !== null ? " DEFAULT {$this->quoteDefault($driver)}" : '',
464
            $this->autoIncrement ? ' AUTO_INCREMENT' : '',
465
        );
466
    }
467
}
468