Passed
Pull Request — master (#428)
by Wilmer
02:55
created

ColumnSchemaBuilder   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 433
Duplicated Lines 0 %

Test Coverage

Coverage 91.23%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 112
c 3
b 0
f 0
dl 0
loc 433
rs 8.8
ccs 104
cts 114
cp 0.9123
wmc 45

31 Methods

Rating   Name   Duplication   Size   Complexity  
A buildAppendString() 0 3 2
A getCheck() 0 3 1
A defaultValue() 0 8 2
A isUnique() 0 3 1
A check() 0 5 1
A getComment() 0 3 1
A buildDefaultString() 0 16 4
A buildUnsignedString() 0 3 1
A buildCommentString() 0 3 1
A isUnsigned() 0 3 1
A getCategoryMap() 0 3 1
A getLength() 0 3 1
A buildCompleteString() 0 15 1
A buildUniqueString() 0 3 2
A unsigned() 0 13 3
A defaultExpression() 0 5 1
A null() 0 5 1
A isNotNull() 0 3 1
A buildNotNullString() 0 11 3
A __construct() 0 4 1
A comment() 0 5 1
A getTypeCategory() 0 3 1
A unique() 0 5 1
A __toString() 0 9 2
A buildCheckString() 0 3 2
A append() 0 5 1
A buildLengthString() 0 11 3
A getType() 0 3 1
A notNull() 0 5 1
A getDefault() 0 3 1
A getAppend() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ColumnSchemaBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ColumnSchemaBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use Stringable;
8
use Yiisoft\Db\Expression\Expression;
9
use Yiisoft\Strings\NumericHelper;
10
11
use function gettype;
12
use function strtr;
13
14
/**
15
 * The ColumnSchemaBuilder class is a utility class that provides a convenient way to create column schemas for use
16
 * with Schema class @see Schema.
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 ColumnSchemaBuilder(Schema::TYPE_INTEGER))->notNull()->defaultValue(0);
25
 * ```
26
 *
27
 * The ColumnSchemaBuilder class provides a fluent interface, which means that the methods can be chained together to
28
 * create a column schema with multiple properties in a single line of code.
29
 */
30
class ColumnSchemaBuilder implements Stringable
31
{
32
    /**
33
     * Internally used constants representing categories that abstract column types fall under.
34
     *
35
     * {@see $categoryMap} for mappings of abstract column types to category.
36
     */
37
    public const CATEGORY_PK = 'pk';
38
    public const CATEGORY_STRING = 'string';
39
    public const CATEGORY_NUMERIC = 'numeric';
40
    public const CATEGORY_TIME = 'time';
41
    public const CATEGORY_OTHER = 'other';
42
43
    protected bool|null $isNotNull = null;
44
    protected bool $isUnique = false;
45
    protected string|null $check = null;
46
    protected mixed $default = null;
47
    protected string|null $append = null;
48
    protected bool $isUnsigned = false;
49
    protected string|null $comment = null;
50
51
    /** @psalm-var string[] */
52
    private array $categoryMap = [
53
        Schema::TYPE_PK => self::CATEGORY_PK,
54
        Schema::TYPE_UPK => self::CATEGORY_PK,
55
        Schema::TYPE_BIGPK => self::CATEGORY_PK,
56
        Schema::TYPE_UBIGPK => self::CATEGORY_PK,
57
        Schema::TYPE_CHAR => self::CATEGORY_STRING,
58
        Schema::TYPE_STRING => self::CATEGORY_STRING,
59
        Schema::TYPE_TEXT => self::CATEGORY_STRING,
60
        Schema::TYPE_TINYINT => self::CATEGORY_NUMERIC,
61
        Schema::TYPE_SMALLINT => self::CATEGORY_NUMERIC,
62
        Schema::TYPE_INTEGER => self::CATEGORY_NUMERIC,
63 38
        Schema::TYPE_BIGINT => self::CATEGORY_NUMERIC,
64
        Schema::TYPE_FLOAT => self::CATEGORY_NUMERIC,
65 38
        Schema::TYPE_DOUBLE => self::CATEGORY_NUMERIC,
66 38
        Schema::TYPE_DECIMAL => self::CATEGORY_NUMERIC,
67 38
        Schema::TYPE_DATETIME => self::CATEGORY_TIME,
68
        Schema::TYPE_TIMESTAMP => self::CATEGORY_TIME,
69
        Schema::TYPE_TIME => self::CATEGORY_TIME,
70
        Schema::TYPE_DATE => self::CATEGORY_TIME,
71
        Schema::TYPE_BINARY => self::CATEGORY_OTHER,
72
        Schema::TYPE_BOOLEAN => self::CATEGORY_NUMERIC,
73
        Schema::TYPE_MONEY => self::CATEGORY_NUMERIC,
74
    ];
75
76 15
    /**
77
     * @psalm-param string[]|int[]|int|string|null $length
78 15
     */
79
    public function __construct(
80 15
        protected string $type,
81
        protected int|string|array|null $length = null
82
    ) {
83
    }
84
85
    /**
86
     * Adds a `NOT NULL` constraint to the column.
87
     *
88
     * @return static The column schema builder instance itself.
89
     *
90 14
     * @see isNotNull
91
     */
92 14
    public function notNull(): static
93
    {
94 14
        $this->isNotNull = true;
95
96
        return $this;
97
    }
98
99
    /**
100
     * Adds a `NULL` constraint to the column.
101
     *
102
     * @return static The column schema builder instance itself.
103
     *
104 1
     * @see isNotNull
105
     */
106 1
    public function null(): static
107
    {
108 1
        $this->isNotNull = false;
109
110
        return $this;
111
    }
112
113
    /**
114
     * Adds a `UNIQUE` constraint to the column.
115
     *
116
     * @return static The column schema builder instance itself.
117
     *
118 11
     * @see isUnique
119
     */
120 11
    public function unique(): static
121
    {
122 11
        $this->isUnique = true;
123
124
        return $this;
125
    }
126
127
    /**
128
     * Specify a `CHECK` constraint for the column.
129
     *
130
     * @param string|null $check The SQL of the `CHECK` constraint to be added.
131
     *
132 14
     * @return static The column schema builder instance itself.
133
     */
134 14
    public function check(string|null $check): static
135 10
    {
136
        $this->check = $check;
137
138 14
        return $this;
139
    }
140 14
141
    /**
142
     * Specify the default value for the column.
143
     *
144
     * @param mixed $default The default value to be used.
145
     *
146
     * @return static The column schema builder instance itself.
147
     */
148
    public function defaultValue(mixed $default): static
149
    {
150 16
        if ($default === null) {
151
            $this->null();
152 16
        }
153
154 16
        $this->default = $default;
155
        return $this;
156
    }
157
158
    /**
159
     * Specifies the comment for column.
160
     *
161
     * @param string|null $comment The comment to be added.
162 20
     *
163
     * @return static The column schema builder instance itself.
164 20
     */
165
    public function comment(string|null $comment): static
166 10
    {
167 10
        $this->comment = $comment;
168
169 10
        return $this;
170 10
    }
171
172 20
    /**
173
     * Marks column as unsigned.
174 20
     *
175
     * @return static The column schema builder instance itself.
176
     */
177
    public function unsigned(): static
178
    {
179
        switch ($this->type) {
180
            case Schema::TYPE_PK:
181
                $this->type = Schema::TYPE_UPK;
182
                break;
183
            case Schema::TYPE_BIGPK:
184
                $this->type = Schema::TYPE_UBIGPK;
185
                break;
186 10
        }
187
        $this->isUnsigned = true;
188 10
189
        return $this;
190 10
    }
191
192
    /**
193
     * Specify the default SQL expression for the column.
194
     *
195
     * @param string $default The SQL expression to be used as default value.
196
     *
197
     * @return static The column schema builder instance itself.
198
     */
199
    public function defaultExpression(string $default): static
200 10
    {
201
        $this->default = new Expression($default);
202 10
203
        return $this;
204 10
    }
205
206
    /**
207
     * Specify additional SQL to be appended to column definition.
208
     *
209
     * Position modifiers will be appended after column definition in databases that support them.
210
     *
211
     * @param string $sql The SQL string to be appended.
212
     *
213
     * @return static The column schema builder instance itself.
214 1
     */
215
    public function append(string $sql): static
216 1
    {
217
        $this->append = $sql;
218 1
219
        return $this;
220
    }
221
222
    /**
223
     * Builds the full string for the column's schema including type, length, default value, not null and other SQL
224
     * fragment.
225
     *
226
     * @return string The SQL fragment that will be used for creating the column.
227
     */
228
    public function __toString(): string
229
    {
230 11
        if ($this->getTypeCategory() === self::CATEGORY_PK) {
231
            $format = '{type}{check}{comment}{append}';
232 11
        } else {
233
            $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{append}';
234 11
        }
235
236
        return $this->buildCompleteString($format);
237
    }
238
239
    /**
240
     * Builds the length, precision part of the column.
241
     *
242 20
     * @return string A string containing the length/precision of the column.
243
     */
244 20
    protected function buildLengthString(): string
245 2
    {
246
        if (empty($this->length)) {
247 20
            return '';
248
        }
249
250 20
        if (is_array($this->length)) {
251
            $this->length = implode(',', $this->length);
252
        }
253
254
        return "({$this->length})";
255
    }
256
257
    /**
258 33
     * Builds the not null constraint for the column.
259
     *
260 33
     * @return string A string 'NOT NULL' if {@see isNotNull} is true, 'NULL' if {@see isNotNull} is false or an empty
261 17
     * string otherwise.
262
     */
263 22
    protected function buildNotNullString(): string
264 5
    {
265
        if ($this->isNotNull === true) {
266
            return ' NOT NULL';
267 22
        }
268
269
        if ($this->isNotNull === false) {
270
            return ' NULL';
271
        }
272
273
        return '';
274
    }
275
276 33
    /**
277
     * Builds the unique constraint for the column.
278 33
     *
279 10
     * @return string A string 'UNIQUE' if {@see isUnique} is true, otherwise it returns an empty string.
280
     */
281
    protected function buildUniqueString(): string
282 29
    {
283 8
        return $this->isUnique ? ' UNIQUE' : '';
284
    }
285
286 26
    /**
287
     * Builds the default value specification for the column.
288
     *
289
     * @return string A string containing the DEFAULT keyword and the default value.
290
     */
291
    protected function buildDefaultString(): string
292
    {
293
        if ($this->default === null) {
294 33
            return $this->isNotNull === false ? ' DEFAULT NULL' : '';
295
        }
296 33
297
        $string = ' DEFAULT ';
298
299
        $string .= match (gettype($this->default)) {
300
            'object', 'integer' => (string)$this->default,
301
            'double' => NumericHelper::normalize((string)$this->default),
302
            'boolean' => $this->default ? 'TRUE' : 'FALSE',
303
            default => "'{$this->default}'",
304 33
        };
305
306 33
        return $string;
307 30
    }
308
309
    /**
310 8
     * Builds the check constraint for the column.
311 8
     *
312 8
     * @return string A string containing the CHECK constraint.
313 5
     */
314 7
    protected function buildCheckString(): string
315 7
    {
316 2
        return !empty($this->check) ? " CHECK ({$this->check})" : '';
317
    }
318
319
    /**
320 2
     * Builds the unsigned string for column. Defaults to unsupported.
321 1
     *
322 1
     * @return string A string containing the UNSIGNED keyword.
323
     */
324 1
    protected function buildUnsignedString(): string
325
    {
326
        return '';
327 8
    }
328
329
    /**
330
     * Builds the custom string that's appended to column definition.
331
     *
332
     * @return string A string containing the custom SQL fragment appended to column definition.
333
     */
334
    protected function buildAppendString(): string
335 33
    {
336
        return !empty($this->append) ? ' ' . $this->append : '';
337 33
    }
338
339
    /**
340
     * @return string|null A string containing the column type category name.
341
     */
342
    protected function getTypeCategory(): string|null
343
    {
344
        return $this->categoryMap[$this->type] ?? null;
345 20
    }
346
347 20
    /**
348
     * Builds the comment specification for the column.
349
     *
350
     * @return string A string containing the COMMENT keyword and the comment itself.
351
     */
352
    protected function buildCommentString(): string
353
    {
354
        return '';
355 27
    }
356
357 27
    /**
358
     * Returns the complete column definition from input format.
359
     *
360
     * @param string $format The format of the definition.
361
     *
362
     * @return string A string containing the complete column definition.
363
     */
364
    protected function buildCompleteString(string $format): string
365 1
    {
366
        $placeholderValues = [
367 1
            '{type}' => $this->type,
368
            '{length}' => $this->buildLengthString(),
369
            '{unsigned}' => $this->buildUnsignedString(),
370
            '{notnull}' => $this->buildNotNullString(),
371
            '{unique}' => $this->buildUniqueString(),
372
            '{default}' => $this->buildDefaultString(),
373
            '{check}' => $this->buildCheckString(),
374
            '{comment}' => $this->buildCommentString(),
375 33
            '{append}' => $this->buildAppendString(),
376
        ];
377 33
378
        return strtr($format, $placeholderValues);
379
    }
380
381
    /**
382
     * @return string|null The column type definition such as INTEGER, VARCHAR, DATETIME, etc.
383
     */
384
    public function getType(): string|null
385 33
    {
386
        return $this->type;
387 33
    }
388
389
    /**
390
     * @return array|int|string|null The column size or precision definition. This is what goes into the parenthesis
391
     * after the column type. This can be either a string, an integer or an array. If it is an array, the array values
392
     * will be joined into a string separated by comma.
393
     */
394
    public function getLength(): array|int|string|null
395 27
    {
396
        return $this->length;
397 27
    }
398
399
    /**
400
     * @return bool|null Whether the column is or not nullable. If this is `true`, a `NOT NULL` constraint will be
401
     * added. If this is `false`, a `NULL` constraint will be added.
402
     */
403
    public function isNotNull(): bool|null
404
    {
405
        return $this->isNotNull;
406
    }
407 33
408
    /**
409
     * @return bool Whether the column values should be unique. If this is `true`, a `UNIQUE` constraint will be added.
410 33
     */
411 33
    public function isUnique(): bool
412 33
    {
413 33
        return $this->isUnique;
414 33
    }
415 33
416 33
    /**
417 33
     * @return string|null The `CHECK` constraint for the column.
418 33
     */
419 33
    public function getCheck(): string|null
420
    {
421
        return $this->check;
422 33
    }
423
424
    /**
425
     * @return mixed The default value of the column.
426
     */
427
    public function getDefault(): mixed
428
    {
429
        return $this->default;
430
    }
431
432
    /**
433
     * @return string|null The SQL string to be appended to column schema definition.
434
     */
435
    public function getAppend(): string|null
436
    {
437
        return $this->append;
438
    }
439
440
    /**
441
     * @return bool Whether the column values should be unsigned. If this is `true`, an `UNSIGNED` keyword will be
442
     * added.
443
     */
444
    public function isUnsigned(): bool
445
    {
446
        return $this->isUnsigned;
447
    }
448
449
    /**
450
     * @return array The mapping of abstract column types (keys) to type categories (values).
451
     */
452
    public function getCategoryMap(): array
453
    {
454
        return $this->categoryMap;
455
    }
456
457
    /**
458
     * @return string|null The comment value of the column.
459
     */
460
    public function getComment(): string|null
461
    {
462
        return $this->comment;
463
    }
464
}
465