Passed
Pull Request — master (#427)
by Wilmer
03:02
created

ColumnSchemaBuilder   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 442
Duplicated Lines 0 %

Test Coverage

Coverage 88.89%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 116
dl 0
loc 442
ccs 104
cts 117
cp 0.8889
rs 8.72
c 3
b 0
f 0
wmc 46

31 Methods

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