Files
WP-LIT-MCP/src/Integrations/Divi/Pages/DiviUpdateAbility.php
T

180 lines
7.6 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\Divi\Pages;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\Compose\ColumnWidths;
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/divi-update: sets or removes values of existing nodes – short-form fields ("set"), full
* form ("attrs", null removes a path or breakpoint), preset and admin label (spec §5.7). Several
* nodes per call, all or nothing (ruling S39): every change is validated first and all errors
* come back together; only then are the attributes Composer::update() computed set. A node may
* appear once per call (ruling S14); set and attrs of one change must not reach the same path
* (ruling S38, checked by Composer::update()).
*/
final class DiviUpdateAbility extends DiviWriteAbility
{
public function name(): string
{
return 'lit/divi-update';
}
public function label(): string
{
return 'Divi-Knoten ändern';
}
public function description(): string
{
return 'Ändert vorhandene Divi-Knoten: changes = Liste von {id, set: {Kurzform-Felder}, attrs: {Attributpfad: '
. '{Breakpoint: Wert}}, preset, group_presets, clear_overrides, label}. null entfernt einen Wert. '
. 'preset = Name/ID oder Liste (Stapel, bis ' . PresetAssignment::MAX_STACK . '; bei gleicher Priorität '
. 'gewinnt das letzte Preset im Stapel); group_presets = {Gruppe: Preset}, z. B. '
. '{"font-body": "Fließtext"}. Überdecken lokale Werte '
. 'ein Preset, meldet die Antwort das; clear_overrides: true entfernt sie (nach set/attrs). Nicht '
. 'genannte Einstellungen bleiben unverändert. Jeder Knoten nur einmal pro Aufruf; denselben Pfad nicht '
. 'zugleich über set und attrs. tree_rev aus lit-divi-outline ist Pflicht; alles oder nichts.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => array_merge(DiviSchema::write(), [
'changes' => [
'type' => 'array',
'minItems' => 1,
'maxItems' => 50,
'items' => [
'type' => 'object',
'properties' => [
'id' => ['type' => 'string'],
'set' => ['type' => 'object', 'description' => 'Kurzform-Felder, z. B. {"text": "Neu"}.'],
'attrs' => [
'type' => 'object',
'description' => 'Vollform: Attributpfad → Breakpoint-Werte.',
],
'preset' => PresetAssignment::valueSchema(
true,
'Preset-Name oder -ID, Liste (Stapel, 1–' . PresetAssignment::MAX_STACK
. ') oder null zum Entfernen. Bei gleicher Priorität gewinnt das letzte '
. 'Preset im Stapel.'
),
'group_presets' => [
'type' => 'object',
'description' => 'Gruppen-Presets: {Gruppe: Preset-Name | Liste | null}. Gruppe = '
. 'Kurzname (font-body), voller Name (divi/font-body) oder Attributpfad; null '
. 'entfernt den Eintrag der Gruppe.',
'additionalProperties' => PresetAssignment::valueSchema(true),
],
'clear_overrides' => [
'type' => 'boolean',
'description' => 'true entfernt lokale Werte, die Werte der zugewiesenen Presets '
. 'überdecken (nach set und attrs).',
],
'label' => [
'type' => ['string', 'null'],
'description' => 'Admin-Label im Divi-Builder; null entfernt.',
],
],
'required' => ['id'],
'additionalProperties' => false,
],
],
]),
'required' => ['post_id', 'tree_rev', 'changes'],
'additionalProperties' => false,
];
}
public function annotations(): array
{
return Annotations::write(true);
}
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
{
$changes = is_array($input['changes'] ?? null) ? array_values($input['changes']) : [];
if ($changes === []) {
return Errors::invalidInput('Keine Änderungen angegeben; nichts wurde gespeichert.', [
'changes' => 'mindestens eine Änderung',
]);
}
$composer = $composition->composer();
$errors = [];
/** @var list<string> $invalidPaths */
$invalidPaths = [];
/** @var array<string, int> $seen id => position of its first change */
$seen = [];
/** @var list<array{0: Block, 1: array<string, mixed>, 2: string}> $updates */
$updates = [];
foreach ($changes as $position => $change) {
$field = sprintf('changes[%d]', $position);
if (! is_array($change)) {
$errors[$field] = 'Objekt mit id erwartet';
continue;
}
$id = is_string($change['id'] ?? null) ? $change['id'] : '';
if ($id !== '' && isset($seen[$id])) {
// Ruling S14: a second change of the same node would overwrite the first silently.
$errors[$field . '.id'] = sprintf(
'Knoten "%s" steht schon in changes[%d]; alle Änderungen eines Knotens in einer Änderung bündeln',
$id,
$seen[$id]
);
continue;
}
$block = $id === '' ? null : $page->index->find($id);
if ($id !== '') {
$seen[$id] = $position;
}
if ($block === null) {
$errors[$field . '.id'] = sprintf('Knoten "%s" gibt es nicht (lit-divi-outline neu lesen)', $id);
continue;
}
$result = $composer->update($block, $change, $field);
$errors += $result['errors'];
array_push($invalidPaths, ...$result['invalid_paths']);
$updates[] = [$block, $result['attrs'], $field];
}
if ($errors === []) {
$errors = ColumnWidths::updateErrors($page->index, $updates);
}
if ($errors !== []) {
return DiviErrors::composed(
['errors' => $errors, 'invalid_paths' => $invalidPaths],
'Änderungen sind ungültig; nichts wurde gespeichert.'
);
}
$updates = array_values(array_filter(
$updates,
static fn (array $update): bool => $update[1] !== $update[0]->attrs()
));
$change = new DiviChange(sprintf('%d Divi-Knoten geändert', count($updates)));
foreach ($updates as [$block, $attrs]) {
// Composer::update() output: every client value already passed the choke point.
$block->setAttrs($attrs);
$change->record($block, 'updated');
}
return $change;
}
}