Passed
Pull Request — 2.x (#226)
by Aleksei
17:15
created

MySQLColumn::createInstance()   F

Complexity

Conditions 19
Paths 541

Size

Total Lines 88
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 23.4381

Importance

Changes 0
Metric Value
cc 19
eloc 53
c 0
b 0
f 0
nc 541
nop 3
dl 0
loc 88
ccs 10
cts 13
cp 0.7692
crap 23.4381
rs 0.9874

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

319
            $after !== '' => " AFTER {$driver->/** @scrutinizer ignore-call */ identifier($after)}",
Loading history...
320
            default => '',
321
        };
322
323
        return $statement;
324
    }
325
326
    public function compare(AbstractColumn $initial): bool
327
    {
328
        $result = parent::compare($initial);
329
330
        if ($this->type === 'varchar' || $this->type === 'varbinary') {
331
            return $result && $this->size === $initial->size;
332
        }
333
334
        return $result;
335
    }
336
337
    public function isUnsigned(): bool
338
    {
339
        return $this->unsigned;
340
    }
341
342
    public function isZerofill(): bool
343
    {
344
        return $this->zerofill;
345
    }
346
347
    public function first(bool $value = true): self
348
    {
349
        $this->first = $value;
350
351
        return $this;
352
    }
353
354
    public function isFirst(): bool
355
    {
356
        return $this->first;
357
    }
358
359
    /**
360
     * @param non-empty-string $column
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
361
     * @return $this
362
     */
363
    public function after(string $column): self
364
    {
365
        $this->after = $column;
366
367
        return $this;
368
    }
369
370
    public function getAfter(): string
371
    {
372
        return $this->after;
373
    }
374
375
    public function set(string|array $values): self
376
    {
377
        $this->type('set');
378
        $this->enumValues = \array_map('strval', \is_array($values) ? $values : \func_get_args());
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...
introduced by
The condition is_array($values) is always true.
Loading history...
379
380
        return $this;
381
    }
382
383
    /**
384
     * @param int<0, max> $size
385
     */
386
    public function varbinary(int $size = 255): self
387
    {
388
        $this->type('varbinary');
389
390
        $size < 0 && throw new SchemaException('Invalid varbinary size value');
391
392
        $this->size = $size;
393
394
        return $this;
395
    }
396
397
    /**
398
     * If a size is provided, a varbinary column of the specified size will be created.
399
     * Otherwise, a blob type column will be created.
400
     *
401
     * @param int<0, max> $size
402
     */
403
    public function binary(int $size = 0): self
404
    {
405
        if ($size > 0) {
406
            return $this->varbinary($size);
407
        }
408
409
        $this->type('blob');
410
411
        return $this;
412
    }
413
414
    public function getComment(): string
415
    {
416
        return $this->comment;
417
    }
418
419
    protected static function isEnum(AbstractColumn $column): bool
420
    {
421
        return $column->getAbstractType() === 'enum' || $column->getAbstractType() === 'set';
422
    }
423
424
    /**
425
     * Ensure that datetime fields are correctly formatted.
426
     *
427
     * @psalm-param non-empty-string $type
428
     *
429
     * @throws DefaultValueException
430
     */
431
    protected function formatDatetime(
432
        string $type,
433
        string|int|\DateTimeInterface $value,
434
    ): \DateTimeInterface|FragmentInterface|string {
435
        if ($value === 'current_timestamp()') {
436
            $value = self::DATETIME_NOW;
437
        }
438
439
        return parent::formatDatetime($type, $value);
440
    }
441
442
    private function sqlStatementInteger(DriverInterface $driver): string
443
    {
444
        return \sprintf(
445
            '%s %s(%s)%s%s%s%s%s',
446
            $driver->identifier($this->name),
447
            $this->type,
448
            $this->size,
449
            $this->unsigned ? ' UNSIGNED' : '',
450
            $this->zerofill ? ' ZEROFILL' : '',
451
            $this->nullable ? ' NULL' : ' NOT NULL',
452
            $this->defaultValue !== null ? " DEFAULT {$this->quoteDefault($driver)}" : '',
453
            $this->autoIncrement ? ' AUTO_INCREMENT' : '',
454
        );
455
    }
456
}
457