Passed
Push — master ( 108ec4...ffa8ed )
by hugh
10:06
created

HTree::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
1
<?php
2
3
namespace HughCube\HTree;
4
5
use HughCube\HTree\Exceptions\UnableBuildTreeException;
6
7
class HTree
8
{
9
    /**
10
     * @var array
11
     */
12
    protected $items = [];
13
14
    /**
15
     * items的id的key.
16
     *
17
     * @var string
18
     */
19
    protected $idKey;
20
21
    /**
22
     * items的parent的key.
23
     *
24
     * @var string
25
     */
26
    protected $parentKey;
27
28
    /**
29
     * items 数据的索引.
30
     *
31
     * @var Index[]
32
     */
33
    protected $indexes = [];
34
35
    /**
36
     * index的tree.
37
     *
38
     * @var Index[]
39
     */
40
    protected $indexTree;
41
42
    /**
43
     * 正在构建的 item.
44
     *
45
     * @var array
46
     */
47
    protected $buildIndexInProcess = [];
48
49
    /**
50
     * 获取实例.
51
     *
52
     * @param string $url
53
     *
54
     * @return static
55
     */
56 1
    public static function instance(
57
        array $items,
58
        $idKey = 'id',
59
        $parentKey = 'parent'
60
    ) {
61 1
        return new static($items, $idKey, $parentKey);
62
    }
63
64
    /**
65
     * Tree constructor.
66
     *
67
     * @param array  $items     构建树形结构的数组, 每个元素必需包含 id, parent 两个属性
68
     * @param string $idKey     id属性的名字
69
     * @param string $parentKey parent属性的名字
70
     */
71 1
    protected function __construct(array $items, $idKey, $parentKey)
72
    {
73 1
        $this->idKey = $idKey;
74 1
        $this->parentKey = $parentKey;
75
76 1
        $this->buildIndex($items);
77
    }
78
79
    /**
80
     * 获取items.
81
     *
82
     * @return array
83
     */
84 4
    public function getItems()
85
    {
86 4
        return $this->items;
87
    }
88
89
    /**
90
     * 获取单个节点数据.
91
     *
92
     * @param string $id id
93
     *
94
     * @return mixed|null
95
     */
96 2
    public function getItem($id)
97
    {
98 2
        return isset($this->items[$id]) ? $this->items[$id] : null;
99
    }
100
101
    /**
102
     * 是否存在某个节点数据.
103
     *
104
     * @param string $id id
105
     *
106
     * @return bool
107
     */
108
    public function hasItem($id)
109
    {
110
        return null != $this->getItem($id);
111
    }
112
113
    /**
114
     * 添加一个节点.
115
     *
116
     * @param array|object $node 节点数据
117
     *
118
     * @return static
119
     */
120 2
    public function addNode($node)
121
    {
122 2
        $this->pushNodeToTree($node);
123 2
        $this->buildIndexTree();
124
    }
125
126
    /**
127
     * 推送一个节点到树形结构.
128
     *
129
     * @param $node
130
     */
131 2
    protected function pushNodeToTree($node)
132
    {
133 2
        $parent = $this->getNodeProperty($node, $this->parentKey);
134 2
        $id = $this->getNodeProperty($node, $this->idKey);
135
136 2
        $this->items[$id] = $node;
137
138 2
        if (empty($parent) || !isset($this->indexes[$parent])) {
139
            $pLevel = $pLeft = 0;
140
        } else {
141 2
            $pLeft = $this->indexes[$parent]->left;
142 2
            $pLevel = $this->indexes[$parent]->level;
143
        }
144
145
        /* 改变其他元素的, 给当前节点留出位置 */
146 2
        foreach ($this->indexes as $index) {
147 2
            if ($index->left > $pLeft) {
148 2
                $index->left += 2;
149
            }
150
151 2
            if ($index->right > $pLeft) {
152 2
                $index->right += 2;
153
            }
154
        }
155
156 2
        $this->indexes[$id] = new Index([
157
            'id'     => $id,
158
            'level'  => $pLevel + 1,
159
            'left'   => $pLeft + 1,
160
            'right'  => ($pLeft + 1) + 1,
161
            'parent' => $parent,
162
        ]);
163
    }
164
165
    /**
166
     * 构建 index 的树形结构.
167
     */
168 2
    protected function buildIndexTree()
169
    {
170 2
        $this->indexTree = [];
171 2
        foreach ($this->indexes as $index) {
172 2
            if (isset($this->indexes[$index->parent])) {
173 2
                $this->indexes[$index->parent]->addChild($index);
174
            } else {
175 2
                $this->indexTree[$index->id] = $index;
176
            }
177
        }
178
    }
179
180
    /**
181
     * 获取节点的子节点.
182
     *
183
     * @param string   $nid        节点id
184
     * @param bool     $onlyId     是否只返回 id
185
     * @param null|int $startLevel 往下多少级-起始, 为空不限制
186
     * @param null|int $endLevel   往下多少级-结束, 为空不限制
187
     * @param bool     $withSelf   结果是否包括自己
188
     *
189
     * @return array
190
     */
191
    public function getChildren(
192
        $nid,
193
        $withSelf = false,
194
        $onlyId = false,
195
        $startLevel = null,
196
        $endLevel = null
197
    ) {
198
        $nodes = $parents = [];
199
200
        /* 先收集一次, 防止父 $nid 不存在不能被收集 */
201
        foreach ($this->items as $id => $item) {
202
            if ($nid == $this->getNodeProperty($item, $this->parentKey)) {
203
                $parents[] = $id;
204
                $nodes[$id] = $item;
205
            }
206
        }
207
208
        /* 收集所有的子节点 */
209
        foreach ($parents as $parent) {
210
            foreach ($this->indexes as $index) {
211
                if ($index->left > $this->indexes[$parent]->left
212
                    && $index->right < $this->indexes[$parent]->right
213
                ) {
214
                    $nodes[$index->id] = $this->items[$index->id];
215
                }
216
            }
217
        }
218
219
        /* 过滤不符合的节点 */
220
        foreach ($nodes as $id => $node) {
221
            $level = $this->indexes[$id]->level;
222
            if ((null === $startLevel || $startLevel <= $level)
223
                && (null === $endLevel || $endLevel >= $level)
224
            ) {
225
                continue;
226
            }
227
228
            unset($nodes[$id]);
229
        }
230
231
        /* 是否返回自己本身 */
232
        if ($withSelf && $this->hasItem($nid)) {
233
            $nodes[$nid] = $this->getItem($nid);
234
        }
235
236
        /* 排序 */
237
        uasort($nodes, function ($a, $b) {
238
            $aLevel = $this
239
                ->getIndex($this->getNodeProperty($a, $this->idKey))->level;
240
241
            $bLevel = $this
242
                ->getIndex($this->getNodeProperty($b, $this->idKey))->level;
243
244
            return ($aLevel == $bLevel) ? 0 : ($aLevel > $bLevel ? 1 : -1);
245
        });
246
247
        if ($onlyId) {
248
            return array_keys($nodes);
249
        }
250
251
        return $nodes;
252
    }
253
254
    /**
255
     * 获取某一节点的父节点.
256
     *
257
     * @param string   $nid    节点id
258
     * @param bool     $onlyId 是否只返回 id
259
     * @param null|int $level  取第几级父节点, 默认取上一级
260
     *
261
     * @return int|mixed|null|string
262
     */
263
    public function getParent($nid, $onlyId = false, $level = null)
264
    {
265
        $index = $this->getIndex($nid);
266
        if (!$index instanceof Index) {
0 ignored issues
show
introduced by
$index is always a sub-type of HughCube\HTree\Index.
Loading history...
267
            return;
268
        }
269
270
        $level = null === $level ? ($index->level - 1) : $level;
271
272
        $parents = $this->getParents($nid, false, $onlyId, $level, $level);
273
        $parents = array_values($parents);
274
275
        return isset($parents[0]) ? $parents[0] : null;
276
    }
277
278
    /**
279
     * 获取某一节点的所有父节点.
280
     *
281
     * @param string   $nid        节点id
282
     * @param bool     $onlyId     是否只返回 id
283
     * @param null|int $startLevel 往上多少级-起始, 为空不限制
284
     * @param null|int $endLevel   往上多少级-结束, 为空不限制
285
     *
286
     * @return array
287
     */
288
    public function getParents(
289
        $nid,
290
        $withSelf = false,
291
        $onlyId = false,
292
        $startLevel = null,
293
        $endLevel = null
294
    ) {
295
        $nodes = [];
296
297
        /* 是否返回自己本身 */
298
        if ($withSelf && $this->hasItem($nid)) {
299
            $nodes[$nid] = $this->getItem($nid);
300
        }
301
302
        if ($this->hasItem($nid)) {
303
            foreach ($this->indexes as $id => $index) {
304
                if ($index->left < $this->indexes[$nid]->left
305
                    && $index->right > $this->indexes[$nid]->right
306
                ) {
307
                    $nodes[$id] = $this->items[$id];
308
                }
309
            }
310
        }
311
312
        foreach ($nodes as $id => $node) {
313
            $level = $this->indexes[$id]->level;
314
            if ((null === $startLevel || $startLevel <= $level)
315
                && (null === $endLevel || $endLevel >= $level)
316
            ) {
317
                continue;
318
            }
319
320
            unset($nodes[$id]);
321
        }
322
323
        /* 排序 */
324
        uasort($nodes, function ($a, $b) {
325
            $aLevel = $this
326
                ->getIndex($this->getNodeProperty($a, $this->idKey))->level;
327
328
            $bLevel = $this
329
                ->getIndex($this->getNodeProperty($b, $this->idKey))->level;
330
331
            return ($aLevel == $bLevel) ? 0 : ($aLevel > $bLevel ? 1 : -1);
332
        });
333
334
        if ($onlyId) {
335
            return array_keys($nodes);
336
        }
337
338
        return $nodes;
339
    }
340
341
    /**
342
     * 树排序.
343
     *
344
     * @param callable $cmpSortCallable
345
     * @param int      $sortType        SORT_ASC | SORT_DESC
346
     *
347
     * @return $this
348
     *
349
     * 如果数字很长, 可以填充为相等长度的字符串, 使用0填充
350
     *
351
     * @see https://www.php.net/manual/en/function.strcmp.php
352
     */
353 2
    public function treeSort(callable $cmpSortCallable, $sortType = SORT_ASC)
354
    {
355 2
        $instance = clone $this;
356
357 2
        $instance->indexTree = $instance->recursiveTreeSort(
358 2
            $instance->indexTree,
359
            $cmpSortCallable,
360
            $sortType
361
        );
362
363 2
        return $instance;
364
    }
365
366
    /**
367
     * 递归排序.
368
     *
369
     * @param Index[]  $indexes
370
     * @param callable $cmpSortCallable
371
     * @param int      $sortType        SORT_ASC | SORT_DESC
372
     *
373
     * @return Index[]
374
     */
375 2
    protected function recursiveTreeSort(
376
        $indexes,
377
        callable $cmpSortCallable,
378
        $sortType
379
    ) {
380
        /** @var Index $index */
381 2
        foreach ($indexes as $index) {
382 2
            $index->children = $this->recursiveTreeSort(
383 2
                $index->children,
384
                $cmpSortCallable,
385
                $sortType
386
            );
387
        }
388
389 2
        uasort(
390
            $indexes,
391 2
            function (Index $a, Index $b) use ($cmpSortCallable, $sortType) {
392 2
                $aSort = $cmpSortCallable($this->items[$a->id]);
393 2
                $bSort = $cmpSortCallable($this->items[$b->id]);
394
395 2
                $cmp = strcmp($aSort, $bSort);
396 2
                if (SORT_ASC == $sortType) {
397 2
                    return (0 == $cmp) ? 0 : ($cmp > 0 ? -1 : 1);
398
                } else {
399
                    return (0 == $cmp) ? 0 : ($cmp > 0 ? 1 : -1);
400
                }
401
            }
402
        );
403
404 2
        return $indexes;
405
    }
406
407
    /**
408
     * 递归遍历每一个元素, 按照指定的顺, 并且可以改变元素的值
409
     *
410
     * @param callable $callable 返回值作为该元素新的值
411
     *
412
     * @return $this
413
     */
414 2
    public function treeMap(callable $callable)
415
    {
416 2
        $instance = clone $this;
417
418 2
        $instance->recursiveTreeMap($instance->indexTree, $callable);
419
420 2
        return $instance;
421
    }
422
423
    /**
424
     * 递归遍历每一个元素.
425
     *
426
     * @param Index[]  $indexes
427
     * @param callable $callable
428
     */
429 2
    protected function recursiveTreeMap($indexes, $callable)
430
    {
431 2
        foreach ($indexes as $index) {
432 2
            $this->items[$index->id] = $callable($this->items[$index->id]);
433 2
            $this->recursiveTreeMap($index->children, $callable);
434
        }
435
    }
436
437
    /**
438
     * 获取树结构.
439
     *
440
     * @param string   $childrenKey          子集的数组key
441
     * @param callable $format               格式化返回的元素
442
     * @param bool     $keepEmptyChildrenKey 是否保留空的ChildrenKey
443
     *
444
     * @return array
445
     */
446
    public function getTree(
447
        $childrenKey = 'children',
448
        $format = null,
449
        $keepEmptyChildrenKey = true
450
    ) {
451
        return $this->recursiveGetTree(
452
            $this->indexTree,
453
            $childrenKey,
454
            $format,
455
            $keepEmptyChildrenKey
456
        );
457
    }
458
459
    /**
460
     * 递归遍历每一个元素.
461
     *
462
     * @param Index[]  $indexes
463
     * @param callable $callable
464
     */
465
    protected function recursiveGetTree(
466
        $indexes,
467
        $childrenKey,
468
        $format,
469
        $keepEmptyChildrenKey
470
    ) {
471
        $nodes = [];
472
        foreach ($indexes as $index) {
473
            $node = null === $format
474
                ? $this->items[$index->id]
475
                : $format($this->items[$index->id]);
476
477
            $children = $this->recursiveGetTree(
478
                $index->children,
479
                $childrenKey,
480
                $format,
481
                $keepEmptyChildrenKey
482
            );
483
            if (!empty($children) || (bool) $keepEmptyChildrenKey) {
484
                $node[$childrenKey] = $children;
485
            }
486
487
            $nodes[] = $node;
488
        }
489
490
        return $nodes;
491
    }
492
493
    /**
494
     * 构建 index.
495
     */
496 1
    protected function buildIndex(array $items)
497
    {
498 1
        $this->index = $this->tree =
0 ignored issues
show
Bug Best Practice introduced by
The property index does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property tree does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
499 1
        $this->indexTree = $this->buildIndexInProcess = [];
500
501 1
        $_ = [];
502 1
        foreach ($items as $item) {
503 1
            $_[$this->getNodeProperty($item, $this->idKey)] = $item;
504
        }
505
506 1
        foreach ($_ as $id => $item) {
507 1
            if (!isset($this->indexes[$id])) {
508 1
                $this->recursiveBuildIndex($id, $_);
509
            }
510
        }
511
512
        $this->buildIndexTree();
513
    }
514
515
    /**
516
     * 递归构建 index.
517
     *
518
     * @param $id
519
     */
520 1
    protected function recursiveBuildIndex($id, &$items)
521
    {
522 1
        if (in_array($id, $this->buildIndexInProcess)) {
523 1
            throw new UnableBuildTreeException(
524 1
                $this->buildIndexInProcess,
525
                '不能构建成一个树形'
526
            );
527
        }
528
529 1
        $this->buildIndexInProcess[$id] = $id;
530
531
        /** @var int $parent 需要处理的节点父节点id */
532 1
        $parent = $this->getNodeProperty($items[$id], $this->parentKey);
533
534
        /* 如果存在父节点, 并且父节点没有被被处理, 先处理父节点 */
535 1
        if (isset($items[$parent]) && !isset($this->indexes[$parent])) {
536 1
            $this->recursiveBuildIndex($parent, $items);
537
        }
538
539
        /* 添加节点 */
540
        $this->pushNodeToTree($items[$id]);
541
        unset($this->buildIndexInProcess[$id]);
542
    }
543
544
    /**
545
     * @param $id
546
     *
547
     * @return Index
548
     */
549 4
    public function getIndex($id)
550
    {
551 4
        return isset($this->indexes[$id]) ? $this->indexes[$id] : null;
552
    }
553
554
    /**
555
     * @return Index[]
556
     */
557
    public function getIndexes()
558
    {
559
        return $this->indexes;
560
    }
561
562
    /**
563
     * 获取数据节点的属性.
564
     *
565
     * @param array|object $node
566
     * @param string       $name
567
     *
568
     * @return mixed
569
     */
570 3
    private function getNodeProperty($node, $name)
571
    {
572 3
        if (is_object($node)) {
573
            return $node->{$name};
574
        }
575
576 3
        return $node[$name];
577
    }
578
}
579