1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Core;
6:
7: /**
8: * This trait makes it possible for you to add child objects
9: * into your object.
10: */
11: trait ContainerTrait
12: {
13: /**
14: * Element shortName => object hash of children objects. If the child is not
15: * trackable, then object will be set to "true" (to avoid extra reference).
16: *
17: * @var array<string, object>
18: */
19: public array $elements = [];
20:
21: /** @var array<string, int> */
22: private $_elementNameCounts = [];
23:
24: /**
25: * Returns unique element name based on desired name.
26: */
27: protected function _uniqueElementName(string $desired): string
28: {
29: if (!isset($this->_elementNameCounts[$desired])) {
30: $this->_elementNameCounts[$desired] = 1;
31: $postfix = '';
32: } else {
33: $postfix = '_' . (++$this->_elementNameCounts[$desired]);
34: }
35:
36: return $desired . $postfix;
37: }
38:
39: /**
40: * If you are using ContainerTrait only, then you can safely
41: * use this add() method. If you are also using factory, or
42: * initializer then redefine add() and call _addContainer, _addFactory.
43: *
44: * @param object|array<mixed, mixed> $obj
45: * @param array<mixed, mixed>|string $args
46: */
47: public function add($obj, $args = []): object
48: {
49: $obj = Factory::factory($obj, is_string($args) ? [] : array_diff_key($args, [true, 'desired_name' => true]));
50:
51: $this->_addContainer($obj, is_string($args) ? ['name' => $args] : $args);
52:
53: if (TraitUtil::hasInitializerTrait($obj)) {
54: if (!$obj->isInitialized()) {
55: $obj->invokeInit();
56: }
57: }
58:
59: return $obj;
60: }
61:
62: /**
63: * Extension to add() method which will perform linking of
64: * the object with the current class.
65: *
66: * @param array{desired_name?: string, name?: string} $args
67: */
68: protected function _addContainer(object $element, array $args): void
69: {
70: // carry on reference to application if we have appScopeTraits set
71: if (TraitUtil::hasAppScopeTrait($this) && TraitUtil::hasAppScopeTrait($element)
72: && (!$element->issetApp() || $element->getApp() !== $this->getApp())
73: ) {
74: $element->setApp($this->getApp());
75: }
76:
77: // if element is not trackable, then we don't need to do anything with it
78: if (!TraitUtil::hasTrackableTrait($element)) {
79: return;
80: }
81:
82: // normalize the arguments, bring name out
83: if (isset($args['desired_name'])) {
84: $name = $this->_uniqueElementName($args['desired_name']);
85: unset($args['desired_name']);
86: } elseif (isset($args['name'])) {
87: $name = $args['name'];
88: unset($args['name']);
89: } elseif ($element->shortName !== null) {
90: $name = $this->_uniqueElementName($element->shortName);
91: } else {
92: $desiredName = $element->getDesiredName();
93: $name = $this->_uniqueElementName($desiredName);
94: }
95:
96: if ($args !== []) {
97: throw (new Exception('Add args for DI are no longer supported'))
98: ->addMoreInfo('arg', $args);
99: }
100:
101: // maybe element already exists
102: if (isset($this->elements[$name])) {
103: throw (new Exception('Element with requested name already exists'))
104: ->addMoreInfo('element', $element)
105: ->addMoreInfo('name', $name)
106: ->addMoreInfo('this', $this)
107: ->addMoreInfo('arg2', $args);
108: }
109:
110: $element->setOwner($this);
111: $element->shortName = $name;
112: if (TraitUtil::hasTrackableTrait($this) && TraitUtil::hasNameTrait($this) && TraitUtil::hasNameTrait($element)) {
113: $element->name = $this->_shorten($this->name ?: '', $element->shortName, $element->name); // @phpstan-ignore-line
114: }
115:
116: $this->elements[$element->shortName] = $element;
117: }
118:
119: /**
120: * Remove child element if it exists.
121: *
122: * @param string|object $shortName short name of the element
123: *
124: * @return $this
125: */
126: public function removeElement($shortName)
127: {
128: if (is_object($shortName)) {
129: $shortName = $shortName->shortName;
130: }
131:
132: if (!isset($this->elements[$shortName])) {
133: throw (new Exception('Child element not found'))
134: ->addMoreInfo('parent', $this)
135: ->addMoreInfo('name', $shortName);
136: }
137:
138: unset($this->elements[$shortName]);
139:
140: return $this;
141: }
142:
143: /**
144: * Method used internally for shortening object names.
145: */
146: protected function _shorten(string $ownerName, string $itemShortName, ?string $origItemName): string
147: {
148: $desired = $origItemName ?? $ownerName . '_' . $itemShortName;
149:
150: if (TraitUtil::hasAppScopeTrait($this)
151: && isset($this->getApp()->maxNameLength)
152: && mb_strlen($desired) > $this->getApp()->maxNameLength
153: ) {
154: if ($origItemName !== null) {
155: throw (new Exception('Element has too long desired name'))
156: ->addMoreInfo('name', $origItemName);
157: }
158:
159: $left = mb_strlen($desired) + 35 - $this->getApp()->maxNameLength;
160: $key = mb_substr($desired, 0, $left);
161: $rest = mb_substr($desired, $left);
162:
163: if (!isset($this->getApp()->uniqueNameHashes[$key])) {
164: $this->getApp()->uniqueNameHashes[$key] = '_' . md5($key);
165: }
166: $desired = $this->getApp()->uniqueNameHashes[$key] . '__' . $rest;
167: }
168:
169: return $desired;
170: }
171:
172: /**
173: * Find child element by its short name. Use in chaining.
174: * Exception if not found.
175: *
176: * @param string $shortName Short name of the child element
177: */
178: public function getElement(string $shortName): object
179: {
180: if (!isset($this->elements[$shortName])) {
181: throw (new Exception('Child element not found'))
182: ->addMoreInfo('parent', $this)
183: ->addMoreInfo('element', $shortName);
184: }
185:
186: return $this->elements[$shortName];
187: }
188:
189: /**
190: * @param string $shortName Short name of the child element
191: */
192: public function hasElement($shortName): bool
193: {
194: return isset($this->elements[$shortName]);
195: }
196: }
197: