Skip to content

Commit 4d9c3af

Browse files
Copilotthorsten
andcommitted
Fix infinite recursion in getCategoryTree by adding cycle detection
Co-authored-by: thorsten <[email protected]>
1 parent e902d0a commit 4d9c3af

File tree

2 files changed

+96
-4
lines changed

2 files changed

+96
-4
lines changed

phpmyfaq/src/phpMyFAQ/Category/Order.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,33 @@ public function setCategoryTree(
120120
* Returns the category tree.
121121
*
122122
* @param stdClass[] $categories
123+
* @param int $parentId
124+
* @param array $visited Track visited categories to prevent infinite recursion
123125
*/
124-
public function getCategoryTree(array $categories, int $parentId = 0): array
126+
public function getCategoryTree(array $categories, int $parentId = 0, array &$visited = []): array
125127
{
126128
$result = [];
127129

128130
foreach ($categories as $category) {
129-
if ((int)$category['parent_id'] === $parentId) {
130-
$childCategories = $this->getCategoryTree($categories, (int)$category['category_id']);
131-
$result[$category['category_id']] = $childCategories;
131+
$categoryId = (int)$category['category_id'];
132+
$categoryParentId = (int)$category['parent_id'];
133+
134+
// Skip if category is its own parent or creates a cycle
135+
if ($categoryId === $categoryParentId) {
136+
continue;
137+
}
138+
139+
if ($categoryParentId === $parentId) {
140+
// Check if this category has already been visited to prevent cycles
141+
if (in_array($categoryId, $visited, true)) {
142+
continue;
143+
}
144+
145+
// Add current category to visited list
146+
$visited[] = $categoryId;
147+
148+
$childCategories = $this->getCategoryTree($categories, $categoryId, $visited);
149+
$result[$categoryId] = $childCategories;
132150
}
133151
}
134152

tests/phpMyFAQ/Category/OrderTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,78 @@ public function testGetParentId(): void
129129

130130
$this->assertEquals($expected, $actual);
131131
}
132+
133+
/**
134+
* Test that getCategoryTree handles self-referencing categories without infinite recursion
135+
*/
136+
public function testGetCategoryTreeWithSelfReference(): void
137+
{
138+
// Simulate a category that references itself as parent
139+
$categories = [
140+
[
141+
'category_id' => 1,
142+
'parent_id' => 1, // Self-reference
143+
'position' => 1,
144+
],
145+
[
146+
'category_id' => 2,
147+
'parent_id' => 0,
148+
'position' => 2,
149+
],
150+
];
151+
152+
$result = $this->categoryOrder->getCategoryTree($categories);
153+
154+
// Category 1 should be skipped due to self-reference
155+
// Only category 2 should be in the result
156+
$expected = [
157+
2 => [],
158+
];
159+
160+
$this->assertEquals($expected, $result);
161+
}
162+
163+
/**
164+
* Test that getCategoryTree handles circular references without infinite recursion
165+
*/
166+
public function testGetCategoryTreeWithCircularReference(): void
167+
{
168+
// Simulate circular reference: 1 -> 2 -> 3 -> 2 (cycle)
169+
$categories = [
170+
[
171+
'category_id' => 1,
172+
'parent_id' => 0,
173+
'position' => 1,
174+
],
175+
[
176+
'category_id' => 2,
177+
'parent_id' => 1,
178+
'position' => 2,
179+
],
180+
[
181+
'category_id' => 3,
182+
'parent_id' => 2,
183+
'position' => 3,
184+
],
185+
[
186+
'category_id' => 2, // Duplicate entry creating circular reference
187+
'parent_id' => 3,
188+
'position' => 4,
189+
],
190+
];
191+
192+
$result = $this->categoryOrder->getCategoryTree($categories);
193+
194+
// Should handle circular reference gracefully
195+
// Category 2 should only be visited once
196+
$expected = [
197+
1 => [
198+
2 => [
199+
3 => [],
200+
],
201+
],
202+
];
203+
204+
$this->assertEquals($expected, $result);
205+
}
132206
}

0 commit comments

Comments
 (0)