|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Spiral, Core Components |
|
4
|
|
|
* |
|
5
|
|
|
* @author Wolfy-J |
|
6
|
|
|
*/ |
|
7
|
|
|
|
|
8
|
|
|
namespace Spiral\ORM\Entities; |
|
9
|
|
|
|
|
10
|
|
|
use Spiral\ORM\Commands\TransactionalCommand; |
|
11
|
|
|
use Spiral\ORM\ContextualCommandInterface; |
|
12
|
|
|
use Spiral\ORM\Exceptions\RelationException; |
|
13
|
|
|
use Spiral\ORM\ORMInterface; |
|
14
|
|
|
use Spiral\ORM\RecordInterface; |
|
15
|
|
|
use Spiral\ORM\RelationInterface; |
|
16
|
|
|
|
|
17
|
|
|
/** |
|
18
|
|
|
* Represent set of entity relations. |
|
19
|
|
|
*/ |
|
20
|
|
|
class RelationMap |
|
21
|
|
|
{ |
|
22
|
|
|
/** |
|
23
|
|
|
* @var array|RelationInterface[] |
|
24
|
|
|
*/ |
|
25
|
|
|
private $relations = []; |
|
26
|
|
|
|
|
27
|
|
|
/** |
|
28
|
|
|
* Parent class name. |
|
29
|
|
|
* |
|
30
|
|
|
* @var string |
|
31
|
|
|
*/ |
|
32
|
|
|
private $class; |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* Parent model. |
|
36
|
|
|
* |
|
37
|
|
|
* @var RecordInterface |
|
38
|
|
|
*/ |
|
39
|
|
|
private $parent; |
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* Relations schema. |
|
43
|
|
|
* |
|
44
|
|
|
* @var array |
|
45
|
|
|
*/ |
|
46
|
|
|
private $schema = []; |
|
47
|
|
|
|
|
48
|
|
|
/** |
|
49
|
|
|
* Associates ORM manager. |
|
50
|
|
|
* |
|
51
|
|
|
* @var ORMInterface |
|
52
|
|
|
*/ |
|
53
|
|
|
protected $orm; |
|
54
|
|
|
|
|
55
|
|
|
/** |
|
56
|
|
|
* @param RecordInterface $record |
|
57
|
|
|
* @param ORMInterface $orm |
|
58
|
|
|
*/ |
|
59
|
|
|
public function __construct(RecordInterface $record, ORMInterface $orm) |
|
60
|
|
|
{ |
|
61
|
|
|
$this->class = get_class($record); |
|
62
|
|
|
$this->parent = $record; |
|
63
|
|
|
$this->schema = (array)$orm->define($this->class, ORMInterface::R_RELATIONS); |
|
64
|
|
|
$this->orm = $orm; |
|
65
|
|
|
} |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* Extract relations data from given entity fields. |
|
69
|
|
|
* |
|
70
|
|
|
* @param array $data |
|
71
|
|
|
*/ |
|
72
|
|
|
public function extractRelations(array &$data) |
|
73
|
|
|
{ |
|
74
|
|
|
//Fetch all relations |
|
75
|
|
|
$relations = array_intersect_key($data, $this->schema); |
|
76
|
|
|
|
|
77
|
|
|
foreach ($relations as $name => $relation) { |
|
78
|
|
|
$this->relations[$name] = $relation; |
|
79
|
|
|
unset($data[$name]); |
|
80
|
|
|
} |
|
81
|
|
|
} |
|
82
|
|
|
|
|
83
|
|
|
/** |
|
84
|
|
|
* Generate command tree with or without relation to parent command in order to specify update |
|
85
|
|
|
* or insert sequence. Commands might define dependencies between each other in order to extend |
|
86
|
|
|
* FK values. |
|
87
|
|
|
* |
|
88
|
|
|
* @param ContextualCommandInterface $parent |
|
89
|
|
|
* |
|
90
|
|
|
* @return ContextualCommandInterface |
|
91
|
|
|
*/ |
|
92
|
|
|
public function queueRelations(ContextualCommandInterface $parent): ContextualCommandInterface |
|
93
|
|
|
{ |
|
94
|
|
|
if (empty($this->relations)) { |
|
95
|
|
|
//No relations exists, nothing to do |
|
96
|
|
|
return $parent; |
|
97
|
|
|
} |
|
98
|
|
|
|
|
99
|
|
|
//We have to execute multiple commands at once |
|
100
|
|
|
$transaction = new TransactionalCommand(); |
|
101
|
|
|
|
|
102
|
|
|
foreach ($this->leadingRelations() as $relation) { |
|
103
|
|
|
//Generating commands needed to save given relation prior to parent command |
|
104
|
|
|
if ($relation->isLoaded()) { |
|
105
|
|
|
$transaction->addCommand($relation->queueCommands($parent)); |
|
106
|
|
|
} |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
//Parent model save operations (true state that this is leading/primary command) |
|
110
|
|
|
$transaction->addCommand($parent, true); |
|
111
|
|
|
|
|
112
|
|
|
foreach ($this->dependedRelations() as $relation) { |
|
113
|
|
|
//Generating commands needed to save relations after parent command being executed |
|
114
|
|
|
if ($relation->isLoaded()) { |
|
115
|
|
|
$transaction->addCommand($relation->queueCommands($parent)); |
|
116
|
|
|
} |
|
117
|
|
|
} |
|
118
|
|
|
|
|
119
|
|
|
return $transaction; |
|
120
|
|
|
} |
|
121
|
|
|
|
|
122
|
|
|
/** |
|
123
|
|
|
* Check if parent entity has associated relation. |
|
124
|
|
|
* |
|
125
|
|
|
* @param string $relation |
|
126
|
|
|
* |
|
127
|
|
|
* @return bool |
|
128
|
|
|
*/ |
|
129
|
|
|
public function has(string $relation): bool |
|
130
|
|
|
{ |
|
131
|
|
|
return isset($this->schema[$relation]); |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
/** |
|
135
|
|
|
* Check if relation has any associated data with it (attention, non loaded relation will be |
|
136
|
|
|
* automatically pre-loaded). |
|
137
|
|
|
* |
|
138
|
|
|
* @param string $relation |
|
139
|
|
|
* |
|
140
|
|
|
* @return bool |
|
141
|
|
|
*/ |
|
142
|
|
|
public function hasRelated(string $relation): bool |
|
143
|
|
|
{ |
|
144
|
|
|
//Checking with only loaded records |
|
145
|
|
|
return $this->get($relation)->hasRelated(); |
|
146
|
|
|
} |
|
147
|
|
|
|
|
148
|
|
|
/** |
|
149
|
|
|
* Data data which is being associated with relation, relation is allowed to return itself if |
|
150
|
|
|
* needed. |
|
151
|
|
|
* |
|
152
|
|
|
* @param string $relation |
|
153
|
|
|
* |
|
154
|
|
|
* @return RelationInterface|RecordInterface|mixed |
|
155
|
|
|
* |
|
156
|
|
|
* @throws RelationException |
|
157
|
|
|
*/ |
|
158
|
|
|
public function getRelated(string $relation) |
|
159
|
|
|
{ |
|
160
|
|
|
return $this->get($relation)->getRelated(); |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
/** |
|
164
|
|
|
* Associated relation with new value (must be compatible with relation format). |
|
165
|
|
|
* |
|
166
|
|
|
* @param string $relation |
|
167
|
|
|
* @param mixed $value |
|
168
|
|
|
* |
|
169
|
|
|
* @throws RelationException |
|
170
|
|
|
*/ |
|
171
|
|
|
public function setRelated(string $relation, $value) |
|
172
|
|
|
{ |
|
173
|
|
|
$this->get($relation)->setRelated($value); |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
/** |
|
177
|
|
|
* Get associated relation instance. |
|
178
|
|
|
* |
|
179
|
|
|
* @param string $relation |
|
180
|
|
|
* |
|
181
|
|
|
* @return RelationInterface |
|
182
|
|
|
*/ |
|
183
|
|
|
public function get(string $relation): RelationInterface |
|
184
|
|
|
{ |
|
185
|
|
|
if (isset($this->relations[$relation]) && $this->relations[$relation] instanceof RelationInterface) { |
|
186
|
|
|
return $this->relations[$relation]; |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
$instance = $this->orm->makeRelation($this->class, $relation); |
|
190
|
|
|
if (array_key_exists($relation, $this->relations)) { |
|
191
|
|
|
//Indicating that relation is loaded |
|
192
|
|
|
$instance = $instance->withContext($this->parent, true, $this->relations[$relation]); |
|
193
|
|
|
} else { |
|
194
|
|
|
//Not loaded relation |
|
195
|
|
|
$instance = $instance->withContext($this->parent, false); |
|
196
|
|
|
} |
|
197
|
|
|
|
|
198
|
|
|
return $this->relations[$relation] = $instance; |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
/** |
|
202
|
|
|
* Information about loaded relations. |
|
203
|
|
|
* |
|
204
|
|
|
* @return array |
|
205
|
|
|
*/ |
|
206
|
|
|
public function __debugInfo() |
|
207
|
|
|
{ |
|
208
|
|
|
$relations = []; |
|
209
|
|
|
|
|
210
|
|
|
foreach ($this->schema as $relation => $schema) { |
|
211
|
|
|
$accessor = $this->get($relation); |
|
212
|
|
|
|
|
213
|
|
|
$type = (new \ReflectionClass($accessor))->getShortName(); |
|
214
|
|
|
$class = (new \ReflectionClass($accessor->getClass()))->getShortName(); |
|
215
|
|
|
|
|
216
|
|
|
//[+] for loaded, [~] for lazy loaded |
|
|
|
|
|
|
217
|
|
|
$relations[$relation] = $type . '(' . $class . ') [' . ($accessor->isLoaded() ? '+]' : '~]'); |
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
return $relations; |
|
221
|
|
|
} |
|
222
|
|
|
|
|
223
|
|
|
/** |
|
224
|
|
|
* list of relations which lead data of parent record (BELONGS_TO). |
|
225
|
|
|
* |
|
226
|
|
|
* Example: |
|
227
|
|
|
* |
|
228
|
|
|
* $post = new Post(); |
|
229
|
|
|
* $post->user = new User(); |
|
230
|
|
|
* |
|
231
|
|
|
* @return RelationInterface[]|\Generator |
|
232
|
|
|
*/ |
|
233
|
|
|
protected function leadingRelations() |
|
234
|
|
|
{ |
|
235
|
|
|
foreach ($this->relations as $relation) { |
|
236
|
|
|
if ($relation instanceof RelationInterface && $relation->isLeading()) { |
|
237
|
|
|
yield $relation; |
|
238
|
|
|
} |
|
239
|
|
|
} |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
/** |
|
243
|
|
|
* list of loaded relations which depend on parent record (HAS_MANY, MANY_TO_MANY and etc). |
|
244
|
|
|
* |
|
245
|
|
|
* Example: |
|
246
|
|
|
* |
|
247
|
|
|
* $post = new Post(); |
|
248
|
|
|
* $post->comments->add(new Comment()); |
|
249
|
|
|
* |
|
250
|
|
|
* @return RelationInterface[]|\Generator |
|
251
|
|
|
*/ |
|
252
|
|
|
protected function dependedRelations() |
|
253
|
|
|
{ |
|
254
|
|
|
foreach ($this->relations as $relation) { |
|
255
|
|
|
if ($relation instanceof RelationInterface && !$relation->isLeading()) { |
|
256
|
|
|
yield $relation; |
|
257
|
|
|
} |
|
258
|
|
|
} |
|
259
|
|
|
} |
|
260
|
|
|
} |
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.