Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.66% covered (success)
89.66%
26 / 29
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TreeBuilder
89.66% covered (success)
89.66%
26 / 29
66.67% covered (warning)
66.67%
6 / 9
15.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 buildAdminCategoryTree
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 buildLinearTree
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 getNodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildNodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBrothers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildTree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 computeLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Tree builder for categories.
5 *
6 *  This Source Code Form is subject to the terms of the Mozilla Public License,
7 *  v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 *  obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2025 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2025-10-18
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Category\Tree;
21
22class TreeBuilder implements TreeBuilderInterface
23{
24    private TreePathResolver $treePathResolver;
25
26    private TreeVisualizer $treeVisualizer;
27
28    private CategoryValidator $categoryValidator;
29
30    public function __construct()
31    {
32        $this->treePathResolver = new TreePathResolver();
33        $this->treeVisualizer = new TreeVisualizer($this->treePathResolver);
34        $this->categoryValidator = new CategoryValidator();
35    }
36
37    /**
38     * Builds the category tree for the admin category overview.
39     * Pure function: does not mutate input.
40     *
41     * @param array<int, array<string, mixed>> $categories Flat list keyed or not by id, each with id,parent_id
42     * @param int $parentId Parent id to expand from
43     * @return array<int, array<array-key, mixed>> Map of categoryId => recursive subtree
44     */
45    public function buildAdminCategoryTree(array $categories, int $parentId = 0): array
46    {
47        $result = [];
48
49        foreach ($categories as $category) {
50            if (!$this->categoryValidator->isValidCategory($category)) {
51                continue;
52            }
53
54            if ((int) $category['parent_id'] === $parentId) {
55                $categoryId = (int) $category['id'];
56                $result[$categoryId] = $this->buildAdminCategoryTree($categories, $categoryId);
57            }
58        }
59
60        return $result;
61    }
62
63    /**
64     * Builds a linear tree (array) with indentation information.
65     * Matches legacy Category::buildCategoryTree output shape.
66     *
67     * @param array<int, array<string, mixed>> $categories
68     * @return array<int, array<string, mixed>>
69     */
70    public function buildLinearTree(array $categories, int $parentId = 0, int $indent = 0): array
71    {
72        $childrenIds = $this->categoryValidator->collectDirectChildren($categories, $parentId);
73
74        if ($childrenIds === []) {
75            return [];
76        }
77
78        $catTree = [];
79        foreach ($childrenIds as $childId) {
80            if (!array_key_exists($childId, $categories)) {
81                continue;
82            }
83
84            $row = $categories[$childId];
85            $row['indent'] = $indent;
86            $catTree[] = $row;
87            $catTree = array_merge($catTree, $this->buildLinearTree($categories, (int) $row['id'], $indent + 1));
88        }
89
90        return $catTree;
91    }
92
93    /**
94     * Delegates to TreePathResolver::getNodes()
95     *
96     * @param array<int, array<string, mixed>> $categoryNames Map id => row (needs parent_id)
97     * @return array<int>
98     */
99    public function getNodes(array $categoryNames, int $categoryId): array
100    {
101        return $this->treePathResolver->getNodes($categoryNames, $categoryId);
102    }
103
104    /**
105     * Delegates to TreePathResolver::getChildren()
106     *
107     * @param array<int, array<int, array<string, mixed>>> $childrenMap parentId => [childId => row]
108     * @return array<int>
109     */
110    public function getChildren(array $childrenMap, int $categoryId): array
111    {
112        return $this->treePathResolver->getChildren($childrenMap, $categoryId);
113    }
114
115    /**
116     * Delegates to TreePathResolver::getChildNodes()
117     *
118     * @param array<int, array<int, array<string, mixed>>> $childrenMap
119     * @return array<int>
120     */
121    public function getChildNodes(array $childrenMap, int $categoryId): array
122    {
123        return $this->treePathResolver->getChildNodes($childrenMap, $categoryId);
124    }
125
126    /**
127     * Delegates to TreePathResolver::getBrothers()
128     *
129     * @param array<int, array<string, mixed>> $categoryNames
130     * @param array<int, array<int, array<string, mixed>>> $children
131     * @return array<int>
132     */
133    public function getBrothers(array $categoryNames, array $children, int $categoryId): array
134    {
135        return $this->treePathResolver->getBrothers($categoryNames, $children, $categoryId);
136    }
137
138    /**
139     * Delegates to TreeVisualizer::buildTree()
140     *
141     * @param array<int, array<string, mixed>> $categoryNames
142     * @param array<int, array<int, array<string, mixed>>> $children
143     * @return array<int, string>
144     */
145    public function buildTree(array $categoryNames, array $children, int $categoryId): array
146    {
147        return $this->treeVisualizer->buildTree($categoryNames, $children, $categoryId);
148    }
149
150    /**
151     * Delegates to TreePathResolver::computeLevel()
152     *
153     * @param array<int, array<string, mixed>> $categoryNames Map id => row (needs parent_id)
154     */
155    public function computeLevel(array $categoryNames, int $categoryId): int
156    {
157        return $this->treePathResolver->computeLevel($categoryNames, $categoryId);
158    }
159}