Completed
Branch feature/pre-split (ce4b6b)
by Anton
03:56
created

HasManySchema   A

Complexity

Total Complexity 6

Size/Duplication

Total Lines 124
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 0
loc 124
rs 10
c 0
b 0
f 0
wmc 6
lcom 1
cbo 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
B inverseDefinition() 0 33 2
B declareTables() 0 42 4
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Schemas\Relations;
8
9
use Spiral\ORM\Exceptions\DefinitionException;
10
use Spiral\ORM\Exceptions\RelationSchemaException;
11
use Spiral\ORM\Record;
12
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
13
use Spiral\ORM\Schemas\InversableRelationInterface;
14
use Spiral\ORM\Schemas\Relations\Traits\ForeignsTrait;
15
use Spiral\ORM\Schemas\Relations\Traits\TablesTrait;
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
class HasManySchema extends AbstractSchema implements InversableRelationInterface
33
{
34
    use TablesTrait, TypecastTrait, ForeignsTrait;
35
36
    /**
37
     * Relation type.
38
     */
39
    const RELATION_TYPE = Record::HAS_MANY;
40
41
    /**
42
     * Options needed in runtime.
43
     */
44
    const PACK_OPTIONS = [Record::INNER_KEY, Record::OUTER_KEY, Record::NULLABLE, Record::WHERE];
45
46
    /**
47
     * {@inheritdoc}
48
     */
49
    const OPTIONS_TEMPLATE = [
50
        //Let's use parent record primary key as default inner key
51
        Record::INNER_KEY         => '{source:primaryKey}',
52
53
        //Outer key will be based on parent record role and inner key name
54
        Record::OUTER_KEY         => '{source:role}_{option:innerKey}',
55
56
        //Set constraints (foreign keys) by default
57
        Record::CREATE_CONSTRAINT => true,
58
59
        //@link https://en.wikipedia.org/wiki/Foreign_key
60
        Record::CONSTRAINT_ACTION => 'CASCADE',
61
62
        //We are going to make all relations nullable by default, so we can add fields to existed
63
        //tables without raising an exceptions
64
        Record::NULLABLE          => true,
65
66
        //Relation allowed to create indexes in outer table
67
        Record::CREATE_INDEXES    => true,
68
69
        //HasMany allow us to define default WHERE statement for relation in a simplified array form
70
        Record::WHERE             => [],
71
    ];
72
73
    /**
74
     *{@inheritdoc}
75
     */
76
    public function inverseDefinition(string $inverseTo): RelationDefinition
77
    {
78
        if (empty($this->definition->targetContext())) {
79
            throw new DefinitionException(sprintf(
80
                "Unable to inverse relation '%s.''%s', unspecified relation target",
81
                $this->definition->sourceContext()->getClass(),
82
                $this->definition->getName()
83
            ));
84
        }
85
86
        /**
87
         * We are going to simply replace outer key with inner key and keep the rest of options intact.
88
         */
89
        $inversed = new RelationDefinition(
90
            $inverseTo,
91
            Record::BELONGS_TO,
92
            $this->definition->sourceContext()->getClass(),
93
            [
94
                Record::INNER_KEY         => $this->option(Record::OUTER_KEY),
95
                Record::OUTER_KEY         => $this->option(Record::INNER_KEY),
96
                Record::CREATE_CONSTRAINT => $this->option(Record::CREATE_CONSTRAINT),
97
                Record::CONSTRAINT_ACTION => $this->option(Record::CONSTRAINT_ACTION),
98
                Record::CREATE_INDEXES    => $this->option(Record::CREATE_INDEXES),
99
                Record::NULLABLE          => $this->option(Record::NULLABLE),
100
            ]
101
        );
102
103
        //In back order :)
104
        return $inversed->withContext(
105
            $this->definition->targetContext(),
106
            $this->definition->sourceContext()
107
        );
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function declareTables(SchemaBuilder $builder): array
114
    {
115
        $sourceTable = $this->sourceTable($builder);
116
        $targetTable = $this->targetTable($builder);
117
118
        if (!$sourceTable->hasColumn($this->option(Record::INNER_KEY))) {
119
            throw new RelationSchemaException(sprintf("Inner key '%s'.'%s' (%s) does not exists",
120
                $sourceTable->getName(),
121
                $this->option(Record::INNER_KEY),
122
                $this->definition->getName()
123
            ));
124
        }
125
126
        //Column to be used as outer key
127
        $outerKey = $targetTable->column($this->option(Record::OUTER_KEY));
128
129
        //Column to be used as inner key
130
        $innerKey = $sourceTable->column($this->option(Record::INNER_KEY));
131
132
        //Syncing types
133
        $outerKey->setType($this->resolveType($innerKey));
134
135
        //If nullable
136
        $outerKey->nullable($this->option(Record::NULLABLE));
137
138
        //Do we need indexes?
139
        if ($this->option(Record::CREATE_INDEXES)) {
140
            $targetTable->index([$outerKey->getName()]);
141
        }
142
143
        if ($this->option(Record::CREATE_CONSTRAINT)) {
144
            $this->createForeign(
145
                $targetTable,
146
                $outerKey,
147
                $innerKey,
148
                $this->option(Record::CONSTRAINT_ACTION),
149
                $this->option(Record::CONSTRAINT_ACTION)
150
            );
151
        }
152
153
        return [$targetTable];
154
    }
155
}