Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.83% covered (success)
97.83%
45 / 46
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CollectionTree
97.83% covered (success)
97.83%
45 / 46
66.67% covered (warning)
66.67%
2 / 3
11
0.00% covered (danger)
0.00%
0 / 1
 build
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 walk
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
7
 arrayFrom
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Panel;
6
7use Traversable;
8
9final class CollectionTree
10{
11    /**
12     * @param list<array<string, mixed>> $nodes
13     * @param list<string> $open
14     * @param callable(string): list<array<string, mixed>> $children
15     * @return list<array{node: array<string, mixed>, depth: int, expanded: bool, last: bool, descendants: list<string>}>
16     */
17    public static function build(array $nodes, array $open, callable $children): array
18    {
19        [$rows] = self::walk(
20            nodes: $nodes,
21            open: array_fill_keys($open, true),
22            children: $children,
23            depth: 0,
24            stack: [],
25        );
26
27        return $rows;
28    }
29
30    /**
31     * @param list<array<string, mixed>> $nodes
32     * @param array<string, true> $open
33     * @param callable(string): list<array<string, mixed>> $children
34     * @param list<string> $stack
35     * @return array{0: list<array{node: array<string, mixed>, depth: int, expanded: bool, last: bool, descendants: list<string>}>, 1: list<string>}
36     */
37    private static function walk(
38        array $nodes,
39        array $open,
40        callable $children,
41        int $depth,
42        array $stack,
43    ): array {
44        $rows = [];
45        $uids = [];
46
47        $lastIndex = count($nodes) - 1;
48        $position = 0;
49
50        foreach ($nodes as $node) {
51            $node = self::arrayFrom($node);
52            $uid = trim((string) ($node['uid'] ?? ''));
53            $expanded =
54                $uid !== ''
55                && isset($open[$uid])
56                && (bool) ($node['hasChildren'] ?? false)
57                && !in_array($uid, $stack, true);
58            $childRows = [];
59            $descendants = [];
60
61            if ($expanded) {
62                [$childRows, $descendants] = self::walk(
63                    nodes: $children($uid),
64                    open: $open,
65                    children: $children,
66                    depth: $depth + 1,
67                    stack: array_merge($stack, [$uid]),
68                );
69            }
70
71            $rows[] = [
72                'node' => $node,
73                'depth' => $depth,
74                'expanded' => $expanded,
75                'last' => $position === $lastIndex,
76                'descendants' => $descendants,
77            ];
78
79            $position++;
80
81            if ($uid !== '') {
82                $uids[] = $uid;
83            }
84
85            array_push($rows, ...$childRows);
86            array_push($uids, ...$descendants);
87        }
88
89        return [$rows, array_values(array_unique($uids))];
90    }
91
92    /** @return array<string, mixed> */
93    private static function arrayFrom(mixed $value): array
94    {
95        if ($value instanceof Traversable) {
96            return iterator_to_array($value);
97        }
98
99        return is_array($value) ? $value : [];
100    }
101}