DocumentCompositor::buildAtomics()   C
last analyzed

Complexity

Conditions 11
Paths 11

Size

Total Lines 45
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 17
nc 11
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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