Files
WP-LIT-MCP/src/Integrations/WordPress/Menus/MenuOperations.php
T

166 lines
5.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Menus;
use LIT\MCP\Abilities\Errors;
use WP_Error;
/**
* Applies add/move/remove operations to the current menu tree and returns the
* desired tree for MenuWriter – so single operations and the declarative mode
* share one validated write path.
*/
final class MenuOperations
{
private const ROOT = 'root';
/**
* @param list<array<string, mixed>> $tree Current tree (MenuTree::build()).
* @param list<mixed> $operations
* @return list<array<string, mixed>>|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<array<string, mixed>> $tree
* @param array<string, array<string, mixed>> $nodes
* @param array<string, list<string>> $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<string, array<string, mixed>> $nodes
* @param array<string, list<string>> $children
*/
private function applyOne(mixed $operation, int $index, array &$nodes, array &$children): ?string
{
if (! is_array($operation)) {
return 'Objekt erwartet.';
}
$parentId = (int) ($operation['parent_id'] ?? 0);
$parentKey = $parentId > 0 ? 'id:' . $parentId : self::ROOT;
if (! 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 ($operation['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<string> $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<string, list<string>> $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<string, list<string>> $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<string, array<string, mixed>> $nodes
* @param array<string, list<string>> $children
* @return list<array<string, mixed>>
*/
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;
}
}