Passed
Pull Request — 2.x (#226)
by
unknown
16:24
created

MySQLColumn::getAfter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

433
            $driver->/** @scrutinizer ignore-call */ 
434
                     identifier($this->name),
Loading history...
434
            $this->type,
435
            $this->size,
436
            $this->unsigned ? ' UNSIGNED' : '',
437
            $this->comment !== '' ? " COMMENT {$driver->quote($this->comment)}" : '',
438
            $this->zerofill ? ' ZEROFILL' : '',
439
            $this->nullable ? ' NULL' : ' NOT NULL',
440
            $this->defaultValue !== null ? " DEFAULT {$this->quoteDefault($driver)}" : '',
441
            $this->autoIncrement ? ' AUTO_INCREMENT' : '',
442
        );
443
    }
444
}
445