• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / ActiveRecord / 4525406810

pending completion
4525406810

push

github

Olivier Laviale
Build schema from attributes

189 of 189 new or added lines in 20 files covered. (100.0%)

1491 of 1873 relevant lines covered (79.6%)

31.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.82
/lib/ActiveRecord/SchemaColumn.php
1
<?php
2

3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace ICanBoogie\ActiveRecord;
13

14
use ICanBoogie\Accessor\AccessorTrait;
15
use LogicException;
16

17
use function array_filter;
18
use function implode;
19
use function in_array;
20
use function is_numeric;
21
use function strtoupper;
22

23
/**
24
 * Representation of a schema column.
25
 *
26
 * @property-read bool $is_serial
27
 * @property-read string $formatted_type_attributes
28
 * @property-read string $formatted_auto_increment
29
 * @property-read string $formatted_collate
30
 * @property-read string $formatted_comment
31
 * @property-read string $formatted_default
32
 * @property-read string $formatted_index
33
 * @property-read string $formatted_null
34
 * @property-read string $formatted_type
35
 */
36
class SchemaColumn
37
{
38
    /**
39
     * @uses get_is_serial
40
     * @uses get_formatted_type_attributes
41
     * @uses get_formatted_auto_increment
42
     * @uses get_formatted_collate
43
     * @uses get_formatted_comment
44
     * @uses get_formatted_default
45
     * @uses get_formatted_key
46
     * @uses get_formatted_null
47
     * @uses get_formatted_type
48
     */
49
    use AccessorTrait;
50

51
    /**
52
     * @param array{
53
     *     type: string,
54
     *     size: string|int|null,
55
     *     unsigned: bool,
56
     *     null: bool,
57
     *     default: mixed,
58
     *     auto_increment: bool,
59
     *     unique: bool,
60
     *     primary: bool,
61
     *     comment: ?string,
62
     *     collate: ?string
63
     * } $an_array
64
     */
65
    public static function __set_state(array $an_array): self
66
    {
67
        return new self(...$an_array);
28✔
68
    }
69

70
    public const TYPE_BLOB = 'BLOB';
71
    public const TYPE_BOOLEAN = 'BOOLEAN';
72
    public const TYPE_DATE = 'DATE';
73
    public const TYPE_DATETIME = 'DATETIME';
74
    public const TYPE_FLOAT = 'FLOAT';
75
    public const TYPE_INT = 'INT';
76
    public const TYPE_TIMESTAMP = 'TIMESTAMP';
77
    public const TYPE_TEXT = 'TEXT';
78
    public const TYPE_CHAR = 'CHAR';
79
    public const TYPE_VARCHAR = 'VARCHAR';
80
    public const TYPE_BINARY = 'BINARY';
81
    public const TYPE_VARBINARY = 'VARBINARY';
82

83
    public const SIZE_TINY = 'TINY';
84
    public const SIZE_SMALL = 'SMALL';
85
    public const SIZE_MEDIUM = 'MEDIUM';
86
    public const SIZE_BIG = 'BIG';
87

88
    public const NOW = 'NOW';
89
    public const CURRENT_TIMESTAMP = 'CURRENT_TIMESTAMP';
90

91
    /*
92
     * https://dev.mysql.com/doc/refman/8.0/en/numeric-types.html
93
     */
94

95
    public static function boolean(
96
        bool $null = false,
97
        bool $unique = false,
98
    ): self {
99
        return new self(
2✔
100
            type: self::TYPE_INT,
2✔
101
            size: 1,
2✔
102
            null: $null,
2✔
103
            unique: $unique,
2✔
104
        );
2✔
105
    }
106

107
    public static function int(
108
        int|string|null $size = null,
109
        bool $unsigned = false,
110
        bool $null = false,
111
        bool $unique = false,
112
    ): self {
113
        return new self(
87✔
114
            type: self::TYPE_INT,
87✔
115
            size: $size,
87✔
116
            unsigned: $unsigned,
87✔
117
            null: $null,
87✔
118
            unique: $unique,
87✔
119
        );
87✔
120
    }
121

122
    public static function float(
123
        ?int $precision = null,
124
        bool $unsigned = false,
125
        bool $null = false,
126
    ): self {
127
        return new self(
1✔
128
            type: self::TYPE_FLOAT,
1✔
129
            size: $precision,
1✔
130
            unsigned: $unsigned,
1✔
131
            null: $null,
1✔
132
        );
1✔
133
    }
134

135
    /**
136
     * @see https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html
137
     */
138
    public static function serial(
139
        bool $primary = false
140
    ): self {
141
        return new self(
112✔
142
            type: self::TYPE_INT,
112✔
143
            size: self::SIZE_BIG,
112✔
144
            unsigned: true,
112✔
145
            null: false,
112✔
146
            auto_increment: true,
112✔
147
            unique: !$primary,
112✔
148
            primary: $primary,
112✔
149
        );
112✔
150
    }
151

152
    public static function foreign(
153
        bool $null = false,
154
        bool $unique = false,
155
        bool $primary = false,
156
    ): self {
157
        return new self(
91✔
158
            type: self::TYPE_INT,
91✔
159
            size: self::SIZE_BIG,
91✔
160
            unsigned: true,
91✔
161
            null: $null,
91✔
162
            unique: $primary ? false : $unique,
91✔
163
            primary: $primary,
91✔
164
        );
91✔
165
    }
166

167
    /*
168
     * https://dev.mysql.com/doc/refman/8.0/en/datetime.html
169
     */
170

171
    public static function date(
172
        bool $null = false,
173
        ?string $default = null,
174
    ): self {
175
        self::assert_datetime_default($default);
8✔
176

177
        return new self(
8✔
178
            type: self::TYPE_DATE,
8✔
179
            null: $null,
8✔
180
            default: $default,
8✔
181
        );
8✔
182
    }
183

184
    public static function datetime(
185
        bool $null = false,
186
        ?string $default = null,
187
    ): self {
188
        self::assert_datetime_default($default);
88✔
189

190
        return new self(
88✔
191
            type: self::TYPE_DATETIME,
88✔
192
            null: $null,
88✔
193
            default: $default,
88✔
194
        );
88✔
195
    }
196

197
    public static function timestamp(
198
        bool $null = false,
199
        ?string $default = null,
200
    ): self {
201
        self::assert_datetime_default($default);
2✔
202

203
        return new self(
2✔
204
            type: self::TYPE_TIMESTAMP,
2✔
205
            null: $null,
2✔
206
            default: $default,
2✔
207
        );
2✔
208
    }
209

210
    public static function assert_datetime_default(?string $default): void
211
    {
212
        if ($default && !in_array($default, [ self::NOW, self::CURRENT_TIMESTAMP ])) {
97✔
213
            throw new LogicException("Can only be one of 'NOW' or 'CURRENT_TIMESTAMP', given: $default.");
×
214
        }
215
    }
216

217
    /*
218
     * https://dev.mysql.com/doc/refman/8.0/en/string-types.html
219
     */
220

221
    public static function char(
222
        int $size = 255,
223
        bool $null = false,
224
        bool $unique = false,
225
        bool $primary = false,
226
        ?string $comment = null,
227
        ?string $collate = null,
228
    ): self {
229
        return new self(
13✔
230
            type: self::TYPE_CHAR,
13✔
231
            size: $size,
13✔
232
            null: $null,
13✔
233
            unique: $unique,
13✔
234
            primary: $primary,
13✔
235
            comment: $comment,
13✔
236
            collate: $collate,
13✔
237
        );
13✔
238
    }
239

240
    public static function varchar(
241
        int $size = 255,
242
        bool $null = false,
243
        bool $unique = false,
244
        bool $primary = false,
245
        ?string $comment = null,
246
        ?string $collate = null,
247
    ): self {
248
        return new self(
113✔
249
            type: self::TYPE_VARCHAR,
113✔
250
            size: $size,
113✔
251
            null: $null,
113✔
252
            unique: $unique,
113✔
253
            primary: $primary,
113✔
254
            comment: $comment,
113✔
255
            collate: $collate,
113✔
256
        );
113✔
257
    }
258

259
    public static function blob(
260
        string|null $size = null,
261
        bool $null = false,
262
        bool $unique = false,
263
        bool $primary = false,
264
        ?string $comment = null,
265
        ?string $collate = null,
266
    ): self {
267
        $size = match ($size) {
×
268
            self::SIZE_SMALL => null,
×
269
            self::SIZE_BIG => 'LONG',
×
270
            default => $size,
×
271
        };
×
272

273
        return new self(
×
274
            type: self::TYPE_BLOB,
×
275
            size: $size,
×
276
            null: $null,
×
277
            unique: $unique,
×
278
            primary: $primary,
×
279
            comment: $comment,
×
280
            collate: $collate,
×
281
        );
×
282
    }
283

284
    public static function text(
285
        string|null $size = null,
286
        bool $null = false,
287
        bool $unique = false,
288
        bool $primary = false,
289
        ?string $comment = null,
290
        ?string $collate = null,
291
    ): self {
292
        $size = match ($size) {
75✔
293
            self::SIZE_SMALL => null,
75✔
294
            self::SIZE_BIG => 'LONG',
75✔
295
            default => $size,
75✔
296
        };
75✔
297

298
        return new self(
75✔
299
            type: self::TYPE_TEXT,
75✔
300
            size: $size,
75✔
301
            null: $null,
75✔
302
            unique: $unique,
75✔
303
            primary: $primary,
75✔
304
            comment: $comment,
75✔
305
            collate: $collate,
75✔
306
        );
75✔
307
    }
308

309
    public readonly string $type;
310

311
    public function __construct(
312
        string $type,
313
        public readonly string|int|null $size = null,
314
        public readonly bool $unsigned = false,
315
        public readonly bool $null = false,
316
        public readonly mixed $default = null,
317
        public readonly bool $auto_increment = false,
318
        public readonly bool $unique = false,
319
        public readonly bool $primary = false,
320
        public readonly ?string $comment = null,
321
        public readonly ?string $collate = null,
322
    ) {
323
        $this->type = strtoupper($type);
142✔
324
    }
325

326
    /**
327
     * @param array{
328
     *     type?: string,
329
     *     size?: string|int|null,
330
     *     unsigned?: bool,
331
     *     null?: bool,
332
     *     default?: mixed,
333
     *     unique?: bool,
334
     *     primary?: bool,
335
     *     comment?: ?string,
336
     *     collate?: ?string
337
     * } $properties
338
     */
339
    public function with(array $properties): self
340
    {
341
        $properties += [
88✔
342
            'type' => $this->type,
88✔
343
            'size' => $this->size,
88✔
344
            'unsigned' => $this->unsigned,
88✔
345
            'null' => $this->null,
88✔
346
            'default' => $this->default,
88✔
347
            'auto_increment' => $this->auto_increment,
88✔
348
            'unique' => $this->unique,
88✔
349
            'primary' => $this->primary,
88✔
350
            'comment' => $this->comment,
88✔
351
            'collate' => $this->collate,
88✔
352
        ];
88✔
353

354
        return new self(...$properties);
88✔
355
    }
356

357
    /**
358
     * Returns the formatted type, including the size.
359
     */
360
    protected function get_formatted_type(): string
361
    {
362
        $type = strtoupper($this->type);
103✔
363
        $size = $this->size;
103✔
364

365
        if (is_numeric($size)) {
103✔
366
            if ($type === self::TYPE_INT) {
87✔
367
                return match ($size) {
7✔
368
                    1 => self::SIZE_TINY . self::TYPE_INT,
7✔
369
                    2 => self::SIZE_SMALL . self::TYPE_INT,
7✔
370
                    3 => self::SIZE_MEDIUM . self::TYPE_INT,
7✔
371
                    4 => self::TYPE_INT,
7✔
372
                    8 => self::SIZE_BIG . self::TYPE_INT,
7✔
373
                    default => "$type($size)",
7✔
374
                };
7✔
375
            }
376

377
            return "$type($size)";
80✔
378
        }
379

380
        if ($size) {
92✔
381
            return strtoupper($size) . $type;
88✔
382
        }
383

384
        return $type;
78✔
385
    }
386

387
    /**
388
     * Returns the formatted default.
389
     */
390
    protected function get_formatted_default(): string
391
    {
392
        $default = $this->default;
103✔
393

394
        return match ($default) {
103✔
395
            null => '',
103✔
396
            self::CURRENT_TIMESTAMP => "DEFAULT CURRENT_TIMESTAMP",
103✔
397
            self::NOW => "DEFAULT NOW",
103✔
398
            default => "DEFAULT '$default'",
103✔
399
        };
103✔
400
    }
401

402
    /**
403
     * Returns the formatted attributes.
404
     */
405
    protected function get_formatted_type_attributes(): string
406
    {
407
        return $this->unsigned ? 'UNSIGNED' : '';
103✔
408
    }
409

410
    /**
411
     * Returns the formatted null.
412
     */
413
    protected function get_formatted_null(): string
414
    {
415
        return $this->null ? 'NULL' : 'NOT NULL';
103✔
416
    }
417

418
    /**
419
     * Returns the formatted index.
420
     */
421
    protected function get_formatted_key(): string
422
    {
423
        return implode(
27✔
424
            ' ',
27✔
425
            array_filter([
27✔
426

427
                $this->unique ? 'UNIQUE' : '',
27✔
428
                $this->primary ? 'PRIMARY KEY' : '',
27✔
429

430
            ])
27✔
431
        );
27✔
432
    }
433

434
    /**
435
     * Returns the formatted comment.
436
     */
437
    protected function get_formatted_comment(): string
438
    {
439
        return $this->comment ? "`$this->comment`" : '';
103✔
440
    }
441

442
    /**
443
     * Returns the formatted charset.
444
     */
445
    protected function get_formatted_collate(): string
446
    {
447
        return $this->collate ? "COLLATE $this->collate" : '';
27✔
448
    }
449

450
    /**
451
     * Returns the formatted auto increment.
452
     */
453
    protected function get_formatted_auto_increment(): string
454
    {
455
        return $this->auto_increment ? 'AUTO_INCREMENT' : '';
103✔
456
    }
457

458
    /**
459
     * Whether the column is a serial column.
460
     */
461
    protected function get_is_serial(): bool
462
    {
463
        return $this->type == self::TYPE_INT
×
464
            && !$this->null
×
465
            && $this->auto_increment
×
466
            && ($this->primary || $this->unique);
×
467
    }
468

469
    /**
470
     * Renders the column into a string.
471
     */
472
    public function render(): string
473
    {
474
        return implode(
27✔
475
            ' ',
27✔
476
            array_filter([
27✔
477
                $this->get_formatted_type(),
27✔
478
                $this->get_formatted_type_attributes(),
27✔
479
                $this->get_formatted_null(),
27✔
480
                $this->get_formatted_default(),
27✔
481
                $this->get_formatted_auto_increment(),
27✔
482
                $this->get_formatted_key(),
27✔
483
                $this->get_formatted_comment(),
27✔
484
                $this->get_formatted_collate(),
27✔
485
            ])
27✔
486
        );
27✔
487
    }
488

489
    public function __toString(): string
490
    {
491
        return $this->render();
27✔
492
    }
493
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc