Passed
Branch master (3daac1)
by Vincent
07:53
created

DependencyTree::offsetExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Bdf\Form\Aggregate\Collection;
4
5
use Bdf\Form\Aggregate\ChildAggregateInterface;
6
use Bdf\Form\Child\ChildInterface;
7
use Iterator;
8
9
/**
10
 * Handle form children dependencies
11
 *
12
 * All element are align on a "level"
13
 * - The "root" level contains all children without dependencies, or the newest elements
14
 * - All child will be added to the "root" level
15
 * - When a child become a dependency of a new added element, it will be shift to higher level (recursively on dependencies of the dependency)
16
 * - A dependency can be registered BEFORE be added on the tree (its depth will be computed before)
17
 *
18
 * Example :
19
 *
20
 * add(E1())       -> lvl0(E1)                          [E1 added to lvl0]
21
 * add(E2())       -> lvl0(E1, E2)                      [E2 added to lvl0]
22
 * add(E3(E2))     -> lvl0(E1, E3), lvl1(E2)            [E3 added to lvl0, E2 shift to lvl1]
23
 * add(E4(E2, E3)) -> lvl0(E1, E4), lvl1(E3), lvl2(E2)  [E4 added to lvl0, E3 shift to lvl1, E2 shift to lvl2]
24
 *
25
 * The iterator of the dependency tree will iterate over "higher" dependencies before, and "root" children at the end
26
 */
27
final class DependencyTree implements \ArrayAccess, \IteratorAggregate, \Countable, ChildrenCollectionInterface
28
{
29
    /**
30
     * @var ChildInterface[]
31
     */
32
    private $children = [];
33
34
    /**
35
     * The first level of dependencies
36
     *
37
     * @var Level
38
     */
39
    private $root;
40
41
    /**
42
     * The last level of dependencies
43
     *
44
     * @var Level
45
     */
46
    private $last;
47
48
    /**
49
     * Get the level of each elements
50
     * An element can be register on the dependency tree, but not in the children array
51
     *
52
     * @var int[]
53
     */
54
    private $depth = [];
55
56
57
    /**
58
     * DependencyTree constructor.
59
     */
60 20
    public function __construct()
61
    {
62 20
        $this->root = new Level();
63 20
        $this->last = $this->root;
64 20
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69 20
    public function add(ChildInterface $child): void
70
    {
71 20
        $this->addNamed($child->name(), $child);
72 20
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 4
    public function has(string $name): bool
78
    {
79 4
        return isset($this->children[$name]);
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     *
85
     * @psalm-suppress PossiblyNullReference
86
     */
87 4
    public function remove(string $name): bool
88
    {
89 4
        if (!$this->has($name)) {
90 1
            return false;
91
        }
92
93 3
        $level = $this->level($name);
94
95 3
        if ($level->number() === 0) {
96
            // The child is not part of a dependency
97
            // Web can remove from index safely
98 2
            $level->remove($name);
99 2
            unset($this->depth[$name]);
100
        } else {
101
            // The child is part of a dependency
102
            // Keep it in the index, but remove its dependencies
103 1
            $level->reset($name);
104
        }
105
106 3
        unset($this->children[$name]);
107
108 3
        return true;
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114 1
    public function offsetExists($offset)
115
    {
116 1
        return $this->has($offset);
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 2
    public function offsetGet($offset)
123
    {
124 2
        return $this->children[$offset];
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130 1
    public function offsetSet($offset, $value)
131
    {
132 1
        $this->add($value);
133 1
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 1
    public function offsetUnset($offset)
139
    {
140 1
        $this->remove($offset);
141 1
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 1
    public function count()
147
    {
148 1
        return count($this->children);
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 3
    public function getIterator(): Iterator
155
    {
156 3
        return new \ArrayIterator($this->children);
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 3
    public function reverseIterator(): Iterator
163
    {
164 3
        return new DependencyIterator($this->children, $this->last, true);
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 2
    public function forwardIterator(): Iterator
171
    {
172 2
        return new DependencyIterator($this->children, $this->root, false);
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 3
    public function all(): array
179
    {
180 3
        return $this->children;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186 1
    public function duplicate(ChildAggregateInterface $newParent): ChildrenCollectionInterface
187
    {
188 1
        $children = [];
189
190 1
        foreach ($this->children as $key => $child) {
191 1
            $children[$key] = $child->setParent($newParent);
192
        }
193
194 1
        $collection = clone $this;
195
196 1
        $collection->children = $children;
197
198 1
        return $collection;
199
    }
200
201
    /**
202
     * Get the level which the child should be added
203
     *
204
     * @param ChildInterface|string $child
205
     *
206
     * @return Level
207
     * @psalm-suppress PossiblyNullReference
208
     * @psalm-suppress InvalidNullableReturnType
209
     * @psalm-suppress NullableReturnStatement
210
     */
211 20
    protected function level($child)
212
    {
213 20
        if (!is_string($child)) {
214 20
            $child = $child->name();
215
        }
216
217 20
        if (!isset($this->depth[$child])) {
218 20
            return $this->root;
219
        }
220
221 9
        $target = $this->depth[$child];
222 9
        $level  = $this->root;
223
224 9
        while ($target > $level->number()) {
225 7
            $level = $level->next();
226
        }
227
228 9
        return $level;
229
    }
230
231
    /**
232
     * Add a child to the dependency tree
233
     *
234
     * @param string $name
235
     * @param ChildInterface $child
236
     */
237 20
    protected function addNamed($name, ChildInterface $child): void
238
    {
239 20
        $this->children[$name] = $child;
240
241 20
        $level = $this->level($child);
242
243 20
        $this->depth = array_merge($this->depth, $level->add(
244 20
            $name,
245 20
            $this->extractDependencies($child, $level)
246
        ));
247
248 20
        $this->last = $level->last() ?: $level;
249 20
    }
250
251
    /**
252
     * Extract dependencies from a child element
253
     * Remove dependencies that has been already registered and with higher level than child
254
     *
255
     * @param ChildInterface $child
256
     * @param Level $level
257
     *
258
     * @return string[]
259
     */
260 20
    protected function extractDependencies(ChildInterface $child, Level $level)
261
    {
262 20
        $dependencies = [];
263
264 20
        foreach ($child->dependencies() as $dependency) {
265 15
            if (!isset($this->depth[$dependency])) {
266 7
                $dependencies[] = $dependency;
267 7
                continue;
268
            }
269
270 15
            if ($this->depth[$dependency] > $level->number()) {
271 10
                continue;
272
            }
273
274 14
            $dependencies[] = $dependency;
275
        }
276
277 20
        return $dependencies;
278
    }
279
}
280