Completed
Push — master ( e03243...fde75b )
by Neomerx
04:59
created

MigrationTrait::binary()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

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 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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\DBALException;
22
use Doctrine\DBAL\Schema\AbstractSchemaManager;
23
use Doctrine\DBAL\Schema\Table;
24
use Doctrine\DBAL\Types\Type;
25
use Limoncello\Contracts\Data\MigrationInterface;
26
use Limoncello\Contracts\Data\ModelSchemaInfoInterface;
27
use Limoncello\Contracts\Data\RelationshipTypes;
28
use Limoncello\Contracts\Data\TimestampFields;
29
use Limoncello\Data\Contracts\MigrationContextInterface;
30
use Psr\Container\ContainerInterface;
31
32
/**
33
 * @package Limoncello\Data
34
 */
35
trait MigrationTrait
36
{
37
    /**
38
     * @var ContainerInterface
39
     */
40
    private $container;
41
42
    /**
43
     * @var array
44
     */
45
    private $enumerations = [];
46
47
    /**
48
     * @inheritdoc
49
     */
50 6
    public function init(ContainerInterface $container): MigrationInterface
51
    {
52 6
        $this->container = $container;
53
54
        /** @var MigrationInterface $self */
55 6
        $self = $this;
56
57 6
        return $self;
58
    }
59
60
    /**
61
     * @return ContainerInterface
62
     */
63 6
    protected function getContainer(): ContainerInterface
64
    {
65 6
        return $this->container;
66
    }
67
68
    /**
69
     * @return Connection
70
     */
71 6
    protected function getConnection(): Connection
72
    {
73 6
        assert($this->getContainer()->has(Connection::class) === true);
74
75 6
        return $this->getContainer()->get(Connection::class);
76
    }
77
78
    /**
79
     * @return ModelSchemaInfoInterface
80
     */
81 2
    protected function getModelSchemas(): ModelSchemaInfoInterface
82
    {
83 2
        assert($this->getContainer()->has(ModelSchemaInfoInterface::class) === true);
84
85 2
        return $this->getContainer()->get(ModelSchemaInfoInterface::class);
86
    }
87
88
    /**
89
     * @return AbstractSchemaManager
90
     */
91 2
    protected function getSchemaManager(): AbstractSchemaManager
92
    {
93 2
        return $this->getConnection()->getSchemaManager();
94
    }
95
96
    /**
97
     * @param string    $modelClass
98
     * @param Closure[] $expressions
99
     *
100
     * @return Table
101
     *
102
     * @throws DBALException
103
     */
104 2
    protected function createTable(string $modelClass, array $expressions = []): Table
105
    {
106 2
        $context   = new MigrationContext($modelClass, $this->getModelSchemas());
107 2
        $tableName = $this->getModelSchemas()->getTable($modelClass);
108 2
        $table     = new Table($tableName);
109 2
        foreach ($expressions as $expression) {
110
            /** @var Closure $expression */
111 2
            $expression($table, $context);
112
        }
113
114 2
        $this->getSchemaManager()->dropAndCreateTable($table);
115
116 2
        return $table;
117
    }
118
119
    /**
120
     * @param string $modelClass
121
     *
122
     * @return void
123
     */
124 1
    protected function dropTableIfExists(string $modelClass): void
125
    {
126 1
        $tableName     = $this->getModelSchemas()->getTable($modelClass);
127 1
        $schemaManager = $this->getSchemaManager();
128
129 1
        if ($schemaManager->tablesExist([$tableName]) === true) {
130 1
            $schemaManager->dropTable($tableName);
131
        }
132
    }
133
134
    /**
135
     * @param string $name
136
     * @param array  $values
137
     *
138
     * @return void
139
     *
140
     * @throws DBALException
141
     */
142 2
    protected function createEnum(string $name, array $values): void
143
    {
144 2
        assert(empty($name) === false);
145
146
        // check all values are strings
147 2
        assert(
148
            call_user_func(function () use ($values): bool {
149 2
                $allAreStrings = true;
150 2
                foreach ($values as $value) {
151 2
                    $allAreStrings = $allAreStrings && is_string($value);
152
                }
153
154 2
                return $allAreStrings;
155 2
            }) === true,
156 2
            'All enum values should be strings.'
157
        );
158
159 2
        assert(array_key_exists($name, $this->enumerations) === false, "Enum name `$name` has already been used.");
160 2
        $this->enumerations[$name] = $values;
161
162 2
        $connection = $this->getConnection();
163 2
        if ($connection->getDriver()->getName() === 'pdo_pgsql') {
164 1
            $valueList = implode("', '", $values);
165 1
            $sql       = "CREATE TYPE $name AS ENUM ('$valueList');";
166 1
            $connection->exec($sql);
167
        }
168
    }
169
170
    /**
171
     * @param string $name
172
     *
173
     * @return void
174
     *
175
     * @throws DBALException
176
     */
177 1
    protected function dropEnumIfExists(string $name): void
178
    {
179 1
        unset($this->enumerations[$name]);
180
181 1
        $connection = $this->getConnection();
182 1
        if ($connection->getDriver()->getName() === 'pdo_pgsql') {
183 1
            $name = $connection->quoteIdentifier($name);
184 1
            $sql  = "DROP TYPE IF EXISTS $name;";
185 1
            $connection->exec($sql);
186
        }
187
    }
188
189
    /**
190
     * @param string $columnName
191
     * @param string $enumName
192
     * @param bool   $notNullable
193
     *
194
     * @return Closure
195
     *
196
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
197
     * @SuppressWarnings(PHPMD.ElseExpression)
198
     * @SuppressWarnings(PHPMD.StaticAccess)
199
     */
200 2
    protected function useEnum(string $columnName, string $enumName, bool $notNullable = true): Closure
201
    {
202 2
        if ($this->getConnection()->getDriver()->getName() === 'pdo_pgsql') {
203
            return function (Table $table) use ($columnName, $enumName): void {
204 1
                $typeName = RawNameType::TYPE_NAME;
205 1
                Type::hasType($typeName) === true ?: Type::addType($typeName, RawNameType::class);
206
                $table
207 1
                    ->addColumn($columnName, $typeName)
208 1
                    ->setCustomSchemaOption($typeName, $enumName);
209 1
            };
210
        } else {
211 1
            $enumValues = $this->enumerations[$enumName];
212
213
            return function (Table $table) use ($columnName, $enumValues, $notNullable) {
214 1
                Type::hasType(EnumType::TYPE_NAME) === true ?: Type::addType(EnumType::TYPE_NAME, EnumType::class);
215
                $table
216 1
                    ->addColumn($columnName, EnumType::TYPE_NAME)
217 1
                    ->setCustomSchemaOption(EnumType::TYPE_NAME, $enumValues)
218 1
                    ->setNotnull($notNullable);
219 1
            };
220
        }
221
    }
222
223
    /**
224
     * @param string $name
225
     *
226
     * @return Closure
227
     */
228 2
    protected function primaryInt(string $name): Closure
229
    {
230
        return function (Table $table) use ($name) {
231 2
            $table->addColumn($name, Type::INTEGER)->setAutoincrement(true)->setUnsigned(true)->setNotnull(true);
232 2
            $table->setPrimaryKey([$name]);
233 2
        };
234
    }
235
236
    /**
237
     * @param string $name
238
     *
239
     * @return Closure
240
     */
241 1
    protected function primaryString(string $name): Closure
242
    {
243
        return function (Table $table, MigrationContextInterface $context) use ($name) {
244 1
            $length = $context->getModelSchemas()->getAttributeLength($context->getModelClass(), $name);
245 1
            $table->addColumn($name, Type::STRING)->setLength($length)->setNotnull(true);
246 1
            $table->setPrimaryKey([$name]);
247 1
        };
248
    }
249
250
    /**
251
     * @param string   $name
252
     * @param null|int $default
253
     *
254
     * @return Closure
255
     */
256 1
    protected function int(string $name, int $default = null): Closure
257
    {
258
        return function (Table $table) use ($name, $default) {
259 1
            $column = $table->addColumn($name, Type::INTEGER)->setUnsigned(false)->setNotnull(true);
260 1
            $default === null ?: $column->setDefault($default);
261 1
        };
262
    }
263
264
    /**
265
     * @param string   $name
266
     * @param null|int $default
267
     *
268
     * @return Closure
269
     */
270 1
    protected function nullableInt(string $name, int $default = null): Closure
271
    {
272
        return function (Table $table) use ($name, $default) {
273 1
            $table->addColumn($name, Type::INTEGER)->setUnsigned(false)->setNotnull(false)->setDefault($default);
274 1
        };
275
    }
276
277
    /**
278
     * @param string   $name
279
     * @param null|int $default
280
     *
281
     * @return Closure
282
     */
283 1
    protected function unsignedInt(string $name, int $default = null): Closure
284
    {
285 1
        return $this->unsignedIntImpl($name, true, $default);
286
    }
287
288
    /**
289
     * @param string   $name
290
     * @param null|int $default
291
     *
292
     * @return Closure
293
     */
294 1
    protected function nullableUnsignedInt(string $name, int $default = null): Closure
295
    {
296 1
        return $this->unsignedIntImpl($name, false, $default);
297
    }
298
299
    /**
300
     * @param string $name
301
     *
302
     * @return Closure
303
     */
304 1
    protected function float(string $name): Closure
305
    {
306
        // precision and scale both seems to be ignored in Doctrine so not much sense to have them as inputs
307
308
        return function (Table $table) use ($name) {
309 1
            $table->addColumn($name, Type::FLOAT)->setNotnull(true);
310 1
        };
311
    }
312
313
    /**
314
     * @param string $name
315
     *
316
     * @return Closure
317
     */
318 1
    protected function string(string $name): Closure
319
    {
320
        return function (Table $table, MigrationContextInterface $context) use ($name) {
321 1
            $length = $context->getModelSchemas()->getAttributeLength($context->getModelClass(), $name);
322 1
            $table->addColumn($name, Type::STRING)->setLength($length)->setNotnull(true);
323 1
        };
324
    }
325
326
    /**
327
     * @param string $name
328
     *
329
     * @return Closure
330
     */
331 1
    protected function nullableString(string $name): Closure
332
    {
333
        return function (Table $table, MigrationContextInterface $context) use ($name) {
334 1
            $length = $context->getModelSchemas()->getAttributeLength($context->getModelClass(), $name);
335 1
            $table->addColumn($name, Type::STRING)->setLength($length)->setNotnull(false);
336 1
        };
337
    }
338
339
    /**
340
     * @param string $name
341
     *
342
     * @return Closure
343
     */
344 1
    protected function text(string $name): Closure
345
    {
346
        return function (Table $table) use ($name) {
347 1
            $table->addColumn($name, Type::TEXT)->setNotnull(true);
348 1
        };
349
    }
350
351
    /**
352
     * @param string $name
353
     *
354
     * @return Closure
355
     */
356 1
    protected function nullableText(string $name): Closure
357
    {
358
        return function (Table $table) use ($name) {
359 1
            $table->addColumn($name, Type::TEXT)->setNotnull(false);
360 1
        };
361
    }
362
363
    /**
364
     * @param string    $name
365
     * @param null|bool $default
366
     *
367
     * @return Closure
368
     */
369 1
    protected function bool(string $name, $default = null): Closure
370
    {
371
        return function (Table $table) use ($name, $default) {
372 1
            $column = $table->addColumn($name, Type::BOOLEAN)->setNotnull(true);
373 1
            if ($default !== null && is_bool($default) === true) {
374 1
                $column->setDefault($default);
375
            }
376 1
        };
377
    }
378
379
    /**
380
     * @param string $name
381
     *
382
     * @return Closure
383
     */
384 1
    protected function binary(string $name): Closure
385
    {
386
        return function (Table $table) use ($name) {
387 1
            $table->addColumn($name, Type::BINARY)->setNotnull(true);
388 1
        };
389
    }
390
391
    /**
392
     * @param string $name
393
     * @param array  $values
394
     *
395
     * @return Closure
396
     *
397
     * @throws DBALException
398
     */
399 1
    protected function enum(string $name, array $values): Closure
400
    {
401 1
        $this->createEnum($name, $values);
402
403 1
        return $this->useEnum($name, $name, true);
404
    }
405
406
    /**
407
     * @param string $name
408
     * @param array  $values
409
     *
410
     * @return Closure
411
     *
412
     * @throws DBALException
413
     */
414 1
    protected function nullableEnum(string $name, array $values): Closure
415
    {
416 1
        $this->createEnum($name, $values);
417
418 1
        return $this->useEnum($name, $name, false);
419
    }
420
421
    /**
422
     * @return Closure
423
     */
424 1
    protected function timestamps(): Closure
425
    {
426
        return function (Table $table, MigrationContextInterface $context) {
427 1
            $modelClass = $context->getModelClass();
428
429 1
            $createdAt = TimestampFields::FIELD_CREATED_AT;
430 1
            $updatedAt = TimestampFields::FIELD_UPDATED_AT;
431 1
            $deletedAt = TimestampFields::FIELD_DELETED_AT;
432
433
            // a list of data columns and `nullable` flag
434 1
            $datesToAdd = [];
435 1
            if ($this->getModelSchemas()->hasAttributeType($modelClass, $createdAt) === true) {
436 1
                $datesToAdd[$createdAt] = true;
437
            }
438 1
            if ($this->getModelSchemas()->hasAttributeType($modelClass, $updatedAt) === true) {
439 1
                $datesToAdd[$updatedAt] = false;
440
            }
441 1
            if ($this->getModelSchemas()->hasAttributeType($modelClass, $deletedAt) === true) {
442 1
                $datesToAdd[$deletedAt] = false;
443
            }
444
445 1
            foreach ($datesToAdd as $column => $isNullable) {
446 1
                $table->addColumn($column, Type::DATETIME)->setNotnull($isNullable);
447
            }
448 1
        };
449
    }
450
451
    /**
452
     * @param string $name
453
     *
454
     * @return Closure
455
     */
456 1
    protected function datetime(string $name): Closure
457
    {
458
        return function (Table $table) use ($name) {
459 1
            $table->addColumn($name, Type::DATETIME)->setNotnull(true);
460 1
        };
461
    }
462
463
    /**
464
     * @param string $name
465
     *
466
     * @return Closure
467
     */
468 1
    protected function nullableDatetime(string $name): Closure
469
    {
470
        return function (Table $table) use ($name) {
471 1
            $table->addColumn($name, Type::DATETIME)->setNotnull(false);
472 1
        };
473
    }
474
475
    /**
476
     * @param string $name
477
     *
478
     * @return Closure
479
     */
480 1
    protected function date(string $name): Closure
481
    {
482
        return function (Table $table) use ($name) {
483 1
            $table->addColumn($name, Type::DATE)->setNotnull(true);
484 1
        };
485
    }
486
487
    /**
488
     * @param string $name
489
     *
490
     * @return Closure
491
     */
492 1
    protected function nullableDate(string $name): Closure
493
    {
494
        return function (Table $table) use ($name) {
495 1
            $table->addColumn($name, Type::DATE)->setNotnull(false);
496 1
        };
497
    }
498
499
    /**
500
     * @param string[] $names
501
     *
502
     * @return Closure
503
     */
504 1
    protected function unique(array $names): Closure
505
    {
506
        return function (Table $table) use ($names) {
507 1
            $table->addUniqueIndex($names);
508 1
        };
509
    }
510
511
    /**
512
     * @param string[] $names
513
     *
514
     * @return Closure
515
     */
516 1
    protected function searchable(array $names): Closure
517
    {
518
        return function (Table $table) use ($names) {
519 1
            $table->addIndex($names, null, ['fulltext']);
520 1
        };
521
    }
522
523
    /**
524
     * @param string $column
525
     * @param string $referredClass
526
     * @param bool   $cascadeDelete
527
     *
528
     * @return Closure
529
     *
530
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
531
     */
532 1
    protected function foreignRelationship(
533
        string $column,
534
        string $referredClass,
535
        bool $cascadeDelete = false
536
    ): Closure {
537
        return function (
538
            Table $table,
539
            MigrationContextInterface $context
540
        ) use (
541 1
            $column,
542 1
            $referredClass,
543 1
            $cascadeDelete
544
        ) {
545 1
            $tableName    = $this->getTableNameForClass($referredClass);
546 1
            $pkName       = $this->getModelSchemas()->getPrimaryKey($referredClass);
547 1
            $columnType   = $this->getModelSchemas()->getAttributeType($context->getModelClass(), $column);
548 1
            $columnLength = $columnType === Type::STRING ?
549 1
                $this->getModelSchemas()->getAttributeLength($context->getModelClass(), $column) : null;
550
551 1
            $closure = $this->foreignColumn($column, $tableName, $pkName, $columnType, $columnLength, $cascadeDelete);
552
553 1
            return $closure($table, $context);
554 1
        };
555
    }
556
557
    /**
558
     * @param string $column
559
     * @param string $referredClass
560
     * @param bool   $cascadeDelete
561
     *
562
     * @return Closure
563
     *
564
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
565
     */
566 1
    protected function nullableForeignRelationship(
567
        string $column,
568
        string $referredClass,
569
        bool $cascadeDelete = false
570
    ): Closure {
571
        return function (
572
            Table $table,
573
            MigrationContextInterface $context
574
        ) use (
575 1
            $column,
576 1
            $referredClass,
577 1
            $cascadeDelete
578
        ) {
579 1
            $tableName    = $this->getTableNameForClass($referredClass);
580 1
            $pkName       = $this->getModelSchemas()->getPrimaryKey($referredClass);
581 1
            $columnType   = $this->getModelSchemas()->getAttributeType($context->getModelClass(), $column);
582 1
            $columnLength = $columnType === Type::STRING ?
583 1
                $this->getModelSchemas()->getAttributeLength($context->getModelClass(), $column) : null;
584
585
            $closure = $this
586 1
                ->nullableForeignColumn($column, $tableName, $pkName, $columnType, $columnLength, $cascadeDelete);
587
588 1
            return $closure($table, $context);
589 1
        };
590
    }
591
592
    /** @noinspection PhpTooManyParametersInspection
593
     * @param string   $localKey
594
     * @param string   $foreignTable
595
     * @param string   $foreignKey
596
     * @param string   $type
597
     * @param int|null $length
598
     * @param bool     $cascadeDelete
599
     *
600
     * @return Closure
601
     *
602
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
603
     */
604 1
    protected function foreignColumn(
605
        string $localKey,
606
        string $foreignTable,
607
        string $foreignKey,
608
        string $type,
609
        ?int $length = null,
610
        bool $cascadeDelete = false
611
    ): Closure {
612 1
        return $this->foreignColumnImpl($localKey, $foreignTable, $foreignKey, $type, $length, true, $cascadeDelete);
613
    }
614
615
    /** @noinspection PhpTooManyParametersInspection
616
     * @param string   $localKey
617
     * @param string   $foreignTable
618
     * @param string   $foreignKey
619
     * @param string   $type
620
     * @param int|null $length
621
     * @param bool     $cascadeDelete
622
     *
623
     * @return Closure
624
     *
625
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
626
     */
627 1
    protected function nullableForeignColumn(
628
        string $localKey,
629
        string $foreignTable,
630
        string $foreignKey,
631
        string $type,
632
        ?int $length = null,
633
        bool $cascadeDelete = false
634
    ): Closure {
635 1
        return $this->foreignColumnImpl($localKey, $foreignTable, $foreignKey, $type, $length, false, $cascadeDelete);
636
    }
637
638
    /**
639
     * @param string $name
640
     * @param bool   $cascadeDelete
641
     *
642
     * @return Closure
643
     *
644
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
645
     */
646 1
    protected function nullableRelationship(string $name, bool $cascadeDelete = false): Closure
647
    {
648 1
        return $this->relationshipImpl($name, false, $cascadeDelete);
649
    }
650
651
    /**
652
     * @param string $name
653
     * @param bool   $cascadeDelete
654
     *
655
     * @return Closure
656
     *
657
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
658
     */
659 1
    protected function relationship(string $name, bool $cascadeDelete = false): Closure
660
    {
661 1
        return $this->relationshipImpl($name, true, $cascadeDelete);
662
    }
663
664
    /**
665
     * @param string $modelClass
666
     *
667
     * @return string
668
     */
669 1
    protected function getTableNameForClass(string $modelClass): string
670
    {
671 1
        assert(
672 1
            $this->getModelSchemas()->hasClass($modelClass),
673 1
            "Table name is not specified for model '$modelClass'."
674
        );
675
676 1
        $tableName = $this->getModelSchemas()->getTable($modelClass);
677
678 1
        return $tableName;
679
    }
680
681
    /**
682
     * @param string $name
683
     * @param mixed  $value
684
     *
685
     * @return Closure
686
     */
687 1
    protected function defaultValue(string $name, $value): Closure
688
    {
689
        return function (Table $table) use ($name, $value) {
690 1
            assert($table->hasColumn($name));
691 1
            $table->getColumn($name)->setDefault($value);
692 1
        };
693
    }
694
695
    /**
696
     * @param string $name
697
     *
698
     * @return Closure
699
     */
700 1
    protected function nullableValue(string $name): Closure
701
    {
702
        return function (Table $table) use ($name) {
703 1
            assert($table->hasColumn($name));
704 1
            $table->getColumn($name)->setNotnull(false);
705 1
        };
706
    }
707
708
    /**
709
     * @param string $name
710
     *
711
     * @return Closure
712
     */
713 1
    protected function notNullableValue(string $name): Closure
714
    {
715
        return function (Table $table) use ($name) {
716 1
            assert($table->hasColumn($name));
717 1
            $table->getColumn($name)->setNotnull(true);
718 1
        };
719
    }
720
721
    /**
722
     * @param string     $name
723
     * @param bool       $notNullable
724
     * @param null|mixed $default
725
     *
726
     * @return Closure
727
     */
728 1
    private function unsignedIntImpl(string $name, bool $notNullable, $default = null): Closure
729
    {
730
        return function (Table $table) use ($name, $notNullable, $default) {
731 1
            $column = $table->addColumn($name, Type::INTEGER)->setUnsigned(true)->setNotnull($notNullable);
732 1
            $default === null ?: $column->setDefault($default);
733 1
        };
734
    }
735
736
    /** @noinspection PhpTooManyParametersInspection
737
     * @param string   $localKey
738
     * @param string   $foreignTable
739
     * @param string   $foreignKey
740
     * @param string   $type
741
     * @param int|null $length
742
     * @param bool     $notNullable
743
     * @param bool     $cascadeDelete
744
     *
745
     * @return Closure
746
     */
747 1
    private function foreignColumnImpl(
748
        string $localKey,
749
        string $foreignTable,
750
        string $foreignKey,
751
        string $type,
752
        ?int $length,
753
        bool $notNullable,
754
        bool $cascadeDelete
755
    ): Closure {
756
        return function (Table $table) use (
757 1
            $localKey,
758 1
            $foreignTable,
759 1
            $foreignKey,
760 1
            $notNullable,
761 1
            $cascadeDelete,
762 1
            $type,
763 1
            $length
764
        ) {
765 1
            $options = $cascadeDelete === true ? ['onDelete' => 'CASCADE'] : [];
766 1
            $column  = $table->addColumn($localKey, $type)->setNotnull($notNullable);
767 1
            $length === null ? $column->setUnsigned(true) : $column->setLength($length);
768 1
            $table->addForeignKeyConstraint($foreignTable, [$localKey], [$foreignKey], $options);
769 1
        };
770
    }
771
772
    /**
773
     * @param string $name
774
     * @param bool   $notNullable
775
     * @param bool   $cascadeDelete
776
     *
777
     * @return Closure
778
     */
779 1
    private function relationshipImpl(string $name, bool $notNullable, bool $cascadeDelete): Closure
780
    {
781
        return function (
782
            Table $table,
783
            MigrationContextInterface $context
784
        ) use (
785 1
            $name,
786 1
            $notNullable,
787 1
            $cascadeDelete
788
        ) {
789 1
            $modelClass = $context->getModelClass();
790
791 1
            assert(
792 1
                $this->getModelSchemas()->hasRelationship($modelClass, $name),
793 1
                "Relationship `$name` not found for model `$modelClass`."
794
            );
795 1
            assert(
796 1
                $this->getModelSchemas()->getRelationshipType($modelClass, $name) === RelationshipTypes::BELONGS_TO,
797 1
                "Relationship `$name` for model `$modelClass` must be `belongsTo`."
798
            );
799
800 1
            $localKey     = $this->getModelSchemas()->getForeignKey($modelClass, $name);
801 1
            $columnType   = $this->getModelSchemas()->getAttributeType($modelClass, $localKey);
802 1
            $columnLength = $columnType === Type::STRING ?
803 1
                $this->getModelSchemas()->getAttributeLength($modelClass, $localKey) : null;
804
805 1
            $otherModelClass = $this->getModelSchemas()->getReverseModelClass($modelClass, $name);
806 1
            $foreignTable    = $this->getModelSchemas()->getTable($otherModelClass);
807 1
            $foreignKey      = $this->getModelSchemas()->getPrimaryKey($otherModelClass);
808
809 1
            $fkClosure = $this->foreignColumnImpl(
810 1
                $localKey,
811 1
                $foreignTable,
812 1
                $foreignKey,
813 1
                $columnType,
814 1
                $columnLength,
815 1
                $notNullable,
816 1
                $cascadeDelete
817
            );
818
819 1
            return $fkClosure($table);
820 1
        };
821
    }
822
}
823