BelongsToSchema::inverseDefinition()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 39
rs 9.296
c 0
b 0
f 0
cc 4
nc 3
nop 2
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Schemas\Relations;
9
10
use Spiral\ORM\Exceptions\DefinitionException;
11
use Spiral\ORM\Exceptions\RelationSchemaException;
12
use Spiral\ORM\Record;
13
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
14
use Spiral\ORM\Schemas\InversableRelationInterface;
15
use Spiral\ORM\Schemas\Relations\Traits\ForeignsTrait;
16
use Spiral\ORM\Schemas\Relations\Traits\TypecastTrait;
17
use Spiral\ORM\Schemas\SchemaBuilder;
18
19
/**
20
 * Declares that parent record belongs to some parent based on value in [inner] key. Basically this
21
 * relation is mirror copy of HasOne/HasMany relation.
22
 *
23
 * BelongsTo relations can not be inversed!
24
 *
25
 * Example, [Post has one User, relation name "author"], user primary key is "id":
26
 * - relation will create inner key "author_id" in "posts" table (or other table name), nullable by
27
 *   default
28
 * - relation will create index on column "author_id" in "posts" table if allowed
29
 * - relation will create foreign key "posts"."author_id" => "users"."id" if allowed
30
 */
31
class BelongsToSchema extends AbstractSchema implements InversableRelationInterface
32
{
33
    use TypecastTrait, ForeignsTrait;
34
35
    /**
36
     * Relation type.
37
     */
38
    const RELATION_TYPE = Record::BELONGS_TO;
39
40
    /**
41
     * Options needed in runtime.
42
     */
43
    const PACK_OPTIONS = [
44
        Record::INNER_KEY,
45
        Record::OUTER_KEY,
46
        Record::NULLABLE,
47
        Record::RELATION_COLUMNS
48
    ];
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    const OPTIONS_TEMPLATE = [
54
        //Outer key is primary key of related record by default
55
        Record::OUTER_KEY         => '{target:primaryKey}',
56
57
        //Inner key will be based on singular name of relation and outer key name
58
        Record::INNER_KEY         => '{relation:singular}_{option:outerKey}',
59
60
        //Set constraints (foreign keys) by default
61
        Record::CREATE_CONSTRAINT => true,
62
63
        //@link https://en.wikipedia.org/wiki/Foreign_key
64
        Record::CONSTRAINT_ACTION => 'CASCADE',
65
66
        //Relation allowed to create indexes in inner table
67
        Record::CREATE_INDEXES    => true,
68
69
        //We are going to make all relations nullable by default, so we can add fields to existed
70
        //tables without raising an exceptions
71
        Record::NULLABLE          => true,
72
    ];
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function inverseDefinition(SchemaBuilder $builder, $inverseTo): \Generator
78
    {
79
        if (!is_array($inverseTo) || count($inverseTo) != 2) {
80
            throw new DefinitionException(
81
                "BelongsTo relation inverse must be defined as [type, outer relation name]"
82
            );
83
        }
84
85
        if (empty($this->definition->targetContext())) {
86
            throw new DefinitionException(sprintf(
87
                "Unable to inverse relation '%s.''%s', unspecified relation target",
88
                $this->definition->sourceContext()->getClass(),
89
                $this->definition->getName()
90
            ));
91
        }
92
93
        /**
94
         * We are going to simply replace outer key with inner key and keep the rest of options intact.
95
         */
96
        $inversed = new RelationDefinition(
97
            $inverseTo[1],
98
            $inverseTo[0],
99
            $this->definition->sourceContext()->getClass(),
100
            [
101
                Record::INNER_KEY         => $this->option(Record::OUTER_KEY),
102
                Record::OUTER_KEY         => $this->option(Record::INNER_KEY),
103
                Record::CREATE_CONSTRAINT => $this->option(Record::CREATE_CONSTRAINT),
104
                Record::CONSTRAINT_ACTION => $this->option(Record::CONSTRAINT_ACTION),
105
                Record::CREATE_INDEXES    => $this->option(Record::CREATE_INDEXES),
106
                Record::NULLABLE          => $this->option(Record::NULLABLE),
107
            ]
108
        );
109
110
        //In back order :)
111
        yield $inversed->withContext(
112
            $this->definition->targetContext(),
113
            $this->definition->sourceContext()
114
        );
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function declareTables(SchemaBuilder $builder): array
121
    {
122
        $sourceTable = $this->sourceTable($builder);
123
        $targetTable = $this->targetTable($builder);
124
125
        if (!$targetTable->hasColumn($this->option(Record::OUTER_KEY))) {
126
            throw new RelationSchemaException(sprintf("Outer key '%s'.'%s' (%s) does not exists",
127
                $targetTable->getName(),
128
                $this->option(Record::OUTER_KEY),
129
                $this->definition->getName()
130
            ));
131
        }
132
133
        //Column to be used as outer key
134
        $outerKey = $targetTable->column($this->option(Record::OUTER_KEY));
135
136
        //Column to be used as inner key
137
        $innerKey = $sourceTable->column($this->option(Record::INNER_KEY));
138
139
        //Syncing types
140
        $innerKey->setType($this->resolveType($outerKey));
141
142
        //If nullable
143
        $innerKey->nullable($this->option(Record::NULLABLE));
144
145
        //Do we need indexes?
146
        if ($this->option(Record::CREATE_INDEXES)) {
147
            //Always belongs to one parent
148
            $sourceTable->index([$innerKey->getName()]);
149
        }
150
151
        if ($this->isConstrained()) {
152
            $this->createForeign(
153
                $sourceTable,
154
                $innerKey,
155
                $outerKey,
156
                $this->option(Record::CONSTRAINT_ACTION),
157
                $this->option(Record::CONSTRAINT_ACTION)
158
            );
159
        }
160
161
        return [$sourceTable];
162
    }
163
}