DependencyTree::reverseIterator()   A
last analyzed

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