Completed
Push — master ( 695b4d...bec134 )
by
unknown
12:54
created

HierarchicalTrait   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 493
Duplicated Lines 6.9 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 3
dl 34
loc 493
rs 6.4799
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
loadChildren() 0 1 ?
A resetHierarchy() 0 6 1
A setMaster() 18 19 3
A hasMaster() 0 4 1
A isTopLevel() 0 4 1
A isLastLevel() 0 4 1
A hierarchyLevel() 0 7 1
A toplevelMaster() 0 9 2
A hasParents() 0 4 1
A hierarchy() 0 15 3
A invertedHierarchy() 0 5 1
A isMasterOf() 0 5 1
A recursiveIsMasterOf() 0 6 1
A hasChildren() 0 5 1
A numChildren() 0 5 1
A recursiveNumChildren() 0 5 1
A setChildren() 0 8 2
A addChild() 16 17 3
A children() 0 9 2
A isChildOf() 0 8 2
A getMaster() 0 4 1
A recursiveIsChildOf() 0 12 4
A hasSiblings() 0 5 1
A numSiblings() 0 5 1
A siblings() 0 16 3
A isSiblingOf() 0 5 1
B objFromIdent() 0 36 9
A loadObjectFromSource() 0 11 2
A loadObjectFromCache() 0 9 2
A addObjectToCache() 0 6 1
modelFactory() 0 1 ?
id() 0 1 ?
objType() 0 1 ?

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 HierarchicalTrait 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 HierarchicalTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Charcoal\Object;
4
5
use InvalidArgumentException;
6
use UnexpectedValueException;
7
8
// From 'charcoal-core'
9
use Charcoal\Model\ModelInterface;
10
11
/**
12
 * Full implementation, as a trait, of the `HierarchicalInterface`
13
 */
14
trait HierarchicalTrait
15
{
16
    /**
17
     * The object's parent, if any, in the hierarchy.
18
     *
19
     * @var HierarchicalInterface|null
20
     */
21
    protected $master;
22
23
    /**
24
     * Store a copy of the object's ancestry.
25
     *
26
     * @var HierarchicalInterface[]|null
27
     */
28
    private $hierarchy = null;
29
30
    /**
31
     * Store a copy of the object's descendants.
32
     *
33
     * @var HierarchicalInterface[]|null
34
     */
35
    private $children;
36
37
    /**
38
     * Store a copy of the object's siblings.
39
     *
40
     * @var HierarchicalInterface[]|null
41
     */
42
    private $siblings;
43
44
    /**
45
     * A store of cached objects.
46
     *
47
     * @var ModelInterface[] $objectCache
48
     */
49
    public static $objectCache = [];
50
51
    /**
52
     * Reset this object's hierarchy.
53
     *
54
     * The object's hierarchy can be rebuilt with {@see self::hierarchy()}.
55
     *
56
     * @return HierarchicalInterface Chainable
57
     */
58
    public function resetHierarchy()
59
    {
60
        $this->hierarchy = null;
61
62
        return $this;
63
    }
64
65
    /**
66
     * Set this object's immediate parent.
67
     *
68
     * @param  mixed $master The object's parent (or master).
69
     * @throws UnexpectedValueException The current object cannot be its own parent.
70
     * @return HierarchicalInterface Chainable
71
     */
72 View Code Duplication
    public function setMaster($master)
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...
73
    {
74
        $master = $this->objFromIdent($master);
75
76
        if ($master instanceof ModelInterface) {
77
            if ($master->id() === $this->id()) {
78
                throw new UnexpectedValueException(sprintf(
79
                    'Can not be ones own parent: %s',
80
                    $master->id()
81
                ));
82
            }
83
        }
84
85
        $this->master = $master;
86
87
        $this->resetHierarchy();
88
89
        return $this;
90
    }
91
92
    /**
93
     * Retrieve this object's immediate parent.
94
     *
95
     * @return HierarchicalInterface|null
96
     */
97
    public function getMaster()
98
    {
99
        return $this->master;
100
    }
101
102
    /**
103
     * Determine if this object has a direct parent.
104
     *
105
     * @return boolean
106
     */
107
    public function hasMaster()
108
    {
109
        return ($this->getMaster() !== null);
110
    }
111
112
    /**
113
     * Determine if this object is the head (top-level) of its hierarchy.
114
     *
115
     * Top-level objects do not have a parent (master).
116
     *
117
     * @return boolean
118
     */
119
    public function isTopLevel()
120
    {
121
        return ($this->getMaster() === null);
122
    }
123
124
    /**
125
     * Determine if this object is the tail (last-level) of its hierarchy.
126
     *
127
     * Last-level objects do not have a children.
128
     *
129
     * @return boolean
130
     */
131
    public function isLastLevel()
132
    {
133
        return !$this->hasChildren();
134
    }
135
136
    /**
137
     * Retrieve this object's position (level) in its hierarchy.
138
     *
139
     * Starts at "1" (top-level).
140
     *
141
     * The level is calculated by loading all ancestors with {@see self::hierarchy()}.
142
     *
143
     * @return integer
144
     */
145
    public function hierarchyLevel()
146
    {
147
        $hierarchy = $this->hierarchy();
148
        $level = (count($hierarchy) + 1);
149
150
        return $level;
151
    }
152
153
    /**
154
     * Retrieve the top-level ancestor of this object.
155
     *
156
     * @return HierarchicalInterface|null
157
     */
158
    public function toplevelMaster()
159
    {
160
        $hierarchy = $this->invertedHierarchy();
161
        if (isset($hierarchy[0])) {
162
            return $hierarchy[0];
163
        } else {
164
            return null;
165
        }
166
    }
167
168
    /**
169
     * Determine if this object has any ancestors.
170
     *
171
     * @return boolean
172
     */
173
    public function hasParents()
174
    {
175
        return !!count($this->hierarchy());
176
    }
177
178
    /**
179
     * Retrieve this object's ancestors (from immediate parent to top-level).
180
     *
181
     * @return array
182
     */
183
    public function hierarchy()
184
    {
185
        if (!isset($this->hierarchy)) {
186
            $hierarchy = [];
187
            $master = $this->getMaster();
188
            while ($master) {
189
                $hierarchy[] = $master;
190
                $master = $master->getMaster();
191
            }
192
193
            $this->hierarchy = $hierarchy;
194
        }
195
196
        return $this->hierarchy;
197
    }
198
199
    /**
200
     * Retrieve this object's ancestors, inverted from top-level to immediate.
201
     *
202
     * @return array
203
     */
204
    public function invertedHierarchy()
205
    {
206
        $hierarchy = $this->hierarchy();
207
        return array_reverse($hierarchy);
208
    }
209
210
    /**
211
     * Determine if the object is the parent of the given object.
212
     *
213
     * @param mixed $child The child (or ID) to match against.
214
     * @return boolean
215
     */
216
    public function isMasterOf($child)
217
    {
218
        $child = $this->objFromIdent($child);
219
        return ($child->getMaster() == $this);
220
    }
221
222
    /**
223
     * Determine if the object is a parent/ancestor of the given object.
224
     *
225
     * @param mixed $child The child (or ID) to match against.
226
     * @return boolean
227
     * @todo Implementation needed.
228
     */
229
    public function recursiveIsMasterOf($child)
230
    {
231
        $child = $this->objFromIdent($child);
0 ignored issues
show
Unused Code introduced by
$child is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
232
233
        return false;
234
    }
235
236
    /**
237
     * Get wether the object has any children at all
238
     * @return boolean
239
     */
240
    public function hasChildren()
241
    {
242
        $numChildren = $this->numChildren();
243
        return ($numChildren > 0);
244
    }
245
246
    /**
247
     * Get the number of children directly under this object.
248
     * @return integer
249
     */
250
    public function numChildren()
251
    {
252
        $children = $this->children();
253
        return count($children);
254
    }
255
256
    /**
257
     * Get the total number of children in the entire hierarchy.
258
     * This method counts all children and sub-children, unlike `numChildren()` which only count 1 level.
259
     * @return integer
260
     */
261
    public function recursiveNumChildren()
262
    {
263
        // TODO
264
        return 0;
265
    }
266
267
    /**
268
     * @param array $children The children to set.
269
     * @return HierarchicalInterface Chainable
270
     */
271
    public function setChildren(array $children)
272
    {
273
        $this->children = [];
274
        foreach ($children as $c) {
275
            $this->addChild($c);
276
        }
277
        return $this;
278
    }
279
280
    /**
281
     * @param mixed $child The child object (or ident) to add.
282
     * @throws UnexpectedValueException The current object cannot be its own child.
283
     * @return HierarchicalInterface Chainable
284
     */
285 View Code Duplication
    public function addChild($child)
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...
286
    {
287
        $child = $this->objFromIdent($child);
288
289
        if ($child instanceof ModelInterface) {
290
            if ($child->id() === $this->id()) {
291
                throw new UnexpectedValueException(sprintf(
292
                    'Can not be ones own child: %s',
293
                    $child->id()
294
                ));
295
            }
296
        }
297
298
        $this->children[] = $child;
299
300
        return $this;
301
    }
302
303
    /**
304
     * Get the children directly under this object.
305
     * @return array
306
     */
307
    public function children()
308
    {
309
        if ($this->children !== null) {
310
            return $this->children;
311
        }
312
313
        $this->children = $this->loadChildren();
314
        return $this->children;
315
    }
316
317
    /**
318
     * @return array
319
     */
320
    abstract public function loadChildren();
321
322
    /**
323
     * @param mixed $master The master object (or ident) to check against.
324
     * @return boolean
325
     */
326
    public function isChildOf($master)
327
    {
328
        $master = $this->objFromIdent($master);
329
        if ($master === null) {
330
            return false;
331
        }
332
        return ($master == $this->getMaster());
333
    }
334
335
    /**
336
     * @param mixed $master The master object (or ident) to check against.
337
     * @return boolean
338
     */
339
    public function recursiveIsChildOf($master)
340
    {
341
        if ($this->isChildOf($master)) {
342
            return true;
343
        }
344
345
        if ($this->hasParents() && $this->getMaster()->recursiveIsChildOf($master)) {
346
            return true;
347
        }
348
349
        return false;
350
    }
351
352
    /**
353
     * @return boolean
354
     */
355
    public function hasSiblings()
356
    {
357
        $numSiblings = $this->numSiblings();
358
        return ($numSiblings > 1);
359
    }
360
361
    /**
362
     * @return integer
363
     */
364
    public function numSiblings()
365
    {
366
        $siblings = $this->siblings();
367
        return count($siblings);
368
    }
369
370
    /**
371
     * Get all the objects on the same level as this one.
372
     * @return array
373
     */
374
    public function siblings()
375
    {
376
        if ($this->siblings !== null) {
377
            return $this->siblings;
378
        }
379
        $master = $this->getMaster();
380
        if ($master === null) {
381
            // Todo: return all top-level objects.
382
            $siblings = [];
383
        } else {
384
            // Todo: Remove "current" object from siblings
385
            $siblings = $master->children();
386
        }
387
        $this->siblings = $siblings;
388
        return $this->siblings;
389
    }
390
391
    /**
392
     * @param mixed $sibling The sibling to check.
393
     * @return boolean
394
     */
395
    public function isSiblingOf($sibling)
396
    {
397
        $sibling = $this->objFromIdent($sibling);
398
        return ($sibling->getMaster() == $this->getMaster());
399
    }
400
401
    /**
402
     * @param mixed $ident The ident.
403
     * @throws InvalidArgumentException If the identifier is not a scalar value.
404
     * @return HierarchicalInterface|null
405
     */
406
    private function objFromIdent($ident)
407
    {
408
        if ($ident === null) {
409
            return null;
410
        }
411
412
        $class = get_called_class();
413
414
        if (is_object($ident) && ($ident instanceof $class)) {
415
            return $ident;
416
        }
417
418
        if (is_array($ident) && isset($ident[$this->key()])) {
0 ignored issues
show
Bug introduced by
It seems like key() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
419
            $ident = $ident[$this->key()];
0 ignored issues
show
Bug introduced by
It seems like key() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
420
        }
421
422
        if (!is_scalar($ident)) {
423
            throw new InvalidArgumentException(sprintf(
424
                'Can not load object (not a scalar or a "%s")',
425
                $class
426
            ));
427
        }
428
429
        $cached = $this->loadObjectFromCache($ident);
430
        if ($cached !== null) {
431
            return $cached;
432
        }
433
434
        $obj = $this->loadObjectFromSource($ident);
435
436
        if ($obj !== null) {
437
            $this->addObjectToCache($obj);
0 ignored issues
show
Documentation introduced by
$obj is of type object<Charcoal\Object\HierarchicalInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
438
        }
439
440
        return $obj;
441
    }
442
443
    /**
444
     * Retrieve an object from the storage source by its ID.
445
     *
446
     * @param mixed $id The object id.
447
     * @return null|HierarchicalInterface
448
     */
449
    private function loadObjectFromSource($id)
450
    {
451
        $obj = $this->modelFactory()->create($this->objType());
452
        $obj->load($id);
453
454
        if ($obj->id()) {
455
            return $obj;
456
        } else {
457
            return null;
458
        }
459
    }
460
461
    /**
462
     * Retrieve an object from the cache store by its ID.
463
     *
464
     * @param mixed $id The object id.
465
     * @return null|HierarchicalInterface
466
     */
467
    private function loadObjectFromCache($id)
468
    {
469
        $objType = $this->objType();
470
        if (isset(static::$objectCache[$objType][$id])) {
471
            return static::$objectCache[$objType][$id];
472
        } else {
473
            return null;
474
        }
475
    }
476
477
    /**
478
     * Add an object to the cache store.
479
     *
480
     * @param ModelInterface $obj The object to store.
481
     * @return HierarchicalInterface Chainable
482
     */
483
    private function addObjectToCache(ModelInterface $obj)
484
    {
485
        static::$objectCache[$this->objType()][$obj->id()] = $obj;
486
487
        return $this;
488
    }
489
490
    /**
491
     * Retrieve the object model factory.
492
     *
493
     * @return \Charcoal\Factory\FactoryInterface
494
     */
495
    abstract public function modelFactory();
496
497
    /**
498
     * @return string
499
     */
500
    abstract public function id();
501
502
    /**
503
     * @return string
504
     */
505
    abstract public function objType();
506
}
507