1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral, Core Components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Spiral\ORM\Schemas\Relations; |
9
|
|
|
|
10
|
|
|
use Doctrine\Common\Inflector\Inflector; |
11
|
|
|
use Spiral\ORM\Exceptions\DefinitionException; |
12
|
|
|
use Spiral\ORM\Exceptions\RelationSchemaException; |
13
|
|
|
use Spiral\ORM\Helpers\ColumnRenderer; |
14
|
|
|
use Spiral\ORM\ORMInterface; |
15
|
|
|
use Spiral\ORM\Record; |
16
|
|
|
use Spiral\ORM\Schemas\Definitions\RelationContext; |
17
|
|
|
use Spiral\ORM\Schemas\Definitions\RelationDefinition; |
18
|
|
|
use Spiral\ORM\Schemas\InversableRelationInterface; |
19
|
|
|
use Spiral\ORM\Schemas\Relations\Traits\ForeignsTrait; |
20
|
|
|
use Spiral\ORM\Schemas\Relations\Traits\MorphedTrait; |
21
|
|
|
use Spiral\ORM\Schemas\Relations\Traits\TypecastTrait; |
22
|
|
|
use Spiral\ORM\Schemas\SchemaBuilder; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* ManyToMorphed relation declares relation between parent record and set of outer records joined by |
26
|
|
|
* common interface. Relation allow to specify inner key (key in parent record), outer key (key in |
27
|
|
|
* outer records), morph key, pivot table name, names of pivot columns to store inner and outer key |
28
|
|
|
* values and set of additional columns. Relation DOES NOT to specify WHERE statement for outer |
29
|
|
|
* records. However you can specify where conditions for PIVOT table. |
30
|
|
|
* |
31
|
|
|
* You can declare this relation using same syntax as for ManyToMany except your target class |
32
|
|
|
* must be an interface. |
33
|
|
|
* |
34
|
|
|
* Attention, be very careful using morphing relations, you must know what you doing! |
35
|
|
|
* Attention #2, relation like that can not be preloaded! |
36
|
|
|
* |
37
|
|
|
* Example [Tag related to many TaggableInterface], relation name "tagged", relation requested to be |
38
|
|
|
* inversed using name "tags": |
39
|
|
|
* - relation will walk should every record implementing TaggableInterface to collect name and |
40
|
|
|
* type of outer keys, if outer key is not consistent across records implementing this interface |
41
|
|
|
* an exception will be raised, let's say that outer key is "id" in every record |
42
|
|
|
* - relation will create pivot table named "tagged_map" (if allowed), where table name generated |
43
|
|
|
* based on relation name (you can change name) |
44
|
|
|
* - relation will create pivot key named "tag_ud" related to Tag primary key |
45
|
|
|
* - relation will create pivot key named "tagged_id" related to primary key of outer records, |
46
|
|
|
* singular relation name used to generate key like that |
47
|
|
|
* - relation will create pivot key named "tagged_type" to store role of outer record |
48
|
|
|
* - relation will create unique index on "tag_id", "tagged_id" and "tagged_type" columns if allowed |
49
|
|
|
* - relation will create additional columns in pivot table if any requested |
50
|
|
|
* |
51
|
|
|
* Using in records: |
52
|
|
|
* You can use inversed relation as usual ManyToMany, however in Tag record relation access will be |
53
|
|
|
* little bit more complex - every linked record will create inner ManyToMany relation: |
54
|
|
|
* $tag->tagged->users->count(); //Where "users" is plural form of one outer records |
55
|
|
|
* |
56
|
|
|
* You can defined your own inner relation names by using MORPHED_ALIASES option when defining |
57
|
|
|
* relation. |
58
|
|
|
* |
59
|
|
|
* Attention, relation do not support WHERE statement on outer records. |
60
|
|
|
* |
61
|
|
|
* @see BelongsToMorhedSchema |
62
|
|
|
* @see ManyToManySchema |
63
|
|
|
*/ |
64
|
|
|
class ManyToMorphedSchema extends AbstractSchema implements InversableRelationInterface |
65
|
|
|
{ |
66
|
|
|
use TypecastTrait, ForeignsTrait, MorphedTrait; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Relation type. |
70
|
|
|
*/ |
71
|
|
|
const RELATION_TYPE = Record::MANY_TO_MORPHED; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Size of string column dedicated to store outer role name. Used in polymorphic relations. |
75
|
|
|
* Even simple relations might include morph key (usually such relations created via inversion |
76
|
|
|
* of polymorphic relation). |
77
|
|
|
* |
78
|
|
|
* @see RecordSchema::getRole() |
79
|
|
|
*/ |
80
|
|
|
const MORPH_COLUMN_SIZE = 32; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Options to be packed. |
84
|
|
|
*/ |
85
|
|
|
const PACK_OPTIONS = [ |
86
|
|
|
Record::PIVOT_TABLE, |
87
|
|
|
Record::OUTER_KEY, |
88
|
|
|
Record::INNER_KEY, |
89
|
|
|
Record::THOUGHT_INNER_KEY, |
90
|
|
|
Record::THOUGHT_OUTER_KEY, |
91
|
|
|
Record::RELATION_COLUMNS, |
92
|
|
|
Record::PIVOT_COLUMNS, |
93
|
|
|
Record::WHERE_PIVOT, |
94
|
|
|
Record::MORPH_KEY, |
95
|
|
|
Record::ORDER_BY |
96
|
|
|
]; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Default postfix for pivot tables. |
100
|
|
|
*/ |
101
|
|
|
const PIVOT_POSTFIX = '_map'; |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* {@inheritdoc} |
105
|
|
|
* |
106
|
|
|
* @invisible |
107
|
|
|
*/ |
108
|
|
|
const OPTIONS_TEMPLATE = [ |
109
|
|
|
//Inner key of parent record will be used to fill "THOUGHT_INNER_KEY" in pivot table |
110
|
|
|
Record::INNER_KEY => '{source:primaryKey}', |
111
|
|
|
|
112
|
|
|
//We are going to use primary key of outer table to fill "THOUGHT_OUTER_KEY" in pivot table |
113
|
|
|
//This is technically "inner" key of outer record, we will name it "outer key" for simplicity |
114
|
|
|
Record::OUTER_KEY => ORMInterface::R_PRIMARY_KEY, |
115
|
|
|
|
116
|
|
|
//Name field where parent record inner key will be stored in pivot table, role + innerKey |
117
|
|
|
//by default |
118
|
|
|
Record::THOUGHT_INNER_KEY => '{source:role}_{option:innerKey}', |
119
|
|
|
|
120
|
|
|
//Name field where inner key of outer record (outer key) will be stored in pivot table, |
121
|
|
|
//role + outerKey by default |
122
|
|
|
Record::THOUGHT_OUTER_KEY => '{relation:name}_id', |
123
|
|
|
|
124
|
|
|
//Declares what specific record pivot record linking to |
125
|
|
|
Record::MORPH_KEY => '{relation:name}_type', |
126
|
|
|
|
127
|
|
|
//Set constraints (foreign keys) by default, attention only set for source table |
128
|
|
|
Record::CREATE_CONSTRAINT => true, |
129
|
|
|
|
130
|
|
|
//@link https://en.wikipedia.org/wiki/Foreign_key |
131
|
|
|
Record::CONSTRAINT_ACTION => 'CASCADE', |
132
|
|
|
|
133
|
|
|
//Relation allowed to create indexes in pivot table |
134
|
|
|
Record::CREATE_INDEXES => true, |
135
|
|
|
|
136
|
|
|
//Name of pivot table to be declared, default value is not stated as it will be generated |
137
|
|
|
//based on roles of inner and outer records |
138
|
|
|
Record::PIVOT_TABLE => '{relation:name}_map', |
139
|
|
|
|
140
|
|
|
//Relation allowed to create pivot table |
141
|
|
|
Record::CREATE_PIVOT => true, |
142
|
|
|
|
143
|
|
|
//Additional set of columns to be added into pivot table, you can use same column definition |
144
|
|
|
//type as you using for your records |
145
|
|
|
Record::PIVOT_COLUMNS => [], |
146
|
|
|
|
147
|
|
|
//Set of default values to be used for pivot table |
148
|
|
|
Record::PIVOT_DEFAULTS => [], |
149
|
|
|
|
150
|
|
|
//WHERE statement in a form of simplified array definition to be applied to pivot table |
151
|
|
|
//data. |
152
|
|
|
Record::WHERE_PIVOT => [], |
153
|
|
|
|
154
|
|
|
//Order |
155
|
|
|
Record::ORDER_BY => [] |
156
|
|
|
]; |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* {@inheritdoc} |
160
|
|
|
*/ |
161
|
|
|
public function inverseDefinition(SchemaBuilder $builder, $inverseTo): \Generator |
162
|
|
|
{ |
163
|
|
|
if (!is_string($inverseTo)) { |
164
|
|
|
throw new DefinitionException("Inversed relation must be specified as string"); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
foreach ($this->findTargets($builder) as $schema) { |
168
|
|
|
/** |
169
|
|
|
* We are going to simply replace outer key with inner key and keep the rest of options intact. |
170
|
|
|
*/ |
171
|
|
|
$inversed = new RelationDefinition( |
172
|
|
|
$inverseTo, |
173
|
|
|
Record::MANY_TO_MANY, |
174
|
|
|
$this->definition->sourceContext()->getClass(), |
175
|
|
|
[ |
176
|
|
|
Record::PIVOT_TABLE => $this->option(Record::PIVOT_TABLE), |
177
|
|
|
Record::OUTER_KEY => $this->option(Record::INNER_KEY), |
178
|
|
|
Record::INNER_KEY => $this->findOuter($builder)->getName(), |
179
|
|
|
Record::THOUGHT_INNER_KEY => $this->option(Record::THOUGHT_OUTER_KEY), |
180
|
|
|
Record::THOUGHT_OUTER_KEY => $this->option(Record::THOUGHT_INNER_KEY), |
181
|
|
|
Record::CREATE_CONSTRAINT => false, |
182
|
|
|
Record::CREATE_INDEXES => $this->option(Record::CREATE_INDEXES), |
183
|
|
|
Record::CREATE_PIVOT => false, //Table creation hes been already handled |
184
|
|
|
//We have to include morphed key in here |
185
|
|
|
Record::PIVOT_COLUMNS => $this->option(Record::PIVOT_COLUMNS) + [ |
186
|
|
|
$this->option(Record::MORPH_KEY) => 'string' |
187
|
|
|
], |
188
|
|
|
Record::WHERE_PIVOT => $this->option(Record::WHERE_PIVOT), |
189
|
|
|
Record::MORPH_KEY => $this->option(Record::MORPH_KEY) |
190
|
|
|
] |
191
|
|
|
); |
192
|
|
|
|
193
|
|
|
//In back order :) |
194
|
|
|
yield $inversed->withContext( |
195
|
|
|
RelationContext::createContent( |
196
|
|
|
$schema, |
197
|
|
|
$builder->requestTable($schema->getTable(), $schema->getDatabase()) |
198
|
|
|
), |
199
|
|
|
$this->definition->sourceContext() |
200
|
|
|
); |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* {@inheritdoc} |
206
|
|
|
* |
207
|
|
|
* Note: pivot table will be build from direction of source, please do not attempt to create |
208
|
|
|
* many to many relations between databases without specifying proper database. |
209
|
|
|
*/ |
210
|
|
|
public function declareTables(SchemaBuilder $builder): array |
211
|
|
|
{ |
212
|
|
|
$sourceContext = $this->definition->sourceContext(); |
213
|
|
|
|
214
|
|
|
if (!interface_exists($target = $this->definition->getTarget())) { |
215
|
|
|
throw new RelationSchemaException("Morphed relations can only be pointed to an interface"); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
if (!$this->option(Record::CREATE_PIVOT)) { |
219
|
|
|
//No pivot table creation were requested, noting really to do |
220
|
|
|
return []; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
$outerKey = $this->findOuter($builder); |
224
|
|
|
if (empty($outerKey)) { |
225
|
|
|
throw new RelationSchemaException("Unable to build morphed relation, no outer record found"); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
//Make sure all tables has same outer |
229
|
|
|
$this->verifyOuter($builder, $outerKey); |
230
|
|
|
|
231
|
|
|
$pivotTable = $builder->requestTable( |
232
|
|
|
$this->option(Record::PIVOT_TABLE), |
233
|
|
|
$sourceContext->getDatabase(), |
234
|
|
|
false, |
235
|
|
|
true |
236
|
|
|
); |
237
|
|
|
|
238
|
|
|
/* |
239
|
|
|
* Declare columns in map/pivot table. |
240
|
|
|
*/ |
241
|
|
|
$thoughtInnerKey = $pivotTable->column($this->option(Record::THOUGHT_INNER_KEY)); |
242
|
|
|
$thoughtInnerKey->nullable(false); |
243
|
|
|
$thoughtInnerKey->setType($this->resolveType( |
244
|
|
|
$sourceContext->getColumn($this->option(Record::INNER_KEY)) |
245
|
|
|
)); |
246
|
|
|
|
247
|
|
|
$thoughtOuterKey = $pivotTable->column($this->option(Record::THOUGHT_OUTER_KEY)); |
248
|
|
|
$thoughtOuterKey->nullable(false); |
249
|
|
|
$thoughtOuterKey->setType($this->resolveType($outerKey)); |
250
|
|
|
|
251
|
|
|
//Morph key |
252
|
|
|
$thoughtMorphKey = $pivotTable->column($this->option(Record::MORPH_KEY)); |
253
|
|
|
$thoughtMorphKey->nullable(false); |
254
|
|
|
$thoughtMorphKey->string(static::MORPH_COLUMN_SIZE); |
255
|
|
|
|
256
|
|
|
/* |
257
|
|
|
* Declare user columns in pivot table. |
258
|
|
|
*/ |
259
|
|
|
$rendered = new ColumnRenderer(); |
260
|
|
|
$rendered->renderColumns( |
261
|
|
|
$this->option(Record::PIVOT_COLUMNS), |
262
|
|
|
$this->option(Record::PIVOT_DEFAULTS), |
263
|
|
|
$pivotTable |
264
|
|
|
); |
265
|
|
|
|
266
|
|
|
//Map might only contain unique link between source and target |
267
|
|
|
if ($this->option(Record::CREATE_INDEXES)) { |
268
|
|
|
$pivotTable->index([ |
269
|
|
|
$thoughtInnerKey->getName(), |
270
|
|
|
$thoughtOuterKey->getName(), |
271
|
|
|
$thoughtMorphKey->getName() |
272
|
|
|
])->unique(); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
//There is only 1 constrain |
276
|
|
|
if ($this->isConstrained()) { |
277
|
|
|
$this->createForeign( |
278
|
|
|
$pivotTable, |
279
|
|
|
$thoughtInnerKey, |
280
|
|
|
$sourceContext->getColumn($this->option(Record::INNER_KEY)), |
281
|
|
|
$this->option(Record::CONSTRAINT_ACTION), |
282
|
|
|
$this->option(Record::CONSTRAINT_ACTION) |
283
|
|
|
); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
return [$pivotTable]; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* {@inheritdoc} |
291
|
|
|
*/ |
292
|
|
|
public function packRelation(SchemaBuilder $builder): array |
293
|
|
|
{ |
294
|
|
|
$packed = parent::packRelation($builder); |
295
|
|
|
$schema = &$packed[ORMInterface::R_SCHEMA]; |
296
|
|
|
|
297
|
|
|
//Must be resolved thought builder (can't be defined manually) |
298
|
|
|
$schema[Record::OUTER_KEY] = $this->findOuter($builder)->getName(); |
299
|
|
|
|
300
|
|
|
//Clarifying location |
301
|
|
|
$schema[Record::PIVOT_DATABASE] = $this->definition->sourceContext()->getDatabase(); |
302
|
|
|
$schema[Record::PIVOT_COLUMNS] = array_keys($schema[Record::PIVOT_COLUMNS]); |
303
|
|
|
|
304
|
|
|
//Ensure that inner keys are always presented |
305
|
|
|
$schema[Record::PIVOT_COLUMNS] = array_merge( |
306
|
|
|
[ |
307
|
|
|
$this->option(Record::THOUGHT_INNER_KEY), |
308
|
|
|
$this->option(Record::THOUGHT_OUTER_KEY), |
309
|
|
|
$this->option(Record::MORPH_KEY) |
310
|
|
|
], |
311
|
|
|
$schema[Record::PIVOT_COLUMNS] |
312
|
|
|
); |
313
|
|
|
|
314
|
|
|
//Model-role mapping |
315
|
|
|
foreach ($this->findTargets($builder) as $outer) { |
316
|
|
|
/* |
|
|
|
|
317
|
|
|
* //Must be pluralized |
318
|
|
|
* $tag->tagged->posts->count(); |
319
|
|
|
*/ |
320
|
|
|
$role = Inflector::pluralize($outer->getRole()); |
321
|
|
|
|
322
|
|
|
//Role => model mapping |
323
|
|
|
$schema[Record::MORPHED_ALIASES][$role] = $outer->getClass(); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
return $packed; |
327
|
|
|
} |
328
|
|
|
} |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.