Completed
Branch feature/pre-split (6ce9be)
by Anton
03:09
created

DocumentCompositor::add()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 15
Ratio 100 %

Importance

Changes 0
Metric Value
dl 15
loc 15
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
rs 9.4285
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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
}