Completed
Push — master ( 3a9351...acf721 )
by Neomerx
06:52
created

MigrationTrait::getModelSchemas()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
crap 1
1
<?php namespace Limoncello\Data\Migrations;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Closure;
20
use Doctrine\DBAL\Connection;
21
use Doctrine\DBAL\Schema\AbstractSchemaManager;
22
use Doctrine\DBAL\Schema\Table;
23
use Doctrine\DBAL\Types\Type;
24
use Limoncello\Contracts\Data\MigrationInterface;
25
use Limoncello\Contracts\Data\ModelSchemaInfoInterface;
26
use Limoncello\Contracts\Data\RelationshipTypes;
27
use Limoncello\Contracts\Data\TimestampFields;
28
use Limoncello\Data\Contracts\MigrationContextInterface;
29
use Psr\Container\ContainerInterface;
30
31
/**
32
 * @package Limoncello\Data
33
 */
34
trait MigrationTrait
35
{
36
    /**
37
     * @var ContainerInterface
38
     */
39
    private $container;
40
41
    /**
42
     * @inheritdoc
43
     */
44 2
    public function init(ContainerInterface $container): MigrationInterface
45
    {
46 2
        $this->container = $container;
47
48
        /** @var MigrationInterface $self */
49 2
        $self = $this;
50
51 2
        return $self;
52
    }
53
54
    /**
55
     * @return ContainerInterface
56
     */
57 2
    protected function getContainer(): ContainerInterface
58
    {
59 2
        return $this->container;
60
    }
61
62
    /**
63
     * @return Connection
64
     */
65 2
    protected function getConnection(): Connection
66
    {
67 2
        assert($this->getContainer()->has(Connection::class) === true);
68
69 2
        return $this->getContainer()->get(Connection::class);
70
    }
71
72
    /**
73
     * @return ModelSchemaInfoInterface
74
     */
75 2
    protected function getModelSchemas(): ModelSchemaInfoInterface
76
    {
77 2
        assert($this->getContainer()->has(ModelSchemaInfoInterface::class) === true);
78
79 2
        return $this->getContainer()->get(ModelSchemaInfoInterface::class);
80
    }
81
82
    /**
83
     * @return AbstractSchemaManager
84
     */
85 2
    protected function getSchemaManager(): AbstractSchemaManager
86
    {
87 2
        return $this->getConnection()->getSchemaManager();
88
    }
89
90
    /**
91
     * @param string    $modelClass
92
     * @param Closure[] $expressions
93
     *
94
     * @return Table
95
     */
96 2
    protected function createTable(string $modelClass, array $expressions = []): Table
97
    {
98 2
        $context   = new MigrationContext($modelClass, $this->getModelSchemas());
99 2
        $tableName = $this->getModelSchemas()->getTable($modelClass);
100 2
        $table     = new Table($tableName);
101 2
        foreach ($expressions as $expression) {
102
            /** @var Closure $expression */
103 2
            $expression($table, $context);
104
        }
105
106 2
        $this->getSchemaManager()->dropAndCreateTable($table);
107
108 2
        return $table;
109
    }
110
111
    /**
112
     * @param string $modelClass
113
     *
114
     * @return void
115
     */
116 1
    protected function dropTableIfExists(string $modelClass): void
117
    {
118 1
        $tableName     = $this->getModelSchemas()->getTable($modelClass);
119 1
        $schemaManager = $this->getSchemaManager();
120
121 1
        if ($schemaManager->tablesExist([$tableName]) === true) {
122 1
            $schemaManager->dropTable($tableName);
123
        }
124
    }
125
126
    /**
127
     * @param string $name
128
     *
129
     * @return Closure
130
     */
131
    protected function primaryInt(string $name): Closure
132
    {
133 2
        return function (Table $table) use ($name) {
134 2
            $table->addColumn($name, Type::INTEGER)->setAutoincrement(true)->setUnsigned(true)->setNotnull(true);
135 2
            $table->setPrimaryKey([$name]);
136 2
        };
137
    }
138
139
    /**
140
     * @param string $name
141
     *
142
     * @return Closure
143
     */
144 View Code Duplication
    protected function primaryString(string $name): Closure
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
145
    {
146 1
        return function (Table $table, MigrationContextInterface $context) use ($name) {
147 1
            $length = $context->getModelSchemas()->getAttributeLength($context->getModelClass(), $name);
148 1
            $table->addColumn($name, Type::STRING)->setLength($length)->setNotnull(true);
149 1
            $table->setPrimaryKey([$name]);
150 1
        };
151
    }
152
153
    /**
154
     * @param string   $name
155
     * @param null|int $default
156
     *
157
     * @return Closure
158
     */
159 1
    protected function unsignedInt(string $name, int $default = null): Closure
160
    {
161 1
        return $this->unsignedIntImpl($name, true, $default);
162
    }
163
164
    /**
165
     * @param string   $name
166
     * @param null|int $default
167
     *
168
     * @return Closure
169
     */
170 1
    protected function nullableUnsignedInt(string $name, int $default = null): Closure
171
    {
172 1
        return $this->unsignedIntImpl($name, false, $default);
173
    }
174
175
    /**
176
     * @param string $name
177
     *
178
     * @return Closure
179
     */
180
    protected function float(string $name): Closure
181
    {
182
        // precision and scale both seems to be ignored in Doctrine so not much sense to have them as inputs
183
184 1
        return function (Table $table) use ($name) {
185 1
            $table->addColumn($name, Type::FLOAT)->setNotnull(true);
186 1
        };
187
    }
188
189
    /**
190
     * @param string $name
191
     *
192
     * @return Closure
193
     */
194 View Code Duplication
    protected function string(string $name): Closure
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
195
    {
196 1
        return function (Table $table, MigrationContextInterface $context) use ($name) {
197 1
            $length = $context->getModelSchemas()->getAttributeLength($context->getModelClass(), $name);
198 1
            $table->addColumn($name, Type::STRING)->setLength($length)->setNotnull(true);
199 1
        };
200
    }
201
202
    /**
203
     * @param string $name
204
     *
205
     * @return Closure
206
     */
207 View Code Duplication
    protected function nullableString(string $name): Closure
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
208
    {
209 1
        return function (Table $table, MigrationContextInterface $context) use ($name) {
210 1
            $length = $context->getModelSchemas()->getAttributeLength($context->getModelClass(), $name);
211 1
            $table->addColumn($name, Type::STRING)->setLength($length)->setNotnull(false);
212 1
        };
213
    }
214
215
    /**
216
     * @param string $name
217
     *
218
     * @return Closure
219
     */
220
    protected function text(string $name): Closure
221
    {
222 1
        return function (Table $table) use ($name) {
223 1
            $table->addColumn($name, Type::TEXT)->setNotnull(true);
224 1
        };
225
    }
226
227
    /**
228
     * @param string $name
229
     *
230
     * @return Closure
231
     */
232
    protected function nullableText(string $name): Closure
233
    {
234 1
        return function (Table $table) use ($name) {
235 1
            $table->addColumn($name, Type::TEXT)->setNotnull(false);
236 1
        };
237
    }
238
239
    /**
240
     * @param string    $name
241
     * @param null|bool $default
242
     *
243
     * @return Closure
244
     */
245 View Code Duplication
    protected function bool(string $name, $default = null): Closure
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
246
    {
247 1
        return function (Table $table) use ($name, $default) {
248 1
            $column = $table->addColumn($name, Type::BOOLEAN)->setNotnull(true);
249 1
            if ($default !== null && is_bool($default) === true) {
250 1
                $column->setDefault($default);
251
            }
252 1
        };
253
    }
254
255
    /**
256
     * @param string $name
257
     * @param array  $values
258
     *
259
     * @return Closure
260
     */
261 1
    protected function enum(string $name, array $values): Closure
262
    {
263 1
        return $this->enumImpl($name, $values, true);
264
    }
265
266
    /**
267
     * @param string $name
268
     * @param array  $values
269
     *
270
     * @return Closure
271
     */
272 1
    protected function nullableEnum(string $name, array $values): Closure
273
    {
274 1
        return $this->enumImpl($name, $values, false);
275
    }
276
277
    /**
278
     * @return Closure
279
     */
280
    protected function timestamps(): Closure
281
    {
282 1
        return function (Table $table, MigrationContextInterface $context) {
283 1
            $modelClass = $context->getModelClass();
284
285 1
            $createdAt = TimestampFields::FIELD_CREATED_AT;
286 1
            $updatedAt = TimestampFields::FIELD_UPDATED_AT;
287 1
            $deletedAt = TimestampFields::FIELD_DELETED_AT;
288
289
            // a list of data columns and `nullable` flag
290 1
            $datesToAdd = [];
291 1
            if ($this->getModelSchemas()->hasAttributeType($modelClass, $createdAt) === true) {
292 1
                $datesToAdd[$createdAt] = true;
293
            }
294 1
            if ($this->getModelSchemas()->hasAttributeType($modelClass, $updatedAt) === true) {
295 1
                $datesToAdd[$updatedAt] = false;
296
            }
297 1
            if ($this->getModelSchemas()->hasAttributeType($modelClass, $deletedAt) === true) {
298 1
                $datesToAdd[$deletedAt] = false;
299
            }
300
301 1
            foreach ($datesToAdd as $column => $isNullable) {
302 1
                $table->addColumn($column, Type::DATETIME)->setNotnull($isNullable);
303
            }
304 1
        };
305
    }
306
307
    /**
308
     * @param string $name
309
     *
310
     * @return Closure
311
     */
312
    protected function datetime(string $name): Closure
313
    {
314 1
        return function (Table $table) use ($name) {
315 1
            $table->addColumn($name, Type::DATETIME)->setNotnull(true);
316 1
        };
317
    }
318
319
    /**
320
     * @param string $name
321
     *
322
     * @return Closure
323
     */
324
    protected function nullableDatetime(string $name): Closure
325
    {
326 1
        return function (Table $table) use ($name) {
327 1
            $table->addColumn($name, Type::DATETIME)->setNotnull(false);
328 1
        };
329
    }
330
331
    /**
332
     * @param string $name
333
     *
334
     * @return Closure
335
     */
336
    protected function date(string $name): Closure
337
    {
338 1
        return function (Table $table) use ($name) {
339 1
            $table->addColumn($name, Type::DATE)->setNotnull(true);
340 1
        };
341
    }
342
343
    /**
344
     * @param string $name
345
     *
346
     * @return Closure
347
     */
348
    protected function nullableDate(string $name): Closure
349
    {
350 1
        return function (Table $table) use ($name) {
351 1
            $table->addColumn($name, Type::DATE)->setNotnull(false);
352 1
        };
353
    }
354
355
    /**
356
     * @param string[] $names
357
     *
358
     * @return Closure
359
     */
360
    protected function unique(array $names): Closure
361
    {
362 1
        return function (Table $table) use ($names) {
363 1
            $table->addUniqueIndex($names);
364 1
        };
365
    }
366
367
    /**
368
     * @param string[] $names
369
     *
370
     * @return Closure
371
     */
372
    protected function searchable(array $names): Closure
373
    {
374 1
        return function (Table $table) use ($names) {
375 1
            $table->addIndex($names, null, ['fulltext']);
376 1
        };
377
    }
378
379
    /**
380
     * @param string $column
381
     * @param string $referredClass
382
     * @param bool   $cascadeDelete
383
     *
384
     * @return Closure
385
     *
386
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
387
     */
388 View Code Duplication
    protected function foreignRelationship(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389
        string $column,
390
        string $referredClass,
391
        bool $cascadeDelete = false
392
    ): Closure {
393 1
        return function (
394
            Table $table,
395
            MigrationContextInterface $context
396
        ) use (
397 1
            $column,
398 1
            $referredClass,
399 1
            $cascadeDelete
400
        ) {
401 1
            $tableName  = $this->getTableNameForClass($referredClass);
402 1
            $pkName     = $this->getModelSchemas()->getPrimaryKey($referredClass);
403 1
            $columnType = $this->getModelSchemas()->getAttributeType($context->getModelClass(), $column);
404
405 1
            $closure = $this->foreignColumn($column, $tableName, $pkName, $columnType, $cascadeDelete);
406
407 1
            return $closure($table, $context);
408 1
        };
409
    }
410
411
    /**
412
     * @param string $column
413
     * @param string $referredClass
414
     * @param bool   $cascadeDelete
415
     *
416
     * @return Closure
417
     *
418
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
419
     */
420 View Code Duplication
    protected function nullableForeignRelationship(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
421
        string $column,
422
        string $referredClass,
423
        bool $cascadeDelete = false
424
    ): Closure {
425 1
        return function (
426
            Table $table,
427
            MigrationContextInterface $context
428
        ) use (
429 1
            $column,
430 1
            $referredClass,
431 1
            $cascadeDelete
432
        ) {
433 1
            $tableName  = $this->getTableNameForClass($referredClass);
434 1
            $pkName     = $this->getModelSchemas()->getPrimaryKey($referredClass);
435 1
            $columnType = $this->getModelSchemas()->getAttributeType($context->getModelClass(), $column);
436
437 1
            $closure = $this->nullableForeignColumn($column, $tableName, $pkName, $columnType, $cascadeDelete);
438
439 1
            return $closure($table, $context);
440 1
        };
441
    }
442
443
    /**
444
     * @param string $localKey
445
     * @param string $foreignTable
446
     * @param string $foreignKey
447
     * @param string $type
448
     * @param bool   $cascadeDelete
449
     *
450
     * @return Closure
451
     *
452
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
453
     */
454 1
    protected function foreignColumn(
455
        string $localKey,
456
        string $foreignTable,
457
        string $foreignKey,
458
        string $type,
459
        bool $cascadeDelete = false
460
    ): Closure {
461 1
        return $this->foreignColumnImpl($localKey, $foreignTable, $foreignKey, $type, true, $cascadeDelete);
462
    }
463
464
    /**
465
     * @param string $localKey
466
     * @param string $foreignTable
467
     * @param string $foreignKey
468
     * @param string $type
469
     * @param bool   $cascadeDelete
470
     *
471
     * @return Closure
472
     *
473
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
474
     */
475 1
    protected function nullableForeignColumn(
476
        string $localKey,
477
        string $foreignTable,
478
        string $foreignKey,
479
        string $type,
480
        bool $cascadeDelete = false
481
    ): Closure {
482 1
        return $this->foreignColumnImpl($localKey, $foreignTable, $foreignKey, $type, false, $cascadeDelete);
483
    }
484
485
    /**
486
     * @param string $name
487
     * @param bool   $cascadeDelete
488
     *
489
     * @return Closure
490
     *
491
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
492
     */
493 1
    protected function nullableRelationship(string $name, bool $cascadeDelete = false): Closure
494
    {
495 1
        return $this->relationshipImpl($name, false, $cascadeDelete);
496
    }
497
498
    /**
499
     * @param string $name
500
     * @param bool   $cascadeDelete
501
     *
502
     * @return Closure
503
     *
504
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
505
     */
506 1
    protected function relationship(string $name, bool $cascadeDelete = false): Closure
507
    {
508 1
        return $this->relationshipImpl($name, true, $cascadeDelete);
509
    }
510
511
    /**
512
     * @param string $modelClass
513
     *
514
     * @return string
515
     */
516 1
    protected function getTableNameForClass(string $modelClass): string
517
    {
518 1
        assert(
519 1
            $this->getModelSchemas()->hasClass($modelClass),
520 1
            "Table name is not specified for model '$modelClass'."
521
        );
522
523 1
        $tableName = $this->getModelSchemas()->getTable($modelClass);
524
525 1
        return $tableName;
526
    }
527
528
    /**
529
     * @param string     $name
530
     * @param bool       $notNullable
531
     * @param null|mixed $default
532
     *
533
     * @return Closure
534
     */
535 View Code Duplication
    private function unsignedIntImpl($name, $notNullable, $default = null): Closure
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
536
    {
537 1
        return function (Table $table) use ($name, $notNullable, $default) {
538 1
            $column = $table->addColumn($name, Type::INTEGER)->setUnsigned(true)->setNotnull($notNullable);
539 1
            $default === null ?: $column->setDefault($default);
540 1
        };
541
    }
542
543
    /**
544
     * @param string $name
545
     * @param array  $values
546
     * @param bool   $notNullable
547
     *
548
     * @return Closure
549
     *
550
     * @SuppressWarnings(PHPMD.StaticAccess)
551
     */
552
    private function enumImpl($name, array $values, $notNullable): Closure
553
    {
554 1
        return function (Table $table) use ($name, $values, $notNullable) {
555 1
            Type::hasType(EnumType::TYPE_NAME) === true ?: Type::addType(EnumType::TYPE_NAME, EnumType::class);
556 1
            EnumType::setValues($values);
557 1
            $table->addColumn($name, EnumType::TYPE_NAME)->setNotnull($notNullable);
558 1
        };
559
    }
560
561
    /** @noinspection PhpTooManyParametersInspection
562
     * @param string $localKey
563
     * @param string $foreignTable
564
     * @param string $foreignKey
565
     * @param string $type
566
     * @param bool   $notNullable
567
     * @param bool   $cascadeDelete
568
     *
569
     * @return Closure
570
     */
571
    private function foreignColumnImpl(
572
        string $localKey,
573
        string $foreignTable,
574
        string $foreignKey,
575
        string $type,
576
        bool $notNullable,
577
        bool $cascadeDelete
578
    ): Closure {
579 1
        return function (Table $table) use (
580 1
            $localKey,
581 1
            $foreignTable,
582 1
            $foreignKey,
583 1
            $notNullable,
584 1
            $cascadeDelete,
585 1
            $type
586
        ) {
587 1
            $options = $cascadeDelete === true ? ['onDelete' => 'CASCADE'] : [];
588 1
            $table->addColumn($localKey, $type)->setUnsigned(true)->setNotnull($notNullable);
589 1
            $table->addForeignKeyConstraint($foreignTable, [$localKey], [$foreignKey], $options);
590 1
        };
591
    }
592
593
    /**
594
     * @param string $name
595
     * @param bool   $notNullable
596
     * @param bool   $cascadeDelete
597
     *
598
     * @return Closure
599
     */
600
    private function relationshipImpl(string $name, bool $notNullable, bool $cascadeDelete): Closure
601
    {
602 1
        return function (
603
            Table $table,
604
            MigrationContextInterface $context
605
        ) use (
606 1
            $name,
607 1
            $notNullable,
608 1
            $cascadeDelete
609
        ) {
610 1
            $modelClass = $context->getModelClass();
611
612 1
            assert(
613 1
                $this->getModelSchemas()->hasRelationship($modelClass, $name),
614 1
                "Relationship `$name` not found for model `$modelClass`."
615
            );
616 1
            assert(
617 1
                $this->getModelSchemas()->getRelationshipType($modelClass, $name) === RelationshipTypes::BELONGS_TO,
618 1
                "Relationship `$name` for model `$modelClass` must be `belongsTo`."
619
            );
620
621 1
            $localKey   = $this->getModelSchemas()->getForeignKey($modelClass, $name);
622 1
            $columnType = $this->getModelSchemas()->getAttributeType($modelClass, $localKey);
623
624 1
            $otherModelClass = $this->getModelSchemas()->getReverseModelClass($modelClass, $name);
625 1
            $foreignTable    = $this->getModelSchemas()->getTable($otherModelClass);
626 1
            $foreignKey      = $this->getModelSchemas()->getPrimaryKey($otherModelClass);
627
628 1
            $fkClosure = $this->foreignColumnImpl(
629 1
                $localKey,
630 1
                $foreignTable,
631 1
                $foreignKey,
632 1
                $columnType,
633 1
                $notNullable,
634 1
                $cascadeDelete
635
            );
636
637 1
            return $fkClosure($table);
638 1
        };
639
    }
640
}
641