1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core\Concern;
4:
5: use Salient\Contract\Core\Entity\Treeable;
6: use Salient\Core\Introspector;
7: use LogicException;
8:
9: /**
10: * Implements Treeable
11: *
12: * @see Treeable
13: */
14: trait TreeableTrait
15: {
16: abstract public static function getParentProperty(): string;
17: abstract public static function getChildrenProperty(): string;
18:
19: /** @var array<class-string<self>,string> */
20: private static $ParentProperties = [];
21: /** @var array<class-string<self>,string> */
22: private static $ChildrenProperties = [];
23:
24: private static function loadHierarchyProperties(): void
25: {
26: $introspector = Introspector::get(static::class);
27:
28: if (!$introspector->IsTreeable) {
29: throw new LogicException(
30: sprintf(
31: '%s does not implement %s or does not return valid parent/child properties',
32: static::class,
33: Treeable::class,
34: )
35: );
36: }
37:
38: self::$ParentProperties[static::class] =
39: $introspector->Properties[$introspector->ParentProperty]
40: ?? $introspector->ParentProperty;
41: self::$ChildrenProperties[static::class] =
42: $introspector->Properties[$introspector->ChildrenProperty]
43: ?? $introspector->ChildrenProperty;
44: }
45:
46: /**
47: * @return static|null
48: */
49: final public function getParent()
50: {
51: if (!isset(self::$ParentProperties[static::class])) {
52: static::loadHierarchyProperties();
53: }
54:
55: $_parent = self::$ParentProperties[static::class];
56:
57: return $this->{$_parent};
58: }
59:
60: /**
61: * @return static[]
62: */
63: final public function getChildren(): array
64: {
65: if (!isset(self::$ChildrenProperties[static::class])) {
66: static::loadHierarchyProperties();
67: }
68:
69: $_children = self::$ChildrenProperties[static::class];
70:
71: return $this->{$_children} ?? [];
72: }
73:
74: /**
75: * @param (Treeable&static)|null $parent
76: * @return $this
77: */
78: final public function setParent($parent)
79: {
80: if (!isset(self::$ParentProperties[static::class])) {
81: static::loadHierarchyProperties();
82: }
83:
84: $_parent = self::$ParentProperties[static::class];
85: $_children = self::$ChildrenProperties[static::class];
86:
87: if ($parent === $this->{$_parent}
88: && ($parent === null
89: || in_array($this, $parent->{$_children} ?: [], true))) {
90: return $this;
91: }
92:
93: // Remove the object from its current parent
94: if ($this->{$_parent} !== null) {
95: $this->{$_parent}->{$_children} =
96: array_values(
97: array_filter(
98: $this->{$_parent}->{$_children},
99: fn($child) => $child !== $this
100: )
101: );
102: }
103:
104: $this->{$_parent} = $parent;
105:
106: if ($parent !== null) {
107: return $this->{$_parent}->{$_children}[] = $this;
108: }
109:
110: return $this;
111: }
112:
113: /**
114: * @param static $child
115: * @return $this
116: */
117: final public function addChild($child)
118: {
119: return $child->setParent($this);
120: }
121:
122: /**
123: * @param static $child
124: * @return $this
125: */
126: final public function removeChild($child)
127: {
128: if (!isset(self::$ParentProperties[static::class])) {
129: static::loadHierarchyProperties();
130: }
131:
132: $_parent = self::$ParentProperties[static::class];
133:
134: if ($child->{$_parent} !== $this) {
135: throw new LogicException('Argument #1 ($child) is not a child of this object');
136: }
137:
138: return $child->setParent(null);
139: }
140:
141: final public function getDepth(): int
142: {
143: if (!isset(self::$ParentProperties[static::class])) {
144: static::loadHierarchyProperties();
145: }
146:
147: $_parent = self::$ParentProperties[static::class];
148:
149: $depth = 0;
150: $parent = $this->{$_parent};
151: while ($parent !== null) {
152: $depth++;
153: $parent = $parent->{$_parent};
154: }
155:
156: return $depth;
157: }
158:
159: final public function countDescendants(): int
160: {
161: if (!isset(self::$ChildrenProperties[static::class])) {
162: static::loadHierarchyProperties();
163: }
164:
165: return $this->doCountDescendants(
166: self::$ChildrenProperties[static::class]
167: );
168: }
169:
170: private function doCountDescendants(string $_children): int
171: {
172: /** @var static[] */
173: $children = $this->{$_children} ?? [];
174:
175: if ($children === []) {
176: return 0;
177: }
178:
179: $count = 0;
180: foreach ($children as $child) {
181: $count += 1 + $child->doCountDescendants($_children);
182: }
183:
184: return $count;
185: }
186: }
187: