Passed
Pull Request — master (#11)
by
unknown
12:10 queued 01:31
created

HierarchicalTrait::getMasterObject()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.3222
c 0
b 0
f 0
cc 5
nc 4
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 string|integer|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
     * The object's parent object, if any, in the hierarchy.
46
     *
47
     * @var HierarchicalInterface|null
48
     */
49
    private $masterObject;
50
51
    /**
52
     * A store of cached objects.
53
     *
54
     * @var ModelInterface[] $objectCache
55
     */
56
    public static $objectCache = [];
57
58
    /**
59
     * Reset this object's hierarchy.
60
     *
61
     * The object's hierarchy can be rebuilt with {@see self::hierarchy()}.
62
     *
63
     * @return HierarchicalInterface Chainable
64
     */
65
    public function resetHierarchy()
66
    {
67
        $this->hierarchy = null;
68
69
        return $this;
70
    }
71
72
    /**
73
     * Set this object's immediate parent.
74
     *
75
     * @param mixed $master The object's parent (or master).
76
     * @return HierarchicalInterface Chainable
77
     */
78
    public function setMaster($master)
79
    {
80
        $this->master = $master;
81
82
        $this->resetHierarchy();
83
84
        return $this;
85
    }
86
87
    /**
88
     * Retrieve this object's immediate parent.
89
     *
90
     * @return string|null
91
     */
92
    public function getMaster()
93
    {
94
        return $this->master;
95
    }
96
97
    /**
98
     * Retrieve this object's immediate parent as object.
99
     * @return HierarchicalInterface|null
100
     * @throws UnexpectedValueException The current object cannot be its own parent.
101
     */
102
    public function getMasterObject()
103
    {
104
        if (!$this->masterObject && $this->hasMaster()) {
105
            $master = $this->objFromIdent($this->getMaster());
106
107
            if ($master instanceof ModelInterface) {
108
                if ($master->id() === $this->id()) {
109
                    throw new UnexpectedValueException(sprintf(
110
                        'Can not be ones own parent: %s',
111
                        $master->id()
112
                    ));
113
                }
114
            }
115
116
            $this->masterObject = $master;
117
        }
118
119
        return $this->masterObject;
120
    }
121
122
    /**
123
     * Determine if this object has a direct parent.
124
     *
125
     * @return boolean
126
     */
127
    public function hasMaster()
128
    {
129
        return ($this->getMaster() !== null);
130
    }
131
132
    /**
133
     * Determine if this object is the head (top-level) of its hierarchy.
134
     *
135
     * Top-level objects do not have a parent (master).
136
     *
137
     * @return boolean
138
     */
139
    public function isTopLevel()
140
    {
141
        return ($this->getMaster() === null);
142
    }
143
144
    /**
145
     * Determine if this object is the tail (last-level) of its hierarchy.
146
     *
147
     * Last-level objects do not have a children.
148
     *
149
     * @return boolean
150
     */
151
    public function isLastLevel()
152
    {
153
        return !$this->hasChildren();
154
    }
155
156
    /**
157
     * Retrieve this object's position (level) in its hierarchy.
158
     *
159
     * Starts at "1" (top-level).
160
     *
161
     * The level is calculated by loading all ancestors with {@see self::hierarchy()}.
162
     *
163
     * @return integer
164
     */
165
    public function hierarchyLevel()
166
    {
167
        $hierarchy = $this->hierarchy();
168
        $level     = (count($hierarchy) + 1);
169
170
        return $level;
171
    }
172
173
    /**
174
     * Retrieve the top-level ancestor of this object.
175
     *
176
     * @return HierarchicalInterface|null
177
     */
178
    public function toplevelMaster()
179
    {
180
        $hierarchy = $this->invertedHierarchy();
181
        if (isset($hierarchy[0])) {
182
            return $hierarchy[0];
183
        } else {
184
            return null;
185
        }
186
    }
187
188
    /**
189
     * Determine if this object has any ancestors.
190
     *
191
     * @return boolean
192
     */
193
    public function hasParents()
194
    {
195
        return !!count($this->hierarchy());
196
    }
197
198
    /**
199
     * Retrieve this object's ancestors (from immediate parent to top-level).
200
     *
201
     * @return array
202
     */
203
    public function hierarchy()
204
    {
205
        if (!isset($this->hierarchy)) {
206
            $hierarchy = [];
207
            $master    = $this->getMasterObject();
208
            while ($master) {
209
                $hierarchy[] = $master;
210
                $master      = $master->getMasterObject();
211
            }
212
213
            $this->hierarchy = $hierarchy;
214
        }
215
216
        return $this->hierarchy;
217
    }
218
219
    /**
220
     * Retrieve this object's ancestors, inverted from top-level to immediate.
221
     *
222
     * @return array
223
     */
224
    public function invertedHierarchy()
225
    {
226
        $hierarchy = $this->hierarchy();
227
228
        return array_reverse($hierarchy);
229
    }
230
231
    /**
232
     * Determine if the object is the parent of the given object.
233
     *
234
     * @param mixed $child The child (or ID) to match against.
235
     * @return boolean
236
     */
237
    public function isMasterOf($child)
238
    {
239
        $child = $this->objFromIdent($child);
240
241
        return ($child->getMaster() === $this->id());
242
    }
243
244
    /**
245
     * Determine if the object is a parent/ancestor of the given object.
246
     *
247
     * @param mixed $child The child (or ID) to match against.
248
     * @return boolean
249
     * @todo Implementation needed.
250
     */
251
    public function recursiveIsMasterOf($child)
252
    {
253
        $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...
254
255
        return false;
256
    }
257
258
    /**
259
     * Get wether the object has any children at all
260
     * @return boolean
261
     */
262
    public function hasChildren()
263
    {
264
        $numChildren = $this->numChildren();
265
266
        return ($numChildren > 0);
267
    }
268
269
    /**
270
     * Get the number of children directly under this object.
271
     * @return integer
272
     */
273
    public function numChildren()
274
    {
275
        $children = $this->children();
276
277
        return count($children);
278
    }
279
280
    /**
281
     * Get the total number of children in the entire hierarchy.
282
     * This method counts all children and sub-children, unlike `numChildren()` which only count 1 level.
283
     * @return integer
284
     */
285
    public function recursiveNumChildren()
286
    {
287
        // TODO
288
        return 0;
289
    }
290
291
    /**
292
     * @param array $children The children to set.
293
     * @return HierarchicalInterface Chainable
294
     */
295
    public function setChildren(array $children)
296
    {
297
        $this->children = [];
298
        foreach ($children as $c) {
299
            $this->addChild($c);
300
        }
301
302
        return $this;
303
    }
304
305
    /**
306
     * @param mixed $child The child object (or ident) to add.
307
     * @return HierarchicalInterface Chainable
308
     * @throws UnexpectedValueException The current object cannot be its own child.
309
     */
310
    public function addChild($child)
311
    {
312
        $child = $this->objFromIdent($child);
313
314
        if ($child instanceof ModelInterface) {
315
            if ($child->id() === $this->id()) {
316
                throw new UnexpectedValueException(sprintf(
317
                    'Can not be ones own child: %s',
318
                    $child->id()
319
                ));
320
            }
321
        }
322
323
        $this->children[] = $child;
324
325
        return $this;
326
    }
327
328
    /**
329
     * Get the children directly under this object.
330
     * @return array
331
     */
332
    public function children()
333
    {
334
        if ($this->children !== null) {
335
            return $this->children;
336
        }
337
338
        $this->children = $this->loadChildren();
339
340
        return $this->children;
341
    }
342
343
    /**
344
     * @return array
345
     */
346
    abstract public function loadChildren();
347
348
    /**
349
     * @param mixed $master The master object (or ident) to check against.
350
     * @return boolean
351
     */
352
    public function isChildOf($master)
353
    {
354
        $master = $this->objFromIdent($master);
355
        if ($master === null) {
356
            return false;
357
        }
358
359
        return ($master->id() === $this->getMaster());
360
    }
361
362
    /**
363
     * @param mixed $master The master object (or ident) to check against.
364
     * @return boolean
365
     */
366
    public function recursiveIsChildOf($master)
367
    {
368
        if ($this->isChildOf($master)) {
369
            return true;
370
        }
371
372
        if ($this->hasParents() && $this->getMasterObject()->recursiveIsChildOf($master)) {
373
            return true;
374
        }
375
376
        return false;
377
    }
378
379
    /**
380
     * @return boolean
381
     */
382
    public function hasSiblings()
383
    {
384
        $numSiblings = $this->numSiblings();
385
386
        return ($numSiblings > 1);
387
    }
388
389
    /**
390
     * @return integer
391
     */
392
    public function numSiblings()
393
    {
394
        $siblings = $this->siblings();
395
396
        return count($siblings);
397
    }
398
399
    /**
400
     * Get all the objects on the same level as this one.
401
     * @return array
402
     */
403
    public function siblings()
404
    {
405
        if ($this->siblings !== null) {
406
            return $this->siblings;
407
        }
408
        $master = $this->getMasterObject();
409
        if ($master === null) {
410
            // Todo: return all top-level objects.
411
            $siblings = [];
412
        } else {
413
            // Todo: Remove "current" object from siblings
414
            $siblings = $master->children();
415
        }
416
        $this->siblings = $siblings;
417
418
        return $this->siblings;
419
    }
420
421
    /**
422
     * @param mixed $sibling The sibling to check.
423
     * @return boolean
424
     */
425
    public function isSiblingOf($sibling)
426
    {
427
        $sibling = $this->objFromIdent($sibling);
428
429
        return ($sibling->getMaster() === $this->getMaster());
430
    }
431
432
    /**
433
     * @param mixed $ident The ident.
434
     * @return HierarchicalInterface|null
435
     * @throws InvalidArgumentException If the identifier is not a scalar value.
436
     */
437
    private function objFromIdent($ident)
438
    {
439
        if ($ident === null) {
440
            return null;
441
        }
442
443
        $class = get_called_class();
444
445
        if (is_object($ident) && ($ident instanceof $class)) {
446
            return $ident;
447
        }
448
449
        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...
450
            $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...
451
        }
452
453
        if (!is_scalar($ident)) {
454
            throw new InvalidArgumentException(sprintf(
455
                'Can not load object (not a scalar or a "%s")',
456
                $class
457
            ));
458
        }
459
460
        $cached = $this->loadObjectFromCache($ident);
461
        if ($cached !== null) {
462
            return $cached;
463
        }
464
465
        $obj = $this->loadObjectFromSource($ident);
466
467
        if ($obj !== null) {
468
            $this->addObjectToCache($obj);
0 ignored issues
show
Compatibility introduced by
$obj of type object<Charcoal\Object\HierarchicalInterface> is not a sub-type of object<Charcoal\Model\ModelInterface>. It seems like you assume a child interface of the interface Charcoal\Object\HierarchicalInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
469
        }
470
471
        return $obj;
472
    }
473
474
    /**
475
     * Retrieve an object from the storage source by its ID.
476
     *
477
     * @param mixed $id The object id.
478
     * @return null|HierarchicalInterface
479
     */
480
    private function loadObjectFromSource($id)
481
    {
482
        $obj = $this->modelFactory()->create($this->objType());
483
        $obj->load($id);
484
485
        if ($obj->id()) {
486
            return $obj;
487
        } else {
488
            return null;
489
        }
490
    }
491
492
    /**
493
     * Retrieve an object from the cache store by its ID.
494
     *
495
     * @param mixed $id The object id.
496
     * @return null|HierarchicalInterface
497
     */
498
    private function loadObjectFromCache($id)
499
    {
500
        $objType = $this->objType();
501
        if (isset(static::$objectCache[$objType][$id])) {
502
            return static::$objectCache[$objType][$id];
503
        } else {
504
            return null;
505
        }
506
    }
507
508
    /**
509
     * Add an object to the cache store.
510
     *
511
     * @param ModelInterface $obj The object to store.
512
     * @return HierarchicalInterface Chainable
513
     */
514
    private function addObjectToCache(ModelInterface $obj)
515
    {
516
        static::$objectCache[$this->objType()][$obj->id()] = $obj;
517
518
        return $this;
519
    }
520
521
    /**
522
     * Retrieve the object model factory.
523
     *
524
     * @return \Charcoal\Factory\FactoryInterface
525
     */
526
    abstract public function modelFactory();
527
528
    /**
529
     * @return string
530
     */
531
    abstract public function id();
532
533
    /**
534
     * @return string
535
     */
536
    abstract public function objType();
537
}
538