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