Completed
Branch feature/pre-split (24875e)
by Anton
03:28
created

BelongsToSchema   A

Complexity

Total Complexity 8

Size/Duplication

Total Lines 134
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 0
loc 134
rs 10
c 0
b 0
f 0
wmc 8
lcom 1
cbo 9

2 Methods

Rating   Name   Duplication   Size   Complexity  
B inverseDefinition() 0 40 4
B declareTables() 0 43 4
1
<?php
2
/**
3
 * 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)
78
    {
79
        if (!is_array($inverseTo) || count($inverseTo) != 2) {
80
            throw new DefinitionException(
81
                "BelongsToRelation 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
        /**
95
         * We are going to simply replace outer key with inner key and keep the rest of options intact.
96
         */
97
        $inversed = new RelationDefinition(
98
            $inverseTo[1],
99
            $inverseTo[0],
100
            $this->definition->sourceContext()->getClass(),
101
            [
102
                Record::INNER_KEY         => $this->option(Record::OUTER_KEY),
103
                Record::OUTER_KEY         => $this->option(Record::INNER_KEY),
104
                Record::CREATE_CONSTRAINT => $this->option(Record::CREATE_CONSTRAINT),
105
                Record::CONSTRAINT_ACTION => $this->option(Record::CONSTRAINT_ACTION),
106
                Record::CREATE_INDEXES    => $this->option(Record::CREATE_INDEXES),
107
                Record::NULLABLE          => $this->option(Record::NULLABLE),
108
            ]
109
        );
110
111
        //In back order :)
112
        return $inversed->withContext(
113
            $this->definition->targetContext(),
114
            $this->definition->sourceContext()
115
        );
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function declareTables(SchemaBuilder $builder): array
122
    {
123
        $sourceTable = $this->sourceTable($builder);
124
        $targetTable = $this->targetTable($builder);
125
126
        if (!$targetTable->hasColumn($this->option(Record::OUTER_KEY))) {
127
            throw new RelationSchemaException(sprintf("Outer key '%s'.'%s' (%s) does not exists",
128
                $targetTable->getName(),
129
                $this->option(Record::OUTER_KEY),
130
                $this->definition->getName()
131
            ));
132
        }
133
134
        //Column to be used as outer key
135
        $outerKey = $targetTable->column($this->option(Record::OUTER_KEY));
136
137
        //Column to be used as inner key
138
        $innerKey = $sourceTable->column($this->option(Record::INNER_KEY));
139
140
        //Syncing types
141
        $innerKey->setType($this->resolveType($outerKey));
142
143
        //If nullable
144
        $innerKey->nullable($this->option(Record::NULLABLE));
145
146
        //Do we need indexes?
147
        if ($this->option(Record::CREATE_INDEXES)) {
148
            //Always belongs to one parent
149
            $sourceTable->index([$innerKey->getName()]);
150
        }
151
152
        if ($this->isConstrained()) {
153
            $this->createForeign(
154
                $sourceTable,
155
                $innerKey,
156
                $outerKey,
157
                $this->option(Record::CONSTRAINT_ACTION),
158
                $this->option(Record::CONSTRAINT_ACTION)
159
            );
160
        }
161
162
        return [$sourceTable];
163
    }
164
}