Passed
Pull Request — 2.x (#219)
by
unknown
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 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
        if ($this->autoIncrement) {
300
            return "{$statement} AUTO_INCREMENT";
301
        }
302
303
        if ($this->comment !== '') {
304
            return "{$statement} COMMENT {$driver->quote($this->comment)}";
305
        }
306
307
        return $statement;
308
    }
309
310
    public function compare(AbstractColumn $initial): bool
311
    {
312
        $result = parent::compare($initial);
313
314
        if ($this->type === 'varchar' || $this->type === 'varbinary') {
315
            return $result && $this->size === $initial->size;
316
        }
317
318
        return $result;
319
    }
320
321
    public function isUnsigned(): bool
322
    {
323
        return $this->unsigned;
324
    }
325
326
    public function isZerofill(): bool
327
    {
328
        return $this->zerofill;
329
    }
330
331
    public function set(string|array $values): self
332
    {
333
        $this->type('set');
334
        $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...
335
336
        return $this;
337
    }
338
339
    /**
340
     * @param int<0, max> $size
341
     */
342
    public function varbinary(int $size = 255): self
343
    {
344
        $this->type('varbinary');
345
346
        $size < 0 && throw new SchemaException('Invalid varbinary size value');
347
348
        $this->size = $size;
349
350
        return $this;
351
    }
352
353
    /**
354
     * If a size is provided, a varbinary column of the specified size will be created.
355
     * Otherwise, a blob type column will be created.
356
     *
357
     * @param int<0, max> $size
358
     */
359
    public function binary(int $size = 0): self
360
    {
361
        if ($size > 0) {
362
            return $this->varbinary($size);
363
        }
364
365
        $this->type('blob');
366
367
        return $this;
368
    }
369
370
    public function getComment(): string
371
    {
372
        return $this->comment;
373
    }
374
375
    protected static function isEnum(AbstractColumn $column): bool
376
    {
377
        return $column->getAbstractType() === 'enum' || $column->getAbstractType() === 'set';
378
    }
379
380
    /**
381
     * Ensure that datetime fields are correctly formatted.
382
     *
383
     * @psalm-param non-empty-string $type
384
     *
385
     * @throws DefaultValueException
386
     */
387
    protected function formatDatetime(
388
        string $type,
389
        string|int|\DateTimeInterface $value,
390
    ): \DateTimeInterface|FragmentInterface|string {
391
        if ($value === 'current_timestamp()') {
392
            $value = self::DATETIME_NOW;
393
        }
394
395
        return parent::formatDatetime($type, $value);
396
    }
397
398
    private function sqlStatementInteger(DriverInterface $driver): string
399
    {
400
        return \sprintf(
401
            '%s %s(%s)%s%s%s%s%s%s',
402
            $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

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