1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral, Core Components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Spiral\ORM\Entities\Relations; |
9
|
|
|
|
10
|
|
|
use Spiral\ORM\CommandInterface; |
11
|
|
|
use Spiral\ORM\Commands\NullCommand; |
12
|
|
|
use Spiral\ORM\Commands\TransactionalCommand; |
13
|
|
|
use Spiral\ORM\ContextualCommandInterface; |
14
|
|
|
use Spiral\ORM\Entities\RecordSelector; |
15
|
|
|
use Spiral\ORM\Entities\Relations\Traits\LookupTrait; |
16
|
|
|
use Spiral\ORM\Entities\Relations\Traits\SyncedTrait; |
17
|
|
|
use Spiral\ORM\Exceptions\RelationException; |
18
|
|
|
use Spiral\ORM\Helpers\WhereDecorator; |
19
|
|
|
use Spiral\ORM\ORMInterface; |
20
|
|
|
use Spiral\ORM\Record; |
21
|
|
|
use Spiral\ORM\RecordInterface; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Attention, this relation delete operation works inside loaded scope! |
25
|
|
|
* |
26
|
|
|
* When empty array assigned to relation it will schedule all related instances to be deleted. |
27
|
|
|
* |
28
|
|
|
* If you wish to load with relation WITHOUT loading previous records use [] initialization. |
29
|
|
|
*/ |
30
|
|
|
class HasManyRelation extends MultipleRelation implements \IteratorAggregate, \Countable |
31
|
|
|
{ |
32
|
|
|
use LookupTrait, SyncedTrait; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Records deleted from list. Potentially pre-schedule command? |
36
|
|
|
* |
37
|
|
|
* @var RecordInterface[] |
38
|
|
|
*/ |
39
|
|
|
private $deleteInstances = []; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* {@inheritdoc} |
43
|
|
|
* |
44
|
|
|
* @throws RelationException |
45
|
|
|
*/ |
46
|
|
|
public function setRelated($value) |
47
|
|
|
{ |
48
|
|
|
$this->loadData(true); |
49
|
|
|
|
50
|
|
|
if (is_null($value)) { |
51
|
|
|
$value = []; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
if (!is_array($value)) { |
55
|
|
|
throw new RelationException("HasMany relation can only be set with array of entities"); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
//Do not add items twice |
59
|
|
|
$matched = []; |
60
|
|
|
foreach ($value as $index => $item) { |
61
|
|
|
if (is_null($item)) { |
62
|
|
|
unset($value[$index]); |
63
|
|
|
continue; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$this->assertValid($item); |
67
|
|
|
if (!empty($instance = $this->matchOne($item))) { |
68
|
|
|
$matched[] = $instance; |
69
|
|
|
unset($value[$index]); |
70
|
|
|
} |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
$this->deleteInstances = array_diff($this->instances, $matched); |
74
|
|
|
$this->instances = $matched + $value; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Iterate over deleted instances. |
79
|
|
|
* |
80
|
|
|
* @return \ArrayIterator |
81
|
|
|
*/ |
82
|
|
|
public function getDeleted() |
83
|
|
|
{ |
84
|
|
|
return new \ArrayIterator($this->deleteInstances); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Add new record into entity set. Attention, usage of this method WILL load relation data |
89
|
|
|
* unless partial. |
90
|
|
|
* |
91
|
|
|
* @param RecordInterface $record |
92
|
|
|
* |
93
|
|
|
* @return self |
94
|
|
|
* |
95
|
|
|
* @throws RelationException |
96
|
|
|
*/ |
97
|
|
|
public function add(RecordInterface $record): self |
98
|
|
|
{ |
99
|
|
|
$this->assertValid($record); |
100
|
|
|
$this->loadData(true)->instances[] = $record; |
101
|
|
|
|
102
|
|
|
return $this; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Delete one record, strict compaction, make sure exactly same instance is given. |
107
|
|
|
* |
108
|
|
|
* @param RecordInterface $record |
109
|
|
|
* |
110
|
|
|
* @return self |
111
|
|
|
* |
112
|
|
|
* @throws RelationException |
113
|
|
|
*/ |
114
|
|
|
public function delete(RecordInterface $record): self |
115
|
|
|
{ |
116
|
|
|
$this->loadData(true); |
117
|
|
|
$this->assertValid($record); |
118
|
|
|
|
119
|
|
|
foreach ($this->instances as $index => $instance) { |
120
|
|
|
if ($this->match($instance, $record)) { |
121
|
|
|
//Remove from save |
122
|
|
|
unset($this->instances[$index]); |
123
|
|
|
$this->deleteInstances[] = $instance; |
124
|
|
|
break; |
125
|
|
|
} |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
$this->instances = array_values($this->instances); |
129
|
|
|
|
130
|
|
|
return $this; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Detach given object from set of instances but do not delete it in database, use it to |
135
|
|
|
* transfer object between sets. |
136
|
|
|
* |
137
|
|
|
* @param \Spiral\ORM\RecordInterface $record |
138
|
|
|
* |
139
|
|
|
* @return \Spiral\ORM\RecordInterface |
140
|
|
|
* |
141
|
|
|
* @throws RelationException When object not presented in a set. |
142
|
|
|
*/ |
143
|
|
|
public function detach(RecordInterface $record): RecordInterface |
144
|
|
|
{ |
145
|
|
|
$this->loadData(true); |
146
|
|
|
foreach ($this->instances as $index => $instance) { |
147
|
|
|
if ($this->match($instance, $record)) { |
148
|
|
|
//Remove from save |
149
|
|
|
unset($this->instances[$index]); |
150
|
|
|
|
151
|
|
|
return $instance; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
throw new RelationException("Record {$record} not found in HasMany relation"); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* {@inheritdoc} |
160
|
|
|
*/ |
161
|
|
|
public function queueCommands(ContextualCommandInterface $parentCommand): CommandInterface |
162
|
|
|
{ |
163
|
|
|
//No autoloading here |
164
|
|
|
|
165
|
|
|
if (empty($this->instances) && empty($this->deleteInstances)) { |
166
|
|
|
return new NullCommand(); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$transaction = new TransactionalCommand(); |
170
|
|
|
|
171
|
|
|
//Delete old instances first |
172
|
|
|
foreach ($this->deleteInstances as $deleted) { |
173
|
|
|
//To de-associate use BELONGS_TO relation |
174
|
|
|
$transaction->addCommand($deleted->queueDelete()); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
//Store all instances |
178
|
|
|
foreach ($this->instances as $instance) { |
179
|
|
|
$transaction->addCommand($this->queueRelated($parentCommand, $instance)); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
//Flushing instances |
183
|
|
|
$this->deleteInstances = []; |
184
|
|
|
|
185
|
|
|
return $transaction; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* @param ContextualCommandInterface $parentCommand |
190
|
|
|
* @param RecordInterface $instance |
191
|
|
|
* |
192
|
|
|
* @return CommandInterface |
193
|
|
|
*/ |
194
|
|
|
protected function queueRelated( |
195
|
|
|
ContextualCommandInterface $parentCommand, |
196
|
|
|
RecordInterface $instance |
197
|
|
|
): CommandInterface { |
198
|
|
|
//Related entity store command |
199
|
|
|
$innerCommand = $instance->queueStore(true); |
200
|
|
|
|
201
|
|
|
if (!$this->isSynced($this->parent, $instance)) { |
202
|
|
|
//Delayed linking |
203
|
|
|
$parentCommand->onExecute(function ($outerCommand) use ($innerCommand) { |
204
|
|
|
$innerCommand->addContext( |
205
|
|
|
$this->key(Record::OUTER_KEY), |
206
|
|
|
$this->lookupKey(Record::INNER_KEY, $this->parent, $outerCommand) |
207
|
|
|
); |
208
|
|
|
|
209
|
|
|
if (!empty($morphKey = $this->key(Record::MORPH_KEY))) { |
210
|
|
|
//HasOne relation support additional morph key |
211
|
|
|
$innerCommand->addContext( |
212
|
|
|
$this->key(Record::MORPH_KEY), |
213
|
|
|
$this->orm->define(get_class($this->parent), ORMInterface::R_ROLE_NAME) |
214
|
|
|
); |
215
|
|
|
} |
216
|
|
|
}); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $innerCommand; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Fetch data from database. Lazy load. |
224
|
|
|
* |
225
|
|
|
* @return array |
226
|
|
|
*/ |
227
|
|
|
protected function loadRelated(): array |
228
|
|
|
{ |
229
|
|
|
$innerKey = $this->parent->getField($this->key(Record::INNER_KEY)); |
230
|
|
|
if (!empty($innerKey)) { |
231
|
|
|
return $this->createSelector($innerKey)->fetchData(); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return []; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Create outer selector for a given inner key value. |
239
|
|
|
* |
240
|
|
|
* @param mixed $innerKey |
241
|
|
|
* |
242
|
|
|
* @return RecordSelector |
243
|
|
|
*/ |
244
|
|
|
protected function createSelector($innerKey): RecordSelector |
245
|
|
|
{ |
246
|
|
|
$selector = $this->orm->selector($this->class)->where( |
247
|
|
|
$this->key(Record::OUTER_KEY), |
248
|
|
|
$innerKey |
|
|
|
|
249
|
|
|
); |
250
|
|
|
|
251
|
|
|
if (!empty($this->schema[Record::WHERE])) { |
252
|
|
|
//Configuring where conditions with alias resolution |
253
|
|
|
$decorator = new WhereDecorator($selector, 'where', $selector->getAlias()); |
254
|
|
|
$decorator->where($this->schema[Record::WHERE]); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
if (!empty($this->key(Record::MORPH_KEY))) { |
258
|
|
|
//Morph key |
259
|
|
|
$decorator = new WhereDecorator($selector, 'where', $selector->getAlias()); |
260
|
|
|
$decorator->where( |
261
|
|
|
'{@}.' . $this->key(Record::MORPH_KEY), |
262
|
|
|
$this->orm->define(get_class($this->parent), ORMInterface::R_ROLE_NAME) |
263
|
|
|
); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
return $selector; |
267
|
|
|
} |
268
|
|
|
} |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.