Tree::removeNodeFromList()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 11
rs 9.4285
cc 3
eloc 7
nc 3
nop 1
1
<?php
2
3
namespace Nayjest\Tree;
4
5
use Nayjest\Collection\Extended\Registry;
6
use Nayjest\Collection\Extended\RegistryInterface;
7
use Nayjest\Tree\Exception\NodeNotFoundException;
8
use Nayjest\Tree\Utils\TreeBuilder;
9
10
/**
11
 * Class for working with tree.
12
 *
13
 * ==== [ RU ] ====
14
 * Класс для работы с деревом.
15
 *
16
 * Особенности реализации:
17
 *  (1) Раздельно хранит реестр именованных узлов (1-a) и конфигурацию дерева (1-b)
18
 *  (2) Замораживает связи между узлами, описанными в конфигурации
19
 * Фичи:
20
 *   1-a.1 Доступ к узлам дерева по имени [ $tree->nodes()->get('name') ]
21
 *         Решает задачи:
22
 *           * Доступ к функциональным(именованным) узлам дерева
23
 *         Альтернативы:
24
 *           * Pекурсивный поиск по свойству
25
 *             + есть из коробки
26
 *             - плохая производительность
27
 *           * Хранение ссылок на узлы (возможно заслуживает внимания)
28
 *             - нужно реализовать обновление ссылок при замене узлов
29
 *         Cons/Pros
30
 *         + Быстрее рекурсивного поиска
31
 *         + Поиск ограничивается реестром, что исключает конфликты имен с элементами нижележащих деревьев
32
 *         - сложнее реализация (рекурсивный поиск есть из коробки)
33
 *   1-a.2 Можно определить функциональную принадлежность узла конкретному дереву
34
 *   1-b.1 Позволяет заменить функциональный узел с сохранением функционвльных потомков
35
 *   2. Выборочное замораживание структуры дерева
36
 *      Решает проблему:
37
 *       Поддержка целосности структуры, однозначность операций т. к. открыто 2 API для модификации дерева
38
 *       ситуации:
39
 *         * узел удален из дерева через node api, но остался в $hierarchy: после $tree->update() он опять туда попадет
40
 *         * через api узлов можно сломать структуру, ожидаемую от дерева, т. е. нельзя завязаться на уонкретное дерево
41
 *      Альтернативы:
42
 *          * Сокрытие дочерних элементов
43
 *            - нельзя вообще никак модифицировать/читать структуру
44
 *          * Readonly root.children
45
 *            - не решает проблему
46
 *          * Readonly children of all nodes in registry
47
 *            - не проще в реализации, но налагает излишние ограничения
48
 *      Cons/Pros
49
 *        + позволяет свободно модифицировать узлы, не считая связей, определенных деревом
50
 *        + Через API узлов нельзя сломать структуру дерева
51
 */
52
class Tree
53
{
54
    const NODE_EXISTS = 1;
55
    const NODE_ADDED = 2;
56
    const PARENT_NODE_NOT_FOUND = 3;
57
58
    /**
59
     * @var array
60
     */
61
    private $hierarchy;
62
63
    /**
64
     * @var NodeInterface[]|RegistryInterface
65
     */
66
    private $nodes;
67
68
    /**
69
     * @var TreeBuilder
70
     */
71
    private $builder;
72
73
    /**
74
     * @var bool
75
     */
76
    private $updateRequired = true;
77
78
    /**
79
     * @var ParentNodeInterface
80
     */
81
    private $root;
82
83
    /**
84
     * Tree constructor.
85
     *
86
     * @param ParentNodeInterface $root
87
     * @param array               $hierarchy
88
     * @param array|Registry      $nodes
89
     */
90
    public function __construct(
91
        ParentNodeInterface $root,
92
        array $hierarchy,
93
        $nodes
94
    ) {
95
        $this->hierarchy = $hierarchy;
96
        $this->nodes = $nodes instanceof Registry ? $nodes : new Registry($nodes);
97
        $this->builder = Utils::getDefaultTreeBuilder();
98
        $this->root = $root;
99
    }
100
101
    /**
102
     * @return ParentNodeInterface
103
     */
104
    public function getRoot()
105
    {
106
        return $this->root;
107
    }
108
109
    /**
110
     * @return NodeInterface[]
111
     */
112
    public function getNodes()
113
    {
114
        return $this->nodes->toArray();
115
    }
116
117
    /**
118
     * @return array
119
     */
120
    public function getHierarchy()
121
    {
122
        return $this->hierarchy;
123
    }
124
125
    /**
126
     * Builds tree based on its nodes registry and hierarchy configuration
127
     * if structure update required.
128
     */
129
    public function build()
130
    {
131
        if ($this->updateRequired) {
132
            $this->updateRequired = false;
133
            foreach ($this->nodes as $node) {
134
                if ($node->parent()) {
135
                    $node->unlock()->detach();
136
                }
137
            }
138
            $this->root->addChildren($this->builder->build($this->hierarchy, $this->nodes->toArray()));
139
            foreach ($this->nodes as $node) {
140
                if ($node->parent() === $this->root || $this->nodes->contains($node->parent())) {
141
                    $node->lock();
142
                }
143
            }
144
        }
145
    }
146
147
    /**
148
     * @param string|null $parentName
149
     * @param string $nodeName
150
     * @param ChildNodeInterface $node
151
     * @return Tree
152
     */
153
    public function append($parentName = null, $nodeName, ChildNodeInterface $node)
154
    {
155
        return $this->add($parentName, $nodeName, $node, false);
156
    }
157
158
    /**
159
     * @param string|null $parentName
160
     * @param string $nodeName
161
     * @param ChildNodeInterface $node
162
     * @return Tree
163
     */
164
    public function prepend($parentName = null, $nodeName, ChildNodeInterface $node)
165
    {
166
        return $this->add($parentName, $nodeName, $node, true);
167
    }
168
169
    /**
170
     * Replaces named tree node to new one.
171
     *
172
     * @param string             $nodeName
173
     * @param ChildNodeInterface $node
174
     *
175
     * @return $this
176
     */
177
    public function replace($nodeName, ChildNodeInterface $node)
178
    {
179
        $this->removeNodeFromList($nodeName);
180
        $this->nodes->set($nodeName, $node);
181
        $this->updateRequired = true;
182
183
        return $this;
184
    }
185
186
    /**
187
     * Adds multiple nodes to tree.
188
     *
189
     * @param string|null $parentName root node will be used if null
190
     * @param array $namedItems array with nodes where keys are node names
191
     * @param bool $prepend if true, nodes will be prepended, otherwise appended to parent
192
     * @return $this
193
     */
194
    public function addMany($parentName, array $namedItems, $prepend = false)
195
    {
196
        foreach ($namedItems as $name => $item) {
197
            $this->add($parentName, $name, $item, $prepend);
198
        }
199
200
        return $this;
201
    }
202
203
    /**
204
     * Finds node by its name.
205
     *
206
     * @param string $nodeName
207
     * @return null|object
208
     */
209
    public function get($nodeName)
210
    {
211
        return $this->nodes->get($nodeName);
212
    }
213
214
    /**
215
     * Returns true if tree contains node with specified name, returns false otherwise.
216
     *
217
     * @param string $nodeName
218
     * @return bool
219
     */
220
    public function has($nodeName)
221
    {
222
        return $this->nodes->hasKey($nodeName);
223
    }
224
225
    /**
226
     * Moves node to another parent.
227
     *
228
     * @param string $nodeName target node name
229
     * @param string|null $newParentName parent node name;  root will be used if null
230
     * @param bool $prepend
231
     * @return $this
232
     */
233
    public function move($nodeName, $newParentName, $prepend = false)
234
    {
235
        if (!$this->nodes->hasKey($nodeName)) {
236
            throw new NodeNotFoundException();
237
        }
238
        $node = $this->nodes->get($nodeName);
239
        $this->remove($nodeName);
240
        $this->add($newParentName, $nodeName, $node, $prepend);
241
242
        return $this;
243
    }
244
245
    /**
246
     * Removes node by its name.
247
     *
248
     * @param string $nodeName
249
     *
250
     * @return $this
251
     */
252
    public function remove($nodeName)
253
    {
254
        $children = self::removeTreeNode($this->hierarchy, $nodeName);
0 ignored issues
show
Unused Code introduced by
$children is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
255
        // @todo remove all children
256
        $this->removeNodeFromList($nodeName);
257
        $this->updateRequired = true;
258
259
        return $this;
260
    }
261
262
    /**
263
     * Adds new tree node. If node exists, replaces it.
264
     *
265
     * @param string|null        $parentName root if null
266
     * @param string             $nodeName   new node name
267
     * @param ChildNodeInterface $node
268
     *
269
     * @return $this
270
     */
271
    protected function add($parentName = null, $nodeName, ChildNodeInterface $node, $prepend = false)
272
    {
273
        if (!self::addTreeNode($this->hierarchy, $parentName, $nodeName, $prepend)) {
274
            throw new NodeNotFoundException(
275
                "Can't add '$nodeName' node to '$parentName': '$parentName' node not found."
276
            );
277
        }
278
        $this->removeNodeFromList($nodeName);
279
        $this->nodes->set($nodeName, $node);
280
        $this->updateRequired = true;
281
282
        return $this;
283
    }
284
285
    /**
286
     * @todo try array_walk_recursive
287
     *
288
     * @param array  $tree
289
     * @param string $parent node name or null for inserting into root node
290
     * @param $node
291
     *
292
     * @throws \Exception
293
     *
294
     * @return bool false if no parent found
295
     */
296
    private static function addTreeNode(array &$tree, $parent, $node, $prepend = false)
297
    {
298
        if ($parent === null) {
299
            if (array_key_exists($node, $tree)) {
300
                throw new \Exception('Node already exists');
301
            }
302
            if ($prepend) {
303
                $tree = array_merge([$node => []], $tree);
304
            } else {
305
                $tree[$node] = [];
306
            }
307
308
            return true;
309
        }
310
        foreach ($tree as $key => &$value) {
311
            if ($key === $parent) {
312
                return self::addTreeNode($value, null, $node, $prepend);
313
            } else {
314
                if (self::addTreeNode($value, $parent, $node, $prepend)) {
315
                    return true;
316
                }
317
            }
318
        }
319
320
        return false;
321
    }
322
323
    /**
324
     * @param array $config
325
     * @param $node
326
     *
327
     * @return false|array children of deleted node
328
     */
329
    private static function removeTreeNode(array &$config, $node)
330
    {
331
        foreach ($config as $key => &$value) {
332
            if ($key === $node) {
333
                $children = $config[$node];
334
                unset($config[$node]);
335
336
                return $children;
337
            } else {
338
                $result = self::removeTreeNode($value, $node);
339
                if ($result !== false) {
340
                    return $result;
341
                }
342
            }
343
        }
344
345
        return false;
346
    }
347
348
    protected function removeNodeFromList($nodeName)
349
    {
350
        if ($this->nodes->hasKey($nodeName)) {
351
            $oldNode = $this->nodes->get($nodeName);
352
            if ($oldNode->parent()) {
353
                $oldNode->unlock()->detach();
354
            }
355
            $this->nodes->removeByKey($nodeName);
356
            $this->updateRequired = true;
357
        }
358
    }
359
}
360