Passed
Pull Request — master (#736)
by
unknown
11:37
created

AbstractColumnSchema::hasTimezone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use DateTimeImmutable;
8
use DateTimeInterface;
9
use DateTimeZone;
10
use Yiisoft\Db\Expression\Expression;
11
use Yiisoft\Db\Expression\ExpressionInterface;
12
use Yiisoft\Db\Helper\DbStringHelper;
13
14
use function gettype;
15
use function in_array;
16
use function is_bool;
17
use function is_float;
18
use function is_resource;
19
20
/**
21
 * Represents the metadata of a column in a database table.
22
 *
23
 * It provides information about the column's name, type, size, precision, and other details.
24
 *
25
 * The `ColumnSchema` class is used to store and retrieve metadata about a column in a database table.
26
 *
27
 * It's typically used in conjunction with the TableSchema class, which represents the metadata of a database table as a
28
 * whole.
29
 *
30
 * Here is an example of how to use the `ColumnSchema` class:
31
 *
32
 * ```php
33
 * use Yiisoft\Db\Schema\ColumnSchema;
34
 *
35
 * $column = new ColumnSchema();
36
 * $column->name('id');
37
 * $column->allowNull(false);
38
 * $column->dbType('int(11)');
39
 * $column->phpType('integer');
40
 * $column->type('integer');
41
 * $column->defaultValue(0);
42
 * $column->autoIncrement(true);
43
 * $column->primaryKey(true);
44
 * ``
45
 */
46
abstract class AbstractColumnSchema implements ColumnSchemaInterface
47
{
48
    private bool $allowNull = false;
49
    private bool $autoIncrement = false;
50
    private string|null $comment = null;
51
    private bool $computed = false;
52
    private string|null $dateTimeFormat = null;
53
    private string|null $dbType = null;
54
    private mixed $defaultValue = null;
55
    private array|null $enumValues = null;
56
    private string|null $extra = null;
57
    private bool $isPrimaryKey = false;
58
    private string|null $phpType = null;
59
    private int|null $precision = null;
60
    private int|null $scale = null;
61
    private int|null $size = null;
62
    private string $type = '';
63
    private bool $unsigned = false;
64
65
    public function __construct(private string $name)
66
    {
67
    }
68
69
    public function allowNull(bool $value): void
70
    {
71
        $this->allowNull = $value;
72
    }
73
74
    public function autoIncrement(bool $value): void
75
    {
76
        $this->autoIncrement = $value;
77
    }
78
79
    public function comment(string|null $value): void
80
    {
81
        $this->comment = $value;
82
    }
83
84
    public function computed(bool $value): void
85
    {
86
        $this->computed = $value;
87
    }
88
89
    public function dateTimeFormat(string|null $value): void
90
    {
91
        $this->dateTimeFormat = $value;
92
    }
93
94
    public function dbType(string|null $value): void
95
    {
96
        $this->dbType = $value;
97
    }
98
99
    public function dbTypecast(mixed $value): mixed
100
    {
101
        /**
102
         * The default implementation does the same as casting for PHP, but it should be possible to override this with
103
         * annotation of an explicit PDO type.
104
         */
105
106
        if ($this->dateTimeFormat !== null) {
107
            if (empty($value) || $value instanceof Expression) {
108
                return $value;
109
            }
110
111
            if (!$this->hasTimezone() && $this->type !== SchemaInterface::TYPE_DATE) {
112
                // if data type does not have timezone DB stores datetime without timezone
113
                // convert datetime to UTC to avoid timezone issues
114
                if (!$value instanceof DateTimeImmutable) {
115
                    // make a copy of $value if change timezone
116
                    if ($value instanceof DateTimeInterface) {
117
                        $value = DateTimeImmutable::createFromInterface($value);
118
                    } elseif (is_string($value)) {
119
                        $value = date_create_immutable($value) ?: $value;
120
                    }
121
                }
122
123
                if ($value instanceof DateTimeImmutable) { // DateTimeInterface does not have the method setTimezone()
124
                    $value = $value->setTimezone(new DateTimeZone('UTC'));
125
                    // Known possible issues:
126
                    // MySQL converts `TIMESTAMP` values from the current time zone to UTC for storage, and back from UTC to the current time zone when retrieve data.
127
                    // Oracle `TIMESTAMP WITH LOCAL TIME ZONE` data stored in the database is normalized to the database time zone. And returns it in the users' local session time zone.
128
                    // Both of them do not store time zone offset and require to convert DateTime to local DB timezone instead of UTC before insert.
129
                    // To solve the issue it requires to set local DB timezone to UTC if the types are in use
130
                }
131
            }
132
133
            if ($value instanceof DateTimeInterface) {
134
                return $value->format($this->dateTimeFormat);
135
            }
136
137
            return (string) $value;
138
        }
139
140
        return $this->typecast($value);
141
    }
142
143
    public function defaultValue(mixed $value): void
144
    {
145
        $this->defaultValue = $value;
146
    }
147
148
    public function enumValues(array|null $value): void
149
    {
150
        $this->enumValues = $value;
151
    }
152
153
    public function extra(string|null $value): void
154
    {
155
        $this->extra = $value;
156
    }
157
158
    public function getComment(): string|null
159
    {
160
        return $this->comment;
161
    }
162
163
    public function getDateTimeFormat(): string|null
164
    {
165
        return $this->dateTimeFormat;
166
    }
167
168
    public function getDbType(): string|null
169
    {
170
        return $this->dbType;
171
    }
172
173
    public function getDefaultValue(): mixed
174
    {
175
        return $this->defaultValue;
176
    }
177
178
    public function getEnumValues(): array|null
179
    {
180
        return $this->enumValues;
181
    }
182
183
    public function getExtra(): string|null
184
    {
185
        return $this->extra;
186
    }
187
188
    public function getName(): string
189
    {
190
        return $this->name;
191
    }
192
193
    public function getPrecision(): int|null
194
    {
195
        return $this->precision;
196
    }
197
198
    public function getPhpType(): string|null
199
    {
200
        return $this->phpType;
201
    }
202
203
    public function getScale(): int|null
204
    {
205
        return $this->scale;
206
    }
207
208
    public function getSize(): int|null
209
    {
210
        return $this->size;
211
    }
212
213
    public function getType(): string
214
    {
215
        return $this->type;
216
    }
217
218
    public function hasTimezone(): bool
219
    {
220
        return false;
221
    }
222
223
    public function isAllowNull(): bool
224
    {
225
        return $this->allowNull;
226
    }
227
228
    public function isAutoIncrement(): bool
229
    {
230
        return $this->autoIncrement;
231
    }
232
233
    public function isComputed(): bool
234
    {
235
        return $this->computed;
236
    }
237
238
    public function isPrimaryKey(): bool
239
    {
240
        return $this->isPrimaryKey;
241
    }
242
243
    public function isUnsigned(): bool
244
    {
245
        return $this->unsigned;
246
    }
247
248
    public function phpType(string|null $value): void
249
    {
250
        $this->phpType = $value;
251
    }
252
253
    /**
254
     * @throws \Exception
255
     */
256
    public function phpTypecast(mixed $value): mixed
257
    {
258
        if (is_string($value) && $this->dateTimeFormat !== null) {
259
            if (!$this->hasTimezone()) {
260
                // if data type does not have timezone datetime was converted to UTC before insert
261
                $datetime = new DateTimeImmutable($value, new DateTimeZone('UTC'));
262
263
                // convert datetime to PHP timezone
264
                return $datetime->setTimezone(new DateTimeZone(date_default_timezone_get()));
265
            }
266
267
            return new DateTimeImmutable($value);
268
        }
269
270
        return $this->typecast($value);
271
    }
272
273
    public function precision(int|null $value): void
274
    {
275
        $this->precision = $value;
276
    }
277
278
    public function primaryKey(bool $value): void
279
    {
280
        $this->isPrimaryKey = $value;
281
    }
282
283
    public function scale(int|null $value): void
284
    {
285
        $this->scale = $value;
286
    }
287
288
    public function size(int|null $value): void
289
    {
290
        $this->size = $value;
291
    }
292
293
    public function type(string $value): void
294
    {
295
        $this->type = $value;
296
    }
297
298
    public function unsigned(bool $value): void
299
    {
300
        $this->unsigned = $value;
301
    }
302
303
    /**
304
     * Converts the input value according to {@see phpType} after retrieval from the database.
305
     *
306
     * If the value is null or an {@see Expression}, it won't be converted.
307
     *
308
     * @param mixed $value The value to be converted.
309
     *
310
     * @return mixed The converted value.
311
     */
312
    protected function typecast(mixed $value): mixed
313
    {
314
        if (
315
            $value === null
316
            || $value === '' && !in_array($this->type, [
317
                SchemaInterface::TYPE_TEXT,
318
                SchemaInterface::TYPE_STRING,
319
                SchemaInterface::TYPE_BINARY,
320
                SchemaInterface::TYPE_CHAR,
321
            ], true)
322
        ) {
323
            return null;
324
        }
325
326
        if ($value instanceof ExpressionInterface) {
327
            return $value;
328
        }
329
330
        return match ($this->phpType) {
331
            gettype($value) => $value,
332
            SchemaInterface::PHP_TYPE_RESOURCE,
333
            SchemaInterface::PHP_TYPE_STRING
334
                => match (true) {
335
                    is_resource($value) => $value,
336
                    /** ensure type cast always has . as decimal separator in all locales */
337
                    is_float($value) => DbStringHelper::normalizeFloat($value),
338
                    is_bool($value) => $value ? '1' : '0',
339
                    default => (string) $value,
340
                },
341
            SchemaInterface::PHP_TYPE_INTEGER => (int) $value,
342
            /** Treating a 0-bit value as false too (@link https://github.com/yiisoft/yii2/issues/9006) */
343
            SchemaInterface::PHP_TYPE_BOOLEAN => $value && $value !== "\0",
344
            SchemaInterface::PHP_TYPE_DOUBLE => (float) $value,
345
            default => $value,
346
        };
347
    }
348
}
349