1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
namespace Spiral\ORM\Entities\Relations; |
8
|
|
|
|
9
|
|
|
use Spiral\Database\Exceptions\QueryException; |
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\RecordIterator; |
15
|
|
|
use Spiral\ORM\Entities\Relations\Traits\MatchTrait; |
16
|
|
|
use Spiral\ORM\Entities\Relations\Traits\PartialTrait; |
17
|
|
|
use Spiral\ORM\Exceptions\RelationException; |
18
|
|
|
use Spiral\ORM\Exceptions\SelectorException; |
19
|
|
|
use Spiral\ORM\Record; |
20
|
|
|
use Spiral\ORM\RecordInterface; |
21
|
|
|
use Spiral\ORM\RelationInterface; |
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 AbstractRelation implements \IteratorAggregate, \Countable |
31
|
|
|
{ |
32
|
|
|
use MatchTrait, PartialTrait; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Loaded list of records. SplObjectStorage? |
36
|
|
|
* |
37
|
|
|
* @var RecordInterface[] |
38
|
|
|
*/ |
39
|
|
|
private $instances = []; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Records deleted from list. Potentially pre-schedule command? |
43
|
|
|
* |
44
|
|
|
* @var RecordInterface[] |
45
|
|
|
*/ |
46
|
|
|
private $deletedInstances = []; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* {@inheritdoc} |
50
|
|
|
*/ |
51
|
|
|
public function hasRelated(): bool |
52
|
|
|
{ |
53
|
|
|
if (!$this->isLoaded()) { |
54
|
|
|
//Lazy loading our relation data |
55
|
|
|
$this->loadData(); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
return !empty($this->instances); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* {@inheritdoc} |
63
|
|
|
*/ |
64
|
|
|
public function withContext( |
65
|
|
|
RecordInterface $parent, |
66
|
|
|
bool $loaded = false, |
67
|
|
|
array $data = null |
68
|
|
|
): RelationInterface { |
69
|
|
|
$hasMany = parent::withContext($parent, $loaded, $data); |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var self $hasMany |
73
|
|
|
*/ |
74
|
|
|
if ($hasMany->loaded) { |
75
|
|
|
//Init all nested models immediately |
76
|
|
|
$hasMany->initInstances(); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
return $hasMany->initInstances(); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* {@inheritdoc} |
84
|
|
|
* |
85
|
|
|
* @throws RelationException |
86
|
|
|
*/ |
87
|
|
|
public function setRelated($value) |
88
|
|
|
{ |
89
|
|
|
$this->loadData(true); |
90
|
|
|
|
91
|
|
|
if (is_null($value)) { |
92
|
|
|
$value = []; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
if (!is_array($value)) { |
96
|
|
|
throw new RelationException("HasMany relation can only be set with array of entities"); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
//Cleaning existed instances |
100
|
|
|
$this->deletedInstances = array_unique(array_merge( |
101
|
|
|
$this->deletedInstances, |
102
|
|
|
$this->instances |
103
|
|
|
)); |
104
|
|
|
|
105
|
|
|
$this->instances = []; |
106
|
|
|
foreach ($value as $item) { |
107
|
|
|
if (!is_null($item)) { |
108
|
|
|
$this->assertValid($item); |
109
|
|
|
$this->instances[] = $item; |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Has many relation represent itself (see getIterator method). |
116
|
|
|
* |
117
|
|
|
* @return $this |
118
|
|
|
*/ |
119
|
|
|
public function getRelated() |
120
|
|
|
{ |
121
|
|
|
return $this; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Iterate over instance set. |
126
|
|
|
* |
127
|
|
|
* @return \ArrayIterator |
128
|
|
|
*/ |
129
|
|
|
public function getIterator() |
130
|
|
|
{ |
131
|
|
|
return new \ArrayIterator($this->loadData(true)->instances); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @return int |
136
|
|
|
*/ |
137
|
|
|
public function count() |
138
|
|
|
{ |
139
|
|
|
return count($this->loadData(true)->instances); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Iterate over deleted instances. |
144
|
|
|
* |
145
|
|
|
* @return \ArrayIterator |
146
|
|
|
*/ |
147
|
|
|
public function getDeleted() |
148
|
|
|
{ |
149
|
|
|
return new \ArrayIterator($this->deletedInstances); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Method will autoload data. |
154
|
|
|
* |
155
|
|
|
* @param array|RecordInterface|mixed $query Fields, entity or PK. |
156
|
|
|
* |
157
|
|
|
* @return bool |
158
|
|
|
*/ |
159
|
|
|
public function has($query): bool |
160
|
|
|
{ |
161
|
|
|
return !empty($this->matchOne($query)); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Add new record into entity set. Attention, usage of this method WILL load relation data |
166
|
|
|
* unless partial. |
167
|
|
|
* |
168
|
|
|
* @param RecordInterface $record |
169
|
|
|
* |
170
|
|
|
* @return self |
171
|
|
|
* |
172
|
|
|
* @throws RelationException |
173
|
|
|
*/ |
174
|
|
|
public function add(RecordInterface $record): self |
175
|
|
|
{ |
176
|
|
|
$this->assertValid($record); |
177
|
|
|
$this->loadData(true)->instances[] = $record; |
178
|
|
|
|
179
|
|
|
return $this; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Delete one record, strict compaction, make sure exactly same instance is given. |
184
|
|
|
* |
185
|
|
|
* @param RecordInterface $record |
186
|
|
|
* |
187
|
|
|
* @return self |
188
|
|
|
* |
189
|
|
|
* @throws RelationException |
190
|
|
|
*/ |
191
|
|
|
public function delete(RecordInterface $record): self |
192
|
|
|
{ |
193
|
|
|
$this->loadData(true); |
194
|
|
|
$this->assertValid($record); |
195
|
|
|
|
196
|
|
|
foreach ($this->instances as $index => $instance) { |
197
|
|
|
if ($instance === $record) { |
198
|
|
|
//Remove from save |
199
|
|
|
unset($this->instances[$index]); |
200
|
|
|
$this->deletedInstances[] = $instance; |
201
|
|
|
break; |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
$this->instances = array_values($this->instances); |
206
|
|
|
|
207
|
|
|
return $this; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Fine one entity for a given query or return null. Method will autoload data. |
212
|
|
|
* |
213
|
|
|
* Example: ->matchOne(['value' => 'something', ...]); |
214
|
|
|
* |
215
|
|
|
* @param array|RecordInterface|mixed $query Fields, entity or PK. |
216
|
|
|
* |
217
|
|
|
* @return RecordInterface|null |
218
|
|
|
*/ |
219
|
|
|
public function matchOne($query) |
220
|
|
|
{ |
221
|
|
|
foreach ($this->loadData(true)->instances as $instance) { |
222
|
|
|
if ($this->match($instance, $query)) { |
223
|
|
|
return $instance; |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
return null; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Return only instances matched given query, performed in memory! Only simple conditions are |
232
|
|
|
* allowed. Not "find" due trademark violation. Method will autoload data. |
233
|
|
|
* |
234
|
|
|
* Example: ->matchMultiple(['value' => 'something', ...]); |
235
|
|
|
* |
236
|
|
|
* @param array|RecordInterface|mixed $query Fields, entity or PK. |
237
|
|
|
* |
238
|
|
|
* @return \ArrayIterator |
239
|
|
|
*/ |
240
|
|
|
public function matchMultiple($query) |
241
|
|
|
{ |
242
|
|
|
$result = []; |
243
|
|
|
foreach ($this->loadData()->instances as $instance) { |
244
|
|
|
if ($this->match($instance, $query)) { |
245
|
|
|
$result[] = $instance; |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
return new \ArrayIterator($result); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* {@inheritdoc} |
254
|
|
|
*/ |
255
|
|
|
public function queueCommands(ContextualCommandInterface $command): CommandInterface |
256
|
|
|
{ |
257
|
|
|
//No autoloading here |
258
|
|
|
|
259
|
|
|
if (empty($this->instances) && empty($this->deletedInstances)) { |
260
|
|
|
return new NullCommand(); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
$transaction = new TransactionalCommand(); |
264
|
|
|
|
265
|
|
|
//Delete old instances first |
266
|
|
|
foreach ($this->deletedInstances as $deleted) { |
267
|
|
|
//To de-associate use BELONGS_TO relation |
268
|
|
|
$transaction->addCommand($deleted->queueDelete()); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
//Store all instances |
272
|
|
|
foreach ($this->instances as $instance) { |
273
|
|
|
$transaction->addCommand($this->queueRelated($command, $instance)); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
//Flushing instances |
277
|
|
|
$this->deletedInstances = []; |
278
|
|
|
|
279
|
|
|
return $transaction; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* {@inheritdoc} |
284
|
|
|
* |
285
|
|
|
* @return self |
286
|
|
|
* |
287
|
|
|
* @throws SelectorException |
288
|
|
|
* @throws QueryException (needs wrapping) |
289
|
|
|
*/ |
290
|
|
|
protected function loadData(bool $autoload = true): self |
291
|
|
|
{ |
292
|
|
|
if ($this->loaded) { |
293
|
|
|
return $this; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
$this->loaded = true; |
297
|
|
|
|
298
|
|
|
if (empty($this->data) || !is_array($this->data)) { |
299
|
|
|
if ($this->autoload && $autoload) { |
300
|
|
|
//Only for non partial selections (excluded already selected) |
301
|
|
|
$this->data = $this->loadRelated(); |
302
|
|
|
} else { |
303
|
|
|
$this->data = []; |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
return $this->initInstances(); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Fetch data from database. Lazy load. |
312
|
|
|
* |
313
|
|
|
* @return array |
314
|
|
|
*/ |
315
|
|
|
protected function loadRelated(): array |
316
|
|
|
{ |
317
|
|
|
$innerKey = $this->key(Record::INNER_KEY); |
318
|
|
|
if (!empty($this->parent->getField($innerKey))) { |
319
|
|
|
return $this->orm |
320
|
|
|
->selector($this->class) |
321
|
|
|
->where($this->key(Record::OUTER_KEY), $this->parent->getField($innerKey)) |
|
|
|
|
322
|
|
|
->fetchData(); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
return []; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Init pre-loaded data. |
330
|
|
|
* |
331
|
|
|
* @return HasManyRelation |
332
|
|
|
*/ |
333
|
|
|
private function initInstances(): self |
334
|
|
|
{ |
335
|
|
|
if (is_array($this->data) && !empty($this->data)) { |
336
|
|
|
//Iterates and instantiate records |
337
|
|
|
$iterator = new RecordIterator($this->data, $this->class, $this->orm); |
338
|
|
|
|
339
|
|
|
foreach ($iterator as $item) { |
340
|
|
|
if ($this->has($item)) { |
341
|
|
|
//Skip duplicates |
342
|
|
|
continue; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
$this->instances[] = $item; |
346
|
|
|
} |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
//Memory free |
350
|
|
|
$this->data = null; |
351
|
|
|
|
352
|
|
|
return $this; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* @param ContextualCommandInterface $command |
357
|
|
|
* @param RecordInterface $instance |
358
|
|
|
* |
359
|
|
|
* @return CommandInterface |
360
|
|
|
*/ |
361
|
|
|
private function queueRelated( |
362
|
|
|
ContextualCommandInterface $command, |
363
|
|
|
RecordInterface $instance |
364
|
|
|
): CommandInterface { |
365
|
|
|
//Related entity store command |
366
|
|
|
$inner = $instance->queueStore(true); |
367
|
|
|
|
368
|
|
|
if (!$this->isSynced($this->parent, $instance)) { |
369
|
|
|
//Syncing FKs |
370
|
|
|
if ($this->key(Record::INNER_KEY) != $this->primaryColumnOf($this->parent)) { |
371
|
|
|
$command->addContext( |
372
|
|
|
$this->key(Record::OUTER_KEY), |
373
|
|
|
$this->parent->getField($this->key(Record::INNER_KEY)) |
374
|
|
|
); |
375
|
|
|
} else { |
376
|
|
|
//Syncing FKs |
377
|
|
|
$command->onExecute(function (ContextualCommandInterface $command) use ($inner) { |
378
|
|
|
$inner->addContext( |
379
|
|
|
$this->key(Record::OUTER_KEY), |
380
|
|
|
$command->primaryKey() |
381
|
|
|
); |
382
|
|
|
}); |
383
|
|
|
} |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
return $inner; |
387
|
|
|
} |
388
|
|
|
} |
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.