1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
namespace Spiral\ODM\Entities; |
8
|
|
|
|
9
|
|
|
use Spiral\Models\PublishableInterface; |
10
|
|
|
use Spiral\Models\Traits\SolidableTrait; |
11
|
|
|
use Spiral\ODM\CompositableInterface; |
12
|
|
|
use Spiral\ODM\Exceptions\CompositorException; |
13
|
|
|
use Spiral\ODM\ODMInterface; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Provides ability to composite multiple documents in a form of array. |
17
|
|
|
* |
18
|
|
|
* Attention, composition will be saved as one big $set operation in case when multiple atomic |
19
|
|
|
* operations applied to it (not supported by Mongo). |
20
|
|
|
*/ |
21
|
|
|
class DocumentCompositor implements |
22
|
|
|
CompositableInterface, |
23
|
|
|
PublishableInterface, |
24
|
|
|
\Countable, |
25
|
|
|
\IteratorAggregate |
26
|
|
|
{ |
27
|
|
|
use SolidableTrait; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Lazy conversion from array to CompositableInterface (ie DocumentEntity). |
31
|
|
|
* |
32
|
|
|
* @var CompositableInterface[] |
33
|
|
|
*/ |
34
|
|
|
private $entities = []; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Set of atomic operation applied to whole composition set. |
38
|
|
|
* |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $atomics = []; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var string |
45
|
|
|
*/ |
46
|
|
|
protected $class; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @invisible |
50
|
|
|
* @var ODMInterface |
51
|
|
|
*/ |
52
|
|
|
protected $odm; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @param string $class |
56
|
|
|
* @param array|CompositableInterface[] $data |
57
|
|
|
* @param ODMInterface $odm |
58
|
|
|
*/ |
59
|
|
|
public function __construct(string $class, array $data, ODMInterface $odm) |
60
|
|
|
{ |
61
|
|
|
$this->class = $class; |
62
|
|
|
$this->odm = $odm; |
63
|
|
|
|
64
|
|
|
//Instantiating composed entities (no data filtering) |
65
|
|
|
$this->entities = $this->createEntities($data, false); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Get primary composition class. |
70
|
|
|
* |
71
|
|
|
* @return string |
72
|
|
|
*/ |
73
|
|
|
public function getClass(): string |
74
|
|
|
{ |
75
|
|
|
return $this->class; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Push new entity to end of set. |
80
|
|
|
* |
81
|
|
|
* Be aware that any added entity will be cloned in order to detach it from passed object: |
82
|
|
|
* $user->addresses->push($address); |
83
|
|
|
* $address->city = 'Minsk'; //this will have no effect of $user->addresses |
84
|
|
|
* |
85
|
|
|
* @param CompositableInterface $entity |
86
|
|
|
* |
87
|
|
|
* @return DocumentCompositor |
88
|
|
|
* |
89
|
|
|
* @throws CompositorException When entity is invalid type. |
90
|
|
|
*/ |
91
|
|
View Code Duplication |
public function push(CompositableInterface $entity): DocumentCompositor |
|
|
|
|
92
|
|
|
{ |
93
|
|
|
//Detaching entity |
94
|
|
|
$entity = clone $entity; |
95
|
|
|
|
96
|
|
|
$this->assertSupported($entity); |
97
|
|
|
|
98
|
|
|
$this->entities[] = $entity; |
99
|
|
|
$this->atomics['$push'][] = $entity; |
100
|
|
|
|
101
|
|
|
return $this; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Add entity to set, only one instance of document must be presented. |
106
|
|
|
* |
107
|
|
|
* Be aware that any added entity will be cloned in order to detach it from passed object: |
108
|
|
|
* $user->addresses->add($address); |
109
|
|
|
* $address->city = 'Minsk'; //this will have no effect of $user->addresses |
110
|
|
|
* |
111
|
|
|
* @param CompositableInterface $entity |
112
|
|
|
* |
113
|
|
|
* @return DocumentCompositor |
114
|
|
|
* |
115
|
|
|
* @throws CompositorException When entity is invalid type. |
116
|
|
|
*/ |
117
|
|
View Code Duplication |
public function add(CompositableInterface $entity): DocumentCompositor |
|
|
|
|
118
|
|
|
{ |
119
|
|
|
//Detaching entity |
120
|
|
|
$entity = clone $entity; |
121
|
|
|
|
122
|
|
|
$this->assertSupported($entity); |
123
|
|
|
|
124
|
|
|
if (!$this->has($entity)) { |
125
|
|
|
$this->entities[] = $entity; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
$this->atomics['$addToSet'][] = $entity; |
129
|
|
|
|
130
|
|
|
return $this; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Pull mathced entities from composition. |
135
|
|
|
* |
136
|
|
|
* $user->addresses->pull($address); |
137
|
|
|
* |
138
|
|
|
* @param CompositableInterface $entity |
139
|
|
|
* |
140
|
|
|
* @return DocumentCompositor |
141
|
|
|
*/ |
142
|
|
View Code Duplication |
public function pull(CompositableInterface $entity): DocumentCompositor |
|
|
|
|
143
|
|
|
{ |
144
|
|
|
//Passing true to get all entity offsets |
145
|
|
|
$targets = $this->find($entity, true); |
146
|
|
|
|
147
|
|
|
foreach ($targets as $offset => $target) { |
148
|
|
|
unset($this->entities[$offset]); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$this->atomics['$pull'][] = clone $entity; |
152
|
|
|
|
153
|
|
|
return $this; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Check if composition contains desired document or document matching query. |
158
|
|
|
* |
159
|
|
|
* Example: |
160
|
|
|
* $user->cards->has(['active' => true]); |
161
|
|
|
* $user->cards->has(new Card(...)); |
162
|
|
|
* |
163
|
|
|
* @param CompositableInterface|array $query |
164
|
|
|
* |
165
|
|
|
* @return bool |
166
|
|
|
*/ |
167
|
|
|
public function has($query): bool |
168
|
|
|
{ |
169
|
|
|
return !empty($this->findOne($query)); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Find document in composition based on given entity or matching query. |
174
|
|
|
* |
175
|
|
|
* $user->cards->findOne(['active' => true]); |
176
|
|
|
* $user->cards->findOne(new Card(...)); |
177
|
|
|
* |
178
|
|
|
* @param CompositableInterface|array $query |
179
|
|
|
* |
180
|
|
|
* @return CompositableInterface|null |
181
|
|
|
*/ |
182
|
|
|
public function findOne($query) |
183
|
|
|
{ |
184
|
|
|
$entities = $this->find($query); |
185
|
|
|
if (empty($entities)) { |
186
|
|
|
return null; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
return current($entities); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Find all entities matching given query (query can be provided in a form of |
194
|
|
|
* CompositableInterface). |
195
|
|
|
* |
196
|
|
|
* $user->cards->find(['active' => true]); |
197
|
|
|
* $user->cards->find(new Card(...)); //Attention, this will likely to return only on match |
198
|
|
|
* |
199
|
|
|
* @param CompositableInterface|array $query |
200
|
|
|
* @param bool $preserveKeys Set to true to keep original offsets. |
201
|
|
|
* |
202
|
|
|
* @return CompositableInterface[] |
203
|
|
|
*/ |
204
|
|
|
public function find($query, bool $preserveKeys = false): array |
205
|
|
|
{ |
206
|
|
|
if ($query instanceof CompositableInterface) { |
207
|
|
|
//Intersecting using values |
208
|
|
|
$query = $query->packValue(); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
$result = []; |
212
|
|
|
foreach ($this->entities as $offset => $entity) { |
213
|
|
|
//Looking for entities using key intersection |
214
|
|
|
if (empty($query) || (array_intersect_assoc($entity->packValue(), $query) == $query)) { |
215
|
|
|
$result[$offset] = $entity; |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
if (!$preserveKeys) { |
220
|
|
|
return array_values($result); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
return $result; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* {@inheritdoc} |
228
|
|
|
* |
229
|
|
|
* Be aware that any added entity will be cloned in order to detach it from passed object: |
230
|
|
|
* $user->addresses->mountValue([$address]); |
231
|
|
|
* $address->city = 'Minsk'; //this will have no effect of $user->addresses |
232
|
|
|
*/ |
233
|
|
|
public function stateValue($data) |
234
|
|
|
{ |
235
|
|
|
//Manually altered compositions must always end in solid state |
236
|
|
|
$this->solidState = true; |
237
|
|
|
$this->entities = []; |
238
|
|
|
|
239
|
|
|
if (!is_array($data)) { |
240
|
|
|
//Unable to initiate |
241
|
|
|
return; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
//Instantiating entities (with filtering enabled) |
245
|
|
|
$this->entities = $this->createEntities($data, true); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* {@inheritdoc} |
250
|
|
|
*/ |
251
|
|
|
public function packValue(): array |
252
|
|
|
{ |
253
|
|
|
return $this->packValues($this->entities); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* {@inheritdoc} |
258
|
|
|
* |
259
|
|
|
* @param bool $changedEntities Reference, will be set to true if any of entities changed |
260
|
|
|
* internally. |
261
|
|
|
*/ |
262
|
|
|
public function hasUpdates(bool &$changedEntities = null): bool |
263
|
|
|
{ |
264
|
|
|
foreach ($this->entities as $entity) { |
265
|
|
|
if ($entity->hasUpdates()) { |
266
|
|
|
$changedEntities = true; |
267
|
|
|
|
268
|
|
|
return true; |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return !empty($this->atomics); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* {@inheritdoc} |
277
|
|
|
*/ |
278
|
|
|
public function flushUpdates() |
279
|
|
|
{ |
280
|
|
|
$this->atomics = []; |
281
|
|
|
|
282
|
|
|
foreach ($this->entities as $entity) { |
283
|
|
|
$entity->flushUpdates(); |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* {@inheritdoc} |
289
|
|
|
*/ |
290
|
|
|
public function buildAtomics(string $container = ''): array |
291
|
|
|
{ |
292
|
|
|
//$changedEntities will be set to true if any of internal entities were changed directly |
293
|
|
|
if (!$this->hasUpdates($changedEntities)) { |
294
|
|
|
return []; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
//Mongo does not support multiple operations for one field, switching to $set (make sure it's |
298
|
|
|
//reasonable) |
299
|
|
View Code Duplication |
if ($this->solidState || count($this->atomics) > 1 || $changedEntities) { |
|
|
|
|
300
|
|
|
//We don't care about atomics in solid state |
301
|
|
|
return ['$set' => [$container => $this->packValue()]]; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
//Aggregate composition specific atomics (pull, push, addToSet) and entity specific atomics |
305
|
|
|
$atomics = []; |
306
|
|
|
|
307
|
|
|
//If entity already presented in any of composition atomics we are not insluding it's own |
308
|
|
|
//offset specific operations into atomics |
309
|
|
|
$excluded = []; |
310
|
|
|
|
311
|
|
|
foreach ($this->atomics as $operation => $items) { |
312
|
|
|
//Collect all atomics handled on |
313
|
|
|
$excluded = array_merge($excluded, $items); |
314
|
|
|
|
315
|
|
|
//Into array form |
316
|
|
|
$atomics[$operation][$container][$operation == '$pull' ? '$in' : '$each'] = $this->packValues($items); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
//Document specific atomic operations (excluding document which are colliding with composition |
320
|
|
|
//specific operations) |
321
|
|
|
foreach ($this->entities as $offset => $document) { |
322
|
|
|
if (in_array($document, $excluded)) { |
323
|
|
|
//Handler on higher level |
324
|
|
|
continue; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
$atomics = array_merge( |
328
|
|
|
$atomics, |
329
|
|
|
$document->buildAtomics((!empty($container) ? $container . '.' : '') . $offset) |
330
|
|
|
); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
return $atomics; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Packs only public values of all nested documents. |
338
|
|
|
* |
339
|
|
|
* @return array |
340
|
|
|
*/ |
341
|
|
|
public function publicFields(): array |
342
|
|
|
{ |
343
|
|
|
$result = []; |
344
|
|
|
foreach ($this->entities as $entity) { |
345
|
|
|
if ($entity instanceof PublishableInterface) { |
346
|
|
|
$result[] = $entity->publicFields(); |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
return $result; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* {@inheritdoc} |
355
|
|
|
*/ |
356
|
|
|
public function jsonSerialize() |
357
|
|
|
{ |
358
|
|
|
return $this->publicFields(); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* @return int |
363
|
|
|
*/ |
364
|
|
|
public function count(): int |
365
|
|
|
{ |
366
|
|
|
return count($this->entities); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @return \ArrayIterator |
371
|
|
|
*/ |
372
|
|
|
public function getIterator(): \ArrayIterator |
373
|
|
|
{ |
374
|
|
|
return new \ArrayIterator($this->entities); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Cloning will be called when object will be embedded into another document. |
379
|
|
|
*/ |
380
|
|
|
public function __clone() |
381
|
|
|
{ |
382
|
|
|
$this->solidState = true; |
383
|
|
|
$this->atomics = []; |
384
|
|
|
|
385
|
|
|
//De-serialize composition in order to ensure that all compositions are recreated |
386
|
|
|
$this->stateValue($this->packValue()); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* @return array |
391
|
|
|
*/ |
392
|
|
|
public function __debugInfo() |
393
|
|
|
{ |
394
|
|
|
return [ |
395
|
|
|
'entities' => array_values($this->entities), |
396
|
|
|
'atomics' => $this->buildAtomics('@compositor') |
397
|
|
|
]; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* Assert that given entity supported by composition. |
402
|
|
|
* |
403
|
|
|
* @param mixed $entity |
404
|
|
|
* |
405
|
|
|
* @throws CompositorException |
406
|
|
|
*/ |
407
|
|
|
protected function assertSupported($entity) |
408
|
|
|
{ |
409
|
|
|
if (!is_object($entity) || !is_a($entity, $this->class)) { |
410
|
|
|
throw new CompositorException(sprintf( |
411
|
|
|
"Only instances of '%s' supported, '%s' given", |
412
|
|
|
$this->class, |
413
|
|
|
is_object($entity) ? get_class($entity) : gettype($entity) |
414
|
|
|
)); |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* Instantiate every entity in composition. |
420
|
|
|
* |
421
|
|
|
* @param array $data |
422
|
|
|
* @param bool $filter |
423
|
|
|
* |
424
|
|
|
* @return CompositableInterface[] |
425
|
|
|
* |
426
|
|
|
* @throws CompositorException |
427
|
|
|
*/ |
428
|
|
|
private function createEntities(array $data, bool $filter = true): array |
429
|
|
|
{ |
430
|
|
|
$result = []; |
431
|
|
|
foreach ($data as $item) { |
432
|
|
|
if ($item instanceof CompositableInterface) { |
433
|
|
|
$this->assertSupported($item); |
434
|
|
|
|
435
|
|
|
//Always clone to detach from original value |
436
|
|
|
$result[] = clone $item; |
437
|
|
|
} else { |
438
|
|
|
$result[] = $this->odm->instantiate($this->class, $item, $filter); |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
return $result; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Pack multiple entities into array form. |
447
|
|
|
* |
448
|
|
|
* @param CompositableInterface[] $entities |
449
|
|
|
* |
450
|
|
|
* @return array |
451
|
|
|
*/ |
452
|
|
|
private function packValues(array $entities): array |
453
|
|
|
{ |
454
|
|
|
$result = []; |
455
|
|
|
foreach ($entities as $entity) { |
456
|
|
|
$result[] = $entity->packValue(); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
return $result; |
460
|
|
|
} |
461
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.