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, but unlike "ContainerTrait" you can use
10: * multiple collections stored as different array properties.
11: *
12: * This class does not offer automatic naming, so if you try
13: * to add another element with same name, it will result in
14: * exception.
15: */
16: trait CollectionTrait
17: {
18: /**
19: * Use this method trait like this:.
20: *
21: * function addField($name, $definition)
22: * {
23: * $field = Field::fromSeed($seed);
24: *
25: * return $this->_addIntoCollection($name, $field, 'fields');
26: * }
27: *
28: * @param string $collection property name
29: */
30: protected function _addIntoCollection(string $name, object $item, string $collection): object
31: {
32: if (!isset($this->{$collection}) || !is_array($this->{$collection})) {
33: throw (new Exception('Collection does not exist'))
34: ->addMoreInfo('collection', $collection);
35: }
36:
37: if ($name === '') {
38: throw (new Exception('Empty name is not supported'))
39: ->addMoreInfo('collection', $collection)
40: ->addMoreInfo('name', $name);
41: }
42:
43: if ($this->_hasInCollection($name, $collection)) {
44: throw (new Exception('Element with the same name already exists in the collection'))
45: ->addMoreInfo('collection', $collection)
46: ->addMoreInfo('name', $name);
47: }
48:
49: // carry on reference to application if we have appScopeTraits set
50: if ((TraitUtil::hasAppScopeTrait($this) && TraitUtil::hasAppScopeTrait($item))
51: && (!$item->issetApp() || $item->getApp() !== $this->getApp())
52: ) {
53: $item->setApp($this->getApp());
54: }
55:
56: // calculate long "name" but only if both are trackables
57: if (TraitUtil::hasTrackableTrait($item)) {
58: $item->shortName = $name;
59: $item->setOwner($this);
60: if (TraitUtil::hasTrackableTrait($this) && TraitUtil::hasNameTrait($this) && TraitUtil::hasNameTrait($item)) {
61: $item->name = $this->_shortenMl($this->name . '-' . $collection, $item->shortName, $item->name); // @phpstan-ignore-line
62: }
63: }
64:
65: if (TraitUtil::hasInitializerTrait($item)) {
66: if (!$item->isInitialized()) {
67: $item->invokeInit();
68: }
69: }
70:
71: $this->{$collection}[$name] = $item;
72:
73: return $item;
74: }
75:
76: /**
77: * Removes element from specified collection.
78: *
79: * @param string $collection property name
80: */
81: protected function _removeFromCollection(string $name, string $collection): void
82: {
83: if (!$this->_hasInCollection($name, $collection)) {
84: throw (new Exception('Element is not in the collection'))
85: ->addMoreInfo('collection', $collection)
86: ->addMoreInfo('name', $name);
87: }
88:
89: unset($this->{$collection}[$name]);
90: }
91:
92: /**
93: * Call this on collections after cloning object. This will clone all collection
94: * elements (which are objects).
95: *
96: * @param string $collectionName property name to be cloned
97: */
98: protected function _cloneCollection(string $collectionName): void
99: {
100: $this->{$collectionName} = array_map(function ($item) {
101: $item = clone $item;
102: if (TraitUtil::hasTrackableTrait($item) && $item->issetOwner()) {
103: $item->unsetOwner()->setOwner($this);
104: }
105:
106: return $item;
107: }, $this->{$collectionName});
108: }
109:
110: /**
111: * Returns true if and only if collection exists and object with given name is presented in it.
112: *
113: * @param string $collection property name
114: */
115: protected function _hasInCollection(string $name, string $collection): bool
116: {
117: return isset($this->{$collection}[$name]);
118: }
119:
120: /**
121: * @param string $collection property name
122: */
123: protected function _getFromCollection(string $name, string $collection): object
124: {
125: $res = $this->{$collection}[$name] ?? null;
126: if ($res === null) {
127: throw (new Exception('Element is not in the collection'))
128: ->addMoreInfo('collection', $collection)
129: ->addMoreInfo('name', $name);
130: }
131:
132: return $res;
133: }
134:
135: /**
136: * Method used internally for shortening object names.
137: *
138: * Identical implementation to ContainerTrait::_shorten.
139: */
140: protected function _shortenMl(string $ownerName, string $itemShortName, ?string $origItemName): string
141: {
142: // ugly hack to deduplicate code
143: $collectionTraitHelper = new class() {
144: use AppScopeTrait;
145: use ContainerTrait;
146:
147: public function shorten(?object $app, string $ownerName, string $itemShortName, ?string $origItemName): string
148: {
149: try {
150: $this->setApp($app);
151:
152: return $this->_shorten($ownerName, $itemShortName, $origItemName);
153: } finally {
154: $this->_app = null; // important for GC
155: }
156: }
157: };
158:
159: return $collectionTraitHelper->shorten(TraitUtil::hasAppScopeTrait($this) ? $this->getApp() : null, $ownerName, $itemShortName, $origItemName);
160: }
161: }
162: