Test Failed
Pull Request — master (#11)
by
unknown
02:35
created

HierarchicalTrait::getMasterObject()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
c 0
b 0
f 0
rs 9.584
cc 4
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|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
        return $this;
83
    }
84
85
    /**
86
     * Retrieve this object's immediate parent.
87
     *
88
     * @return string|null
89
     */
90
    public function getMaster()
91
    {
92
        return $this->master;
93
    }
94
95
    /**
96
     * Retrieve this object's immediate parent as object.
97
     * @return HierarchicalInterface|null
98
     * @throws UnexpectedValueException The current object cannot be its own parent.
99
     */
100
    public function getMasterObject()
101
    {
102
        if (!isset($this->masterObject)) {
103
            $master = $this->objFromIdent($this->getMaster());
104
105
            if ($master instanceof ModelInterface) {
106
                if ($master->id() === $this->id()) {
107
                    throw new UnexpectedValueException(sprintf(
108
                        'Can not be ones own parent: %s',
109
                        $master->id()
110
                    ));
111
                }
112
            }
113
114
            $this->resetHierarchy();
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
        error_log(var_export($level, true));
171
172
        return $level;
173
    }
174
175
    /**
176
     * Retrieve the top-level ancestor of this object.
177
     *
178
     * @return HierarchicalInterface|null
179
     */
180
    public function toplevelMaster()
181
    {
182
        $hierarchy = $this->invertedHierarchy();
183
        if (isset($hierarchy[0])) {
184
            return $hierarchy[0];
185
        } else {
186
            return null;
187
        }
188
    }
189
190
    /**
191
     * Determine if this object has any ancestors.
192
     *
193
     * @return boolean
194
     */
195
    public function hasParents()
196
    {
197
        return !!count($this->hierarchy());
198
    }
199
200
    /**
201
     * Retrieve this object's ancestors (from immediate parent to top-level).
202
     *
203
     * @return array
204
     */
205
    public function hierarchy()
206
    {
207
        if (!isset($this->hierarchy)) {
208
            $hierarchy = [];
209
            $master    = $this->getMasterObject();
210
            while ($master) {
211
                $hierarchy[] = $master;
212
                $master      = $master->getMasterObject();
213
            }
214
215
            $this->hierarchy = $hierarchy;
216
        }
217
218
        return $this->hierarchy;
219
    }
220
221
    /**
222
     * Retrieve this object's ancestors, inverted from top-level to immediate.
223
     *
224
     * @return array
225
     */
226
    public function invertedHierarchy()
227
    {
228
        $hierarchy = $this->hierarchy();
229
230
        return array_reverse($hierarchy);
231
    }
232
233
    /**
234
     * Determine if the object is the parent of the given object.
235
     *
236
     * @param mixed $child The child (or ID) to match against.
237
     * @return boolean
238
     */
239
    public function isMasterOf($child)
240
    {
241
        $child = $this->objFromIdent($child);
242
243
        return ($child->getMaster() === $this->id());
244
    }
245
246
    /**
247
     * Determine if the object is a parent/ancestor of the given object.
248
     *
249
     * @param mixed $child The child (or ID) to match against.
250
     * @return boolean
251
     * @todo Implementation needed.
252
     */
253
    public function recursiveIsMasterOf($child)
254
    {
255
        $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...
256
257
        return false;
258
    }
259
260
    /**
261
     * Get wether the object has any children at all
262
     * @return boolean
263
     */
264
    public function hasChildren()
265
    {
266
        $numChildren = $this->numChildren();
267
268
        return ($numChildren > 0);
269
    }
270
271
    /**
272
     * Get the number of children directly under this object.
273
     * @return integer
274
     */
275
    public function numChildren()
276
    {
277
        $children = $this->children();
278
279
        return count($children);
280
    }
281
282
    /**
283
     * Get the total number of children in the entire hierarchy.
284
     * This method counts all children and sub-children, unlike `numChildren()` which only count 1 level.
285
     * @return integer
286
     */
287
    public function recursiveNumChildren()
288
    {
289
        // TODO
290
        return 0;
291
    }
292
293
    /**
294
     * @param array $children The children to set.
295
     * @return HierarchicalInterface Chainable
296
     */
297
    public function setChildren(array $children)
298
    {
299
        $this->children = [];
300
        foreach ($children as $c) {
301
            $this->addChild($c);
302
        }
303
304
        return $this;
305
    }
306
307
    /**
308
     * @param mixed $child The child object (or ident) to add.
309
     * @return HierarchicalInterface Chainable
310
     * @throws UnexpectedValueException The current object cannot be its own child.
311
     */
312
    public function addChild($child)
313
    {
314
        $child = $this->objFromIdent($child);
315
316
        if ($child instanceof ModelInterface) {
317
            if ($child->id() === $this->id()) {
318
                throw new UnexpectedValueException(sprintf(
319
                    'Can not be ones own child: %s',
320
                    $child->id()
321
                ));
322
            }
323
        }
324
325
        $this->children[] = $child;
326
327
        return $this;
328
    }
329
330
    /**
331
     * Get the children directly under this object.
332
     * @return array
333
     */
334
    public function children()
335
    {
336
        if ($this->children !== null) {
337
            return $this->children;
338
        }
339
340
        $this->children = $this->loadChildren();
341
342
        return $this->children;
343
    }
344
345
    /**
346
     * @return array
347
     */
348
    abstract public function loadChildren();
349
350
    /**
351
     * @param mixed $master The master object (or ident) to check against.
352
     * @return boolean
353
     */
354
    public function isChildOf($master)
355
    {
356
        $master = $this->objFromIdent($master);
357
        if ($master === null) {
358
            return false;
359
        }
360
361
        return ($master->id() == $this->getMaster());
362
    }
363
364
    /**
365
     * @param mixed $master The master object (or ident) to check against.
366
     * @return boolean
367
     */
368
    public function recursiveIsChildOf($master)
369
    {
370
        if ($this->isChildOf($master)) {
371
            return true;
372
        }
373
374
        if ($this->hasParents() && $this->getMasterObject()->recursiveIsChildOf($master)) {
375
            return true;
376
        }
377
378
        return false;
379
    }
380
381
    /**
382
     * @return boolean
383
     */
384
    public function hasSiblings()
385
    {
386
        $numSiblings = $this->numSiblings();
387
388
        return ($numSiblings > 1);
389
    }
390
391
    /**
392
     * @return integer
393
     */
394
    public function numSiblings()
395
    {
396
        $siblings = $this->siblings();
397
398
        return count($siblings);
399
    }
400
401
    /**
402
     * Get all the objects on the same level as this one.
403
     * @return array
404
     */
405
    public function siblings()
406
    {
407
        if ($this->siblings !== null) {
408
            return $this->siblings;
409
        }
410
        $master = $this->getMasterObject();
411
        if ($master === null) {
412
            // Todo: return all top-level objects.
413
            $siblings = [];
414
        } else {
415
            // Todo: Remove "current" object from siblings
416
            $siblings = $master->children();
417
        }
418
        $this->siblings = $siblings;
419
420
        return $this->siblings;
421
    }
422
423
    /**
424
     * @param mixed $sibling The sibling to check.
425
     * @return boolean
426
     */
427
    public function isSiblingOf($sibling)
428
    {
429
        $sibling = $this->objFromIdent($sibling);
430
431
        return ($sibling->getMaster() === $this->getMaster());
432
    }
433
434
    /**
435
     * @param mixed $ident The ident.
436
     * @return HierarchicalInterface|null
437
     * @throws InvalidArgumentException If the identifier is not a scalar value.
438
     */
439
    private function objFromIdent($ident)
440
    {
441
        if ($ident === null) {
442
            return null;
443
        }
444
445
        $class = get_called_class();
446
447
        if (is_object($ident) && ($ident instanceof $class)) {
448
            return $ident;
449
        }
450
451
        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...
452
            $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...
453
        }
454
455
        if (!is_scalar($ident)) {
456
            throw new InvalidArgumentException(sprintf(
457
                'Can not load object (not a scalar or a "%s")',
458
                $class
459
            ));
460
        }
461
462
        $cached = $this->loadObjectFromCache($ident);
463
        if ($cached !== null) {
464
            return $cached;
465
        }
466
467
        $obj = $this->loadObjectFromSource($ident);
468
469
        if ($obj !== null) {
470
            $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...
471
        }
472
473
        return $obj;
474
    }
475
476
    /**
477
     * Retrieve an object from the storage source by its ID.
478
     *
479
     * @param mixed $id The object id.
480
     * @return null|HierarchicalInterface
481
     */
482
    private function loadObjectFromSource($id)
483
    {
484
        $obj = $this->modelFactory()->create($this->objType());
485
        $obj->load($id);
486
487
        if ($obj->id()) {
488
            return $obj;
489
        } else {
490
            return null;
491
        }
492
    }
493
494
    /**
495
     * Retrieve an object from the cache store by its ID.
496
     *
497
     * @param mixed $id The object id.
498
     * @return null|HierarchicalInterface
499
     */
500
    private function loadObjectFromCache($id)
501
    {
502
        $objType = $this->objType();
503
        if (isset(static::$objectCache[$objType][$id])) {
504
            return static::$objectCache[$objType][$id];
505
        } else {
506
            return null;
507
        }
508
    }
509
510
    /**
511
     * Add an object to the cache store.
512
     *
513
     * @param ModelInterface $obj The object to store.
514
     * @return HierarchicalInterface Chainable
515
     */
516
    private function addObjectToCache(ModelInterface $obj)
517
    {
518
        static::$objectCache[$this->objType()][$obj->id()] = $obj;
519
520
        return $this;
521
    }
522
523
    /**
524
     * Retrieve the object model factory.
525
     *
526
     * @return \Charcoal\Factory\FactoryInterface
527
     */
528
    abstract public function modelFactory();
529
530
    /**
531
     * @return string
532
     */
533
    abstract public function id();
534
535
    /**
536
     * @return string
537
     */
538
    abstract public function objType();
539
}
540