> $tree Current tree (MenuTree::build()). * @param list $operations * @return list>|WP_Error */ public function apply(array $tree, array $operations): array|WP_Error { $nodes = []; $children = [self::ROOT => []]; $this->flatten($tree, self::ROOT, $nodes, $children); foreach ($operations as $index => $operation) { $error = $this->applyOne($operation, (int) $index, $nodes, $children); if ($error !== null) { return Errors::invalidInput( 'Menü-Operation ungültig; nichts wurde geändert.', ['operations.' . $index => $error] ); } } return $this->nest(self::ROOT, $nodes, $children); } /** * @param list> $tree * @param array> $nodes * @param array> $children */ private function flatten(array $tree, string $parent, array &$nodes, array &$children): void { foreach ($tree as $item) { $key = 'id:' . (int) $item['id']; $nodes[$key] = ['id' => (int) $item['id']]; $children[$parent][] = $key; $children[$key] = []; $this->flatten((array) ($item['children'] ?? []), $key, $nodes, $children); } } /** * @param array> $nodes * @param array> $children */ private function applyOne(mixed $operation, int $index, array &$nodes, array &$children): ?string { if (! is_array($operation)) { return 'Objekt erwartet.'; } $op = $operation['op'] ?? ''; $parentId = (int) ($operation['parent_id'] ?? 0); $parentKey = $parentId > 0 ? 'id:' . $parentId : self::ROOT; // parent_id only matters where something is placed; remove ignores it. if (in_array($op, ['add', 'move'], true) && ! isset($children[$parentKey])) { return 'parent_id ist kein Eintrag dieses Menüs.'; } $position = isset($operation['position']) ? (int) $operation['position'] : null; $key = 'id:' . (int) ($operation['id'] ?? 0); switch ($op) { case 'add': if (! is_array($operation['item'] ?? null)) { return 'item fehlt.'; } $newKey = 'new:' . $index; $nodes[$newKey] = array_diff_key($operation['item'], ['id' => true, 'children' => true]); $children[$newKey] = []; $this->insert($children[$parentKey], $newKey, $position); return null; case 'move': if (! isset($nodes[$key])) { return 'id ist kein Eintrag dieses Menüs.'; } if ($key === $parentKey || $this->isDescendant($parentKey, $key, $children)) { return 'Ein Eintrag kann nicht unter sich selbst verschoben werden.'; } $this->detach($key, $children); $this->insert($children[$parentKey], $key, $position); return null; case 'remove': if (! isset($nodes[$key])) { return 'id ist kein Eintrag dieses Menüs.'; } [$owner, $at] = $this->detach($key, $children); array_splice($children[$owner], $at, 0, $children[$key]); unset($nodes[$key], $children[$key]); return null; default: return 'op muss add, move oder remove sein.'; } } /** @param list $list */ private function insert(array &$list, string $key, ?int $position): void { if ($position === null || $position >= count($list)) { $list[] = $key; return; } array_splice($list, max(0, $position), 0, [$key]); } /** * @param array> $children * @return array{0: string, 1: int} Former parent key and index. */ private function detach(string $key, array &$children): array { foreach ($children as $parent => $list) { $index = array_search($key, $list, true); if ($index !== false) { array_splice($children[$parent], (int) $index, 1); return [(string) $parent, (int) $index]; } } return [self::ROOT, count($children[self::ROOT])]; } /** @param array> $children */ private function isDescendant(string $candidate, string $ancestor, array $children): bool { foreach ($children[$ancestor] ?? [] as $child) { if ($child === $candidate || $this->isDescendant($candidate, $child, $children)) { return true; } } return false; } /** * @param array> $nodes * @param array> $children * @return list> */ private function nest(string $parent, array $nodes, array $children): array { $result = []; foreach ($children[$parent] ?? [] as $key) { $result[] = array_merge($nodes[$key], ['children' => $this->nest($key, $nodes, $children)]); } return $result; } }