fix: keep menu items in parent cycles, tighten menu write tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 00:07:44 +02:00
co-authored by Claude Opus 5.5
parent 1d59d1d2ad
commit 954f683dd2
4 changed files with 95 additions and 7 deletions
+40 -5
View File
@@ -21,20 +21,55 @@ final class MenuTree
public static function build(array $items): array
{
$nodes = [];
$parentOf = [];
foreach ($items as $item) {
$nodes[(int) $item->ID] = self::node($item);
$id = (int) $item->ID;
$nodes[$id] = self::node($item);
$parentOf[$id] = (int) $item->menu_item_parent;
}
$children = [];
foreach ($items as $item) {
$id = (int) $item->ID;
$parent = (int) $item->menu_item_parent;
$children[$parent !== $id && isset($nodes[$parent]) ? $parent : 0][] = $id;
foreach (array_keys($nodes) as $id) {
$children[self::topLevelParent($id, $parentOf, $nodes)][] = $id;
}
return self::attach(0, $nodes, $children);
}
/**
* The parent to attach $id under: its own menu_item_parent, unless that would
* make $id unreachable from the root – a self-parent, a parent missing from
* this menu, or a chain of parents that cycles back on itself instead of
* reaching the root (0) within count($nodes) steps (e.g. A's parent is B and
* B's parent is A). Any of these fall back to the top level (0): a broken or
* cyclic parent link must never make an item disappear from the tree.
*
* @param array<int, int> $parentOf
* @param array<int, array<string, mixed>> $nodes
*/
private static function topLevelParent(int $id, array $parentOf, array $nodes): int
{
$parent = $parentOf[$id];
if ($parent === $id || ! isset($nodes[$parent])) {
return 0;
}
$current = $parent;
for ($steps = 0; $steps < count($nodes); $steps++) {
if ($current === 0) {
return $parent;
}
if (! isset($nodes[$current])) {
return 0;
}
$current = $parentOf[$current];
}
return 0;
}
/** @return array{id: int, title: string, type: string, object: string, object_id: int, url: string} */
public static function node(object $item): array
{