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

DocumentEntity::iocContainer()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 0
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * 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\SchematicEntity;
15
use Spiral\Models\Traits\SolidStateTrait;
16
use Spiral\ODM\Entities\DocumentCompositor;
17
use Spiral\ODM\Entities\DocumentInstantiator;
18
use Spiral\ODM\Exceptions\AccessorException;
19
use Spiral\ODM\Exceptions\AggregationException;
20
use Spiral\ODM\Exceptions\FieldException;
21
use Spiral\ODM\Helpers\AggregationHelper;
22
use Spiral\ODM\Schemas\Definitions\CompositionDefinition;
23
24
/**
25
 * Primary class for spiral ODM, provides ability to pack it's own updates in a form of atomic
26
 * updates.
27
 *
28
 * You can use same properties to configure entity as in DataEntity + schema property.
29
 *
30
 * Example:
31
 *
32
 * class Test extends DocumentEntity
33
 * {
34
 *    const SCHEMA = [
35
 *       'name' => 'string'
36
 *    ];
37
 * }
38
 *
39
 * Configuration properties:
40
 * - schema
41
 * - defaults
42
 * - secured (* by default)
43
 * - fillable
44
 */
45
abstract class DocumentEntity extends SchematicEntity implements CompositableInterface
46
{
47
    use SaturateTrait, SolidStateTrait;
48
49
    /**
50
     * Set of schema sections needed to describe entity behaviour.
51
     */
52
    const SH_INSTANTIATION = 0;
53
    const SH_DEFAULTS      = 1;
54
    const SH_COMPOSITIONS  = 6;
55
    const SH_AGGREGATIONS  = 7;
56
57
    /**
58
     * Constants used to describe aggregation relations (also used internally to identify
59
     * composition).
60
     *
61
     * Example:
62
     * 'items' => [self::MANY => Item::class, ['parentID' => 'key::_id']]
63
     *
64
     * @see DocumentEntity::SCHEMA
65
     */
66
    const MANY = 778;
67
    const ONE  = 899;
68
69
    /**
70
     * Class responsible for instance construction.
71
     */
72
    const INSTANTIATOR = DocumentInstantiator::class;
73
74
    /**
75
     * Document fields, accessors and relations. ODM will generate setters and getters for some
76
     * fields based on their types.
77
     *
78
     * Example, fields:
79
     * const SCHEMA = [
80
     *      '_id'    => 'MongoId', //Primary key field
81
     *      'value'  => 'string',  //Default string field
82
     *      'values' => ['string'] //ScalarArray accessor will be applied for fields like that
83
     * ];
84
     *
85
     * Compositions:
86
     * const SCHEMA = [
87
     *     ...,
88
     *     'child'       => Child::class,   //One document are composited, for example user Profile
89
     *     'many'        => [Child::class]  //Compositor accessor will be applied, allows to
90
     *                                      //composite many document instances
91
     * ];
92
     *
93
     * Documents can extend each other, in this case schema will also be inherited.
94
     *
95
     * Attention, make sure you properly set FILLABLE option in parent class to use constructions
96
     * like:
97
     * $parent->child = [...];
98
     *
99
     * or
100
     * $parent->setFields(['child'=>[...]]);
101
     *
102
     * @var array
103
     */
104
    const SCHEMA = [];
105
106
    /**
107
     * Default field values.
108
     *
109
     * @var array
110
     */
111
    const DEFAULTS = [];
112
113
    /**
114
     * Model behaviour configurations.
115
     */
116
    const SECURED   = '*';
117
    const HIDDEN    = [];
118
    const FILLABLE  = [];
119
    const SETTERS   = [];
120
    const GETTERS   = [];
121
    const ACCESSORS = [];
122
123
    /**
124
     * Document behaviour schema.
125
     *
126
     * @var array
127
     */
128
    private $schema = [];
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
129
130
    /**
131
     * Document field updates (changed values).
132
     *
133
     * @var array
134
     */
135
    private $updates = [];
136
137
    /**
138
     * Parent ODM instance, responsible for aggregations and lazy loading operations.
139
     *
140
     * @invisible
141
     * @var ODMInterface
142
     */
143
    protected $odm;
144
145
    /**
146
     * {@inheritdoc}
147
     *
148
     * @param ODMInterface $odm To lazy create nested document ang aggregations.
149
     *
150
     * @throws ScopeException When no ODM instance can be resolved.
151
     */
152
    public function __construct($fields = [], array $schema = null, ODMInterface $odm = null)
153
    {
154
        //We can use global container as fallback if no default values were provided
155
        $this->odm = $this->saturate($odm, ODMInterface::class);
156
157
        //Use supplied schema or fetch one from ODM
158
        $this->schema = !empty($schema) ? $schema : $this->odm->define(
159
            static::class,
160
            ODMInterface::D_SCHEMA
161
        );
162
163
        $fields = is_array($fields) ? $fields : [];
164
        if (!empty($this->schema[self::SH_DEFAULTS])) {
165
            //Merging with default values
166
            $fields = array_replace_recursive($this->schema[self::SH_DEFAULTS], $fields);
167
        }
168
169
        parent::__construct($fields, $this->schema);
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     *
175
     * Tracks field changes.
176
     */
177
    public function setField(string $name, $value, bool $filter = true)
178
    {
179
        if (!$this->hasField($name)) {
180
            //We are only allowing to modify existed fields, this is strict schema
181
            throw new FieldException("Undefined field '{$name}' in '" . static::class . "'");
182
        }
183
184
        //Original field value
185
        $original = $this->getField($name, null, false);
186
187
        parent::setField($name, $value, $filter);
188
189
        if (!array_key_exists($name, $this->updates)) {
190
            //Let's keep track of how field looked before first change
191
            $this->updates[$name] = $original instanceof AccessorInterface
192
                ? $original->packValue()
193
                : $original;
194
        }
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     *
200
     * Will restore default value if presented.
201
     */
202
    public function __unset($offset)
203
    {
204
        if (!$this->isNullable($offset)) {
205
            throw new FieldException("Unable to unset not nullable field '{$offset}'");
206
        }
207
208
        $this->setField($offset, null, false);
209
    }
210
211
    /**
212
     * Provides ability to invoke document aggregation.
213
     *
214
     * @param string $method
215
     * @param array  $arguments
216
     *
217
     * @return mixed|null|AccessorInterface|CompositableInterface|Document|Entities\DocumentSelector
218
     */
219
    public function __call(string $method, array $arguments)
220
    {
221
        if (isset($this->schema[self::SH_AGGREGATIONS][$method])) {
222
            if (!empty($arguments)) {
223
                throw new AggregationException("Aggregation method call except 0 parameters");
224
            }
225
226
            $helper = new AggregationHelper($this, $this->schema, $this->odm);
227
228
            return $helper->createAggregation($method);
229
        }
230
231
        return parent::__call($method, $arguments);
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     *
237
     * @param string $field Check once specific field changes.
238
     */
239
    public function hasUpdates(string $field = null): bool
240
    {
241
        //Check updates for specific field
242
        if (!empty($field)) {
243
            if (array_key_exists($field, $this->updates)) {
244
                return true;
245
            }
246
247
            //Do not force accessor creation
248
            $value = $this->getField($field, null, false);
249
            if ($value instanceof CompositableInterface && $value->hasUpdates()) {
250
                return true;
251
            }
252
253
            return false;
254
        }
255
256
        if (!empty($this->updates)) {
257
            return true;
258
        }
259
260
        //Do not force accessor creation
261
        foreach ($this->getFields(false) as $value) {
262
            //Checking all fields for changes (handled internally)
263
            if ($value instanceof CompositableInterface && $value->hasUpdates()) {
264
                return true;
265
            }
266
        }
267
268
        return false;
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function buildAtomics(string $container = null): array
275
    {
276
        if (!$this->hasUpdates() && !$this->isSolid()) {
277
            return [];
278
        }
279
280
        if ($this->isSolid()) {
281
            if (!empty($container)) {
282
                //Simple nested document in solid state
283
                return ['$set' => $this->packValue(false)];
284
            }
285
286
            //No parent container
287
            return ['$set' => $this->packValue(false)];
288
        }
289
290
        //Aggregate atomics from every nested composition
291
        $atomics = [];
292
293
        foreach ($this->getFields(false) as $field => $value) {
294
            if ($value instanceof CompositableInterface) {
295
                $atomics = array_merge_recursive(
296
                    $atomics,
297
                    $value->buildAtomics((!empty($container) ? $container . '.' : '') . $field)
298
                );
299
300
                continue;
301
            }
302
303
            foreach ($atomics as $atomic => $operations) {
304
                if (array_key_exists($field, $operations) && $atomic != '$set') {
305
                    //Property already changed by atomic operation
306
                    continue;
307
                }
308
            }
309
310
            if (array_key_exists($field, $this->updates)) {
311
                //Generating set operation for changed field
312
                $atomics['$set'][(!empty($container) ? $container . '.' : '') . $field] = $value;
313
            }
314
        }
315
316
        return $atomics;
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322
    public function flushUpdates()
323
    {
324
        $this->updates = [];
325
326
        foreach ($this->getFields(false) as $value) {
327
            if ($value instanceof CompositableInterface) {
328
                $value->flushUpdates();
329
            }
330
        }
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     *
336
     * @param bool $includeID Set to false to exclude _id from packed fields.
337
     */
338
    public function packValue(bool $includeID = true)
339
    {
340
        $values = parent::packValue();
341
342
        if (!$includeID) {
343
            unset($values['_id']);
344
        }
345
346
        return $values;
347
    }
348
349
    /**
350
     * Since most of ODM documents might contain ObjectIDs and other fields we will try to normalize
351
     * them into string values.
352
     *
353
     * @return array
354
     */
355
    public function publicFields(): array
356
    {
357
        $public = parent::publicFields();
358
359
        array_walk_recursive($public, function (&$value) {
360
            if ($value instanceof ObjectID) {
1 ignored issue
show
Bug introduced by
The class MongoDB\BSON\ObjectID does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
361
                $value = (string)$value;
362
            }
363
        });
364
365
        return $public;
366
    }
367
368
    /**
369
     * Cloning will be called when object will be embedded into another document.
370
     */
371
    public function __clone()
372
    {
373
        //Since document embedded as one piece let's ensure that it is solid
374
        $this->solidState = true;
375
376
        //De-serialize document in order to ensure that all compositions are recreated
377
        $this->setValue($this->packValue());
378
    }
379
380
    /**
381
     * @return array
382
     */
383
    public function __debugInfo()
384
    {
385
        return [
386
            'fields'  => $this->getFields(),
387
            'atomics' => $this->hasUpdates() ? $this->buildAtomics() : []
388
        ];
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     *
394
     * @see CompositionDefinition
395
     */
396 View Code Duplication
    protected function getMutator(string $field, string $mutator)
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...
397
    {
398
        /**
399
         * Every document composition is valid accessor but defined a bit differently.
400
         */
401
        if (isset($this->schema[self::SH_COMPOSITIONS][$field])) {
402
            return $this->schema[self::SH_COMPOSITIONS][$field];
403
        }
404
405
        return parent::getMutator($field, $mutator);
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411
    protected function isNullable(string $field): bool
412
    {
413
        if (array_key_exists($field, $this->schema[self::SH_DEFAULTS])) {
414
            //Only fields with default null value can be nullable
415
            return is_null($this->schema[self::SH_DEFAULTS][$field]);
416
        }
417
418
        //You can redefine custom logic to indicate what fields are nullable
419
        return false;
420
    }
421
422
    /**
423
     * {@inheritdoc}
424
     *
425
     * DocumentEntity will pass ODM instance as part of accessor context.
426
     *
427
     * @see CompositionDefinition
428
     */
429
    protected function createAccessor(
430
        $accessor,
431
        string $field,
432
        $value,
433
        array $context = []
434
    ): AccessorInterface {
435
        if (is_array($accessor)) {
436
            //We are working with definition of composition.
437
            switch ($accessor[0]) {
438
                case self::ONE:
439
                    //Singular embedded document
440
                    return $this->odm->instantiate($accessor[1], $value);
441
                case self::MANY:
442
                    return new DocumentCompositor($accessor[1], $value, $this->odm);
443
            }
444
445
            throw new AccessorException("Invalid accessor definition for field '{$field}'");
446
        }
447
448
        //Field as a context
449
        return parent::createAccessor($accessor, $field, $value, $context + ['odm' => $this->odm]);
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455
    protected function iocContainer()
456
    {
457
        if (!empty($this->odm) || $this->odm instanceof Component) {
458
            //Forwarding IoC scope to parent ODM instance
459
            return $this->odm->iocContainer();
460
        }
461
462
        return parent::iocContainer();
463
    }
464
}