Completed
Branch feature/pre-split (7b42f5)
by Anton
03:44
created

DocumentEntity::__clone()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 0
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ODM;
9
10
use Spiral\Core\Component;
11
use Spiral\Core\Traits\SaturateTrait;
12
use Spiral\Models\AccessorInterface;
13
use Spiral\Models\Events\EntityEvent;
14
use Spiral\Models\PublishableInterface;
15
use Spiral\Models\SchematicEntity;
16
use Spiral\ODM\Exceptions\DefinitionException;
17
use Spiral\ODM\Exceptions\DocumentException;
18
use Spiral\ODM\Exceptions\FieldException;
19
use Spiral\ODM\Exceptions\ODMException;
20
21
/**
22
 * Primary class for spiral ODM, provides ability to pack it's own updates in a form of atomic
23
 * updates.
24
 *
25
 * You can use same properties to configure entity as in DataEntity + schema property.
26
 *
27
 * Example:
28
 *
29
 * class Test extends DocumentEntity
30
 * {
31
 *    private $schema = [
32
 *       'name' => 'string'
33
 *    ];
34
 * }
35
 *
36
 * Configuration properties:
37
 * - schema
38
 * - defaults
39
 * - secured (* by default)
40
 * - fillable
41
 * - validates
42
 */
43
abstract class DocumentEntity extends SchematicEntity implements CompositableInterface
44
{
45
    use SaturateTrait;
46
47
    /**
48
     * Helper constant to identify atomic SET operations.
49
     */
50
    const ATOMIC_SET = '$set';
51
52
    /**
53
     * Tells ODM component that Document class must be resolved using document fields. ODM must
54
     * match fields to every child of this documents and find best match. This is default definition
55
     * behaviour.
56
     *
57
     * Example:
58
     * > Class A: _id, name, address
59
     * > Class B extends A: _id, name, address, email
60
     * < Class B will be used to represent all documents with existed email field.
61
     *
62
     * @see DocumentSchema
63
     */
64
    const DEFINITION_FIELDS = 1;
65
66
    /**
67
     * Tells ODM that logical method (defineClass) must be used to define document class. Method
68
     * will receive document fields as input and must return document class name.
69
     *
70
     * Example:
71
     * > Class A: _id, name, type (a)
72
     * > Class B extends A: _id, name, type (b)
73
     * > Class C extends B: _id, name, type (c)
74
     * < Static method in class A (parent) should return A, B or C based on type field value (as
75
     * example).
76
     *
77
     * Attention, ODM will always ask TOP PARENT (in collection) to define class when you loading
78
     * documents from collections.
79
     *
80
     * @see defineClass($fields)
81
     * @see DocumentSchema
82
     */
83
    const DEFINITION_LOGICAL = 2;
84
85
    /**
86
     * Indication to ODM component of method to resolve Document class using it's fieldset. This
87
     * constant is required due Document can inherit another Document.
88
     */
89
    const DEFINITION = self::DEFINITION_FIELDS;
90
91
    /**
92
     * Automatically convert "_id" to "id" in publicFields() method.
93
     */
94
    const REMOVE_ID_UNDERSCORE = true;
95
96
    /**
97
     * Constants used to describe aggregation relations.
98
     *
99
     * Example:
100
     * 'items' => [self::MANY => 'Models\Database\Item', [
101
     *      'parentID' => 'key::_id'
102
     * ]]
103
     *
104
     * @see odmSchema::$schema
105
     */
106
    const MANY = 778;
107
    const ONE  = 899;
108
109
    /**
110
     * Errors in nested documents and accessors.
111
     *
112
     * @var array
113
     */
114
    private $innerErrors = [];
0 ignored issues
show
Unused Code introduced by
The property $innerErrors is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
115
116
    /**
117
     * SolidState will force document to be saved as one big data set without any atomic operations
118
     * (dirty fields).
119
     *
120
     * @var bool
121
     */
122
    private $solidState = false;
123
124
    /**
125
     * Document field updates (changed values).
126
     *
127
     * @var array
128
     */
129
    private $updates = [];
130
131
    /**
132
     * User specified set of atomic operation to be applied to document on save() call.
133
     *
134
     * @var array
135
     */
136
    private $atomics = [];
137
138
    /**
139
     * @var ODMInterface|ODM
140
     */
141
    protected $odm = null;
142
143
    /**
144
     * Model schema provided by ODM component.
145
     *
146
     * @var array
147
     */
148
    protected $odmSchema = [];
149
150
    /**
151
     * {@inheritdoc}
152
     *
153
     * @param array|null $schema
154
     */
155
    public function __construct(
156
        $fields,
157
        ODMInterface $odm = null,
158
        $schema = null
159
    ) {
160
        //We can use global container as fallback if no default values were provided
161
        $this->odm = $this->saturate($odm, ODMInterface::class);
162
        $this->odmSchema = !empty($schema) ? $schema : $this->odm->schema(static::class);
163
164
        $fields = is_array($fields) ? $fields : [];
165
        if (!empty($this->odmSchema[ODM::D_DEFAULTS])) {
166
            /*
167
             * Merging with default values
168
             */
169
            $fields = array_replace_recursive($this->odmSchema[ODM::D_DEFAULTS], $fields);
170
        }
171
172
        parent::__construct($fields, $this->odmSchema);
173
    }
174
175
    /**
176
     * Change document solid state. SolidState will force document to be saved as one big data set
177
     * without any atomic operations (dirty fields).
178
     *
179
     * @param bool $solidState
180
     * @param bool $forceUpdate Mark all fields as changed to force update later.
181
     *
182
     * @return $this
183
     */
184
    public function solidState($solidState, $forceUpdate = false)
185
    {
186
        $this->solidState = $solidState;
187
188
        if ($forceUpdate) {
189
            $this->updates = $this->odmSchema[ODM::D_DEFAULTS];
190
        }
191
192
        return $this;
193
    }
194
195
    /**
196
     * Is document is solid state?
197
     *
198
     * @see solidState()
199
     *
200
     * @return bool
201
     */
202
    public function isSolid()
203
    {
204
        return $this->solidState;
205
    }
206
207
    /**
208
     * Check if document has parent.
209
     *
210
     * @return bool
211
     */
212
    public function isEmbedded()
213
    {
214
        return !empty($this->parent);
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     *
220
     * @todo change to clone
221
     */
222
    public function __clone()
223
    {
224
        if (empty($this->parent)) {
225
226
            //Moving under new parent
227
            return $this->solidState(true, true);
228
        }
229
230
        /**
231
         * @var DocumentEntity $document
232
         */
233
        $document = new static(
234
            $this->serializeData(),
235
            $this->odm,
236
            $this->odmSchema
237
        );
238
239
        return $document->solidState(true, true);
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245
    public function setValue($data)
246
    {
247
        return $this->setFields($data);
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     *
253
     * Must track field updates.
254
     */
255
    public function setField($name, $value, $filter = true)
256
    {
257
        if (!$this->hasField($name)) {
258
            throw new FieldException("Undefined field '{$name}' in '" . static::class . "'");
259
        }
260
261
        //Original field value
262
        $original = $this->getField($name, null, false);
263
264
        parent::setField($name, $value, $filter);
265
266 View Code Duplication
        if (!array_key_exists($name, $this->updates)) {
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...
267
            $this->updates[$name] = $original instanceof AccessorInterface
268
                ? $original->serializeData()
269
                : $original;
270
        }
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     *
276
     * Will restore default value if presented.
277
     */
278
    public function __unset($offset)
279
    {
280
        if (!array_key_exists($offset, $this->updates)) {
281
            //Let document know that field value changed, but without overwriting previous change
282
            $this->updates[$offset] = isset($this->odmSchema[ODM::D_DEFAULTS][$offset])
283
                ? $this->odmSchema[ODM::D_DEFAULTS][$offset]
284
                : null;
285
        }
286
287
        $this->setField($offset, null, false);
288
        if (isset($this->odmSchema[ODM::D_DEFAULTS][$offset])) {
289
            $this->setField($offset, $this->odmSchema[ODM::D_DEFAULTS][$offset], false);
290
        }
291
    }
292
293
    /**
294
     * Alias for atomic operation $set. Attention, this operation is not identical to setField()
295
     * method, it performs low level operation and can be used only on simple fields. No filters
296
     * will be applied to field!
297
     *
298
     * @param string $field
299
     * @param mixed  $value
300
     *
301
     * @return $this
302
     *
303
     * @throws DocumentException
304
     */
305
    public function set($field, $value)
306
    {
307
        if ($this->hasUpdates($field, true)) {
308
            throw new FieldException(
309
                "Unable to apply multiple atomic operation to field '{$field}'"
310
            );
311
        }
312
313
        $this->setField($field, $value);
314
315
        //Filtered
316
        $this->atomics[self::ATOMIC_SET][$field] = $this->getFields($value);
317
318
        return $this;
319
    }
320
321
    /**
322
     * Alias for atomic operation $inc.
323
     *
324
     * @param string $field
325
     * @param string $value
326
     *
327
     * @return $this
328
     *
329
     * @throws DocumentException
330
     */
331
    public function inc($field, $value)
332
    {
333
        if ($this->hasUpdates($field, true) && !isset($this->atomics['$inc'][$field])) {
334
            throw new FieldException(
335
                "Unable to apply multiple atomic operation to field '{$field}'"
336
            );
337
        }
338
339
        if (!isset($this->atomics['$inc'][$field])) {
340
            $this->atomics['$inc'][$field] = 0;
341
        }
342
343
        $this->atomics['$inc'][$field] += $value;
344
        $this->setField($field, $this->getField($field) + $value);
345
346
        return $this;
347
    }
348
349
    /**
350
     * {@inheritdoc}
351
     */
352
    public function defaultValue()
353
    {
354
        return $this->odmSchema[ODM::D_DEFAULTS];
355
    }
356
357
    /**
358
     * {@inheritdoc}
359
     *
360
     * Include every composition public data into result.
361
     */
362
    public function publicFields()
363
    {
364
        $result = [];
365
366
        foreach ($this->getKeys() as $field) {
367
            if (in_array($field, $this->odmSchema[ODM::D_HIDDEN])) {
368
                //We might need to use isset in future, for performance
369
                continue;
370
            }
371
372
            /*
373
             * @var mixed|array|DocumentAccessorInterface|CompositableInterface
374
             */
375
            $value = $this->getField($field);
376
377
            if ($value instanceof PublishableInterface) {
378
                $result[$field] = $value->publicFields();
379
                continue;
380
            }
381
382
            if ($value instanceof \MongoId) {
383
                $value = (string)$value;
384
            }
385
386
            if (is_array($value)) {
387
                array_walk_recursive($value, function (&$value) {
388
                    if ($value instanceof \MongoId) {
389
                        $value = (string)$value;
390
                    }
391
                });
392
            }
393
394
            if (static::REMOVE_ID_UNDERSCORE && $field == '_id') {
395
                $field = 'id';
396
            }
397
398
            $result[$field] = $value;
399
        }
400
401
        return $result;
402
    }
403
404
    /**
405
     * {@inheritdoc}
406
     *
407
     * @param string $field       Specific field name to check for updates.
408
     * @param bool   $atomicsOnly Check if field has any atomic operation associated with.
409
     */
410
    public function hasUpdates($field = null, $atomicsOnly = false)
411
    {
412 View Code Duplication
        if (empty($field)) {
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...
413
            if (!empty($this->updates) || !empty($this->atomics)) {
414
                return true;
415
            }
416
417
            foreach ($this->getFields(false) as $value) {
418
                if ($value instanceof DocumentAccessorInterface && $value->hasUpdates()) {
419
                    return true;
420
                }
421
            }
422
423
            return false;
424
        }
425
426
        foreach ($this->atomics as $operations) {
427
            if (array_key_exists($field, $operations)) {
428
                //Property already changed by atomic operation
429
                return true;
430
            }
431
        }
432
433
        if ($atomicsOnly) {
434
            return false;
435
        }
436
437
        if (array_key_exists($field, $this->updates)) {
438
            return true;
439
        }
440
441
        $value = $this->getField($field);
442
        if ($value instanceof DocumentAccessorInterface && $value->hasUpdates()) {
443
            return true;
444
        }
445
446
        return false;
447
    }
448
449
    /**
450
     * {@inheritdoc}
451
     */
452
    public function flushUpdates()
453
    {
454
        $this->updates = $this->atomics = [];
455
456
        foreach ($this->getFields(false) as $value) {
457
            if ($value instanceof DocumentAccessorInterface) {
458
                $value->flushUpdates();
459
            }
460
        }
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466
    public function buildAtomics($container = '')
467
    {
468
        if (!$this->hasUpdates() && !$this->isSolid()) {
469
            return [];
470
        }
471
472
        if ($this->isSolid()) {
473
            if (!empty($container)) {
474
                //Simple nested document in solid state
475
                return [self::ATOMIC_SET => [$container => $this->serializeData()]];
476
            }
477
478
            //Direct document save
479
            $atomics = [self::ATOMIC_SET => $this->serializeData()];
480
            unset($atomics[self::ATOMIC_SET]['_id']);
481
482
            return $atomics;
483
        }
484
485
        if (empty($container)) {
486
            $atomics = $this->atomics;
487
        } else {
488
            $atomics = [];
489
490
            foreach ($this->atomics as $atomic => $fields) {
491
                foreach ($fields as $field => $value) {
492
                    $atomics[$atomic][$container . '.' . $field] = $value;
493
                }
494
            }
495
        }
496
497
        foreach ($this->getFields(false) as $field => $value) {
498
            if ($field == '_id') {
499
                continue;
500
            }
501
502
            if ($value instanceof DocumentAccessorInterface) {
503
                $atomics = array_merge_recursive(
504
                    $atomics,
505
                    $value->buildAtomics(($container ? $container . '.' : '') . $field)
506
                );
507
508
                continue;
509
            }
510
511
            foreach ($atomics as $atomic => $operations) {
512
                if (array_key_exists($field, $operations) && $atomic != self::ATOMIC_SET) {
513
                    //Property already changed by atomic operation
514
                    continue;
515
                }
516
            }
517
518
            if (array_key_exists($field, $this->updates)) {
519
                //Generating set operation for changed field
520
                $atomics[self::ATOMIC_SET][($container ? $container . '.' : '') . $field] = $value;
521
            }
522
        }
523
524
        return $atomics;
525
    }
526
527
    /**
528
     * @return array
529
     */
530
    public function __debugInfo()
531
    {
532
        return [
533
            'fields'  => $this->getFields(),
534
            'atomics' => $this->hasUpdates() ? $this->buildAtomics() : []
535
        ];
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     *
541
     * Accessor options include field type resolved by DocumentSchema.
542
     *
543
     * @throws ODMException
544
     * @throws DefinitionException
545
     */
546
    protected function createAccessor($accessor, $value)
547
    {
548
        $options = null;
549
        if (is_array($accessor)) {
550
            list($accessor, $options) = $accessor;
551
        }
552
553
        if ($accessor == ODM::CMP_ONE) {
554
            //Pointing to document instance
555
            return $this->odm->document($options, $value);
556
        }
557
558
        //Additional options are supplied for CompositableInterface
559
        return new $accessor($value, $this->odm, $options);
560
    }
561
562
    /**
563
     * {@inheritdoc}
564
     */
565 View Code Duplication
    protected function iocContainer()
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...
566
    {
567
        if (empty($this->odm) || !$this->odm instanceof Component) {
568
            return parent::iocContainer();
569
        }
570
571
        return $this->odm->iocContainer();
572
    }
573
574
    /**
575
     * Create document entity using given ODM instance or load parent ODM via shared container.
576
     *
577
     * @see   Component::staticContainer()
578
     *
579
     * @param array        $fields Model fields to set, will be passed thought filters.
580
     * @param ODMInterface $odm    ODMInterface component, global container will be called if not
581
     *                             instance provided.
582
     *
583
     * @return DocumentEntity
584
     *
585
     * @event created($document)
586
     */
587 View Code Duplication
    public static function create($fields = [], ODMInterface $odm = null)
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...
588
    {
589
        /**
590
         * @var DocumentEntity $document
591
         */
592
        $document = new static([], null, $odm);
593
594
        //Forcing validation (empty set of fields is not valid set of fields)
595
        $document->setFields($fields)->dispatch('created', new EntityEvent($document));
596
597
        return $document;
598
    }
599
600
    /**
601
     * Called by ODM with set of loaded fields. Must return name of appropriate class.
602
     *
603
     * @param array        $fields
604
     * @param ODMInterface $odm
605
     *
606
     * @return string
607
     *
608
     * @throws DefinitionException
609
     */
610
    public static function defineClass(array $fields, ODMInterface $odm)
611
    {
612
        throw new DefinitionException('Class definition method has not been implemented');
613
    }
614
}