Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.00% covered (success)
98.00%
49 / 50
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CollectionTree
98.00% covered (success)
98.00%
49 / 50
66.67% covered (warning)
66.67%
2 / 3
13
0.00% covered (danger)
0.00%
0 / 1
 build
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 walk
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
9
 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, guides: list<int>, 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            guides: [],
26        );
27
28        return $rows;
29    }
30
31    /**
32     * @param list<array<string, mixed>> $nodes
33     * @param array<string, true> $open
34     * @param callable(string): list<array<string, mixed>> $children
35     * @param list<string> $stack
36     * @param list<int> $guides depths whose sibling line runs on through these rows
37     * @return array{0: list<array{node: array<string, mixed>, depth: int, expanded: bool, last: bool, guides: list<int>, descendants: list<string>}>, 1: list<string>}
38     */
39    private static function walk(
40        array $nodes,
41        array $open,
42        callable $children,
43        int $depth,
44        array $stack,
45        array $guides,
46    ): array {
47        $rows = [];
48        $uids = [];
49
50        $lastIndex = count($nodes) - 1;
51        $position = 0;
52
53        foreach ($nodes as $node) {
54            $node = self::arrayFrom($node);
55            $uid = trim((string) ($node['uid'] ?? ''));
56            $expanded =
57                $uid !== ''
58                && isset($open[$uid])
59                && (bool) ($node['hasChildren'] ?? false)
60                && !in_array($uid, $stack, true);
61            $last = $position === $lastIndex;
62            $childRows = [];
63            $descendants = [];
64
65            if ($expanded) {
66                // Root rows draw no sibling line, so only a deeper, non-last
67                // node passes its line down through its children.
68                [$childRows, $descendants] = self::walk(
69                    nodes: $children($uid),
70                    open: $open,
71                    children: $children,
72                    depth: $depth + 1,
73                    stack: array_merge($stack, [$uid]),
74                    guides: $depth > 0 && !$last ? array_merge($guides, [$depth]) : $guides,
75                );
76            }
77
78            $rows[] = [
79                'node' => $node,
80                'depth' => $depth,
81                'expanded' => $expanded,
82                'last' => $last,
83                'guides' => $guides,
84                'descendants' => $descendants,
85            ];
86
87            $position++;
88
89            if ($uid !== '') {
90                $uids[] = $uid;
91            }
92
93            array_push($rows, ...$childRows);
94            array_push($uids, ...$descendants);
95        }
96
97        return [$rows, array_values(array_unique($uids))];
98    }
99
100    /** @return array<string, mixed> */
101    private static function arrayFrom(mixed $value): array
102    {
103        if ($value instanceof Traversable) {
104            return iterator_to_array($value);
105        }
106
107        return is_array($value) ? $value : [];
108    }
109}