Completed
Branch feature/pre-split (54fe62)
by Anton
03:12
created

DocumentEntity   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 371
Duplicated Lines 2.96 %

Coupling/Cohesion

Components 3
Dependencies 7

Importance

Changes 0
Metric Value
dl 11
loc 371
c 0
b 0
f 0
rs 8.6206
wmc 50
lcom 3
cbo 7

13 Methods

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

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ODM;
8
9
use Spiral\Core\Component;
10
use Spiral\Core\Exceptions\ScopeException;
11
use Spiral\Core\Traits\SaturateTrait;
12
use Spiral\Models\AccessorInterface;
13
use Spiral\Models\SchematicEntity;
14
use Spiral\Models\Traits\SolidStateTrait;
15
use Spiral\ODM\Entities\DocumentInstantiator;
16
use Spiral\ODM\Exceptions\FieldException;
17
18
/**
19
 * Primary class for spiral ODM, provides ability to pack it's own updates in a form of atomic
20
 * updates.
21
 *
22
 * You can use same properties to configure entity as in DataEntity + schema property.
23
 *
24
 * Example:
25
 *
26
 * class Test extends DocumentEntity
27
 * {
28
 *    const SCHEMA = [
29
 *       'name' => 'string'
30
 *    ];
31
 * }
32
 *
33
 * Configuration properties:
34
 * - schema
35
 * - defaults
36
 * - secured (* by default)
37
 * - fillable
38
 */
39
abstract class DocumentEntity extends SchematicEntity implements CompositableInterface
40
{
41
    use SaturateTrait, SolidStateTrait;
42
43
    /**
44
     * Set of schema sections needed to describe entity behaviour.
45
     */
46
    const SH_INSTANTIATION = 0;
47
    const SH_DEFAULTS      = 1;
48
    const SH_COMPOSITIONS  = 6;
49
    const SH_AGGREGATIONS  = 7;
50
51
    /**
52
     * Constants used to describe aggregation relations (also used internally to identify
53
     * composition).
54
     *
55
     * Example:
56
     * 'items' => [self::MANY => Item::class, ['parentID' => 'key::_id']]
57
     *
58
     * @see DocumentEntity::SCHEMA
59
     */
60
    const MANY = 778;
61
    const ONE  = 899;
62
63
    /**
64
     * Class responsible for instance construction.
65
     */
66
    const INSTANTIATOR = DocumentInstantiator::class;
67
68
    /**
69
     * Document fields, accessors and relations. ODM will generate setters and getters for some
70
     * fields based on their types.
71
     *
72
     * Example, fields:
73
     * const SCHEMA = [
74
     *      '_id'    => 'MongoId', //Primary key field
75
     *      'value'  => 'string',  //Default string field
76
     *      'values' => ['string'] //ScalarArray accessor will be applied for fields like that
77
     * ];
78
     *
79
     * Compositions:
80
     * const SCHEMA = [
81
     *     ...,
82
     *     'child'       => Child::class,   //One document are composited, for example user Profile
83
     *     'many'        => [Child::class]  //Compositor accessor will be applied, allows to
84
     *                                      //composite many document instances
85
     * ];
86
     *
87
     * Documents can extend each other, in this case schema will also be inherited.
88
     *
89
     * Attention, make sure you properly set FILLABLE option in parent class to use constructions
90
     * like:
91
     * $parent->child = [...];
92
     *
93
     * or
94
     * $parent->setFields(['child'=>[...]]);
95
     *
96
     * @var array
97
     */
98
    const SCHEMA = [];
99
100
    /**
101
     * Default field values.
102
     *
103
     * @var array
104
     */
105
    const DEFAULTS = [];
106
107
    /**
108
     * Model behaviour configurations.
109
     */
110
    const SECURED   = '*';
111
    const HIDDEN    = [];
112
    const FILLABLE  = [];
113
    const SETTERS   = [];
114
    const GETTERS   = [];
115
    const ACCESSORS = [];
116
117
    /**
118
     * Document behaviour schema.
119
     *
120
     * @var array
121
     */
122
    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...
123
124
    /**
125
     * Document field updates (changed values).
126
     *
127
     * @var array
128
     */
129
    private $updates = [];
130
131
    /**
132
     * Parent ODM instance, responsible for aggregations and lazy loading operations.
133
     *
134
     * @invisible
135
     * @var ODMInterface
136
     */
137
    protected $odm;
138
139
    /**
140
     * {@inheritdoc}
141
     *
142
     * @param ODMInterface $odm To lazy create nested document ang aggregations.
143
     *
144
     * @throws ScopeException When no ODM instance can be resolved.
145
     */
146
    public function __construct($fields = [], array $schema = null, ODMInterface $odm = null)
147
    {
148
        //We can use global container as fallback if no default values were provided
149
        $this->odm = $this->saturate($odm, ODMInterface::class);
150
151
        //Use supplied schema or fetch one from ODM
152
        $this->schema = !empty($schema) ? $schema : $this->odm->define(
153
            static::class,
154
            ODMInterface::D_SCHEMA
155
        );
156
157
        $fields = is_array($fields) ? $fields : [];
158
        if (!empty($this->schema[self::SH_DEFAULTS])) {
159
            //Merging with default values
160
            $fields = array_replace_recursive($this->schema[self::SH_DEFAULTS], $fields);
161
        }
162
163
        parent::__construct($fields, $this->schema);
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     *
169
     * Tracks field changes.
170
     */
171
    public function setField(string $name, $value, bool $filter = true)
172
    {
173
        if (!$this->hasField($name)) {
174
            //We are only allowing to modify existed fields, this is strict schema
175
            throw new FieldException("Undefined field '{$name}' in '" . static::class . "'");
176
        }
177
178
        //Original field value
179
        $original = $this->getField($name, null, false);
180
181
        parent::setField($name, $value, $filter);
182
183
        if (!array_key_exists($name, $this->updates)) {
184
            //Let's keep track of how field looked before first change
185
            $this->updates[$name] = $original instanceof AccessorInterface
186
                ? $original->packValue()
187
                : $original;
188
        }
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     *
194
     * @param string $field Check once specific field changes.
195
     */
196
    public function hasUpdates(string $field = null): bool
197
    {
198
        //Check updates for specific field
199
        if (!empty($field)) {
200
            if (array_key_exists($field, $this->updates)) {
201
                return true;
202
            }
203
204
            //Do not force accessor creation
205
            $value = $this->getField($field, null, false);
206
            if ($value instanceof CompositableInterface && $value->hasUpdates()) {
207
                return true;
208
            }
209
210
            return false;
211
        }
212
213
        if (!empty($this->updates)) {
214
            return true;
215
        }
216
217
        //Do not force accessor creation
218
        foreach ($this->getFields(false) as $value) {
219
            //Checking all fields for changes (handled internally)
220
            if ($value instanceof CompositableInterface && $value->hasUpdates()) {
221
                return true;
222
            }
223
        }
224
225
        return false;
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function buildAtomics(string $container = null): array
232
    {
233
        if (!$this->hasUpdates() && !$this->isSolid()) {
234
            return [];
235
        }
236
237
        if ($this->isSolid()) {
238
            if (!empty($container)) {
239
                //Simple nested document in solid state
240
                return ['$set' => $this->packValue(false)];
241
            }
242
243
            //No parent container
244
            return ['$set' => $this->packValue(false)];
245
        }
246
247
        //Aggregate atomics from every nested composition
248
        $atomics = [];
249
250
        foreach ($this->getFields(false) as $field => $value) {
251
            if ($value instanceof CompositableInterface) {
252
                $atomics = array_merge_recursive(
253
                    $atomics,
254
                    $value->buildAtomics((!empty($container) ? $container . '.' : '') . $field)
255
                );
256
257
                continue;
258
            }
259
260
            foreach ($atomics as $atomic => $operations) {
261
                if (array_key_exists($field, $operations) && $atomic != '$set') {
262
                    //Property already changed by atomic operation
263
                    continue;
264
                }
265
            }
266
267
            if (array_key_exists($field, $this->updates)) {
268
                //Generating set operation for changed field
269
                $atomics['$set'][(!empty($container) ? $container . '.' : '') . $field] = $value;
270
            }
271
        }
272
273
        return $atomics;
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function flushUpdates()
280
    {
281
        $this->updates = [];
282
283
        foreach ($this->getFields(false) as $value) {
284
            if ($value instanceof CompositableInterface) {
285
                $value->flushUpdates();
286
            }
287
        }
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     *
293
     * @param bool $includeID Set to false to exclude _id from packed fields.
294
     */
295
    public function packValue(bool $includeID = true)
296
    {
297
        $values = parent::packValue();
298
299
        if (!$includeID) {
300
            unset($values['_id']);
301
        }
302
303
        return $values;
304
    }
305
306
    /**
307
     * Since most of ODM documents might contain ObjectIDs and other fields we will try to normalize
308
     * them into string values.
309
     *
310
     * @return array
311
     */
312
    public function publicFields(): array
313
    {
314
        return parent::publicFields();
315
    }
316
317
    /**
318
     * Cloning will be called when object will be embedded into another document.
319
     */
320
    public function __clone()
321
    {
322
        //Since document embedded as one piece let's ensure that it is solid
323
        $this->solidState = true;
324
325
        //De-serialize document in order to ensure that all compositions are recreated
326
        $this->setValue($this->packValue());
327
    }
328
329
    /**
330
     * @return array
331
     */
332
    public function __debugInfo()
333
    {
334
        return [
335
            'fields'  => $this->getFields(),
336
            'atomics' => $this->hasUpdates() ? $this->buildAtomics() : []
337
        ];
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     */
343 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...
344
    {
345
        /**
346
         * Every document composition is valid accessor but defined a bit differently.
347
         */
348
        if (isset($this->schema[self::SH_COMPOSITIONS][$field])) {
349
            return $this->schema[self::SH_COMPOSITIONS][$field];
350
        }
351
352
        return parent::getMutator($field, $mutator);
353
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358
    protected function isNullable(string $field): bool
359
    {
360
        if (array_key_exists($field, $this->schema[self::SH_DEFAULTS])) {
361
            //Only fields with default null value can be nullable
362
            return is_null($this->schema[self::SH_DEFAULTS][$field]);
363
        }
364
365
        //You can redefine custom logic to indicate what fields are nullable
366
        return false;
367
    }
368
369
    /**
370
     * {@inheritdoc}
371
     *
372
     * DocumentEntity will pass ODM instance as part of accessor context.
373
     */
374
    protected function createAccessor(
375
        $accessor,
376
        string $field,
377
        $value,
378
        array $context = []
379
    ): AccessorInterface {
380
        if (is_array($accessor)) {
381
            /**
382
             * We are working with definition of composition.
383
             */
384
            switch ($accessor[0]) {
385
                case self::ONE:
386
                    //Singular embedded document
387
                    return $this->odm->instantiate($accessor[1], $value);
388
                case self::MANY:
389
                    return 'compositor';
390
            }
391
        }
392
393
        //Field as a context
394
        return parent::createAccessor($accessor, $field, $value, $context + ['odm' => $this->odm]);
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    protected function iocContainer()
401
    {
402
        if (!empty($this->odm) || $this->odm instanceof Component) {
403
            //Forwarding IoC scope to parent ODM instance
404
            return $this->odm->iocContainer();
405
        }
406
407
        return parent::iocContainer();
408
    }
409
}