Completed
Branch feature/pre-split (669609)
by Anton
03:30
created

ManyToManySchema::declareTables()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 75
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 42
nc 6
nop 1
dl 0
loc 75
rs 8.4736
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Schemas\Relations;
8
9
use Spiral\Database\Schemas\Prototypes\AbstractTable;
10
use Spiral\ORM\Exceptions\RelationSchemaException;
11
use Spiral\ORM\Helpers\ColumnRenderer;
12
use Spiral\ORM\ORMInterface;
13
use Spiral\ORM\Record;
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
 * ManyToMany relation declares that two records related to each other using pivot table data.
21
 * Relation allow to specify inner key (key in parent record), outer key (key in outer record),
22
 * pivot table name, names of pivot columns to store inner and outer key values and set of
23
 * additional columns. Relation allow specifying default WHERE statement for outer records and
24
 * pivot table separately.
25
 *
26
 * Attention, MANY to MANY can only be used inside same database.
27
 *
28
 * Example (User related to many Tag records):
29
 * - relation will create pivot table named "tag_user_map" (if allowed), where table name generated
30
 *   based on roles of inner and outer tables sorted in ABC order (you can change name)
31
 * - relation will create pivot key named "user_id" related to User primary key
32
 * - relation will create pivot key named "tag_id" related to Tag primary key
33
 * - relation will create unique index on "user_id" and "tag_id" columns if allowed
34
 * - relation will create foreign key "tag_user_map"."user_id" => "users"."id" if allowed
35
 * - relation will create foreign key "tag_user_map"."tag_id" => "tags"."id" if allowed
36
 * - relation will create additional columns in pivot table if any requested
37
 */
38
class ManyToManySchema extends AbstractSchema //implements InversableRelationInterface
39
{
40
    use TablesTrait, TypecastTrait, ForeignsTrait;
41
42
    /**
43
     * Relation type.
44
     */
45
    const RELATION_TYPE = Record::MANY_TO_MANY;
46
47
    /**
48
     * Options to be packed.
49
     */
50
    const PACK_OPTIONS = [
51
        Record::PIVOT_TABLE,
52
        Record::OUTER_KEY,
53
        Record::INNER_KEY,
54
        Record::THOUGHT_INNER_KEY,
55
        Record::THOUGHT_OUTER_KEY,
56
        Record::RELATION_COLUMNS,
57
        Record::PIVOT_COLUMNS,
58
        Record::WHERE_PIVOT,
59
        Record::WHERE
60
    ];
61
62
    /**
63
     * Default postfix for pivot tables.
64
     */
65
    const PIVOT_POSTFIX = '_map';
66
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @invisible
71
     */
72
    const OPTIONS_TEMPLATE = [
73
        //Inner key of parent record will be used to fill "THOUGHT_INNER_KEY" in pivot table
74
        Record::INNER_KEY         => '{source:primaryKey}',
75
76
        //We are going to use primary key of outer table to fill "THOUGHT_OUTER_KEY" in pivot table
77
        //This is technically "inner" key of outer record, we will name it "outer key" for simplicity
78
        Record::OUTER_KEY         => '{target:primaryKey}',
79
80
        //Name field where parent record inner key will be stored in pivot table, role + innerKey
81
        //by default
82
        Record::THOUGHT_INNER_KEY => '{source:role}_{option:innerKey}',
83
84
        //Name field where inner key of outer record (outer key) will be stored in pivot table,
85
        //role + outerKey by default
86
        Record::THOUGHT_OUTER_KEY => '{target:role}_{option:outerKey}',
87
88
        //Set constraints in pivot table (foreign keys)
89
        Record::CREATE_CONSTRAINT => true,
90
91
        //@link https://en.wikipedia.org/wiki/Foreign_key
92
        Record::CONSTRAINT_ACTION => 'CASCADE',
93
94
        //Relation allowed to create indexes in pivot table
95
        Record::CREATE_INDEXES    => true,
96
97
        //Name of pivot table to be declared, default value is not stated as it will be generated
98
        //based on roles of inner and outer records
99
        Record::PIVOT_TABLE       => null,
100
101
        //Relation allowed to create pivot table
102
        Record::CREATE_PIVOT      => true,
103
104
        //Additional set of columns to be added into pivot table, you can use same column definition
105
        //type as you using for your records
106
        Record::PIVOT_COLUMNS     => [],
107
108
        //Set of default values to be used for pivot table
109
        Record::PIVOT_DEFAULTS    => [],
110
111
        //WHERE statement in a form of simplified array definition to be applied to pivot table
112
        //data.
113
        Record::WHERE_PIVOT       => [],
114
115
        //WHERE statement to be applied for data in outer data while loading relation data
116
        //can not be inversed. Attention, WHERE conditions not used in has(), link() and sync()
117
        //methods.
118
        Record::WHERE             => [],
119
    ];
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function packRelation(AbstractTable $table): array
125
    {
126
        $packed = parent::packRelation($table);
127
128
        //Let's clarify pivot columns
129
        $schema = $packed[ORMInterface::R_SCHEMA];
130
        $schema[Record::PIVOT_TABLE] = $this->pivotTable();
131
        $schema[Record::PIVOT_COLUMNS] = array_keys($schema[Record::PIVOT_COLUMNS]);
132
133
        //Ensure that inner keys are always presented
134
        $schema[Record::PIVOT_COLUMNS] = array_merge(
135
            [
136
                $this->option(Record::THOUGHT_INNER_KEY),
137
                $this->option(Record::THOUGHT_OUTER_KEY)
138
            ],
139
            $schema[Record::PIVOT_COLUMNS]
140
        );
141
142
        $packed[ORMInterface::R_SCHEMA] = $schema;
143
144
        return $packed;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     *
150
     * Note: pivot table will be build from direction of source, please do not attempt to create
151
     * many to many relations between databases without specifying proper database.
152
     */
153
    public function declareTables(SchemaBuilder $builder): array
154
    {
155
        if (!$this->option(Record::CREATE_PIVOT)) {
156
            //No pivot table creation were requested, noting really to do
157
            return [];
158
        }
159
160
        $sourceTable = $this->sourceTable($builder);
161
        $targetTable = $this->targetTable($builder);
162
163
        $sourceContext = $this->definition->sourceContext();
164
        $targetContext = $this->definition->targetContext();
165
166
        if ($sourceTable->getDriver() != $targetTable->getDriver()) {
167
            throw new RelationSchemaException(
168
                "ManyToMany relations can only exists inside same database"
169
            );
170
        }
171
172
        $pivotTable = $builder->requestTable(
173
            $this->pivotTable(),
174
            $sourceContext->getDatabase(),
175
            true,
176
            true
177
        );
178
179
        /*
180
         * Declare columns in map/pivot table.
181
         */
182
        $innerKey = $pivotTable->column($this->option(Record::THOUGHT_INNER_KEY));
183
        $innerKey->setType($this->resolveType(
184
            $sourceContext->getColumn($this->option(Record::INNER_KEY))
185
        ));
186
187
        $outerKey = $pivotTable->column($this->option(Record::THOUGHT_OUTER_KEY));
188
        $outerKey->setType($this->resolveType(
189
            $targetContext->getColumn($this->option(Record::OUTER_KEY))
190
        ));
191
192
        /*
193
         * Declare user columns in pivot table.
194
         */
195
        $rendered = new ColumnRenderer();
196
        $rendered->renderColumns(
197
            $this->option(Record::PIVOT_COLUMNS),
198
            $this->option(Record::PIVOT_DEFAULTS),
199
            $pivotTable
200
        );
201
202
        //Map might only contain unique link between source and target
203
        if ($this->option(Record::CREATE_INDEXES)) {
204
            $pivotTable->index([$innerKey->getName(), $outerKey->getName()])->unique();
205
        }
206
207
        //There is 2 constrains between map table and source and table
208
        if ($this->isConstrained()) {
209
            $this->createForeign(
210
                $pivotTable,
211
                $innerKey,
212
                $sourceContext->getColumn($this->option(Record::INNER_KEY)),
213
                $this->option(Record::CONSTRAINT_ACTION),
214
                $this->option(Record::CONSTRAINT_ACTION)
215
            );
216
217
            $this->createForeign(
218
                $pivotTable,
219
                $outerKey,
220
                $targetContext->getColumn($this->option(Record::OUTER_KEY)),
221
                $this->option(Record::CONSTRAINT_ACTION),
222
                $this->option(Record::CONSTRAINT_ACTION)
223
            );
224
        }
225
226
        return [$pivotTable];
227
    }
228
229
    /**
230
     * Generate name of pivot table or fetch if from schema.
231
     *
232
     * @return string
233
     */
234
    public function pivotTable(): string
235
    {
236
        if (!empty($this->option(Record::PIVOT_TABLE))) {
237
            return $this->option(Record::PIVOT_TABLE);
238
        }
239
240
        $source = $this->definition->sourceContext();
241
        $target = $this->definition->targetContext();
242
243
        //Generating pivot table name
244
        $names = [$source->getRole(), $target->getRole()];
245
        asort($names);
246
247
        return implode('_', $names) . static::PIVOT_POSTFIX;
248
    }
249
}