Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
36 / 40
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
TreePathResolver
90.00% covered (success)
90.00%
36 / 40
50.00% covered (danger)
50.00%
3 / 6
20.40
0.00% covered (danger)
0.00%
0 / 1
 getNodes
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getChildNodes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getBrothers
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 computeLevel
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 getValidParentId
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
1<?php
2
3/**
4 * Resolves paths and relationships in category trees.
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-19
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Category\Tree;
21
22final class TreePathResolver
23{
24    /**
25     * Returns a node path from root to the given category id.
26     *
27     * @param array<int, array<string, mixed>> $categoryName Map id => row (needs parent_id)
28     * @return array<int>
29     */
30    public function getNodes(array $categoryName, int $categoryId): array
31    {
32        if ($categoryId <= 0) {
33            return [];
34        }
35
36        $nodes = [$categoryId];
37        $currentCategoryId = $categoryId;
38
39        while ($currentCategoryId > 0) {
40            $parentId = $this->getValidParentId($categoryName, $currentCategoryId);
41            if ($parentId === null) {
42                break;
43            }
44
45            array_unshift($nodes, $parentId);
46            $currentCategoryId = $parentId;
47        }
48
49        return $nodes;
50    }
51
52    /**
53     * Returns direct children IDs for a category.
54     *
55     * @param array<int, array<int, array<string, mixed>>> $childrenMap parentId => [childId => row]
56     * @return array<int>
57     */
58    public function getChildren(array $childrenMap, int $categoryId): array
59    {
60        return array_key_exists($categoryId, $childrenMap) ? array_keys($childrenMap[$categoryId]) : [];
61    }
62
63    /**
64     * Returns all descendant IDs of a category.
65     *
66     * @param array<int, array<int, array<string, mixed>>> $childrenMap
67     * @return array<int>
68     */
69    public function getChildNodes(array $childrenMap, int $categoryId): array
70    {
71        $result = [];
72        if (!array_key_exists($categoryId, $childrenMap)) {
73            return $result;
74        }
75
76        foreach (array_keys($childrenMap[$categoryId]) as $childId) {
77            $result[] = (int) $childId;
78            $result = array_merge($result, $this->getChildNodes($childrenMap, (int) $childId));
79        }
80
81        return $result;
82    }
83
84    /**
85     * Returns siblings (brothers) of a category including itself.
86     *
87     * @param array<int, array<string, mixed>> $categoryName
88     * @param array<int, array<int, array<string, mixed>>> $childrenMap
89     * @return array<int>
90     */
91    public function getBrothers(array $categoryName, array $childrenMap, int $categoryId): array
92    {
93        $parentId = (int) ($categoryName[$categoryId]['parent_id'] ?? 0);
94        return $this->getChildren($childrenMap, $parentId);
95    }
96
97    /**
98     * Computes the depth level of a category within the tree (root has 0).
99     *
100     * @param array<int, array<string, mixed>> $categoryName Map id => row (needs parent_id)
101     */
102    public function computeLevel(array $categoryName, int $categoryId): int
103    {
104        $alreadyListed = [$categoryId];
105        $level = 0;
106        while (
107            array_key_exists($categoryId, $categoryName)
108            && array_key_exists('parent_id', $categoryName[$categoryId])
109            && (int) $categoryName[$categoryId]['parent_id'] !== 0
110        ) {
111            ++$level;
112            $categoryId = (int) $categoryName[$categoryId]['parent_id'];
113            if (in_array($categoryId, $alreadyListed, strict: true)) {
114                break;
115            }
116
117            $alreadyListed[] = $categoryId;
118        }
119
120        return $level;
121    }
122
123    /**
124     * Gets valid parent ID for a category, or null if none exists.
125     *
126     * @param array<int, array<string, mixed>> $categoryName
127     */
128    private function getValidParentId(array $categoryName, int $currentCategoryId): ?int
129    {
130        if (!array_key_exists($currentCategoryId, $categoryName)) {
131            return null;
132        }
133
134        $parentId = (int) ($categoryName[$currentCategoryId]['parent_id'] ?? 0);
135
136        if ($parentId <= 0 || $parentId === $currentCategoryId) {
137            return null;
138        }
139
140        if (!array_key_exists($parentId, $categoryName)) {
141            return null;
142        }
143
144        return $parentId;
145    }
146}