Passed
Pull Request — 2.x (#219)
by
unknown
18:41
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
 */
36
class MySQLColumn extends AbstractColumn
37
{
38
    /**
39
     * Default timestamp expression (driver specific).
40
     */
41
    public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
42
43
    public const EXCLUDE_FROM_COMPARE = ['size', 'timezone', 'userType', 'attributes'];
44
    protected const INTEGER_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'];
45
46
    protected array $mapping = [
47
        //Primary sequences
48
        'primary'     => [
49
            'type'          => 'int',
50
            'size'          => 11,
51
            'autoIncrement' => true,
52
            'nullable'      => false,
53
        ],
54
        'smallPrimary'  => [
55
            'type'          => 'smallint',
56
            'size'          => 6,
57
            'autoIncrement' => true,
58
            'nullable'      => false,
59
        ],
60
        'bigPrimary'  => [
61
            'type'          => 'bigint',
62
            'size'          => 20,
63
            'autoIncrement' => true,
64
            'nullable'      => false,
65
        ],
66
67
        //Enum type (mapped via method)
68
        'enum'        => 'enum',
69
70
        //Set type (mapped via method)
71
        'set'         => 'set',
72
73
        //Logical types
74
        'boolean'     => ['type' => 'tinyint', 'size' => 1],
75
76
        //Integer types (size can always be changed with size method), longInteger has method alias
77
        //bigInteger
78
        'integer'     => ['type' => 'int', 'size' => 11, 'unsigned' => false, 'zerofill' => false],
79
        'tinyInteger' => ['type' => 'tinyint', 'size' => 4, 'unsigned' => false, 'zerofill' => false],
80
        'smallInteger' => ['type' => 'smallint', 'size' => 6, 'unsigned' => false, 'zerofill' => false],
81
        'bigInteger'  => ['type' => 'bigint', 'size' => 20, 'unsigned' => false, 'zerofill' => false],
82
83
        //String with specified length (mapped via method)
84
        'string'      => ['type' => 'varchar', 'size' => 255],
85
86
        //Generic types
87
        'text'        => 'text',
88
        'tinyText'    => 'tinytext',
89
        'mediumText'  => 'mediumtext',
90
        'longText'    => 'longtext',
91
92
        //Real types
93
        'double'      => 'double',
94
        'float'       => 'float',
95
96
        //Decimal type (mapped via method)
97
        'decimal'     => 'decimal',
98
99
        //Date and Time types
100
        'datetime'    => 'datetime',
101
        'date'        => 'date',
102
        'time'        => 'time',
103
        'timestamp'   => ['type' => 'timestamp', 'defaultValue' => null],
104
105
        //Binary types
106
        'binary'      => 'blob',
107
        'tinyBinary'  => 'tinyblob',
108
        'longBinary'  => 'longblob',
109
        'varbinary'   => ['type' => 'varbinary', 'size' => 255],
110
111
        //Additional types
112
        'json'        => 'json',
113
        'uuid'        => ['type' => 'varchar', 'size' => 36],
114
    ];
115
    protected array $reverseMapping = [
116
        'primary'     => [['type' => 'int', 'autoIncrement' => true]],
117
        'bigPrimary'  => ['serial', ['type' => 'bigint', 'size' => 20, 'autoIncrement' => true]],
118
        'enum'        => ['enum'],
119
        'set'         => ['set'],
120
        'boolean'     => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]],
121
        'integer'     => ['int', 'integer', 'mediumint'],
122
        'tinyInteger' => ['tinyint'],
123
        'smallInteger' => ['smallint'],
124
        'bigInteger'  => ['bigint'],
125
        'string'      => ['varchar', 'char'],
126
        'text'        => ['text'],
127
        'tinyText'    => ['tinytext'],
128
        'mediumText'  => ['mediumtext'],
129
        'longText'    => ['longtext'],
130
        'double'      => ['double'],
131
        'float'       => ['float', 'real'],
132
        'decimal'     => ['decimal'],
133
        'datetime'    => ['datetime'],
134 474
        'date'        => ['date'],
135
        'time'        => ['time'],
136 474
        'timestamp'   => ['timestamp'],
137
        'binary'      => ['blob', 'binary', 'varbinary'],
138 474
        'tinyBinary'  => ['tinyblob'],
139
        'longBinary'  => ['longblob'],
140 214
        'json'        => ['json'],
141
    ];
142
143 474
    /**
144
     * List of types forbids default value set.
145 474
     */
146 474
    protected array $forbiddenDefaults = [
147 332
        'text',
148
        'mediumtext',
149
        'tinytext',
150 456
        'longtext',
151
        'blob',
152
        'tinyblob',
153
        'longblob',
154
        'json',
155
    ];
156 470
157
    #[ColumnAttribute(
158 470
        ['int', 'tinyint', 'smallint', 'bigint', 'varchar', 'varbinary', 'time', 'datetime', 'timestamp'],
159
    )]
160 470
    protected int $size = 0;
161 470
162 470
    /**
163 470
     * Column is auto incremental.
164
     */
165
    #[ColumnAttribute(self::INTEGER_TYPES)]
166 470
    protected bool $autoIncrement = false;
167 470
168 470
    /**
169
     * Unsigned integer type. Related to {@see INTEGER_TYPES} only.
170
     */
171
    #[ColumnAttribute(self::INTEGER_TYPES)]
172
    protected bool $unsigned = false;
173
174
    /**
175
     * Zerofill option. Related to {@see INTEGER_TYPES} only.
176 470
     */
177
    #[ColumnAttribute(self::INTEGER_TYPES)]
178 470
    protected bool $zerofill = false;
179 470
180 280
    /**
181
     * Column comment.
182 280
     */
183 162
    #[ColumnAttribute]
184 162
    protected string $comment = '';
185
186 262
    /**
187
     * @psalm-param non-empty-string $table
188
     */
189
    public static function createInstance(string $table, array $schema, ?\DateTimeZone $timezone = null): self
190
    {
191 470
        $column = new self($table, $schema['Field'], $timezone);
192 446
193 446
        $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...
194 338
        $column->comment = $schema['Comment'];
195 338
        $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...
196 320
        $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...
197 6
        $column->autoIncrement = \stripos($schema['Extra'], 'auto_increment') !== false;
198 6
199 316
        if (
200 2
            !\preg_match(
201 2
                '/^(?P<type>[a-z]+)(?:\((?P<options>[^)]+)\))?(?: (?P<attr>[a-z ]+))?/',
202
                $column->type,
203
                $matches,
204
            )
205
        ) {
206
            //No extra definitions
207 470
            return $column;
208 152
        }
209
210 152
        $column->type = $matches['type'];
211
212
        $options = [];
213
        if (!empty($matches['options'])) {
214 462
            $options = \explode(',', $matches['options']);
215
216
            if (\count($options) > 1) {
217
                $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...
218
                $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...
219
            } else {
220 462
                $column->size = (int) $options[0];
221 462
            }
222
        }
223
224
        if (!empty($matches['attr'])) {
225
            if (\in_array($column->type, self::INTEGER_TYPES, true)) {
226
                $intAttr = \array_map('trim', \explode(' ', $matches['attr']));
227 462
                if (\in_array('unsigned', $intAttr, true)) {
228
                    $column->unsigned = true;
229
                }
230
                if (\in_array('zerofill', $intAttr, true)) {
231
                    $column->zerofill = true;
232
                }
233
                unset($intAttr);
234
            }
235
        }
236
237 170
        // since 8.0 database does not provide size for some columns
238
        if ($column->size === 0) {
239
            switch ($column->type) {
240
                case 'int':
241 170
                    $column->size = 11;
242
                    break;
243
                case 'bigint':
244
                    $column->size = 20;
245 170
                    break;
246
                case 'tinyint':
247
                    $column->size = 4;
248
                    break;
249
                case 'smallint':
250
                    $column->size = 6;
251
                    break;
252
            }
253
        }
254
255
        //Fetching enum and set values
256
        if ($options !== [] && static::isEnum($column)) {
257
            $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...
258
259
            return $column;
260
        }
261
262
        //Default value conversions
263
        if ($column->type === 'bit' && $column->hasDefaultValue()) {
264
            //Cutting b\ and '
265
            $column->defaultValue = new Fragment($column->defaultValue);
266
        }
267
268
        if (
269
            $column->defaultValue === '0000-00-00 00:00:00'
270
            && $column->getAbstractType() === 'timestamp'
271
        ) {
272
            //Normalizing default value for timestamps
273
            $column->defaultValue = 0;
274
        }
275
276
        return $column;
277
    }
278
279
    /**
280
     * @psalm-return non-empty-string
281
     */
282
    public function sqlStatement(DriverInterface $driver): string
283
    {
284
        if (\in_array($this->type, self::INTEGER_TYPES, true)) {
285
            return $this->sqlStatementInteger($driver);
286
        }
287
288
        $defaultValue = $this->defaultValue;
289
290
        if (\in_array($this->type, $this->forbiddenDefaults, true)) {
291
            //Flushing default value for forbidden types
292
            $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...
293
        }
294
295
        $statement = parent::sqlStatement($driver);
296
297
        $this->defaultValue = $defaultValue;
298
        if ($this->autoIncrement) {
299
            return "{$statement} AUTO_INCREMENT";
300
        }
301
302
        return $statement;
303
    }
304
305
    public function compare(AbstractColumn $initial): bool
306
    {
307
        $result = parent::compare($initial);
308
309
        if ($this->type === 'varchar' || $this->type === 'varbinary') {
310
            return $result && $this->size === $initial->size;
311
        }
312
313
        return $result;
314
    }
315
316
    public function isUnsigned(): bool
317
    {
318
        return $this->unsigned;
319
    }
320
321
    public function isZerofill(): bool
322
    {
323
        return $this->zerofill;
324
    }
325
326
    public function set(string|array $values): self
327
    {
328
        $this->type('set');
329
        $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...
330
331
        return $this;
332
    }
333
334
    /**
335
     * @param int<0, max> $size
336
     */
337
    public function varbinary(int $size = 255): self
338
    {
339
        $this->type('varbinary');
340
341
        $size < 0 && throw new SchemaException('Invalid varbinary size value');
342
343
        $this->size = $size;
344
345
        return $this;
346
    }
347
348
    /**
349
     * If a size is provided, a varbinary column of the specified size will be created.
350
     * Otherwise, a blob type column will be created.
351
     *
352
     * @param int<0, max> $size
353
     */
354
    public function binary(int $size = 0): self
355
    {
356
        if ($size > 0) {
357
            return $this->varbinary($size);
358
        }
359
360
        $this->type('blob');
361
362
        return $this;
363
    }
364
365
    public function getComment(): string
366
    {
367
        return $this->comment;
368
    }
369
370
    protected static function isEnum(AbstractColumn $column): bool
371
    {
372
        return $column->getAbstractType() === 'enum' || $column->getAbstractType() === 'set';
373
    }
374
375
    /**
376
     * Ensure that datetime fields are correctly formatted.
377
     *
378
     * @psalm-param non-empty-string $type
379
     *
380
     * @throws DefaultValueException
381
     */
382
    protected function formatDatetime(
383
        string $type,
384
        string|int|\DateTimeInterface $value,
385
    ): \DateTimeInterface|FragmentInterface|string {
386
        if ($value === 'current_timestamp()') {
387
            $value = self::DATETIME_NOW;
388
        }
389
390
        return parent::formatDatetime($type, $value);
391
    }
392
393
    private function sqlStatementInteger(DriverInterface $driver): string
394
    {
395
        return \sprintf(
396
            '%s %s(%s)%s%s%s%s%s',
397
            $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

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