HasManySchema::inverseDefinition()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 37
rs 9.328
c 0
b 0
f 0
cc 3
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 simple has many relation. Relations like that used when parent record has many child
21
 * with
22
 * [outer] key linked to value of [inner] key of parent mode. Relation allow specifying default
23
 * WHERE statement. Attention, WHERE statement will not be used in populating newly created record
24
 * fields.
25
 *
26
 * Example, [User has many Comments], user primary key is "id":
27
 * - relation will create outer key "user_id" in "comments" table (or other table name), nullable
28
 *   by default
29
 * - relation will create index on column "user_id" in "comments" table if allowed
30
 * - relation will create foreign key "comments"."user_id" => "users"."id" if allowed
31
 *
32
 * Note: relation can point to morphed records
33
 */
34
class HasManySchema extends AbstractSchema implements InversableRelationInterface
35
{
36
    use TypecastTrait, ForeignsTrait;
37
38
    /**
39
     * Relation type.
40
     */
41
    const RELATION_TYPE = Record::HAS_MANY;
42
43
    /**
44
     * Options needed in runtime.
45
     */
46
    const PACK_OPTIONS = [
47
        Record::INNER_KEY,
48
        Record::OUTER_KEY,
49
        Record::NULLABLE,
50
        Record::WHERE,
51
        Record::RELATION_COLUMNS,
52
        Record::MORPH_KEY,
53
        Record::ORDER_BY
54
    ];
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    const OPTIONS_TEMPLATE = [
60
        //Let's use parent record primary key as default inner key
61
        Record::INNER_KEY         => '{source:primaryKey}',
62
63
        //Outer key will be based on parent record role and inner key name
64
        Record::OUTER_KEY         => '{source:role}_{option:innerKey}',
65
66
        //Set constraints (foreign keys) by default
67
        Record::CREATE_CONSTRAINT => true,
68
69
        //@link https://en.wikipedia.org/wiki/Foreign_key
70
        Record::CONSTRAINT_ACTION => 'CASCADE',
71
72
        //We are going to make all relations nullable by default, so we can add fields to existed
73
        //tables without raising an exceptions
74
        Record::NULLABLE          => true,
75
76
        //Relation allowed to create indexes in outer table
77
        Record::CREATE_INDEXES    => true,
78
79
        //HasMany allow us to define default WHERE statement for relation in a simplified array form
80
        Record::WHERE             => [],
81
82
        //Relation can point to morphed record
83
        Record::MORPH_KEY         => null,
84
85
        //Order
86
        Record::ORDER_BY          => []
87
    ];
88
89
    /**
90
     *{@inheritdoc}
91
     */
92
    public function inverseDefinition(SchemaBuilder $builder, $inverseTo): \Generator
93
    {
94
        if (!is_string($inverseTo)) {
95
            throw new DefinitionException("Inversed relation must be specified as string");
96
        }
97
98
        if (empty($this->definition->targetContext())) {
99
            throw new DefinitionException(sprintf(
100
                "Unable to inverse relation %s.%s, unspecified relation target",
101
                $this->definition->sourceContext()->getClass(),
102
                $this->definition->getName()
103
            ));
104
        }
105
106
        /**
107
         * We are going to simply replace outer key with inner key and keep the rest of options intact.
108
         */
109
        $inversed = new RelationDefinition(
110
            $inverseTo,
111
            Record::BELONGS_TO,
112
            $this->definition->sourceContext()->getClass(),
113
            [
114
                Record::INNER_KEY         => $this->option(Record::OUTER_KEY),
115
                Record::OUTER_KEY         => $this->option(Record::INNER_KEY),
116
                Record::CREATE_CONSTRAINT => $this->option(Record::CREATE_CONSTRAINT),
117
                Record::CONSTRAINT_ACTION => $this->option(Record::CONSTRAINT_ACTION),
118
                Record::CREATE_INDEXES    => $this->option(Record::CREATE_INDEXES),
119
                Record::NULLABLE          => $this->option(Record::NULLABLE),
120
            ]
121
        );
122
123
        //In back order :)
124
        yield $inversed->withContext(
125
            $this->definition->targetContext(),
126
            $this->definition->sourceContext()
127
        );
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function declareTables(SchemaBuilder $builder): array
134
    {
135
        $sourceTable = $this->sourceTable($builder);
136
        $targetTable = $this->targetTable($builder);
137
138
        if (!$sourceTable->hasColumn($this->option(Record::INNER_KEY))) {
139
            throw new RelationSchemaException(sprintf("Inner key '%s'.'%s' (%s) does not exists",
140
                $sourceTable->getName(),
141
                $this->option(Record::INNER_KEY),
142
                $this->definition->getName()
143
            ));
144
        }
145
146
        //Column to be used as outer key
147
        $outerKey = $targetTable->column($this->option(Record::OUTER_KEY));
148
149
        //Column to be used as inner key
150
        $innerKey = $sourceTable->column($this->option(Record::INNER_KEY));
151
152
        //Syncing types
153
        $outerKey->setType($this->resolveType($innerKey));
154
155
        //If nullable
156
        $outerKey->nullable($this->option(Record::NULLABLE));
157
158
        //Do we need indexes?
159
        if ($this->option(Record::CREATE_INDEXES)) {
160
            $targetTable->index([$outerKey->getName()]);
161
        }
162
163
        if ($this->isConstrained()) {
164
            $this->createForeign(
165
                $targetTable,
166
                $outerKey,
167
                $innerKey,
168
                $this->option(Record::CONSTRAINT_ACTION),
169
                $this->option(Record::CONSTRAINT_ACTION)
170
            );
171
        }
172
173
        return [$targetTable];
174
    }
175
}