Completed
Branch feature/pre-split (211a78)
by Anton
07:24 queued 03:41
created

DocumentEntity::odmSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 6
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\EntityInterface;
14
use Spiral\Models\Events\EntityEvent;
15
use Spiral\Models\PublishableInterface;
16
use Spiral\Models\SchematicEntity;
17
use Spiral\ODM\Exceptions\DefinitionException;
18
use Spiral\ODM\Exceptions\DocumentException;
19
use Spiral\ODM\Exceptions\FieldException;
20
use Spiral\ODM\Exceptions\ODMException;
21
use Spiral\Validation\ValidatesInterface;
22
23
/**
24
 * Primary class for spiral ODM, provides ability to pack it's own updates in a form of atomic
25
 * updates.
26
 *
27
 * You can use same properties to configure entity as in DataEntity + schema property.
28
 *
29
 * Example:
30
 *
31
 * class Test extends DocumentEntity
32
 * {
33
 *    private $schema = [
34
 *       'name' => 'string'
35
 *    ];
36
 * }
37
 *
38
 * Configuration properties:
39
 * - schema
40
 * - defaults
41
 * - secured (* by default)
42
 * - fillable
43
 * - validates
44
 */
45
abstract class DocumentEntity extends SchematicEntity implements CompositableInterface
46
{
47
    use SaturateTrait;
48
49
    /**
50
     * We are going to inherit parent validation rules, this will let spiral translator know about
51
     * it and merge i18n messages.
52
     *
53
     * @see TranslatorTrait
54
     */
55
    const I18N_INHERIT_MESSAGES = true;
56
57
    /**
58
     * Helper constant to identify atomic SET operations.
59
     */
60
    const ATOMIC_SET = '$set';
61
62
    /**
63
     * Tells ODM component that Document class must be resolved using document fields. ODM must
64
     * match fields to every child of this documents and find best match. This is default definition
65
     * behaviour.
66
     *
67
     * Example:
68
     * > Class A: _id, name, address
69
     * > Class B extends A: _id, name, address, email
70
     * < Class B will be used to represent all documents with existed email field.
71
     *
72
     * @see DocumentSchema
73
     */
74
    const DEFINITION_FIELDS = 1;
75
76
    /**
77
     * Tells ODM that logical method (defineClass) must be used to define document class. Method
78
     * will receive document fields as input and must return document class name.
79
     *
80
     * Example:
81
     * > Class A: _id, name, type (a)
82
     * > Class B extends A: _id, name, type (b)
83
     * > Class C extends B: _id, name, type (c)
84
     * < Static method in class A (parent) should return A, B or C based on type field value (as
85
     * example).
86
     *
87
     * Attention, ODM will always ask TOP PARENT (in collection) to define class when you loading
88
     * documents from collections.
89
     *
90
     * @see defineClass($fields)
91
     * @see DocumentSchema
92
     */
93
    const DEFINITION_LOGICAL = 2;
94
95
    /**
96
     * Indication to ODM component of method to resolve Document class using it's fieldset. This
97
     * constant is required due Document can inherit another Document.
98
     */
99
    const DEFINITION = self::DEFINITION_FIELDS;
100
101
    /**
102
     * Automatically convert "_id" to "id" in publicFields() method.
103
     */
104
    const REMOVE_ID_UNDERSCORE = true;
105
106
    /**
107
     * Constants used to describe aggregation relations.
108
     *
109
     * Example:
110
     * 'items' => [self::MANY => 'Models\Database\Item', [
111
     *      'parentID' => 'key::_id'
112
     * ]]
113
     *
114
     * @see odmSchema::$schema
115
     */
116
    const MANY = 778;
117
    const ONE  = 899;
118
119
    /**
120
     * Errors in nested documents and accessors.
121
     *
122
     * @var array
123
     */
124
    private $innerErrors = [];
125
126
    /**
127
     * SolidState will force document to be saved as one big data set without any atomic operations
128
     * (dirty fields).
129
     *
130
     * @var bool
131
     */
132
    private $solidState = false;
133
134
    /**
135
     * Document field updates (changed values).
136
     *
137
     * @var array
138
     */
139
    private $updates = [];
140
141
    /**
142
     * User specified set of atomic operation to be applied to document on save() call.
143
     *
144
     * @var array
145
     */
146
    private $atomics = [];
147
148
    /**
149
     * @invisible
150
     *
151
     * @todo change this concept in future
152
     * @var EntityInterface
153
     */
154
    protected $parent = null;
155
156
    /**
157
     * @var ODMInterface|ODM
158
     */
159
    protected $odm = null;
160
161
    /**
162
     * Model schema provided by ODM component.
163
     *
164
     * @var array
165
     */
166
    protected $odmSchema = [];
167
168
    /**
169
     * {@inheritdoc}
170
     *
171
     * @param array|null $schema
172
     */
173
    public function __construct(
174
        $fields,
175
        EntityInterface $parent = null,
176
        ODMInterface $odm = null,
177
        $schema = null
178
    ) {
179
        $this->parent = $parent;
180
181
        //We can use global container as fallback if no default values were provided
182
        $this->odm = $this->saturate($odm, ODMInterface::class);
183
        $this->odmSchema = !empty($schema) ? $schema : $this->odm->schema(static::class);
184
185
        if (empty($fields)) {
186
            //Default state for an empty model - invalid
187
            $this->invalidate();
188
        }
189
190
        $fields = is_array($fields) ? $fields : [];
191
        if (!empty($this->odmSchema[ODM::D_DEFAULTS])) {
192
            /*
193
             * Merging with default values
194
             */
195
            $fields = array_replace_recursive($this->odmSchema[ODM::D_DEFAULTS], $fields);
196
        }
197
198
        parent::__construct($fields, $this->odmSchema);
199
    }
200
201
    /**
202
     * Change document solid state. SolidState will force document to be saved as one big data set
203
     * without any atomic operations (dirty fields).
204
     *
205
     * @param bool $solidState
206
     * @param bool $forceUpdate Mark all fields as changed to force update later.
207
     *
208
     * @return $this
209
     */
210
    public function solidState($solidState, $forceUpdate = false)
211
    {
212
        $this->solidState = $solidState;
213
214
        if ($forceUpdate) {
215
            $this->updates = $this->odmSchema[ODM::D_DEFAULTS];
216
        }
217
218
        return $this;
219
    }
220
221
    /**
222
     * Is document is solid state?
223
     *
224
     * @see solidState()
225
     *
226
     * @return bool
227
     */
228
    public function isSolid()
229
    {
230
        return $this->solidState;
231
    }
232
233
    /**
234
     * Check if document has parent.
235
     *
236
     * @return bool
237
     */
238
    public function isEmbedded()
239
    {
240
        return !empty($this->parent);
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    public function embed(EntityInterface $parent)
247
    {
248
        if (empty($this->parent)) {
249
            $this->parent = $parent;
250
251
            //Moving under new parent
252
            return $this->solidState(true, true);
253
        }
254
255
        if ($parent === $this->parent) {
256
            return $this;
257
        }
258
259
        /**
260
         * @var DocumentEntity $document
261
         */
262
        $document = new static(
263
            $this->serializeData(),
264
            $parent,
265
            $this->odm,
266
            $this->odmSchema
267
        );
268
269
        return $document->solidState(true, true);
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function setValue($data)
276
    {
277
        return $this->setFields($data);
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     *
283
     * Must track field updates.
284
     */
285
    public function setField($name, $value, $filter = true)
286
    {
287
        if (!$this->hasField($name)) {
288
            throw new FieldException("Undefined field '{$name}' in '" . static::class . "'");
289
        }
290
291
        //Original field value
292
        $original = $this->getField($name, null, false);
293
294
        parent::setField($name, $value, $filter);
295
296 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...
297
            $this->updates[$name] = $original instanceof AccessorInterface
298
                ? $original->serializeData()
299
                : $original;
300
        }
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     *
306
     * Will restore default value if presented.
307
     */
308
    public function __unset($offset)
309
    {
310
        if (!array_key_exists($offset, $this->updates)) {
311
            //Let document know that field value changed, but without overwriting previous change
312
            $this->updates[$offset] = isset($this->odmSchema[ODM::D_DEFAULTS][$offset])
313
                ? $this->odmSchema[ODM::D_DEFAULTS][$offset]
314
                : null;
315
        }
316
317
        $this->setField($offset, null, false);
318
        if (isset($this->odmSchema[ODM::D_DEFAULTS][$offset])) {
319
            $this->setField($offset, $this->odmSchema[ODM::D_DEFAULTS][$offset], false);
320
        }
321
    }
322
323
    /**
324
     * Alias for atomic operation $set. Attention, this operation is not identical to setField()
325
     * method, it performs low level operation and can be used only on simple fields. No filters
326
     * will be applied to field!
327
     *
328
     * @param string $field
329
     * @param mixed  $value
330
     *
331
     * @return $this
332
     *
333
     * @throws DocumentException
334
     */
335
    public function set($field, $value)
336
    {
337
        if ($this->hasUpdates($field, true)) {
338
            throw new FieldException(
339
                "Unable to apply multiple atomic operation to field '{$field}'"
340
            );
341
        }
342
343
        $this->setField($field, $value);
344
345
        //Filtered
346
        $this->atomics[self::ATOMIC_SET][$field] = $this->getFields($value);
347
348
        return $this;
349
    }
350
351
    /**
352
     * Alias for atomic operation $inc.
353
     *
354
     * @param string $field
355
     * @param string $value
356
     *
357
     * @return $this
358
     *
359
     * @throws DocumentException
360
     */
361
    public function inc($field, $value)
362
    {
363
        if ($this->hasUpdates($field, true) && !isset($this->atomics['$inc'][$field])) {
364
            throw new FieldException(
365
                "Unable to apply multiple atomic operation to field '{$field}'"
366
            );
367
        }
368
369
        if (!isset($this->atomics['$inc'][$field])) {
370
            $this->atomics['$inc'][$field] = 0;
371
        }
372
373
        $this->atomics['$inc'][$field] += $value;
374
        $this->setField($field, $this->getField($field) + $value);
375
376
        return $this;
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382
    public function defaultValue()
383
    {
384
        return $this->odmSchema[ODM::D_DEFAULTS];
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     *
390
     * Include every composition public data into result.
391
     */
392
    public function publicFields()
393
    {
394
        $result = [];
395
396
        foreach ($this->getKeys() as $field) {
397
            if (in_array($field, $this->odmSchema[ODM::D_HIDDEN])) {
398
                //We might need to use isset in future, for performance
399
                continue;
400
            }
401
402
            /*
403
             * @var mixed|array|DocumentAccessorInterface|CompositableInterface
404
             */
405
            $value = $this->getField($field);
406
407
            if ($value instanceof PublishableInterface) {
408
                $result[$field] = $value->publicFields();
409
                continue;
410
            }
411
412
            if ($value instanceof \MongoId) {
413
                $value = (string)$value;
414
            }
415
416
            if (is_array($value)) {
417
                array_walk_recursive($value, function (&$value) {
418
                    if ($value instanceof \MongoId) {
419
                        $value = (string)$value;
420
                    }
421
                });
422
            }
423
424
            if (static::REMOVE_ID_UNDERSCORE && $field == '_id') {
425
                $field = 'id';
426
            }
427
428
            $result[$field] = $value;
429
        }
430
431
        return $result;
432
    }
433
434
    /**
435
     * {@inheritdoc}
436
     *
437
     * @param string $field       Specific field name to check for updates.
438
     * @param bool   $atomicsOnly Check if field has any atomic operation associated with.
439
     */
440
    public function hasUpdates($field = null, $atomicsOnly = false)
441
    {
442 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...
443
            if (!empty($this->updates) || !empty($this->atomics)) {
444
                return true;
445
            }
446
447
            foreach ($this->getFields(false) as $value) {
448
                if ($value instanceof DocumentAccessorInterface && $value->hasUpdates()) {
449
                    return true;
450
                }
451
            }
452
453
            return false;
454
        }
455
456
        foreach ($this->atomics as $operations) {
457
            if (array_key_exists($field, $operations)) {
458
                //Property already changed by atomic operation
459
                return true;
460
            }
461
        }
462
463
        if ($atomicsOnly) {
464
            return false;
465
        }
466
467
        if (array_key_exists($field, $this->updates)) {
468
            return true;
469
        }
470
471
        $value = $this->getField($field);
472
        if ($value instanceof DocumentAccessorInterface && $value->hasUpdates()) {
473
            return true;
474
        }
475
476
        return false;
477
    }
478
479
    /**
480
     * {@inheritdoc}
481
     */
482
    public function flushUpdates()
483
    {
484
        $this->updates = $this->atomics = [];
485
486
        foreach ($this->getFields(false) as $value) {
487
            if ($value instanceof DocumentAccessorInterface) {
488
                $value->flushUpdates();
489
            }
490
        }
491
    }
492
493
    /**
494
     * {@inheritdoc}
495
     */
496
    public function buildAtomics($container = '')
497
    {
498
        if (!$this->hasUpdates() && !$this->isSolid()) {
499
            return [];
500
        }
501
502
        if ($this->isSolid()) {
503
            if (!empty($container)) {
504
                //Simple nested document in solid state
505
                return [self::ATOMIC_SET => [$container => $this->serializeData()]];
506
            }
507
508
            //Direct document save
509
            $atomics = [self::ATOMIC_SET => $this->serializeData()];
510
            unset($atomics[self::ATOMIC_SET]['_id']);
511
512
            return $atomics;
513
        }
514
515
        if (empty($container)) {
516
            $atomics = $this->atomics;
517
        } else {
518
            $atomics = [];
519
520
            foreach ($this->atomics as $atomic => $fields) {
521
                foreach ($fields as $field => $value) {
522
                    $atomics[$atomic][$container . '.' . $field] = $value;
523
                }
524
            }
525
        }
526
527
        foreach ($this->getFields(false) as $field => $value) {
528
            if ($field == '_id') {
529
                continue;
530
            }
531
532
            if ($value instanceof DocumentAccessorInterface) {
533
                $atomics = array_merge_recursive(
534
                    $atomics,
535
                    $value->buildAtomics(($container ? $container . '.' : '') . $field)
536
                );
537
538
                continue;
539
            }
540
541
            foreach ($atomics as $atomic => $operations) {
542
                if (array_key_exists($field, $operations) && $atomic != self::ATOMIC_SET) {
543
                    //Property already changed by atomic operation
544
                    continue;
545
                }
546
            }
547
548
            if (array_key_exists($field, $this->updates)) {
549
                //Generating set operation for changed field
550
                $atomics[self::ATOMIC_SET][($container ? $container . '.' : '') . $field] = $value;
551
            }
552
        }
553
554
        return $atomics;
555
    }
556
557
    /**
558
     * @return array
559
     */
560
    public function __debugInfo()
561
    {
562
        return [
563
            'fields'  => $this->getFields(),
564
            'atomics' => $this->hasUpdates() ? $this->buildAtomics() : [],
565
            'errors'  => $this->getErrors(),
566
        ];
567
    }
568
569
    /**
570
     * {@inheritdoc}
571
     */
572
    public function isValid()
573
    {
574
        return parent::isValid() && empty($this->innerErrors);
575
    }
576
577
    /**
578
     * {@inheritdoc}
579
     */
580
    public function getErrors($reset = false)
581
    {
582
        return parent::getErrors($reset) + $this->innerErrors;
583
    }
584
585
    /**
586
     * {@inheritdoc}
587
     *
588
     * Will validate every CompositableInterface instance.
589
     *
590
     * @param bool $reset
591
     *
592
     * @throws DocumentException
593
     */
594 View Code Duplication
    protected function validate($reset = false)
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...
595
    {
596
        $this->innerErrors = [];
597
598
        //Validating all compositions
599
        foreach ($this->odmSchema[ODM::D_COMPOSITIONS] as $field) {
600
601
            $composition = $this->getField($field);
602
            if (!$composition instanceof ValidatesInterface) {
603
                //Something weird.
604
                continue;
605
            }
606
607
            if (!$composition->isValid()) {
608
                $this->innerErrors[$field] = $composition->getErrors($reset);
609
            }
610
        }
611
612
        parent::validate($reset);
613
614
        return $this->hasErrors() && empty($this->innerErrors);
615
    }
616
617
    /**
618
     * {@inheritdoc}
619
     *
620
     * Accessor options include field type resolved by DocumentSchema.
621
     *
622
     * @throws ODMException
623
     * @throws DefinitionException
624
     */
625
    protected function createAccessor($accessor, $value)
626
    {
627
        $options = null;
628
        if (is_array($accessor)) {
629
            list($accessor, $options) = $accessor;
630
        }
631
632
        if ($accessor == ODM::CMP_ONE) {
633
            //Pointing to document instance
634
            return $this->odm->document($options, $value, $this);
635
        }
636
637
        //Additional options are supplied for CompositableInterface
638
        return new $accessor($value, $this, $this->odm, $options);
639
    }
640
641
    /**
642
     * {@inheritdoc}
643
     */
644 View Code Duplication
    protected function container()
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...
645
    {
646
        if (empty($this->odm) || !$this->odm instanceof Component) {
647
            return parent::container();
648
        }
649
650
        return $this->odm->container();
651
    }
652
653
    /**
654
     * Create document entity using given ODM instance or load parent ODM via shared container.
655
     *
656
     * @see   Component::staticContainer()
657
     *
658
     * @param array        $fields Model fields to set, will be passed thought filters.
659
     * @param ODMInterface $odm    ODMInterface component, global container will be called if not
660
     *                             instance provided.
661
     *
662
     * @return DocumentEntity
663
     *
664
     * @event created($document)
665
     */
666 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...
667
    {
668
        /**
669
         * @var DocumentEntity $document
670
         */
671
        $document = new static([], null, $odm);
672
673
        //Forcing validation (empty set of fields is not valid set of fields)
674
        $document->setFields($fields)->dispatch('created', new EntityEvent($document));
675
676
        return $document;
677
    }
678
679
    /**
680
     * Called by ODM with set of loaded fields. Must return name of appropriate class.
681
     *
682
     * @param array        $fields
683
     * @param ODMInterface $odm
684
     *
685
     * @return string
686
     *
687
     * @throws DefinitionException
688
     */
689
    public static function defineClass(array $fields, ODMInterface $odm)
0 ignored issues
show
Unused Code introduced by
The parameter $fields is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $odm is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
690
    {
691
        throw new DefinitionException('Class definition method has not been implemented');
692
    }
693
}