Issues (1)

src/HTree.php (1 issue)

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