Lines 54.16% 39 / 72
Methods 55.55% 5 / 9
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 transform 100.00% 4 / 4 100.00% 1 / 1 1
 buildTree 100.00% 5 / 5 100.00% 1 / 1 1
 transformRecursive 7.14% 2 / 28 0.00% 0 / 1 34.82
 getSymbol 0.00% 0 / 5 0.00% 0 / 1 12
 expand 100.00% 3 / 3 100.00% 1 / 1 2
 collapseAll 100.00% 5 / 5 100.00% 1 / 1 5
 expandTo 93.33% 14 / 15 0.00% 0 / 1 6.01
 getLineCategory 83.33% 5 / 6 0.00% 0 / 1 5.12
28class CategoryTreeNavigator
29{
30    public function __construct(
31        private TreeBuilder $treeBuilder = new TreeBuilder(),
32    ) {
33    }
34
35    /**
36     * Transforms the linear array into a 1D array in tree order with info.
37     *
38     * @return array<array<string, mixed>>
39     */
40    public function transform(CategoryCache $categoryCache, int $categoryId): array
41    {
42        $entries = [];
43        $tree = $this->buildTree($categoryCache, $categoryId);
44        $this->transformRecursive($categoryCache, $tree, indent: 0, entries: $entries);
45        return $entries;
46    }
47
48    /**
49     * Builds tree structure for a category.
50     *
51     * @return array<array-key, mixed>
52     */
53    private function buildTree(CategoryCache $categoryCache, int $categoryId): array
54    {
55        return $this->treeBuilder->buildTree(
56            $categoryCache->getCategoryNames(),
57            $categoryCache->getChildren(),
58            $categoryId,
59        );
60    }
61
62    /**
63     * Recursively transforms tree structure into a flat list.
64     *
65     * @param array<array-key, mixed> $tree
66     * @param array<array<string, mixed>> $entries
67     */
68    private function transformRecursive(CategoryCache $categoryCache, array $tree, int $indent, array &$entries): void
69    {
70        // Skip invalid or empty trees
71        if ($tree === [] || !array_key_exists('id', $tree)) {
72            return;
73        }
74
75        $categoryId = (int) $tree['id'];
76        $parentId = (int) ($tree['parent_id'] ?? 0);
77        $children = $tree['children'] ?? [];
78        $children = is_array($children) ? $children : [];
79        $numChildren = count($children);
80
81        $symbol = $this->getSymbol($categoryCache, $categoryId, $parentId, $numChildren);
82
83        $entry = [
84            'id' => $categoryId,
85            'lang' => $tree['lang'] ?? '',
86            'parent_id' => $parentId,
87            'name' => $tree['name'] ?? '',
88            'description' => $tree['description'] ?? '',
89            'user_id' => $tree['user_id'] ?? 0,
90            'group_id' => $tree['group_id'] ?? -1,
91            'active' => $tree['active'] ?? 0,
92            'show_home' => $tree['show_home'] ?? 0,
93            'image' => $tree['image'] ?? '',
94            'level' => $indent,
95            'symbol' => $symbol,
96            'numChildren' => $numChildren,
97        ];
98
99        $entries[] = $entry;
100
101        foreach ($children as $child) {
102            if (!is_array($child)) {
103                continue;
104            }
105
106            $this->transformRecursive($categoryCache, $child, $indent + 1, $entries);
107        }
108    }
109
110    /**
111     * Gets the symbol for tree rendering.
112     */
113    private function getSymbol(CategoryCache $categoryCache, int $categoryId, int $parentId, int $numChildren): string
114    {
115        if ($numChildren > 0) {
116            return 'plus';
117        }
118
119        $siblings = $categoryCache->getChildren()[$parentId] ?? [];
120        $array = array_keys($siblings);
121        return $categoryId === end($array) ? 'angle' : 'medium';
122    }
123
124    /**
125     * Expands a category node in the tree tab.
126     */
127    public function expand(CategoryCache $categoryCache, int $categoryId): void
128    {
129        $lineIndex = $this->getLineCategory($categoryCache, $categoryId);
130        if ($lineIndex >= 0) {
131            $categoryCache->updateTreeTabEntry($lineIndex, ['symbol' => 'minus']);
132        }
133    }
134
135    /**
136     * Collapses all nodes in the tree tab.
137     */
138    public function collapseAll(CategoryCache $categoryCache): void
139    {
140        $numTreeTab = $categoryCache->countTreeTab();
141        for ($i = 0; $i < $numTreeTab; ++$i) {
142            $entry = $categoryCache->getTreeTabEntry($i);
143            if ($entry !== null && array_key_exists('symbol', $entry) && $entry['symbol'] === 'minus') {
144                $categoryCache->updateTreeTabEntry($i, ['symbol' => 'plus']);
145            }
146        }
147    }
148
149    /**
150     * Expands a tree from root to the given category.
151     */
152    public function expandTo(CategoryCache $categoryCache, int $categoryId): void
153    {
154        $this->collapseAll($categoryCache);
155        $ascendants = $this->treeBuilder->getNodes($categoryCache->getCategoryNames(), $categoryId);
156        $ascendants[] = $categoryId;
157        $numAscendants = count($ascendants);
158
159        for ($i = 0; $i < $numAscendants; ++$i) {
160            $lineIndex = $this->getLineCategory($categoryCache, $ascendants[$i]);
161            if ($lineIndex < 0) {
162                continue;
163            }
164
165            $entry = $categoryCache->getTreeTabEntry($lineIndex);
166            if ($entry !== null && array_key_exists('numChildren', $entry)) {
167                $numChildren = (int) $entry['numChildren'];
168                if ($numChildren > 0) {
169                    $this->expand($categoryCache, $ascendants[$i]);
170                    continue;
171                }
172
173                break;
174            }
175        }
176    }
177
178    /**
179     * Gets the line number where to find the node in the tree tab.
180     */
181    private function getLineCategory(CategoryCache $categoryCache, int $categoryId): int
182    {
183        $num = $categoryCache->countTreeTab();
184        for ($i = 0; $i < $num; ++$i) {
185            $entry = $categoryCache->getTreeTabEntry($i);
186            if ($entry !== null && array_key_exists('id', $entry) && $entry['id'] === $categoryId) {
187                return $i;
188            }
189        }
190
191        return -1;
192    }
193}