Passed
Push — master ( 9f7d35...3f1c7e )
by Wilmer
08:50 queued 06:32
created

ColumnSchemaBuilder::after()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
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 Yiisoft\Db\Connection\Connection;
8
use Yiisoft\Db\Expression\Expression;
9
use Yiisoft\Strings\NumericHelper;
10
use Yiisoft\Strings\StringHelper;
11
12
/**
13
 * ColumnSchemaBuilder helps to define database schema types using a PHP interface.
14
 *
15
 * See {@see SchemaBuilderTrait} for more detailed description and usage examples.
16
 */
17
class ColumnSchemaBuilder
18
{
19
    /**
20
     * Internally used constants representing categories that abstract column types fall under.
21
     *
22
     * {@see $categoryMap} for mappings of abstract column types to category.
23
     */
24
    public const CATEGORY_PK = 'pk';
25
    public const CATEGORY_STRING = 'string';
26
    public const CATEGORY_NUMERIC = 'numeric';
27
    public const CATEGORY_TIME = 'time';
28
    public const CATEGORY_OTHER = 'other';
29
30
    private ?string $type = null;
31
    private $length;
32
    private ?bool $isNotNull = null;
33
    private bool $isUnique = false;
34
    private ?string $check = null;
35
    private $default;
36
    private $append;
37
    private bool $isUnsigned = false;
38
    private ?string $after = null;
39
    private bool $isFirst = false;
40
    private array $categoryMap = [
41
        Schema::TYPE_PK        => self::CATEGORY_PK,
42
        Schema::TYPE_UPK       => self::CATEGORY_PK,
43
        Schema::TYPE_BIGPK     => self::CATEGORY_PK,
44
        Schema::TYPE_UBIGPK    => self::CATEGORY_PK,
45
        Schema::TYPE_CHAR      => self::CATEGORY_STRING,
46
        Schema::TYPE_STRING    => self::CATEGORY_STRING,
47
        Schema::TYPE_TEXT      => self::CATEGORY_STRING,
48
        Schema::TYPE_TINYINT   => self::CATEGORY_NUMERIC,
49
        Schema::TYPE_SMALLINT  => self::CATEGORY_NUMERIC,
50
        Schema::TYPE_INTEGER   => self::CATEGORY_NUMERIC,
51
        Schema::TYPE_BIGINT    => self::CATEGORY_NUMERIC,
52
        Schema::TYPE_FLOAT     => self::CATEGORY_NUMERIC,
53
        Schema::TYPE_DOUBLE    => self::CATEGORY_NUMERIC,
54
        Schema::TYPE_DECIMAL   => self::CATEGORY_NUMERIC,
55
        Schema::TYPE_DATETIME  => self::CATEGORY_TIME,
56
        Schema::TYPE_TIMESTAMP => self::CATEGORY_TIME,
57
        Schema::TYPE_TIME      => self::CATEGORY_TIME,
58
        Schema::TYPE_DATE      => self::CATEGORY_TIME,
59
        Schema::TYPE_BINARY    => self::CATEGORY_OTHER,
60
        Schema::TYPE_BOOLEAN   => self::CATEGORY_NUMERIC,
61
        Schema::TYPE_MONEY     => self::CATEGORY_NUMERIC,
62
    ];
63
    private ?Connection $db;
64
    private ?string $comment = null;
65
66 30
    public function __construct(string $type, $length = null, ?Connection $db = null)
67
    {
68 30
        $this->type = $type;
69 30
        $this->length = $length;
70 30
        $this->db = $db;
71 30
    }
72
73
    /**
74
     * Adds a `NOT NULL` constraint to the column.
75
     *
76
     * {@see isNotNull}
77
     *
78
     * @return $this
79
     */
80 12
    public function notNull(): self
81
    {
82 12
        $this->isNotNull = true;
83
84 12
        return $this;
85
    }
86
87
    /**
88
     * Adds a `NULL` constraint to the column.
89
     *
90
     * {@see isNotNull}
91
     *
92
     * @return $this
93
     */
94 10
    public function null(): self
95
    {
96 10
        $this->isNotNull = false;
97
98 10
        return $this;
99
    }
100
101
    /**
102
     * Adds a `UNIQUE` constraint to the column.
103
     *
104
     * {@see isUnique}
105
     *
106
     * @return $this
107
     */
108 1
    public function unique(): self
109
    {
110 1
        $this->isUnique = true;
111
112 1
        return $this;
113
    }
114
115
    /**
116
     * Sets a `CHECK` constraint for the column.
117
     *
118
     * @param string $check the SQL of the `CHECK` constraint to be added.
119
     *
120
     * @return $this
121
     */
122 9
    public function check(?string $check): self
123
    {
124 9
        $this->check = $check;
125
126 9
        return $this;
127
    }
128
129
    /**
130
     * Specify the default value for the column.
131
     *
132
     * @param mixed $default the default value.
133
     *
134
     * @return $this
135
     */
136 11
    public function defaultValue($default): self
137
    {
138 11
        if ($default === null) {
139 8
            $this->null();
140
        }
141
142 11
        $this->default = $default;
143
144 11
        return $this;
145
    }
146
147
    /**
148
     * Specifies the comment for column.
149
     *
150
     * @param string $comment the comment
151
     *
152
     * @return $this
153
     */
154 13
    public function comment(?string $comment): self
155
    {
156 13
        $this->comment = $comment;
157
158 13
        return $this;
159
    }
160
161
    /**
162
     * Marks column as unsigned.
163
     *
164
     * @return $this
165
     */
166 16
    public function unsigned(): self
167
    {
168 16
        switch ($this->type) {
169
            case Schema::TYPE_PK:
170 8
                $this->type = Schema::TYPE_UPK;
171 8
                break;
172
            case Schema::TYPE_BIGPK:
173 8
                $this->type = Schema::TYPE_UBIGPK;
174 8
                break;
175
        }
176 16
        $this->isUnsigned = true;
177
178 16
        return $this;
179
    }
180
181
    /**
182
     * Adds an `AFTER` constraint to the column.
183
     *
184
     * Note: MySQL, Oracle support only.
185
     *
186
     * @param string $after the column after which $this column will be added.
187
     *
188
     * @return $this
189
     */
190 8
    public function after(string $after): self
191
    {
192 8
        $this->after = $after;
193
194 8
        return $this;
195
    }
196
197
    /**
198
     * Adds an `FIRST` constraint to the column.
199
     *
200
     * Note: MySQL, Oracle support only.
201
     *
202
     * @return $this
203
     */
204 8
    public function first(): self
205
    {
206 8
        $this->isFirst = true;
207
208 8
        return $this;
209
    }
210
211
    /**
212
     * Specify the default SQL expression for the column.
213
     *
214
     * @param string $default the default value expression.
215
     *
216
     * @return $this
217
     */
218 1
    public function defaultExpression(string $default): self
219
    {
220 1
        $this->default = new Expression($default);
221
222 1
        return $this;
223
    }
224
225
    /**
226
     * Specify additional SQL to be appended to column definition.
227
     *
228
     * Position modifiers will be appended after column definition in databases that support them.
229
     *
230
     * @param string $sql the SQL string to be appended.
231
     *
232
     * @return $this
233
     */
234 9
    public function append(string $sql): self
235
    {
236 9
        $this->append = $sql;
237
238 9
        return $this;
239
    }
240
241
    /**
242
     * Builds the full string for the column's schema.
243
     *
244
     * @return string
245
     */
246 15
    public function __toString(): string
247
    {
248 15
        switch ($this->getTypeCategory()) {
249 15
            case self::CATEGORY_PK:
250 2
                $format = '{type}{check}{comment}{append}';
251 2
                break;
252
            default:
253 15
                $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{append}';
254
        }
255
256 15
        return $this->buildCompleteString($format);
257
    }
258
259
    /**
260
     * Builds the length/precision part of the column.
261
     *
262
     * @return string
263
     */
264 26
    protected function buildLengthString(): string
265
    {
266 26
        if ($this->length === null || $this->length === []) {
267 13
            return '';
268
        }
269 18
        if (is_array($this->length)) {
270 4
            $this->length = implode(',', $this->length);
271
        }
272
273 18
        return "({$this->length})";
274
    }
275
276
    /**
277
     * Builds the not null constraint for the column.
278
     *
279
     * @return string returns 'NOT NULL' if {@see isNotNull} is true, 'NULL' if {@see isNotNull} is false or an empty
280
     * string otherwise.
281
     */
282 26
    protected function buildNotNullString(): string
283
    {
284 26
        if ($this->isNotNull === true) {
285 8
            return ' NOT NULL';
286
        }
287
288 23
        if ($this->isNotNull === false) {
289 6
            return ' NULL';
290
        }
291
292 21
        return '';
293
    }
294
295
    /**
296
     * Builds the unique constraint for the column.
297
     *
298
     * @return string returns string 'UNIQUE' if {@see isUnique} is true, otherwise it returns an empty string.
299
     */
300 26
    protected function buildUniqueString(): string
301
    {
302 26
        return $this->isUnique ? ' UNIQUE' : '';
303
    }
304
305
    /**
306
     * Builds the default value specification for the column.
307
     *
308
     * @return string string with default value of column.
309
     */
310 26
    protected function buildDefaultString(): string
311
    {
312 26
        if ($this->default === null) {
313 24
            return $this->isNotNull === false ? ' DEFAULT NULL' : '';
314
        }
315
316 7
        $string = ' DEFAULT ';
317 7
        switch (gettype($this->default)) {
318 7
            case 'object':
319 5
            case 'integer':
320 6
                $string .= (string) $this->default;
321 6
                break;
322 2
            case 'double':
323
                // ensure type cast always has . as decimal separator in all locales
324
                $string .= NumericHelper::normalize($this->default);
325
                break;
326 2
            case 'boolean':
327 1
                $string .= $this->default ? 'TRUE' : 'FALSE';
328 1
                break;
329
            default:
330 1
                $string .= "'{$this->default}'";
331
        }
332
333 7
        return $string;
334
    }
335
336
    /**
337
     * Builds the check constraint for the column.
338
     *
339
     * @return string a string containing the CHECK constraint.
340
     */
341 26
    protected function buildCheckString(): string
342
    {
343 26
        return $this->check !== null ? " CHECK ({$this->check})" : '';
344
    }
345
346
    /**
347
     * Builds the unsigned string for column. Defaults to unsupported.
348
     *
349
     * @return string a string containing UNSIGNED keyword.
350
     */
351 15
    protected function buildUnsignedString(): string
352
    {
353 15
        return '';
354
    }
355
356
    /**
357
     * Builds the after constraint for the column. Defaults to unsupported.
358
     *
359
     * @return string a string containing the AFTER constraint.
360
     */
361 20
    protected function buildAfterString(): string
362
    {
363 20
        return '';
364
    }
365
366
    /**
367
     * Builds the first constraint for the column. Defaults to unsupported.
368
     *
369
     * @return string a string containing the FIRST constraint.
370
     */
371 1
    protected function buildFirstString(): string
372
    {
373 1
        return '';
374
    }
375
376
    /**
377
     * Builds the custom string that's appended to column definition.
378
     *
379
     * @return string custom string to append.
380
     */
381 26
    protected function buildAppendString(): string
382
    {
383 26
        return $this->append !== null ? ' ' . $this->append : '';
384
    }
385
386
    /**
387
     * Returns the category of the column type.
388
     *
389
     * @return string|null a string containing the column type category name.
390
     */
391 26
    protected function getTypeCategory(): ?string
392
    {
393 26
        return $this->categoryMap[$this->type] ?? null;
394
    }
395
396
    /**
397
     * Builds the comment specification for the column.
398
     *
399
     * @return string a string containing the COMMENT keyword and the comment itself
400
     */
401 20
    protected function buildCommentString(): string
402
    {
403 20
        return '';
404
    }
405
406
    /**
407
     * Returns the complete column definition from input format.
408
     *
409
     * @param string $format the format of the definition.
410
     *
411
     * @return string a string containing the complete column definition.
412
     */
413 26
    protected function buildCompleteString(string $format): string
414
    {
415
        $placeholderValues = [
416 26
            '{type}'     => $this->type,
417 26
            '{length}'   => $this->buildLengthString(),
418 26
            '{unsigned}' => $this->buildUnsignedString(),
419 26
            '{notnull}'  => $this->buildNotNullString(),
420 26
            '{unique}'   => $this->buildUniqueString(),
421 26
            '{default}'  => $this->buildDefaultString(),
422 26
            '{check}'    => $this->buildCheckString(),
423 26
            '{comment}'  => $this->buildCommentString(),
424 26
            '{pos}'      => $this->isFirst ? $this->buildFirstString() : $this->buildAfterString(),
425 26
            '{append}'   => $this->buildAppendString(),
426
        ];
427
428 26
        return strtr($format, $placeholderValues);
429
    }
430
431
    /**
432
     * @return string|null the column type definition such as INTEGER, VARCHAR, DATETIME, etc.
433
     */
434
    public function getType(): ?string
435
    {
436
        return $this->type;
437
    }
438
439
    /**
440
     * @return int|string|array column size or precision definition. This is what goes into the parenthesis after the
441
     * column type. This can be either a string, an integer or an array. If it is an array, the array values will be
442
     * joined into a string separated by comma.
443
     */
444
    public function getLength()
445
    {
446
        return $this->length;
447
    }
448
449
    /**
450
     * @return bool|null whether the column is or not nullable. If this is `true`, a `NOT NULL` constraint will be
451
     * added. If this is `false`, a `NULL` constraint will be added.
452
     */
453
    public function getIsNotNull(): ?bool
454
    {
455
        return $this->isNotNull;
456
    }
457
458
    /**
459
     * @return bool whether the column values should be unique. If this is `true`, a `UNIQUE` constraint will be added.
460
     */
461
    public function isUnique(): bool
462
    {
463
        return $this->isUnique;
464
    }
465
466
    /**
467
     * @return string|null the `CHECK` constraint for the column.
468
     */
469
    public function getCheck(): ?string
470
    {
471
        return $this->check;
472
    }
473
474
    /**
475
     * @return mixed default value of the column.
476
     */
477
    public function getDefault()
478
    {
479
        return $this->default;
480
    }
481
482
    /**
483
     * @return mixed SQL string to be appended to column schema definition.
484
     */
485
    public function getAppend()
486
    {
487
        return $this->append;
488
    }
489
490
    /**
491
     * @return bool whether the column values should be unsigned. If this is `true`, an `UNSIGNED` keyword will be
492
     * added.
493
     */
494 11
    public function isUnsigned(): bool
495
    {
496 11
        return $this->isUnsigned;
497
    }
498
499
    /**
500
     * @return string|null the column after which this column will be added.
501
     */
502 6
    public function getAfter(): ?string
503
    {
504 6
        return $this->after;
505
    }
506
507
    /**
508
     * @return bool whether this column is to be inserted at the beginning of the table.
509
     */
510 1
    public function isFirst(): bool
511
    {
512 1
        return $this->isFirst;
513
    }
514
515
    /**
516
     * @return array mapping of abstract column types (keys) to type categories (values).
517
     */
518
    public function getCategoryMap(): array
519
    {
520
        return $this->categoryMap;
521
    }
522
523
    /**
524
     * @return Connection|null the current database connection. It is used mainly to escape strings safely
525
     * when building the final column schema string.
526
     */
527 3
    public function getDb(): ?Connection
528
    {
529 3
        return $this->db;
530
    }
531
532
    /**
533
     * @return string comment value of the column.
534
     */
535 6
    public function getComment(): ?string
536
    {
537 6
        return $this->comment;
538
    }
539
}
540