Passed
Pull Request — 2.x (#66)
by Alexander
19:42
created

MySQLColumn::formatDatetime()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 9
ccs 0
cts 0
cp 0
crap 6
rs 10
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
        'bigInteger'  => ['type' => 'bigint', 'size' => 20],
59
60
        //String with specified length (mapped via method)
61
        'string'      => ['type' => 'varchar', 'size' => 255],
62
63
        //Generic types
64
        'text'        => 'text',
65
        'tinyText'    => 'tinytext',
66
        'longText'    => 'longtext',
67
68
        //Real types
69
        'double'      => 'double',
70
        'float'       => 'float',
71
72
        //Decimal type (mapped via method)
73
        'decimal'     => 'decimal',
74
75
        //Date and Time types
76
        'datetime'    => 'datetime',
77
        'date'        => 'date',
78
        'time'        => 'time',
79
        'timestamp'   => ['type' => 'timestamp', 'defaultValue' => null],
80
81
        //Binary types
82
        'binary'      => 'blob',
83
        'tinyBinary'  => 'tinyblob',
84
        'longBinary'  => 'longblob',
85
86
        //Additional types
87
        'json'        => 'text',
88
        'uuid'        => ['type' => 'varchar', 'size' => 36],
89
    ];
90
91
    protected array $reverseMapping = [
92
        'primary'     => [['type' => 'int', 'autoIncrement' => true]],
93
        'bigPrimary'  => ['serial', ['type' => 'bigint', 'autoIncrement' => true]],
94
        'enum'        => ['enum'],
95
        'boolean'     => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]],
96
        'integer'     => ['int', 'integer', 'smallint', 'mediumint'],
97
        'tinyInteger' => ['tinyint'],
98
        'bigInteger'  => ['bigint'],
99
        'string'      => ['varchar', 'char'],
100
        'text'        => ['text', 'mediumtext'],
101
        'tinyText'    => ['tinytext'],
102
        'longText'    => ['longtext'],
103
        'double'      => ['double'],
104
        'float'       => ['float', 'real'],
105
        'decimal'     => ['decimal'],
106
        'datetime'    => ['datetime'],
107
        'date'        => ['date'],
108
        'time'        => ['time'],
109
        'timestamp'   => ['timestamp'],
110
        'binary'      => ['blob', 'binary', 'varbinary'],
111
        'tinyBinary'  => ['tinyblob'],
112
        'longBinary'  => ['longblob'],
113
    ];
114
115
    /**
116
     * List of types forbids default value set.
117
     */
118
    protected array $forbiddenDefaults = [
119
        'text',
120
        'mediumtext',
121
        'tinytext',
122
        'longtext',
123
        'blog',
124
        'tinyblob',
125
        'longblob',
126
    ];
127
128
    /**
129
     * Column is auto incremental.
130
     */
131
    protected bool $autoIncrement = false;
132
133
    /**
134 474
     * @psalm-return non-empty-string
135
     */
136 474
    public function sqlStatement(DriverInterface $driver): string
137
    {
138 474
        $defaultValue = $this->defaultValue;
139
140 214
        if (\in_array($this->type, $this->forbiddenDefaults, true)) {
141
            //Flushing default value for forbidden types
142
            $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...
143 474
        }
144
145 474
        $statementParts = parent::sqlStatementParts($driver);
146 474
147 332
        if (in_array($this->type, self::ENGINE_INTEGER_TYPES)) {
148
            $attr = array_filter(array_intersect_key($this->attributes, ['unsigned' => false, 'zerofill' => false]));
149
            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...
150 456
                array_splice($statementParts, 3, 0, array_keys($attr));
151
            }
152
        }
153
154
        $this->defaultValue = $defaultValue;
155
        if ($this->autoIncrement) {
156 470
            $statementParts[] = 'AUTO_INCREMENT';
157
        }
158 470
159
        return implode(' ', $statementParts);
160 470
    }
161 470
162 470
    /**
163 470
     * @psalm-param non-empty-string $table
164
     */
165
    public static function createInstance(string $table, array $schema, \DateTimeZone $timezone = null): self
166 470
    {
167 470
        $column = new self($table, $schema['Field'], $timezone);
168 470
169
        $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...
170
        $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...
171
        $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...
172
        $column->autoIncrement = stripos($schema['Extra'], 'auto_increment') !== false;
173
174
        if (
175
            !preg_match(
176 470
                '/^(?P<type>[a-z]+)(?:\((?P<options>[^\)]+)\))?(?: (?P<attr>[a-z ]+))?/',
177
                $column->type,
178 470
                $matches
179 470
            )
180 280
        ) {
181
            //No extra definitions
182 280
            return $column;
183 162
        }
184 162
185
        $column->type = $matches['type'];
186 262
187
        $options = [];
188
        if (!empty($matches['options'])) {
189
            $options = explode(',', $matches['options']);
190
191 470
            if (count($options) > 1) {
192 446
                $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...
193 446
                $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...
194 338
            } else {
195 338
                $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...
196 320
            }
197 6
        }
198 6
199 316
        if (!empty($matches['attr'])) {
200 2
            if (in_array($column->type, self::ENGINE_INTEGER_TYPES)) {
201 2
                $intAttr = array_map('trim', explode(' ', $matches['attr']));
202
                if (in_array('unsigned', $intAttr)) {
203
                    $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...
204
                }
205
                if (in_array('zerofill', $intAttr)) {
206
                    $column->attributes['zerofill'] = true;
207 470
                }
208 152
                unset($intAttr);
209
            }
210 152
        }
211
212
        // since 8.0 database does not provide size for some of the columns
213
        if ($column->size === 0) {
214 462
            switch ($column->type) {
215
                case 'int':
216
                    $column->size = 11;
217
                    break;
218
                case 'bigint':
219
                    $column->size = 20;
220 462
                    break;
221 462
                case 'tinyint':
222
                    if ($column->size !== 1) {
223
                        $column->size = 4;
224
                    }
225
            }
226
        }
227 462
228
        //Fetching enum values
229
        if ($options !== [] && $column->getAbstractType() === 'enum') {
230
            $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...
231
232
            return $column;
233
        }
234
235
        //Default value conversions
236
        if ($column->type === 'bit' && $column->hasDefaultValue()) {
237 170
            //Cutting b\ and '
238
            $column->defaultValue = new Fragment($column->defaultValue);
239
        }
240
241 170
        if (
242
            $column->defaultValue === '0000-00-00 00:00:00'
243
            && $column->getAbstractType() === 'timestamp'
244
        ) {
245 170
            //Normalizing default value for timestamps
246
            $column->defaultValue = 0;
247
        }
248
249
        return $column;
250
    }
251
252
    public function compare(AbstractColumn $initial): bool
253
    {
254
        assert($initial instanceof self);
255
        if (!parent::compare($initial)) {
256
            return false;
257
        }
258
259
        if (in_array($this->type, self::ENGINE_INTEGER_TYPES)) {
260
            $attr = ['unsigned' => false, 'zerofill' => false];
261
            foreach ($attr as $a => $def) {
262
                if (($this->attributes[$a] ?? $def) !== ($initial->attributes[$a] ?? $def)) {
263
                    return false;
264
                }
265
            }
266
        }
267
268
        return true;
269
    }
270
271
    /**
272
     * Ensure that datetime fields are correctly formatted.
273
     *
274
     * @psalm-param non-empty-string $type
275
     *
276
     * @throws DefaultValueException
277
     */
278
    protected function formatDatetime(
279
        string $type,
280
        string|int|\DateTimeInterface $value
281
    ): \DateTimeInterface|FragmentInterface|string {
282
        if ($value === 'current_timestamp()') {
283
            $value = self::DATETIME_NOW;
284
        }
285
286
        return parent::formatDatetime($type, $value);
287
    }
288
}
289