Completed
Branch feature/pre-split (cb15b4)
by Anton
03:23
created

DocumentEntity   C

Complexity

Total Complexity 59

Size/Duplication

Total Lines 458
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 12

Importance

Changes 0
Metric Value
dl 0
loc 458
rs 6.1904
c 0
b 0
f 0
wmc 59
lcom 2
cbo 12

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
A getField() 0 12 3
A setField() 0 15 2
A __unset() 0 8 2
A __call() 0 14 3
D hasUpdates() 0 31 9
C buildAtomics() 0 44 13
A packValue() 0 10 2
A __clone() 0 9 1
A __debugInfo() 0 7 2
A getMutator() 0 11 2
A isNullable() 0 10 2
B createAccessor() 0 22 4
A iocContainer() 0 9 2
A registerChange() 0 11 3
A flushUpdates() 0 10 3
A publicValue() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like DocumentEntity often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentEntity, and based on these observations, apply Extract Interface, too.

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\SchematicEntity;
15
use Spiral\Models\Traits\SolidableTrait;
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\DocumentException;
21
use Spiral\ODM\Exceptions\FieldException;
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 $fields = [], 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
        $fields = is_array($fields) ? $fields : [];
175
        if (!empty($this->documentSchema[self::SH_DEFAULTS])) {
176
            //Merging with default values
177
            $fields = array_replace_recursive($this->documentSchema[self::SH_DEFAULTS], $fields);
178
        }
179
180
        parent::__construct($fields, $this->documentSchema);
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function getField(string $name, $default = null, bool $filter = true)
187
    {
188
        if (!$this->hasField($name) && !isset($this->documentSchema[self::SH_COMPOSITIONS][$name])) {
189
            throw new FieldException(sprintf(
190
                "No such property '%s' in '%s', check schema being relevant",
191
                $name,
192
                get_called_class()
193
            ));
194
        }
195
196
        return parent::getField($name, $default, $filter);
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     *
202
     * Tracks field changes.
203
     */
204
    public function setField(string $name, $value, bool $filter = true)
205
    {
206
        if (!$this->hasField($name)) {
207
            //We are only allowing to modify existed fields, this is strict schema
208
            throw new FieldException(sprintf(
209
                "No such property '%s' in '%s', check schema being relevant",
210
                $name,
211
                get_called_class()
212
            ));
213
        }
214
215
        $this->registerChange($name);
216
217
        parent::setField($name, $value, $filter);
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     *
223
     * Will restore default value if presented.
224
     */
225
    public function __unset($offset)
226
    {
227
        if (!$this->isNullable($offset)) {
228
            throw new FieldException("Unable to unset not nullable field '{$offset}'");
229
        }
230
231
        $this->setField($offset, null, false);
232
    }
233
234
    /**
235
     * Provides ability to invoke document aggregation.
236
     *
237
     * @param string $method
238
     * @param array  $arguments
239
     *
240
     * @return mixed|null|AccessorInterface|CompositableInterface|Document|Entities\DocumentSelector
241
     */
242
    public function __call($method, array $arguments)
243
    {
244
        if (isset($this->documentSchema[self::SH_AGGREGATIONS][$method])) {
245
            if (!empty($arguments)) {
246
                throw new AggregationException("Aggregation method call except 0 parameters");
247
            }
248
249
            $helper = new AggregationHelper($this, $this->odm);
250
251
            return $helper->createAggregation($method);
252
        }
253
254
        throw new DocumentException("Undefined method call '{$method}' in '" . get_called_class() . "'");
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     *
260
     * @param string $field Check once specific field changes.
261
     */
262
    public function hasUpdates(string $field = null): bool
263
    {
264
        //Check updates for specific field
265
        if (!empty($field)) {
266
            if (array_key_exists($field, $this->changes)) {
267
                return true;
268
            }
269
270
            //Do not force accessor creation
271
            $value = $this->getField($field, null, false);
272
            if ($value instanceof CompositableInterface && $value->hasUpdates()) {
273
                return true;
274
            }
275
276
            return false;
277
        }
278
279
        if (!empty($this->changes)) {
280
            return true;
281
        }
282
283
        //Do not force accessor creation
284
        foreach ($this->getFields(false) as $value) {
285
            //Checking all fields for changes (handled internally)
286
            if ($value instanceof CompositableInterface && $value->hasUpdates()) {
287
                return true;
288
            }
289
        }
290
291
        return false;
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297
    public function buildAtomics(string $container = null): array
298
    {
299
        if (!$this->hasUpdates() && !$this->isSolid()) {
300
            return [];
301
        }
302
303
        if ($this->isSolid()) {
304
            if (!empty($container)) {
305
                //Simple nested document in solid state
306
                return ['$set' => [$container => $this->packValue(false)]];
307
            }
308
309
            //No parent container
310
            return ['$set' => $this->packValue(false)];
311
        }
312
313
        //Aggregate atomics from every nested composition
314
        $atomics = [];
315
316
        foreach ($this->getFields(false) as $field => $value) {
317
            if ($value instanceof CompositableInterface) {
318
                $atomics = array_merge_recursive(
319
                    $atomics,
320
                    $value->buildAtomics((!empty($container) ? $container . '.' : '') . $field)
321
                );
322
323
                continue;
324
            }
325
326
            foreach ($atomics as $atomic => $operations) {
327
                if (array_key_exists($field, $operations) && $atomic != '$set') {
328
                    //Property already changed by atomic operation
329
                    continue;
330
                }
331
            }
332
333
            if (array_key_exists($field, $this->changes)) {
334
                //Generating set operation for changed field
335
                $atomics['$set'][(!empty($container) ? $container . '.' : '') . $field] = $value;
336
            }
337
        }
338
339
        return $atomics;
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345
    public function flushUpdates()
346
    {
347
        $this->changes = [];
348
349
        foreach ($this->getFields(false) as $field => $value) {
350
            if ($value instanceof CompositableInterface) {
351
                $value->flushUpdates();
352
            }
353
        }
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     *
359
     * @param bool $includeID Set to false to exclude _id from packed fields.
360
     */
361
    public function packValue(bool $includeID = true)
362
    {
363
        $values = parent::packValue();
364
365
        if (!$includeID) {
366
            unset($values['_id']);
367
        }
368
369
        return $values;
370
    }
371
372
    /**
373
     * Since most of ODM documents might contain ObjectIDs and other fields we will try to normalize
374
     * them into string values.
375
     *
376
     * @return array
377
     */
378
    public function publicValue(): array
379
    {
380
        $public = parent::publicValue();
381
382
        array_walk_recursive($public, function (&$value) {
383
            if ($value instanceof ObjectID) {
384
                $value = (string)$value;
385
            }
386
        });
387
388
        return $public;
389
    }
390
391
    /**
392
     * Cloning will be called when object will be embedded into another document.
393
     */
394
    public function __clone()
395
    {
396
        //De-serialize document in order to ensure that all compositions are recreated
397
        $this->stateValue($this->packValue());
398
399
        //Since document embedded as one piece let's ensure that it is solid
400
        $this->solidState = true;
401
        $this->changes = [];
402
    }
403
404
    /**
405
     * @return array
406
     */
407
    public function __debugInfo()
408
    {
409
        return [
410
            'fields'  => $this->getFields(),
411
            'atomics' => $this->hasUpdates() ? $this->buildAtomics() : [],
412
        ];
413
    }
414
415
    /**
416
     * {@inheritdoc}
417
     *
418
     * @see CompositionDefinition
419
     */
420
    protected function getMutator(string $field, string $mutator)
421
    {
422
        /**
423
         * Every document composition is valid accessor but defined a bit differently.
424
         */
425
        if (isset($this->documentSchema[self::SH_COMPOSITIONS][$field])) {
426
            return $this->documentSchema[self::SH_COMPOSITIONS][$field];
427
        }
428
429
        return parent::getMutator($field, $mutator);
430
    }
431
432
    /**
433
     * {@inheritdoc}
434
     */
435
    protected function isNullable(string $field): bool
436
    {
437
        if (array_key_exists($field, $this->documentSchema[self::SH_DEFAULTS])) {
438
            //Only fields with default null value can be nullable
439
            return is_null($this->documentSchema[self::SH_DEFAULTS][$field]);
440
        }
441
442
        //Values unknown to schema always nullable
443
        return true;
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     *
449
     * DocumentEntity will pass ODM instance as part of accessor context.
450
     *
451
     * @see CompositionDefinition
452
     */
453
    protected function createAccessor(
454
        $accessor,
455
        string $name,
456
        $value,
457
        array $context = []
458
    ): AccessorInterface {
459
        if (is_array($accessor)) {
460
            //We are working with definition of composition.
461
            switch ($accessor[0]) {
462
                case self::ONE:
463
                    //Singular embedded document
464
                    return $this->odm->make($accessor[1], $value, false);
465
                case self::MANY:
466
                    return new DocumentCompositor($accessor[1], $value, $this->odm);
467
            }
468
469
            throw new AccessorException("Invalid accessor definition for field '{$name}'");
470
        }
471
472
        //Field as a context
473
        return parent::createAccessor($accessor, $name, $value, $context + ['odm' => $this->odm]);
474
    }
475
476
    /**
477
     * {@inheritdoc}
478
     */
479
    protected function iocContainer()
480
    {
481
        if ($this->odm instanceof Component) {
482
            //Forwarding IoC scope to parent ODM instance
483
            return $this->odm->iocContainer();
484
        }
485
486
        return parent::iocContainer();
487
    }
488
489
    /**
490
     * @param string $name
491
     */
492
    private function registerChange(string $name)
493
    {
494
        $original = $this->getField($name, null, false);
495
496
        if (!array_key_exists($name, $this->changes)) {
497
            //Let's keep track of how field looked before first change
498
            $this->changes[$name] = $original instanceof AccessorInterface
499
                ? $original->packValue()
500
                : $original;
501
        }
502
    }
503
}