Passed
Pull Request — 2.x (#226)
by Aleksei
18:10
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
 * @method $this|AbstractColumn 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'];
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
        'uuid'        => ['type' => 'varchar', 'size' => 36],
116
    ];
117
    protected array $reverseMapping = [
118
        'primary'     => [['type' => 'int', 'autoIncrement' => true]],
119
        'bigPrimary'  => ['serial', ['type' => 'bigint', 'size' => 20, 'autoIncrement' => true]],
120
        'enum'        => ['enum'],
121
        'set'         => ['set'],
122
        'boolean'     => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]],
123
        'integer'     => ['int', 'integer', 'mediumint'],
124
        'tinyInteger' => ['tinyint'],
125
        'smallInteger' => ['smallint'],
126
        'bigInteger'  => ['bigint'],
127
        'string'      => ['varchar', 'char'],
128
        'text'        => ['text'],
129
        'tinyText'    => ['tinytext'],
130
        'mediumText'  => ['mediumtext'],
131
        'longText'    => ['longtext'],
132
        'double'      => ['double'],
133
        'float'       => ['float', 'real'],
134 474
        'decimal'     => ['decimal'],
135
        'datetime'    => ['datetime'],
136 474
        'date'        => ['date'],
137
        'time'        => ['time'],
138 474
        'timestamp'   => ['timestamp'],
139
        'binary'      => ['blob', 'binary', 'varbinary'],
140 214
        'tinyBinary'  => ['tinyblob'],
141
        'longBinary'  => ['longblob'],
142
        'json'        => ['json'],
143 474
    ];
144
145 474
    /**
146 474
     * List of types forbids default value set.
147 332
     */
148
    protected array $forbiddenDefaults = [
149
        'text',
150 456
        'mediumtext',
151
        'tinytext',
152
        'longtext',
153
        'blob',
154
        'tinyblob',
155
        'longblob',
156 470
        'json',
157
    ];
158 470
159
    #[ColumnAttribute(
160 470
        ['int', 'tinyint', 'smallint', 'bigint', 'varchar', 'varbinary', 'time', 'datetime', 'timestamp'],
161 470
    )]
162 470
    protected int $size = 0;
163 470
164
    /**
165
     * Column is auto incremental.
166 470
     */
167 470
    #[ColumnAttribute(self::INTEGER_TYPES)]
168 470
    protected bool $autoIncrement = false;
169
170
    /**
171
     * Unsigned integer type. Related to {@see INTEGER_TYPES} only.
172
     */
173
    #[ColumnAttribute(self::INTEGER_TYPES)]
174
    protected bool $unsigned = false;
175
176 470
    /**
177
     * Zerofill option. Related to {@see INTEGER_TYPES} only.
178 470
     */
179 470
    #[ColumnAttribute(self::INTEGER_TYPES)]
180 280
    protected bool $zerofill = false;
181
182 280
    /**
183 162
     * Column comment.
184 162
     */
185
    #[ColumnAttribute]
186 262
    protected string $comment = '';
187
188
    /**
189
     * Column name to position after.
190
     */
191 470
    #[ColumnAttribute]
192 446
    protected string $after = '';
193 446
194 338
    /**
195 338
     * Whether the column should be positioned first.
196 320
     */
197 6
    #[ColumnAttribute]
198 6
    protected bool $first = false;
199 316
200 2
    /**
201 2
     * @psalm-param non-empty-string $table
202
     */
203
    public static function createInstance(string $table, array $schema, ?\DateTimeZone $timezone = null): self
204
    {
205
        $column = new self($table, $schema['Field'], $timezone);
206
207 470
        $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...
208 152
        $column->comment = $schema['Comment'];
209
        $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...
210 152
        $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...
211
        $column->autoIncrement = \stripos($schema['Extra'], 'auto_increment') !== false;
212
213
        if (
214 462
            !\preg_match(
215
                '/^(?P<type>[a-z]+)(?:\((?P<options>[^)]+)\))?(?: (?P<attr>[a-z ]+))?/',
216
                $column->type,
217
                $matches,
218
            )
219
        ) {
220 462
            //No extra definitions
221 462
            return $column;
222
        }
223
224
        $column->type = $matches['type'];
225
226
        $options = [];
227 462
        if (!empty($matches['options'])) {
228
            $options = \explode(',', $matches['options']);
229
230
            if (\count($options) > 1) {
231
                $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...
232
                $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...
233
            } else {
234
                $column->size = (int) $options[0];
235
            }
236
        }
237 170
238
        if (!empty($matches['attr'])) {
239
            if (\in_array($column->type, self::INTEGER_TYPES, true)) {
240
                $intAttr = \array_map('trim', \explode(' ', $matches['attr']));
241 170
                if (\in_array('unsigned', $intAttr, true)) {
242
                    $column->unsigned = true;
243
                }
244
                if (\in_array('zerofill', $intAttr, true)) {
245 170
                    $column->zerofill = true;
246
                }
247
                unset($intAttr);
248
            }
249
        }
250
251
        // since 8.0 database does not provide size for some columns
252
        if ($column->size === 0) {
253
            switch ($column->type) {
254
                case 'int':
255
                    $column->size = 11;
256
                    break;
257
                case 'bigint':
258
                    $column->size = 20;
259
                    break;
260
                case 'tinyint':
261
                    $column->size = 4;
262
                    break;
263
                case 'smallint':
264
                    $column->size = 6;
265
                    break;
266
            }
267
        }
268
269
        //Fetching enum and set values
270
        if ($options !== [] && static::isEnum($column)) {
271
            $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...
272
273
            return $column;
274
        }
275
276
        //Default value conversions
277
        if ($column->type === 'bit' && $column->hasDefaultValue()) {
278
            //Cutting b\ and '
279
            $column->defaultValue = new Fragment($column->defaultValue);
280
        }
281
282
        if (
283
            $column->defaultValue === '0000-00-00 00:00:00'
284
            && $column->getAbstractType() === 'timestamp'
285
        ) {
286
            //Normalizing default value for timestamps
287
            $column->defaultValue = 0;
288
        }
289
290
        return $column;
291
    }
292
293
    /**
294
     * @psalm-return non-empty-string
295
     */
296
    public function sqlStatement(DriverInterface $driver): string
297
    {
298
        if (\in_array($this->type, self::INTEGER_TYPES, true)) {
299
            $statement = $this->sqlStatementInteger($driver);
300
        } else {
301
            $defaultValue = $this->defaultValue;
302
303
            if (\in_array($this->type, $this->forbiddenDefaults, true)) {
304
                // Flushing default value for forbidden types
305
                $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...
306
            }
307
308
            $statement = parent::sqlStatement($driver);
309
310
            $this->defaultValue = $defaultValue;
311
        }
312
313
        $this->comment === '' or $statement .= " COMMENT {$driver->quote($this->comment)}";
314
315
        $first = $this->first;
316
        $after = $first ? '' : $this->after;
317
318
        $statement .= match (true) {
319
            $first => ' FIRST',
320
            $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

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