Test Failed
Push — master ( c4aa6a...a5e42e )
by Mathieu
03:19
created

HierarchicalTrait::getMaster()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
        $master = $this->objFromIdent($master);
342
        if ($master === null) {
343
            return false;
344
        }
345
        // TODO
346
    }
347
348
    /**
349
     * @return boolean
350
     */
351
    public function hasSiblings()
352
    {
353
        $numSiblings = $this->numSiblings();
354
        return ($numSiblings > 1);
355
    }
356
357
    /**
358
     * @return integer
359
     */
360
    public function numSiblings()
361
    {
362
        $siblings = $this->siblings();
363
        return count($siblings);
364
    }
365
366
    /**
367
     * Get all the objects on the same level as this one.
368
     * @return array
369
     */
370
    public function siblings()
371
    {
372
        if ($this->siblings !== null) {
373
            return $this->siblings;
374
        }
375
        $master = $this->getMaster();
376
        if ($master === null) {
377
            // Todo: return all top-level objects.
378
            $siblings = [];
379
        } else {
380
            // Todo: Remove "current" object from siblings
381
            $siblings = $master->children();
382
        }
383
        $this->siblings = $siblings;
384
        return $this->siblings;
385
    }
386
387
    /**
388
     * @param mixed $sibling The sibling to check.
389
     * @return boolean
390
     */
391
    public function isSiblingOf($sibling)
392
    {
393
        $sibling = $this->objFromIdent($sibling);
394
        return ($sibling->getMaster() == $this->getMaster());
395
    }
396
397
    /**
398
     * @param mixed $ident The ident.
399
     * @throws InvalidArgumentException If the identifier is not a scalar value.
400
     * @return HierarchicalInterface|null
401
     */
402
    private function objFromIdent($ident)
403
    {
404
        if ($ident === null) {
405
            return null;
406
        }
407
408
        $class = get_called_class();
409
410
        if (is_object($ident) && ($ident instanceof $class)) {
411
            return $ident;
412
        }
413
414
        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...
415
            $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...
416
        }
417
418
        if (!is_scalar($ident)) {
419
            throw new InvalidArgumentException(sprintf(
420
                'Can not load object (not a scalar or a "%s")',
421
                $class
422
            ));
423
        }
424
425
        $cached = $this->loadObjectFromCache($ident);
426
        if ($cached !== null) {
427
            return $cached;
428
        }
429
430
        $obj = $this->loadObjectFromSource($ident);
431
432
        if ($obj !== null) {
433
            $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...
434
        }
435
436
        return $obj;
437
    }
438
439
    /**
440
     * Retrieve an object from the storage source by its ID.
441
     *
442
     * @param mixed $id The object id.
443
     * @return null|HierarchicalInterface
444
     */
445
    private function loadObjectFromSource($id)
446
    {
447
        $obj = $this->modelFactory()->create($this->objType());
448
        $obj->load($id);
449
450
        if ($obj->id()) {
451
            return $obj;
452
        } else {
453
            return null;
454
        }
455
    }
456
457
    /**
458
     * Retrieve an object from the cache store by its ID.
459
     *
460
     * @param mixed $id The object id.
461
     * @return null|HierarchicalInterface
462
     */
463
    private function loadObjectFromCache($id)
464
    {
465
        $objType = $this->objType();
466
        if (isset(static::$objectCache[$objType][$id])) {
467
            return static::$objectCache[$objType][$id];
468
        } else {
469
            return null;
470
        }
471
    }
472
473
    /**
474
     * Add an object to the cache store.
475
     *
476
     * @param ModelInterface $obj The object to store.
477
     * @return HierarchicalInterface Chainable
478
     */
479
    private function addObjectToCache(ModelInterface $obj)
480
    {
481
        static::$objectCache[$this->objType()][$obj->id()] = $obj;
482
483
        return $this;
484
    }
485
486
    /**
487
     * Retrieve the object model factory.
488
     *
489
     * @return \Charcoal\Factory\FactoryInterface
490
     */
491
    abstract public function modelFactory();
492
493
    /**
494
     * @return string
495
     */
496
    abstract public function id();
497
498
    /**
499
     * @return string
500
     */
501
    abstract public function objType();
502
}
503