Passed
Pull Request — 2.x (#66)
by Aleksei
17:33
created

MySQLColumn::compare()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 4
nop 1
dl 0
loc 17
ccs 0
cts 0
cp 0
crap 30
rs 9.6111
c 0
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\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
20
/**
21
 * Attention! You can use only one timestamp or datetime with DATETIME_NOW setting! Thought, it will
22
 * work on multiple fields with MySQL 5.6.6+ version.
23
 */
24
class MySQLColumn extends AbstractColumn
25
{
26
    /**
27
     * Default timestamp expression (driver specific).
28
     */
29
    public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
30
31
    protected const ENGINE_INTEGER_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'];
32
33
    protected array $mapping = [
34
        //Primary sequences
35
        'primary'     => [
36
            'type'          => 'int',
37
            'size'          => 11,
38
            'autoIncrement' => true,
39
            'nullable'      => false,
40
        ],
41
        'bigPrimary'  => [
42
            'type'          => 'bigint',
43
            'size'          => 20,
44
            'autoIncrement' => true,
45
            'nullable'      => false,
46
        ],
47
48
        //Enum type (mapped via method)
49
        'enum'        => 'enum',
50
51
        //Logical types
52
        'boolean'     => ['type' => 'tinyint', 'size' => 1],
53
54
        //Integer types (size can always be changed with size method), longInteger has method alias
55
        //bigInteger
56
        'integer'     => ['type' => 'int', 'size' => 11],
57
        'tinyInteger' => ['type' => 'tinyint', 'size' => 4],
58
        'smallInteger'=> ['type' => 'smallint', 'size' => 6],
59
        'bigInteger'  => ['type' => 'bigint', 'size' => 20],
60
61
        //String with specified length (mapped via method)
62
        'string'      => ['type' => 'varchar', 'size' => 255],
63
64
        //Generic types
65
        'text'        => 'text',
66
        'tinyText'    => 'tinytext',
67
        'longText'    => 'longtext',
68
69
        //Real types
70
        'double'      => 'double',
71
        'float'       => 'float',
72
73
        //Decimal type (mapped via method)
74
        'decimal'     => 'decimal',
75
76
        //Date and Time types
77
        'datetime'    => 'datetime',
78
        'date'        => 'date',
79
        'time'        => 'time',
80
        'timestamp'   => ['type' => 'timestamp', 'defaultValue' => null],
81
82
        //Binary types
83
        'binary'      => 'blob',
84
        'tinyBinary'  => 'tinyblob',
85
        'longBinary'  => 'longblob',
86
87
        //Additional types
88
        'json'        => 'text',
89
        'uuid'        => ['type' => 'varchar', 'size' => 36],
90
    ];
91
92
    protected array $reverseMapping = [
93
        'primary'     => [['type' => 'int', 'autoIncrement' => true]],
94
        'bigPrimary'  => ['serial', ['type' => 'bigint', 'autoIncrement' => true]],
95
        'enum'        => ['enum'],
96
        'boolean'     => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]],
97
        'integer'     => ['int', 'integer', 'mediumint'],
98
        'tinyInteger' => ['tinyint'],
99
        'smallInteger'=> ['smallint'],
100
        'bigInteger'  => ['bigint'],
101
        'string'      => ['varchar', 'char'],
102
        'text'        => ['text', 'mediumtext'],
103
        'tinyText'    => ['tinytext'],
104
        'longText'    => ['longtext'],
105
        'double'      => ['double'],
106
        'float'       => ['float', 'real'],
107
        'decimal'     => ['decimal'],
108
        'datetime'    => ['datetime'],
109
        'date'        => ['date'],
110
        'time'        => ['time'],
111
        'timestamp'   => ['timestamp'],
112
        'binary'      => ['blob', 'binary', 'varbinary'],
113
        'tinyBinary'  => ['tinyblob'],
114
        'longBinary'  => ['longblob'],
115
    ];
116
117
    /**
118
     * List of types forbids default value set.
119
     */
120
    protected array $forbiddenDefaults = [
121
        'text',
122
        'mediumtext',
123
        'tinytext',
124
        'longtext',
125
        'blob',
126
        'tinyblob',
127
        'longblob',
128
    ];
129
130
    /**
131
     * Column is auto incremental.
132
     */
133
    protected bool $autoIncrement = false;
134 474
135
    /**
136 474
     * @psalm-return non-empty-string
137
     */
138 474
    public function sqlStatement(DriverInterface $driver): string
139
    {
140 214
        $defaultValue = $this->defaultValue;
141
142
        if (\in_array($this->type, $this->forbiddenDefaults, true)) {
143 474
            //Flushing default value for forbidden types
144
            $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...
145 474
        }
146 474
147 332
        $statementParts = parent::sqlStatementParts($driver);
148
149
        if (in_array($this->type, self::ENGINE_INTEGER_TYPES)) {
150 456
            $attr = array_filter(array_intersect_key($this->attributes, ['unsigned' => false, 'zerofill' => false]));
151
            if ($attr) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attr of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
152
                array_splice($statementParts, 3, 0, array_keys($attr));
153
            }
154
        }
155
156 470
        $this->defaultValue = $defaultValue;
157
        if ($this->autoIncrement) {
158 470
            $statementParts[] = 'AUTO_INCREMENT';
159
        }
160 470
161 470
        return implode(' ', $statementParts);
162 470
    }
163 470
164
    /**
165
     * @psalm-param non-empty-string $table
166 470
     */
167 470
    public static function createInstance(string $table, array $schema, \DateTimeZone $timezone = null): self
168 470
    {
169
        $column = new self($table, $schema['Field'], $timezone);
170
171
        $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...
172
        $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...
173
        $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...
174
        $column->autoIncrement = stripos($schema['Extra'], 'auto_increment') !== false;
175
176 470
        if (
177
            !preg_match(
178 470
                '/^(?P<type>[a-z]+)(?:\((?P<options>[^\)]+)\))?(?: (?P<attr>[a-z ]+))?/',
179 470
                $column->type,
180 280
                $matches
181
            )
182 280
        ) {
183 162
            //No extra definitions
184 162
            return $column;
185
        }
186 262
187
        $column->type = $matches['type'];
188
189
        $options = [];
190
        if (!empty($matches['options'])) {
191 470
            $options = explode(',', $matches['options']);
192 446
193 446
            if (count($options) > 1) {
194 338
                $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...
195 338
                $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...
196 320
            } else {
197 6
                $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...
198 6
            }
199 316
        }
200 2
201 2
        if (!empty($matches['attr'])) {
202
            if (in_array($column->type, self::ENGINE_INTEGER_TYPES)) {
203
                $intAttr = array_map('trim', explode(' ', $matches['attr']));
204
                if (in_array('unsigned', $intAttr)) {
205
                    $column->attributes['unsigned'] = true;
0 ignored issues
show
Bug Best Practice introduced by
The property attributes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
206
                }
207 470
                if (in_array('zerofill', $intAttr)) {
208 152
                    $column->attributes['zerofill'] = true;
209
                }
210 152
                unset($intAttr);
211
            }
212
        }
213
214 462
        // since 8.0 database does not provide size for some of the columns
215
        if ($column->size === 0) {
216
            switch ($column->type) {
217
                case 'int':
218
                    $column->size = 11;
219
                    break;
220 462
                case 'bigint':
221 462
                    $column->size = 20;
222
                    break;
223
                case 'tinyint':
224
                    $column->size = 4;
225
                    break;
226
                case 'smallint':
227 462
                    $column->size = 6;
228
                    break;
229
            }
230
        }
231
232
233
        //Fetching enum values
234
        if ($options !== [] && $column->getAbstractType() === 'enum') {
235
            $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...
236
237 170
            return $column;
238
        }
239
240
        //Default value conversions
241 170
        if ($column->type === 'bit' && $column->hasDefaultValue()) {
242
            //Cutting b\ and '
243
            $column->defaultValue = new Fragment($column->defaultValue);
244
        }
245 170
246
        if (
247
            $column->defaultValue === '0000-00-00 00:00:00'
248
            && $column->getAbstractType() === 'timestamp'
249
        ) {
250
            //Normalizing default value for timestamps
251
            $column->defaultValue = 0;
252
        }
253
254
        return $column;
255
    }
256
257
    public function compare(AbstractColumn $initial): bool
258
    {
259
        assert($initial instanceof self);
260
        if (!parent::compare($initial)) {
261
            return false;
262
        }
263
264
        if (in_array($this->type, self::ENGINE_INTEGER_TYPES)) {
265
            $attr = ['unsigned' => false, 'zerofill' => false];
266
            foreach ($attr as $a => $def) {
267
                if (($this->attributes[$a] ?? $def) !== ($initial->attributes[$a] ?? $def)) {
268
                    return false;
269
                }
270
            }
271
        }
272
273
        return true;
274
    }
275
276
    /**
277
     * Ensure that datetime fields are correctly formatted.
278
     *
279
     * @psalm-param non-empty-string $type
280
     *
281
     * @throws DefaultValueException
282
     */
283
    protected function formatDatetime(
284
        string $type,
285
        string|int|\DateTimeInterface $value
286
    ): \DateTimeInterface|FragmentInterface|string {
287
        if ($value === 'current_timestamp()') {
288
            $value = self::DATETIME_NOW;
289
        }
290
291
        return parent::formatDatetime($type, $value);
292
    }
293
}
294