Passed
Push — 2.x ( 749fd6...811329 )
by Aleksei
20:07
created

MySQLColumn::set()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 0
cts 0
cp 0
crap 6
rs 10
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\Injection\Fragment;
17
use Cycle\Database\Injection\FragmentInterface;
18
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...
19
use Cycle\Database\Schema\Attribute\ColumnAttribute;
20
21
/**
22
 * Attention! You can use only one timestamp or datetime with DATETIME_NOW setting! Thought, it will
23
 * work on multiple fields with MySQL 5.6.6+ version.
24
 *
25
 * @method $this|AbstractColumn primary(int $size, bool $unsigned = false, $zerofill = false)
26
 * @method $this|AbstractColumn bigPrimary(int $size, bool $unsigned = false, $zerofill = false)
27
 * @method $this|AbstractColumn integer(int $size, bool $unsigned = false, $zerofill = false)
28
 * @method $this|AbstractColumn tinyInteger(int $size, bool $unsigned = false, $zerofill = false)
29
 * @method $this|AbstractColumn smallInteger(int $size, bool $unsigned = false, $zerofill = false)
30
 * @method $this|AbstractColumn bigInteger(int $size, bool $unsigned = false, $zerofill = false)
31
 * @method $this|AbstractColumn unsigned(bool $value)
32
 * @method $this|AbstractColumn zerofill(bool $value)
33
 */
34
class MySQLColumn extends AbstractColumn
35
{
36
    /**
37
     * Default timestamp expression (driver specific).
38
     */
39
    public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
40
41
    protected const INTEGER_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'];
42
43
    protected array $mapping = [
44
        //Primary sequences
45
        'primary'     => [
46
            'type'          => 'int',
47
            'size'          => 11,
48
            'autoIncrement' => true,
49
            'nullable'      => false,
50
        ],
51
        'bigPrimary'  => [
52
            'type'          => 'bigint',
53
            'size'          => 20,
54
            'autoIncrement' => true,
55
            'nullable'      => false,
56
        ],
57
58
        //Enum type (mapped via method)
59
        'enum'        => 'enum',
60
61
        //Set type (mapped via method)
62
        'set'         => 'set',
63
64
        //Logical types
65
        'boolean'     => ['type' => 'tinyint', 'size' => 1],
66
67
        //Integer types (size can always be changed with size method), longInteger has method alias
68
        //bigInteger
69
        'integer'     => ['type' => 'int', 'size' => 11, 'unsigned' => false, 'zerofill' => false],
70
        'tinyInteger' => ['type' => 'tinyint', 'size' => 4, 'unsigned' => false, 'zerofill' => false],
71
        'smallInteger'=> ['type' => 'smallint', 'size' => 6, 'unsigned' => false, 'zerofill' => false],
72
        'bigInteger'  => ['type' => 'bigint', 'size' => 20, 'unsigned' => false, 'zerofill' => false],
73
74
        //String with specified length (mapped via method)
75
        'string'      => ['type' => 'varchar', 'size' => 255],
76
77
        //Generic types
78
        'text'        => 'text',
79
        'tinyText'    => 'tinytext',
80
        'longText'    => 'longtext',
81
82
        //Real types
83
        'double'      => 'double',
84
        'float'       => 'float',
85
86
        //Decimal type (mapped via method)
87
        'decimal'     => 'decimal',
88
89
        //Date and Time types
90
        'datetime'    => 'datetime',
91
        'date'        => 'date',
92
        'time'        => 'time',
93
        'timestamp'   => ['type' => 'timestamp', 'defaultValue' => null],
94
95
        //Binary types
96
        'binary'      => 'blob',
97
        'tinyBinary'  => 'tinyblob',
98
        'longBinary'  => 'longblob',
99
100
        //Additional types
101
        'json'        => 'text',
102
        'uuid'        => ['type' => 'varchar', 'size' => 36],
103
    ];
104
105
    protected array $reverseMapping = [
106
        'primary'     => [['type' => 'int', 'autoIncrement' => true]],
107
        'bigPrimary'  => ['serial', ['type' => 'bigint', 'size' => 20, 'autoIncrement' => true]],
108
        'enum'        => ['enum'],
109
        'set'         => ['set'],
110
        'boolean'     => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]],
111
        'integer'     => ['int', 'integer', 'mediumint'],
112
        'tinyInteger' => ['tinyint'],
113
        'smallInteger'=> ['smallint'],
114
        'bigInteger'  => ['bigint'],
115
        'string'      => ['varchar', 'char'],
116
        'text'        => ['text', 'mediumtext'],
117
        'tinyText'    => ['tinytext'],
118
        'longText'    => ['longtext'],
119
        'double'      => ['double'],
120
        'float'       => ['float', 'real'],
121
        'decimal'     => ['decimal'],
122
        'datetime'    => ['datetime'],
123
        'date'        => ['date'],
124
        'time'        => ['time'],
125
        'timestamp'   => ['timestamp'],
126
        'binary'      => ['blob', 'binary', 'varbinary'],
127
        'tinyBinary'  => ['tinyblob'],
128
        'longBinary'  => ['longblob'],
129
    ];
130
131
    /**
132
     * List of types forbids default value set.
133
     */
134 474
    protected array $forbiddenDefaults = [
135
        'text',
136 474
        'mediumtext',
137
        'tinytext',
138 474
        'longtext',
139
        'blob',
140 214
        'tinyblob',
141
        'longblob',
142
    ];
143 474
144
    /**
145 474
     * Column is auto incremental.
146 474
     */
147 332
    #[ColumnAttribute(self::INTEGER_TYPES)]
148
    protected bool $autoIncrement = false;
149
150 456
    /**
151
     * Unsigned integer type. Related to {@see INTEGER_TYPES} only.
152
     */
153
    #[ColumnAttribute(self::INTEGER_TYPES)]
154
    protected bool $unsigned = false;
155
156 470
    /**
157
     * Zerofill option. Related to {@see INTEGER_TYPES} only.
158 470
     */
159
    #[ColumnAttribute(self::INTEGER_TYPES)]
160 470
    protected bool $zerofill = false;
161 470
162 470
    /**
163 470
     * @psalm-return non-empty-string
164
     */
165
    public function sqlStatement(DriverInterface $driver): string
166 470
    {
167 470
        if (\in_array($this->type, self::INTEGER_TYPES, true)) {
168 470
            return $this->sqlStatementInteger($driver);
169
        }
170
171
        $defaultValue = $this->defaultValue;
172
173
        if (\in_array($this->type, $this->forbiddenDefaults, true)) {
174
            //Flushing default value for forbidden types
175
            $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...
176 470
        }
177
178 470
        $statement = parent::sqlStatement($driver);
179 470
180 280
        $this->defaultValue = $defaultValue;
181
        if ($this->autoIncrement) {
182 280
            return "{$statement} AUTO_INCREMENT";
183 162
        }
184 162
185
        return $statement;
186 262
    }
187
188
    /**
189
     * @psalm-param non-empty-string $table
190
     */
191 470
    public static function createInstance(string $table, array $schema, \DateTimeZone $timezone = null): self
192 446
    {
193 446
        $column = new self($table, $schema['Field'], $timezone);
194 338
195 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...
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];
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
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
    public function compare(AbstractColumn $initial): bool
281
    {
282
        assert($initial instanceof self);
283
        return ! (!parent::compare($initial));
284
    }
285
286
    public function isUnsigned(): bool
287
    {
288
        return $this->unsigned;
289
    }
290
291
    public function isZerofill(): bool
292
    {
293
        return $this->zerofill;
294
    }
295
296
    public function set(string|array $values): self
297
    {
298
        $this->type('set');
299
        $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...
300
301
        return $this;
302
    }
303
304
    /**
305
     * Ensure that datetime fields are correctly formatted.
306
     *
307
     * @psalm-param non-empty-string $type
308
     *
309
     * @throws DefaultValueException
310
     */
311
    protected function formatDatetime(
312
        string $type,
313
        string|int|\DateTimeInterface $value
314
    ): \DateTimeInterface|FragmentInterface|string {
315
        if ($value === 'current_timestamp()') {
316
            $value = self::DATETIME_NOW;
317
        }
318
319
        return parent::formatDatetime($type, $value);
320
    }
321
322
    protected static function isEnum(AbstractColumn $column): bool
323
    {
324
        return $column->getAbstractType() === 'enum' || $column->getAbstractType() === 'set';
325
    }
326
327
    private function sqlStatementInteger(DriverInterface $driver): string
328
    {
329
        return \sprintf(
330
            '%s %s(%s)%s%s%s%s%s',
331
            $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

331
            $driver->/** @scrutinizer ignore-call */ 
332
                     identifier($this->name),
Loading history...
332
            $this->type,
333
            $this->size,
334
            $this->unsigned ? ' UNSIGNED' : '',
335
            $this->zerofill ? ' ZEROFILL' : '',
336
            $this->nullable ? ' NULL' : ' NOT NULL',
337
            $this->defaultValue !== null ? " DEFAULT {$this->quoteDefault($driver)}" : '',
338
            $this->autoIncrement ? ' AUTO_INCREMENT' : ''
339
        );
340
    }
341
}
342