1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core\Concern;
4:
5: use Salient\Contract\Core\Entity\Treeable;
6: use Salient\Core\Reflection\ClassReflection;
7: use Salient\Utility\Exception\ShouldNotHappenException;
8: use InvalidArgumentException;
9:
10: /**
11: * @api
12: *
13: * @phpstan-require-implements Treeable
14: */
15: trait TreeableTrait
16: {
17: /** @var array<class-string<self>,array{string,string}> */
18: private static array $TreeableProperties;
19:
20: /**
21: * @inheritDoc
22: */
23: public static function getRelationships(): array
24: {
25: return [];
26: }
27:
28: /**
29: * @inheritDoc
30: */
31: public function getParent()
32: {
33: [$_parent] = self::$TreeableProperties[static::class]
34: ?? self::getTreeableProperties();
35: return $this->{$_parent} ?? null;
36: }
37:
38: /**
39: * @inheritDoc
40: */
41: public function getChildren(): array
42: {
43: [, $_children] = self::$TreeableProperties[static::class]
44: ?? self::getTreeableProperties();
45: return $this->{$_children} ?? [];
46: }
47:
48: /**
49: * @inheritDoc
50: */
51: public function setParent($parent)
52: {
53: [$_parent, $_children] = self::$TreeableProperties[static::class]
54: ?? self::getTreeableProperties();
55:
56: if ($parent === ($this->{$_parent} ?? null) && (
57: $parent === null
58: || in_array($this, $parent->{$_children} ?? [], true)
59: )) {
60: return $this;
61: }
62:
63: if (isset($this->{$_parent})) {
64: $this->{$_parent}->{$_children} = array_values(array_filter(
65: $this->{$_parent}->{$_children} ?? [],
66: fn($child) => $child !== $this,
67: ));
68: }
69:
70: $this->{$_parent} = $parent;
71: if ($parent !== null) {
72: $parent->{$_children}[] = $this;
73: }
74: return $this;
75: }
76:
77: /**
78: * @inheritDoc
79: */
80: public function addChild($child)
81: {
82: return $child->setParent($this);
83: }
84:
85: /**
86: * @inheritDoc
87: */
88: public function removeChild($child)
89: {
90: if ($child->getParent() !== $this) {
91: throw new InvalidArgumentException('Invalid child');
92: }
93: return $child->setParent(null);
94: }
95:
96: /**
97: * @inheritDoc
98: */
99: public function getDepth(): int
100: {
101: [$_parent] = self::$TreeableProperties[static::class]
102: ?? self::getTreeableProperties();
103: $depth = 0;
104: $parent = $this;
105: while ($parent = $parent->{$_parent}) {
106: $depth++;
107: }
108: return $depth;
109: }
110:
111: /**
112: * @inheritDoc
113: */
114: public function countDescendants(): int
115: {
116: [, $_children] = self::$TreeableProperties[static::class]
117: ?? self::getTreeableProperties();
118: return $this->doCountDescendants($_children);
119: }
120:
121: private function doCountDescendants(string $_children): int
122: {
123: /** @var static[] */
124: $children = $this->{$_children} ?? [];
125: if (!$children) {
126: return 0;
127: }
128: $count = 0;
129: foreach ($children as $child) {
130: $count += 1 + $child->doCountDescendants($_children);
131: }
132: return $count;
133: }
134:
135: /**
136: * @return array{string,string}
137: */
138: private static function getTreeableProperties(): array
139: {
140: $class = new ClassReflection(static::class);
141: if ($class->isTreeable()) {
142: return self::$TreeableProperties[static::class] = [
143: $class->getParentProperty(),
144: $class->getChildrenProperty(),
145: ];
146: }
147:
148: // @codeCoverageIgnoreStart
149: throw new ShouldNotHappenException(sprintf(
150: '%s does not implement %s',
151: static::class,
152: Treeable::class,
153: ));
154: // @codeCoverageIgnoreEnd
155: }
156: }
157: