Passed
Push — 2.x ( a18c78...0f4623 )
by Aleksei
41:02 queued 21:04
created

MySQLColumn::createInstance()   F

Complexity

Conditions 19
Paths 541

Size

Total Lines 88
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 19.7049

Importance

Changes 0
Metric Value
cc 19
eloc 53
c 0
b 0
f 0
nc 541
nop 3
dl 0
loc 88
ccs 21
cts 24
cp 0.875
crap 19.7049
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'];
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
     * @psalm-param non-empty-string $table
189
     */
190
    public static function createInstance(string $table, array $schema, ?\DateTimeZone $timezone = null): self
191 470
    {
192 446
        $column = new self($table, $schema['Field'], $timezone);
193 446
194 338
        $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...
195 338
        $column->comment = $schema['Comment'];
196 320
        $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...
197 6
        $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...
198 6
        $column->autoIncrement = \stripos($schema['Extra'], 'auto_increment') !== false;
199 316
200 2
        if (
201 2
            !\preg_match(
202
                '/^(?P<type>[a-z]+)(?:\((?P<options>[^)]+)\))?(?: (?P<attr>[a-z ]+))?/',
203
                $column->type,
204
                $matches,
205
            )
206
        ) {
207 470
            //No extra definitions
208 152
            return $column;
209
        }
210 152
211
        $column->type = $matches['type'];
212
213
        $options = [];
214 462
        if (!empty($matches['options'])) {
215
            $options = \explode(',', $matches['options']);
216
217
            if (\count($options) > 1) {
218
                $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...
219
                $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...
220 462
            } else {
221 462
                $column->size = (int) $options[0];
222
            }
223
        }
224
225
        if (!empty($matches['attr'])) {
226
            if (\in_array($column->type, self::INTEGER_TYPES, true)) {
227 462
                $intAttr = \array_map('trim', \explode(' ', $matches['attr']));
228
                if (\in_array('unsigned', $intAttr, true)) {
229
                    $column->unsigned = true;
230
                }
231
                if (\in_array('zerofill', $intAttr, true)) {
232
                    $column->zerofill = true;
233
                }
234
                unset($intAttr);
235
            }
236
        }
237 170
238
        // since 8.0 database does not provide size for some columns
239
        if ($column->size === 0) {
240
            switch ($column->type) {
241 170
                case 'int':
242
                    $column->size = 11;
243
                    break;
244
                case 'bigint':
245 170
                    $column->size = 20;
246
                    break;
247
                case 'tinyint':
248
                    $column->size = 4;
249
                    break;
250
                case 'smallint':
251
                    $column->size = 6;
252
                    break;
253
            }
254
        }
255
256
        //Fetching enum and set values
257
        if ($options !== [] && static::isEnum($column)) {
258
            $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...
259
260
            return $column;
261
        }
262
263
        //Default value conversions
264
        if ($column->type === 'bit' && $column->hasDefaultValue()) {
265
            //Cutting b\ and '
266
            $column->defaultValue = new Fragment($column->defaultValue);
267
        }
268
269
        if (
270
            $column->defaultValue === '0000-00-00 00:00:00'
271
            && $column->getAbstractType() === 'timestamp'
272
        ) {
273
            //Normalizing default value for timestamps
274
            $column->defaultValue = 0;
275
        }
276
277
        return $column;
278
    }
279
280
    /**
281
     * @psalm-return non-empty-string
282
     */
283
    public function sqlStatement(DriverInterface $driver): string
284
    {
285
        if (\in_array($this->type, self::INTEGER_TYPES, true)) {
286
            return $this->sqlStatementInteger($driver);
287
        }
288
289
        $defaultValue = $this->defaultValue;
290
291
        if (\in_array($this->type, $this->forbiddenDefaults, true)) {
292
            //Flushing default value for forbidden types
293
            $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...
294
        }
295
296
        $statement = parent::sqlStatement($driver);
297
298
        $this->defaultValue = $defaultValue;
299
300
        if ($this->comment !== '') {
301
            return "{$statement} COMMENT {$driver->quote($this->comment)}";
302
        }
303
304
        return $statement;
305
    }
306
307
    public function compare(AbstractColumn $initial): bool
308
    {
309
        $result = parent::compare($initial);
310
311
        if ($this->type === 'varchar' || $this->type === 'varbinary') {
312
            return $result && $this->size === $initial->size;
313
        }
314
315
        return $result;
316
    }
317
318
    public function isUnsigned(): bool
319
    {
320
        return $this->unsigned;
321
    }
322
323
    public function isZerofill(): bool
324
    {
325
        return $this->zerofill;
326
    }
327
328
    public function set(string|array $values): self
329
    {
330
        $this->type('set');
331
        $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...
332
333
        return $this;
334
    }
335
336
    /**
337
     * @param int<0, max> $size
338
     */
339
    public function varbinary(int $size = 255): self
340
    {
341
        $this->type('varbinary');
342
343
        $size < 0 && throw new SchemaException('Invalid varbinary size value');
344
345
        $this->size = $size;
346
347
        return $this;
348
    }
349
350
    /**
351
     * If a size is provided, a varbinary column of the specified size will be created.
352
     * Otherwise, a blob type column will be created.
353
     *
354
     * @param int<0, max> $size
355
     */
356
    public function binary(int $size = 0): self
357
    {
358
        if ($size > 0) {
359
            return $this->varbinary($size);
360
        }
361
362
        $this->type('blob');
363
364
        return $this;
365
    }
366
367
    public function getComment(): string
368
    {
369
        return $this->comment;
370
    }
371
372
    protected static function isEnum(AbstractColumn $column): bool
373
    {
374
        return $column->getAbstractType() === 'enum' || $column->getAbstractType() === 'set';
375
    }
376
377
    /**
378
     * Ensure that datetime fields are correctly formatted.
379
     *
380
     * @psalm-param non-empty-string $type
381
     *
382
     * @throws DefaultValueException
383
     */
384
    protected function formatDatetime(
385
        string $type,
386
        string|int|\DateTimeInterface $value,
387
    ): \DateTimeInterface|FragmentInterface|string {
388
        if ($value === 'current_timestamp()') {
389
            $value = self::DATETIME_NOW;
390
        }
391
392
        return parent::formatDatetime($type, $value);
393
    }
394
395
    private function sqlStatementInteger(DriverInterface $driver): string
396
    {
397
        return \sprintf(
398
            '%s %s(%s)%s%s%s%s%s%s',
399
            $driver->identifier($this->name),
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

399
            $driver->/** @scrutinizer ignore-call */ 
400
                     identifier($this->name),
Loading history...
400
            $this->type,
401
            $this->size,
402
            $this->unsigned ? ' UNSIGNED' : '',
403
            $this->comment !== '' ? " COMMENT {$driver->quote($this->comment)}" : '',
404
            $this->zerofill ? ' ZEROFILL' : '',
405
            $this->nullable ? ' NULL' : ' NOT NULL',
406
            $this->defaultValue !== null ? " DEFAULT {$this->quoteDefault($driver)}" : '',
407
            $this->autoIncrement ? ' AUTO_INCREMENT' : '',
408
        );
409
    }
410
}
411