Issues (43)

src/Schema/Builder/AbstractColumn.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema\Builder;
6
7
use Yiisoft\Db\Expression\Expression;
8
use Yiisoft\Db\Helper\DbStringHelper;
9
use Yiisoft\Db\Schema\SchemaInterface;
10
11
use function gettype;
12
use function implode;
13
use function strtr;
14
15
/**
16
 * Is a utility class that provides a convenient way to create column schemas for {@see AbstractSchema}.
17
 *
18
 * It provides methods for specifying the properties of a column, such as its type, size, default value, and whether it
19
 * is nullable or not. It also provides a method for creating a column schema based on the specified properties.
20
 *
21
 * For example, the following code creates a column schema for an integer column:
22
 *
23
 * ```php
24
 * $column = (new Column(SchemaInterface::TYPE_INTEGER))->notNull()->defaultValue(0);
25
 * ```
26
 *
27
 * Provides a fluent interface, which means that the methods can be chained together to create a column schema with
28
 * many properties in a single line of code.
29
 */
30
abstract class AbstractColumn implements ColumnInterface
31
{
32
    /**
33
     * Allows you to group and define the abstract column type as primary key.
34
     */
35
    public const TYPE_CATEGORY_PK = 'pk';
36
    /**
37
     * Allows you to group and define the abstract column type as `string`.
38
     */
39
    public const TYPE_CATEGORY_STRING = 'string';
40
    /**
41
     * Allows you to group and define the abstract column type as `numeric`.
42
     */
43
    public const TYPE_CATEGORY_NUMERIC = 'numeric';
44
    /**
45
     * Allows you to group and define the abstract column type as `time`.
46
     */
47
    public const TYPE_CATEGORY_TIME = 'time';
48
    /**
49
     * Allows you to group and define the abstract column type as `other`.
50
     */
51
    public const TYPE_CATEGORY_OTHER = 'other';
52
    /**
53
     * Allows you to group and define the abstract column type as `uuid`.
54
     */
55
    public const TYPE_CATEGORY_UUID = 'uuid';
56
    /**
57
     * Allows you to group and define the abstract column type as `uuid` primary key.
58
     */
59
    public const TYPE_CATEGORY_UUID_PK = 'uuid_pk';
60
61
    protected bool|null $isNotNull = null;
62
    protected bool $isUnique = false;
63
    protected string|null $check = null;
64
    protected mixed $default = null;
65
    protected string|null $append = null;
66
    protected bool $isUnsigned = false;
67
    protected string|null $comment = null;
68
    protected string $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{append}';
69
70
    /** @psalm-var string[] */
71
    private array $categoryMap = [
72
        SchemaInterface::TYPE_PK => self::TYPE_CATEGORY_PK,
73
        SchemaInterface::TYPE_UPK => self::TYPE_CATEGORY_PK,
74
        SchemaInterface::TYPE_BIGPK => self::TYPE_CATEGORY_PK,
75
        SchemaInterface::TYPE_UBIGPK => self::TYPE_CATEGORY_PK,
76
        SchemaInterface::TYPE_CHAR => self::TYPE_CATEGORY_STRING,
77
        SchemaInterface::TYPE_STRING => self::TYPE_CATEGORY_STRING,
78
        SchemaInterface::TYPE_TEXT => self::TYPE_CATEGORY_STRING,
79
        SchemaInterface::TYPE_TINYINT => self::TYPE_CATEGORY_NUMERIC,
80
        SchemaInterface::TYPE_SMALLINT => self::TYPE_CATEGORY_NUMERIC,
81
        SchemaInterface::TYPE_INTEGER => self::TYPE_CATEGORY_NUMERIC,
82
        SchemaInterface::TYPE_BIGINT => self::TYPE_CATEGORY_NUMERIC,
83
        SchemaInterface::TYPE_FLOAT => self::TYPE_CATEGORY_NUMERIC,
84
        SchemaInterface::TYPE_DOUBLE => self::TYPE_CATEGORY_NUMERIC,
85
        SchemaInterface::TYPE_DECIMAL => self::TYPE_CATEGORY_NUMERIC,
86
        SchemaInterface::TYPE_DATETIME => self::TYPE_CATEGORY_TIME,
87
        SchemaInterface::TYPE_TIMESTAMP => self::TYPE_CATEGORY_TIME,
88
        SchemaInterface::TYPE_TIME => self::TYPE_CATEGORY_TIME,
89
        SchemaInterface::TYPE_DATE => self::TYPE_CATEGORY_TIME,
90
        SchemaInterface::TYPE_BINARY => self::TYPE_CATEGORY_OTHER,
91
        SchemaInterface::TYPE_BOOLEAN => self::TYPE_CATEGORY_NUMERIC,
92
        SchemaInterface::TYPE_MONEY => self::TYPE_CATEGORY_NUMERIC,
93
        SchemaInterface::TYPE_UUID => self::TYPE_CATEGORY_UUID,
94
        SchemaInterface::TYPE_UUID_PK => self::TYPE_CATEGORY_UUID_PK,
95
    ];
96
97
    /**
98
     * @psalm-param string[]|int[]|int|string|null $length
99
     */
100
    public function __construct(
101
        protected string $type,
102
        protected int|string|array|null $length = null
103
    ) {
104
    }
105
106
    public function notNull(): static
107
    {
108
        $this->isNotNull = true;
109
        return $this;
110
    }
111
112
    public function null(): static
113
    {
114
        $this->isNotNull = false;
115
        return $this;
116
    }
117
118
    public function unique(): static
119
    {
120
        $this->isUnique = true;
121
        return $this;
122
    }
123
124
    public function check(string|null $sql): static
125
    {
126
        $this->check = $sql;
127
        return $this;
128
    }
129
130
    public function defaultValue(mixed $default): static
131
    {
132
        if ($default === null) {
133
            $this->null();
134
        }
135
136
        $this->default = $default;
137
138
        return $this;
139
    }
140
141
    public function comment(string|null $comment): static
142
    {
143
        $this->comment = $comment;
144
        return $this;
145
    }
146
147
    /**
148
     * Marks column as unsigned.
149
     */
150
    public function unsigned(): static
151
    {
152
        $this->type = match ($this->type) {
153
            SchemaInterface::TYPE_PK => SchemaInterface::TYPE_UPK,
154
            SchemaInterface::TYPE_BIGPK => SchemaInterface::TYPE_UBIGPK,
155
            default => $this->type,
156
        };
157
158
        $this->isUnsigned = true;
159
160
        return $this;
161
    }
162
163
    public function defaultExpression(string $sql): static
164
    {
165
        $this->default = new Expression($sql);
166
        return $this;
167
    }
168
169
    public function setFormat(string $format): void
170
    {
171
        $this->format = $format;
172
    }
173
174
    public function append(string $sql): static
175
    {
176
        $this->append = $sql;
177
        return $this;
178
    }
179
180
    public function asString(): string
181
    {
182
        $format = match ($this->getTypeCategory()) {
183
            self::TYPE_CATEGORY_PK => '{type}{check}{comment}{append}',
184
            self::TYPE_CATEGORY_UUID => '{type}{notnull}{unique}{default}{check}{comment}{append}',
185
            self::TYPE_CATEGORY_UUID_PK => '{type}{notnull}{default}{check}{comment}{append}',
186
            default => $this->format,
187
        };
188
189
        return $this->buildCompleteString($format);
190
    }
191
192
    public function getType(): string|null
193
    {
194
        return $this->type;
195
    }
196
197
    public function getLength(): array|int|string|null
198
    {
199
        return $this->length;
200
    }
201
202
    public function isNotNull(): bool|null
203
    {
204
        return $this->isNotNull;
205
    }
206
207
    public function isUnique(): bool
208
    {
209
        return $this->isUnique;
210
    }
211
212
    public function getCheck(): string|null
213
    {
214
        return $this->check;
215
    }
216
217
    public function getDefault(): mixed
218
    {
219
        return $this->default;
220
    }
221
222
    public function getAppend(): string|null
223
    {
224
        return $this->append;
225
    }
226
227
    public function isUnsigned(): bool
228
    {
229
        return $this->isUnsigned;
230
    }
231
232
    public function getCategoryMap(): array
233
    {
234
        return $this->categoryMap;
235
    }
236
237
    public function getComment(): string|null
238
    {
239
        return $this->comment;
240
    }
241
242
    /**
243
     * Builds the length, precision part of the column.
244
     *
245
     * @return string A string containing the length/precision of the column.
246
     */
247
    protected function buildLengthString(): string
248
    {
249
        if (empty($this->length)) {
250
            return '';
251
        }
252
253
        if (is_array($this->length)) {
254
            $this->length = implode(',', $this->length);
255
        }
256
257
        return '(' . $this->length . ')';
0 ignored issues
show
Are you sure $this->length of type array|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

257
        return '(' . /** @scrutinizer ignore-type */ $this->length . ')';
Loading history...
258
    }
259
260
    /**
261
     * Builds the not null constraint for the column.
262
     *
263
     * @return string A string 'NOT NULL' if {@see isNotNull} is true, 'NULL' if {@see isNotNull} is false or an empty
264
     * string otherwise.
265
     */
266
    protected function buildNotNullString(): string
267
    {
268
        if ($this->isNotNull === true) {
269
            return ' NOT NULL';
270
        }
271
272
        if ($this->isNotNull === false) {
273
            return ' NULL';
274
        }
275
276
        return '';
277
    }
278
279
    /**
280
     * Builds the unique constraint for the column.
281
     *
282
     * @return string A string 'UNIQUE' if {@see isUnique} is true, otherwise it returns an empty string.
283
     */
284
    protected function buildUniqueString(): string
285
    {
286
        return $this->isUnique ? ' UNIQUE' : '';
287
    }
288
289
    /**
290
     * Return the default value for the column.
291
     *
292
     * @return string|null string with default value of column.
293
     */
294
    protected function buildDefaultValue(): string|null
295
    {
296
        if ($this->default === null) {
297
            return $this->isNotNull === false ? 'NULL' : null;
298
        }
299
300
        return match (gettype($this->default)) {
301
            'object', 'integer' => (string) $this->default,
302
            'double' => DbStringHelper::normalizeFloat((string) $this->default),
303
            'boolean' => $this->default ? 'TRUE' : 'FALSE',
304
            default => "'$this->default'",
305
        };
306
    }
307
308
    /**
309
     * Builds the default value specification for the column.
310
     *
311
     * @return string A string containing the DEFAULT keyword and the default value.
312
     */
313
    protected function buildDefaultString(): string
314
    {
315
        $defaultValue = $this->buildDefaultValue();
316
        if ($defaultValue === null) {
317
            return '';
318
        }
319
320
        return ' DEFAULT ' . $defaultValue;
321
    }
322
323
    /**
324
     * Builds the check constraint for the column.
325
     *
326
     * @return string A string containing the CHECK constraint.
327
     */
328
    protected function buildCheckString(): string
329
    {
330
        return !empty($this->check) ? " CHECK ($this->check)" : '';
331
    }
332
333
    /**
334
     * Builds the unsigned string for column. Defaults to unsupported.
335
     *
336
     * @return string A string containing the UNSIGNED keyword.
337
     */
338
    protected function buildUnsignedString(): string
339
    {
340
        return '';
341
    }
342
343
    /**
344
     * Builds the custom string that's appended to column definition.
345
     *
346
     * @return string A string containing the custom SQL fragment appended to column definition.
347
     */
348
    protected function buildAppendString(): string
349
    {
350
        return !empty($this->append) ? ' ' . $this->append : '';
351
    }
352
353
    /**
354
     * @return string|null A string containing the column type category name.
355
     */
356
    protected function getTypeCategory(): string|null
357
    {
358
        return $this->categoryMap[$this->type] ?? null;
359
    }
360
361
    /**
362
     * Builds the comment specification for the column.
363
     *
364
     * @return string A string containing the COMMENT keyword and the comment itself.
365
     */
366
    protected function buildCommentString(): string
367
    {
368
        return '';
369
    }
370
371
    /**
372
     * Returns the complete column definition from input format.
373
     *
374
     * @param string $format The format of the definition.
375
     *
376
     * @return string A string containing the complete column definition.
377
     */
378
    protected function buildCompleteString(string $format): string
379
    {
380
        $placeholderValues = [
381
            '{type}' => $this->type,
382
            '{length}' => $this->buildLengthString(),
383
            '{unsigned}' => $this->buildUnsignedString(),
384
            '{notnull}' => $this->buildNotNullString(),
385
            '{unique}' => $this->buildUniqueString(),
386
            '{default}' => $this->buildDefaultString(),
387
            '{check}' => $this->buildCheckString(),
388
            '{comment}' => $this->buildCommentString(),
389
            '{append}' => $this->buildAppendString(),
390
        ];
391
392
        return strtr($format, $placeholderValues);
393
    }
394
}
395