Completed
Branch feature/pre-split (656bce)
by Anton
04:24
created

DocumentEntity::flushUpdates()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework, Core Components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ODM;
8
9
use MongoDB\BSON\ObjectID;
10
use Spiral\Core\Component;
11
use Spiral\Core\Exceptions\ScopeException;
12
use Spiral\Core\Traits\SaturateTrait;
13
use Spiral\Models\AccessorInterface;
14
use Spiral\Models\Exceptions\AccessException;
15
use Spiral\Models\SchematicEntity;
16
use Spiral\Models\Traits\SolidableTrait;
17
use Spiral\ODM\Entities\DocumentCompositor;
18
use Spiral\ODM\Entities\DocumentInstantiator;
19
use Spiral\ODM\Exceptions\AccessorException;
20
use Spiral\ODM\Exceptions\AggregationException;
21
use Spiral\ODM\Exceptions\DocumentException;
22
use Spiral\ODM\Helpers\AggregationHelper;
23
use Spiral\ODM\Schemas\Definitions\CompositionDefinition;
24
25
/**
26
 * Primary class for spiral ODM, provides ability to pack it's own updates in a form of atomic
27
 * updates.
28
 *
29
 * You can use same properties to configure entity as in DataEntity + schema property.
30
 *
31
 * Example:
32
 *
33
 * class Test extends DocumentEntity
34
 * {
35
 *    const SCHEMA = [
36
 *       'name' => 'string'
37
 *    ];
38
 * }
39
 *
40
 * Configuration properties:
41
 * - schema
42
 * - defaults
43
 * - secured (* by default)
44
 * - fillable
45
 */
46
abstract class DocumentEntity extends SchematicEntity implements CompositableInterface
47
{
48
    use SaturateTrait, SolidableTrait;
49
50
    /*
51
     * Begin set of behaviour and description constants.
52
     * ================================================
53
     */
54
55
    /**
56
     * Set of schema sections needed to describe entity behaviour.
57
     */
58
    const SH_INSTANTIATION = 0;
59
    const SH_DEFAULTS      = 1;
60
    const SH_COMPOSITIONS  = 6;
61
    const SH_AGGREGATIONS  = 7;
62
63
    /**
64
     * Constants used to describe aggregation relations (also used internally to identify
65
     * composition).
66
     *
67
     * Example:
68
     * 'items' => [self::MANY => Item::class, ['parentID' => 'key::_id']]
69
     *
70
     * @see DocumentEntity::SCHEMA
71
     */
72
    const MANY = 778;
73
    const ONE  = 899;
74
75
    /*
76
     * ================================================
77
     * End set of behaviour and description constants.
78
     */
79
80
    /**
81
     * Class responsible for instance construction.
82
     */
83
    const INSTANTIATOR = DocumentInstantiator::class;
84
85
    /**
86
     * Document fields, accessors and relations. ODM will generate setters and getters for some
87
     * fields based on their types.
88
     *
89
     * Example, fields:
90
     * const SCHEMA = [
91
     *      '_id'    => 'MongoId', //Primary key field
92
     *      'value'  => 'string',  //Default string field
93
     *      'values' => ['string'] //ScalarArray accessor will be applied for fields like that
94
     * ];
95
     *
96
     * Compositions:
97
     * const SCHEMA = [
98
     *     ...,
99
     *     'child'       => Child::class,   //One document are composited, for example user Profile
100
     *     'many'        => [Child::class]  //Compositor accessor will be applied, allows to
101
     *                                      //composite many document instances
102
     * ];
103
     *
104
     * Documents can extend each other, in this case schema will also be inherited.
105
     *
106
     * Attention, make sure you properly set FILLABLE option in parent class to use constructions
107
     * like:
108
     * $parent->child = [...];
109
     *
110
     * or
111
     * $parent->setFields(['child'=>[...]]);
112
     *
113
     * @var array
114
     */
115
    const SCHEMA = [];
116
117
    /**
118
     * Default field values.
119
     *
120
     * @var array
121
     */
122
    const DEFAULTS = [];
123
124
    /**
125
     * Model behaviour configurations.
126
     */
127
    const SECURED   = '*';
128
    const HIDDEN    = [];
129
    const FILLABLE  = [];
130
    const SETTERS   = [];
131
    const GETTERS   = [];
132
    const ACCESSORS = [];
133
134
    /**
135
     * Document behaviour schema.
136
     *
137
     * @var array
138
     */
139
    private $documentSchema = [];
140
141
    /**
142
     * Document field updates (changed values).
143
     *
144
     * @var array
145
     */
146
    private $changes = [];
147
148
    /**
149
     * Parent ODM instance, responsible for aggregations and lazy loading operations.
150
     *
151
     * @invisible
152
     * @var ODMInterface
153
     */
154
    protected $odm;
155
156
    /**
157
     * {@inheritdoc}
158
     *
159
     * @param ODMInterface $odm To lazy create nested document ang aggregations.
160
     *
161
     * @throws ScopeException When no ODM instance can be resolved.
162
     */
163
    public function __construct(array $data = [], ODMInterface $odm = null, array $schema = null)
164
    {
165
        //We can use global container as fallback if no default values were provided
166
        $this->odm = $this->saturate($odm, ODMInterface::class);
167
168
        //Use supplied schema or fetch one from ODM
169
        $this->documentSchema = !empty($schema) ? $schema : $this->odm->define(
170
            static::class,
171
            ODMInterface::D_SCHEMA
172
        );
173
174
        $data = is_array($data) ? $data : [];
175
        if (!empty($this->documentSchema[self::SH_DEFAULTS])) {
176
            //Merging with default values
177
            $data = array_replace_recursive($this->documentSchema[self::SH_DEFAULTS], $data);
178
        }
179
180
        parent::__construct($data, $this->documentSchema);
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function getField(string $name, $default = null, bool $filter = true)
187
    {
188
        $this->assertField($name);
189
190
        return parent::getField($name, $default, $filter);
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     *
196
     * Tracks field changes.
197
     */
198
    public function setField(string $name, $value, bool $filter = true)
199
    {
200
        $this->assertField($name);
201
        $this->registerChange($name);
202
203
        parent::setField($name, $value, $filter);
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     *
209
     * Will restore default value if presented.
210
     */
211
    public function __unset($offset)
212
    {
213
        if (!$this->isNullable($offset)) {
214
            throw new AccessException("Unable to unset not nullable field '{$offset}'");
215
        }
216
217
        $this->setField($offset, null, false);
218
    }
219
220
    /**
221
     * Provides ability to invoke document aggregation.
222
     *
223
     * @param string $method
224
     * @param array  $arguments
225
     *
226
     * @return mixed|null|AccessorInterface|CompositableInterface|Document|Entities\DocumentSelector
227
     */
228
    public function __call($method, array $arguments)
229
    {
230
        if (isset($this->documentSchema[self::SH_AGGREGATIONS][$method])) {
231
            if (!empty($arguments)) {
232
                throw new AggregationException("Aggregation method call except 0 parameters");
233
            }
234
235
            $helper = new AggregationHelper($this, $this->odm);
236
237
            return $helper->createAggregation($method);
238
        }
239
240
        throw new DocumentException("Undefined method call '{$method}' in '" . get_called_class() . "'");
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     *
246
     * @param string $field Check once specific field changes.
247
     */
248
    public function hasChanges(string $field = null): bool
249
    {
250
        //Check updates for specific field
251
        if (!empty($field)) {
252
            if (array_key_exists($field, $this->changes)) {
253
                return true;
254
            }
255
256
            //Do not force accessor creation
257
            $value = $this->getField($field, null, false);
258
            if ($value instanceof CompositableInterface && $value->hasChanges()) {
259
                return true;
260
            }
261
262
            return false;
263
        }
264
265
        if (!empty($this->changes)) {
266
            return true;
267
        }
268
269
        //Do not force accessor creation
270
        foreach ($this->getFields(false) as $value) {
271
            //Checking all fields for changes (handled internally)
272
            if ($value instanceof CompositableInterface && $value->hasChanges()) {
273
                return true;
274
            }
275
        }
276
277
        return false;
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function buildAtomics(string $container = null): array
284
    {
285
        if (!$this->hasChanges() && !$this->isSolid()) {
286
            return [];
287
        }
288
289
        if ($this->isSolid()) {
290
            if (!empty($container)) {
291
                //Simple nested document in solid state
292
                return ['$set' => [$container => $this->packValue(false)]];
293
            }
294
295
            //No parent container
296
            return ['$set' => $this->packValue(false)];
297
        }
298
299
        //Aggregate atomics from every nested composition
300
        $atomics = [];
301
302
        foreach ($this->getFields(false) as $field => $value) {
303
            if ($value instanceof CompositableInterface) {
304
                $atomics = array_merge_recursive(
305
                    $atomics,
306
                    $value->buildAtomics((!empty($container) ? $container . '.' : '') . $field)
307
                );
308
309
                continue;
310
            }
311
312
            foreach ($atomics as $atomic => $operations) {
313
                if (array_key_exists($field, $operations) && $atomic != '$set') {
314
                    //Property already changed by atomic operation
315
                    continue;
316
                }
317
            }
318
319
            if (array_key_exists($field, $this->changes)) {
320
                //Generating set operation for changed field
321
                $atomics['$set'][(!empty($container) ? $container . '.' : '') . $field] = $value;
322
            }
323
        }
324
325
        return $atomics;
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331
    public function flushChanges()
332
    {
333
        $this->changes = [];
334
335
        foreach ($this->getFields(false) as $field => $value) {
336
            if ($value instanceof CompositableInterface) {
337
                $value->flushChanges();
338
            }
339
        }
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     *
345
     * @param bool $includeID Set to false to exclude _id from packed fields.
346
     */
347
    public function packValue(bool $includeID = true)
348
    {
349
        $values = parent::packValue();
350
351
        if (!$includeID) {
352
            unset($values['_id']);
353
        }
354
355
        return $values;
356
    }
357
358
    /**
359
     * Since most of ODM documents might contain ObjectIDs and other fields we will try to normalize
360
     * them into string values.
361
     *
362
     * @return array
363
     */
364
    public function publicValue(): array
365
    {
366
        $public = parent::publicValue();
367
368
        array_walk_recursive($public, function (&$value) {
369
            if ($value instanceof ObjectID) {
370
                $value = (string)$value;
371
            }
372
        });
373
374
        return $public;
375
    }
376
377
    /**
378
     * Cloning will be called when object will be embedded into another document.
379
     */
380
    public function __clone()
381
    {
382
        //De-serialize document in order to ensure that all compositions are recreated
383
        $this->stateValue($this->packValue());
384
385
        //Since document embedded as one piece let's ensure that it is solid
386
        $this->solidState = true;
387
        $this->changes = [];
388
    }
389
390
    /**
391
     * @return array
392
     */
393
    public function __debugInfo()
394
    {
395
        return [
396
            'fields'  => $this->getFields(),
397
            'atomics' => $this->hasChanges() ? $this->buildAtomics() : [],
398
        ];
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     *
404
     * @see CompositionDefinition
405
     */
406
    protected function getMutator(string $field, string $mutator)
407
    {
408
        /**
409
         * Every document composition is valid accessor but defined a bit differently.
410
         */
411
        if (isset($this->documentSchema[self::SH_COMPOSITIONS][$field])) {
412
            return $this->documentSchema[self::SH_COMPOSITIONS][$field];
413
        }
414
415
        return parent::getMutator($field, $mutator);
416
    }
417
418
    /**
419
     * {@inheritdoc}
420
     */
421
    protected function isNullable(string $field): bool
422
    {
423
        if (array_key_exists($field, $this->documentSchema[self::SH_DEFAULTS])) {
424
            //Only fields with default null value can be nullable
425
            return is_null($this->documentSchema[self::SH_DEFAULTS][$field]);
426
        }
427
428
        //Values unknown to schema always nullable
429
        return true;
430
    }
431
432
    /**
433
     * {@inheritdoc}
434
     *
435
     * DocumentEntity will pass ODM instance as part of accessor context.
436
     *
437
     * @see CompositionDefinition
438
     */
439
    protected function createAccessor(
440
        $accessor,
441
        string $name,
442
        $value,
443
        array $context = []
444
    ): AccessorInterface {
445
        if (is_array($accessor)) {
446
            //We are working with definition of composition.
447
            switch ($accessor[0]) {
448
                case self::ONE:
449
                    //Singular embedded document
450
                    return $this->odm->make($accessor[1], $value, false);
451
                case self::MANY:
452
                    return new DocumentCompositor($accessor[1], $value, $this->odm);
453
            }
454
455
            throw new AccessorException("Invalid accessor definition for field '{$name}'");
456
        }
457
458
        //Field as a context
459
        return parent::createAccessor($accessor, $name, $value, $context + ['odm' => $this->odm]);
460
    }
461
462
    /**
463
     * {@inheritdoc}
464
     */
465
    protected function iocContainer()
466
    {
467
        if ($this->odm instanceof Component) {
468
            //Forwarding IoC scope to parent ODM instance
469
            return $this->odm->iocContainer();
470
        }
471
472
        return parent::iocContainer();
473
    }
474
475
    /**
476
     * @param string $name
477
     */
478
    private function registerChange(string $name)
479
    {
480
        $original = $this->getField($name, null, false);
481
482
        if (!array_key_exists($name, $this->changes)) {
483
            //Let's keep track of how field looked before first change
484
            $this->changes[$name] = $original instanceof AccessorInterface
485
                ? $original->packValue()
486
                : $original;
487
        }
488
    }
489
490
    /**
491
     * @param string $name
492
     *
493
     * @throws AccessException
494
     */
495
    private function assertField(string $name)
496
    {
497
        if (!$this->hasField($name)) {
498
            throw new AccessException(sprintf(
499
                "No such property '%s' in '%s', check schema being relevant",
500
                $name,
501
                get_called_class()
502
            ));
503
        }
504
    }
505
}