refactor: WriteFailed named constructors, Rollback runner, Errors read-back helpers (H6)
- WriteFailed::rolledBack()/partial() replace the 11-parameter constructor (now private); the hint/state of either error are plain $hint/$state. - Rollback::attempt(...steps) runs every undo step and collects failure messages; used by ContentCreate::discard, ContentUpdate::rollBack, MediaUpdate::rollBack and DiviSaver::restore. - Errors::clientMessage/codeOf/fieldsOf replace DiviReplaceTextAbility::reason, the JSON digging in MediaUpdateAbility::failure and ContentCreateAbility::fields. - Errors docblock documents the exception/WP_Error split. - ContentCreateAbility::discard takes the client reason as a parameter (both callers still pass REASON_EXTRAS; changing the Divi text needs a ruling). Error payloads unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,14 @@ namespace LIT\MCP\Integrations\WordPress\Content;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\Rollback;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\WordPress\PostChangeResult;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
@@ -137,7 +139,7 @@ final class ContentCreateAbility implements Ability
|
||||
// A hook threw – the row may or may not exist and its ID is unknown; nothing is
|
||||
// searched or deleted, the client is told to check (lit_partial_write without id).
|
||||
$this->logPartial($postType, '', 'Anlegen abgebrochen; Entwurf möglicherweise vorhanden');
|
||||
throw new WriteFailed(self::REASON_INSERT, false, null, null, $e);
|
||||
throw WriteFailed::partial(self::REASON_INSERT, $e);
|
||||
}
|
||||
if ($postId instanceof WP_Error) {
|
||||
return Errors::invalidInput('WordPress hat den Eintrag abgelehnt: ' . $postId->get_error_message());
|
||||
@@ -153,7 +155,7 @@ final class ContentCreateAbility implements Ability
|
||||
try {
|
||||
$layout = $withDivi ? $this->divi?->write($postId, $postType, self::sections($input)) : null;
|
||||
} catch (Throwable $e) {
|
||||
$this->discard($postId, $postType, $e);
|
||||
$this->discard($postId, $postType, $e, self::REASON_EXTRAS);
|
||||
}
|
||||
if ($layout instanceof WP_Error) {
|
||||
return $this->refuse($postId, $postType, $layout);
|
||||
@@ -162,7 +164,7 @@ final class ContentCreateAbility implements Ability
|
||||
try {
|
||||
$this->writer->writeExtras($postId, $postType, $prepared, $input);
|
||||
} catch (Throwable $e) {
|
||||
$this->discard($postId, $postType, $e);
|
||||
$this->discard($postId, $postType, $e, self::REASON_EXTRAS);
|
||||
}
|
||||
|
||||
$warnings = array_merge($prepared['warnings'], $layout['warnings'] ?? []);
|
||||
@@ -207,7 +209,7 @@ final class ContentCreateAbility implements Ability
|
||||
return $prepared instanceof WP_Error ? $prepared : $divi;
|
||||
}
|
||||
|
||||
$fields = array_merge(self::fields($prepared), self::fields($divi));
|
||||
$fields = array_merge(Errors::fieldsOf($prepared), Errors::fieldsOf($divi));
|
||||
if ($fields === []) {
|
||||
return $prepared;
|
||||
}
|
||||
@@ -217,14 +219,6 @@ final class ContentCreateAbility implements Ability
|
||||
: Errors::invalidInput('Eingaben sind ungültig; nichts wurde angelegt.', $fields);
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
private static function fields(WP_Error $error): array
|
||||
{
|
||||
$data = $error->get_error_data();
|
||||
|
||||
return is_array($data) && is_array($data['fields'] ?? null) ? $data['fields'] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
* @param list<string> $warnings
|
||||
@@ -254,42 +248,38 @@ final class ContentCreateAbility implements Ability
|
||||
|
||||
/**
|
||||
* Rolls the brand-new draft back: terms it created, then the draft itself (force –
|
||||
* it cannot have been used anywhere yet).
|
||||
* it cannot have been used anywhere yet). $reason is the client text of the error.
|
||||
*
|
||||
* @throws WriteFailed Always.
|
||||
*/
|
||||
private function discard(int $postId, string $postType, Throwable $cause): never
|
||||
private function discard(int $postId, string $postType, Throwable $cause, string $reason): never
|
||||
{
|
||||
$problems = [];
|
||||
try {
|
||||
$this->writer->deleteCreatedTerms();
|
||||
} catch (Throwable $e) {
|
||||
$problems[] = $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
if (! wp_delete_post($postId, true)) {
|
||||
$problems[] = sprintf('draft %d could not be deleted', $postId);
|
||||
$problems = Rollback::attempt(
|
||||
fn () => $this->writer->deleteCreatedTerms(),
|
||||
static function () use ($postId): void {
|
||||
try {
|
||||
$deleted = wp_delete_post($postId, true);
|
||||
} catch (Throwable $e) {
|
||||
throw new RuntimeException(
|
||||
sprintf('draft %d could not be deleted: %s', $postId, $e->getMessage()),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
if (! $deleted) {
|
||||
throw new RuntimeException(sprintf('draft %d could not be deleted', $postId));
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$problems[] = sprintf('draft %d could not be deleted: %s', $postId, $e->getMessage());
|
||||
}
|
||||
);
|
||||
|
||||
$detail = sprintf('Extras of new post %d could not be written: %s', $postId, $cause->getMessage());
|
||||
if ($problems === []) {
|
||||
throw new WriteFailed(self::REASON_EXTRAS, true, null, null, $cause, $detail);
|
||||
throw WriteFailed::rolledBack($reason, $cause, $detail);
|
||||
}
|
||||
|
||||
$summary = sprintf('Entwurf %d: Anlegen nur teilweise zurückgenommen', $postId);
|
||||
$this->logPartial($postType, (string) $postId, $summary);
|
||||
throw new WriteFailed(
|
||||
self::REASON_EXTRAS,
|
||||
false,
|
||||
$postId,
|
||||
null,
|
||||
$cause,
|
||||
$detail . '; rollback: ' . implode('; ', $problems)
|
||||
);
|
||||
throw WriteFailed::partial($reason, $cause, $detail . '; rollback: ' . implode('; ', $problems), $postId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,15 +301,12 @@ final class ContentCreateAbility implements Ability
|
||||
if (! $deleted) {
|
||||
$summary = sprintf('Entwurf %d: Anlegen nicht zurückgenommen', $postId);
|
||||
$this->logPartial($postType, (string) $postId, $summary);
|
||||
$fields = $denied instanceof WP_Error ? self::fields($denied) : $denied;
|
||||
throw new WriteFailed(
|
||||
$fields = $denied instanceof WP_Error ? Errors::fieldsOf($denied) : $denied;
|
||||
throw WriteFailed::partial(
|
||||
($denied instanceof WP_Error ? 'Ungültiger Divi-Inhalt: ' : 'Keine Berechtigung für ')
|
||||
. implode(', ', array_keys($fields)) . '.',
|
||||
false,
|
||||
$postId,
|
||||
null,
|
||||
null,
|
||||
sprintf('New post %d refused after the insert; draft could not be deleted', $postId)
|
||||
detail: sprintf('New post %d refused after the insert; draft could not be deleted', $postId),
|
||||
postId: $postId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\WordPress\Content;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\Rollback;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
@@ -218,32 +219,21 @@ final class ContentUpdateAbility implements Ability
|
||||
private function rollBack(WP_Post $post, array $before, ?int $revisionId, array $snapshot, Throwable $cause): never
|
||||
{
|
||||
$postId = (int) $post->ID;
|
||||
$steps = [
|
||||
$failures = Rollback::attempt(
|
||||
fn () => $this->writer->deleteCreatedTerms(),
|
||||
fn () => $this->writer->restore($postId, (string) $post->post_type, $snapshot),
|
||||
fn () => $this->restoreRow($post, $before, $revisionId),
|
||||
];
|
||||
$failures = [];
|
||||
foreach ($steps as $step) {
|
||||
try {
|
||||
$step();
|
||||
} catch (Throwable $e) {
|
||||
$failures[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
fn () => $this->restoreRow($post, $before, $revisionId)
|
||||
);
|
||||
|
||||
$detail = sprintf('Update of post %d failed: %s', $postId, $cause->getMessage());
|
||||
if ($failures === []) {
|
||||
throw new WriteFailed(
|
||||
throw WriteFailed::rolledBack(
|
||||
self::REASON,
|
||||
true,
|
||||
$postId,
|
||||
$revisionId,
|
||||
$cause,
|
||||
$detail,
|
||||
rolledBackState: $revisionId === null
|
||||
? null
|
||||
: Errors::notSavedKeeping(sprintf('Revision %d', $revisionId))
|
||||
state: $revisionId === null ? null : Errors::notSavedKeeping(sprintf('Revision %d', $revisionId)),
|
||||
postId: $postId,
|
||||
revisionId: $revisionId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,13 +244,12 @@ final class ContentUpdateAbility implements Ability
|
||||
sprintf('"%s": Änderung nur teilweise zurückgenommen', $post->post_title),
|
||||
$revisionId
|
||||
);
|
||||
throw new WriteFailed(
|
||||
throw WriteFailed::partial(
|
||||
self::REASON,
|
||||
false,
|
||||
$postId,
|
||||
$revisionId,
|
||||
$cause,
|
||||
$detail . '; rollback incomplete: ' . implode('; ', $failures)
|
||||
$detail . '; rollback incomplete: ' . implode('; ', $failures),
|
||||
$postId,
|
||||
$revisionId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -454,19 +454,22 @@ class MenuWriter
|
||||
. ($this->deleted !== [] ? '; already deleted: ' . implode(', ', $this->deleted) : '')
|
||||
. ($problems !== [] ? '; rollback: ' . implode('; ', $problems) : '');
|
||||
|
||||
throw new WriteFailed(
|
||||
'Das Menü konnte nicht gespeichert werden.',
|
||||
$rolledBack,
|
||||
null,
|
||||
null,
|
||||
$reason = 'Das Menü konnte nicht gespeichert werden.';
|
||||
$details = ['menu_id' => $menuId, 'previous_items' => MenuTree::build(array_values($current))];
|
||||
if ($rolledBack) {
|
||||
throw WriteFailed::rolledBack($reason, $cause, $detail, details: $details);
|
||||
}
|
||||
|
||||
throw WriteFailed::partial(
|
||||
$reason,
|
||||
$cause,
|
||||
$detail,
|
||||
sprintf('Menü %d ist möglicherweise teilweise geändert worden.', $menuId),
|
||||
'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand mit lit/menu-get prüfen und ihn ggf. '
|
||||
. 'mit lit/menu-set-items (beide über mcp-adapter-execute-ability) anhand von previous_items (Stand '
|
||||
. 'vorher) wiederherstellen; entfernte Einträge müssen dabei neu angelegt werden. Dem Nutzer Bescheid '
|
||||
. 'geben.',
|
||||
['menu_id' => $menuId, 'previous_items' => MenuTree::build(array_values($current))]
|
||||
state: sprintf('Menü %d ist möglicherweise teilweise geändert worden.', $menuId),
|
||||
hint: 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand mit lit/menu-get prüfen und ihn '
|
||||
. 'ggf. mit lit/menu-set-items (beide über mcp-adapter-execute-ability) anhand von previous_items '
|
||||
. '(Stand vorher) wiederherstellen; entfernte Einträge müssen dabei neu angelegt werden. Dem Nutzer '
|
||||
. 'Bescheid geben.',
|
||||
details: $details
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ final class WpSettingsUpdateAbility implements Ability
|
||||
|
||||
$reason = 'Eine Einstellung konnte nicht gespeichert werden.';
|
||||
if ($stuck === []) {
|
||||
throw new WriteFailed($reason, true, null, null, $cause);
|
||||
throw WriteFailed::rolledBack($reason, $cause);
|
||||
}
|
||||
|
||||
$this->log->recordPartial(
|
||||
@@ -247,17 +247,14 @@ final class WpSettingsUpdateAbility implements Ability
|
||||
implode(',', $stuck),
|
||||
'Einstellungen nur teilweise zurückgenommen: ' . implode(', ', $stuck)
|
||||
);
|
||||
throw new WriteFailed(
|
||||
throw WriteFailed::partial(
|
||||
$reason,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
$cause,
|
||||
$cause->getMessage() . '; not restored: ' . implode(', ', $stuck),
|
||||
sprintf('Möglicherweise auf dem neuen, nicht vollständigen Stand: %s.', implode(', ', $stuck)),
|
||||
'Aktuelle Werte über lit/wp-settings-get (mcp-adapter-execute-ability) prüfen und ggf. erneut auf die '
|
||||
. 'gewünschten Werte setzen.',
|
||||
['keys' => $stuck]
|
||||
state: sprintf('Möglicherweise auf dem neuen, nicht vollständigen Stand: %s.', implode(', ', $stuck)),
|
||||
hint: 'Aktuelle Werte über lit/wp-settings-get (mcp-adapter-execute-ability) prüfen und ggf. erneut auf '
|
||||
. 'die gewünschten Werte setzen.',
|
||||
details: ['keys' => $stuck]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,13 +182,9 @@ class PostCloner
|
||||
|
||||
if ($cloneId instanceof WP_Error || (int) $cloneId === 0) {
|
||||
$reason = $cloneId instanceof WP_Error ? $cloneId->get_error_message() : 'unknown error';
|
||||
throw new WriteFailed(
|
||||
throw WriteFailed::rolledBack(
|
||||
$label . ' konnte nicht angelegt werden.',
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
sprintf('Clone (%s) of post %d could not be created: %s', $markerKey, $originalId, $reason)
|
||||
detail: sprintf('Clone (%s) of post %d could not be created: %s', $markerKey, $originalId, $reason)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,10 +212,10 @@ class PostCloner
|
||||
$deleted = wp_delete_post($cloneId, true);
|
||||
$detail = sprintf('Clone %d of post %d failed: %s', $cloneId, (int) $original->ID, $cause->getMessage());
|
||||
if ($deleted) {
|
||||
throw new WriteFailed($reason, true, null, null, $cause, $detail . '; clone deleted');
|
||||
throw WriteFailed::rolledBack($reason, $cause, $detail . '; clone deleted');
|
||||
}
|
||||
|
||||
throw new WriteFailed($reason, false, $cloneId, null, $cause, $detail . '; clone could not be deleted');
|
||||
throw WriteFailed::partial($reason, $cause, $detail . '; clone could not be deleted', $cloneId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -126,7 +126,7 @@ final class PostReplaceWithCopyAbility implements Ability
|
||||
try {
|
||||
$snapshot = $this->cloner->snapshot($original);
|
||||
} catch (Throwable $e) {
|
||||
throw new WriteFailed(self::REASON, true, $originalId, null, $e);
|
||||
throw WriteFailed::rolledBack(self::REASON, $e, postId: $originalId);
|
||||
}
|
||||
|
||||
// Throws WriteFailed (backup deleted again) before anything touches the original.
|
||||
@@ -275,13 +275,12 @@ final class PostReplaceWithCopyAbility implements Ability
|
||||
sprintf('"%s": Austausch nur teilweise zurückgenommen', $original->post_title),
|
||||
$revisionId
|
||||
);
|
||||
throw new WriteFailed(
|
||||
throw WriteFailed::partial(
|
||||
self::REASON,
|
||||
false,
|
||||
$originalId,
|
||||
$revisionId,
|
||||
$cause,
|
||||
$detail . '; rollback incomplete: ' . $e->getMessage(),
|
||||
$originalId,
|
||||
$revisionId,
|
||||
sprintf(
|
||||
'Eintrag %d ist möglicherweise teilweise geändert worden (Stand vorher: Revision %s); die '
|
||||
. 'vollständige Sicherung des vorherigen Stands liegt als Eintrag %d im Papierkorb.',
|
||||
@@ -297,17 +296,14 @@ final class PostReplaceWithCopyAbility implements Ability
|
||||
);
|
||||
}
|
||||
|
||||
throw new WriteFailed(
|
||||
throw WriteFailed::rolledBack(
|
||||
self::REASON,
|
||||
true,
|
||||
$originalId,
|
||||
$revisionId,
|
||||
$cause,
|
||||
$detail,
|
||||
null,
|
||||
null,
|
||||
$details,
|
||||
rolledBackState: Errors::notSavedKeeping(sprintf('Sicherung %d im Papierkorb', $backupId))
|
||||
state: Errors::notSavedKeeping(sprintf('Sicherung %d im Papierkorb', $backupId)),
|
||||
details: $details,
|
||||
postId: $originalId,
|
||||
revisionId: $revisionId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,16 +33,13 @@ final class StatusWrite
|
||||
sprintf('"%s": Statusänderung abgebrochen, Stand unklar', $post->post_title)
|
||||
);
|
||||
|
||||
throw new WriteFailed(
|
||||
throw WriteFailed::partial(
|
||||
$reason,
|
||||
false,
|
||||
$postId,
|
||||
null,
|
||||
$e,
|
||||
'',
|
||||
sprintf('Status von Eintrag %d ist möglicherweise schon geändert worden.', $postId),
|
||||
'Den Aufruf nicht einfach wiederholen. Zuerst mit lit/content-get (direkt: lit-content-get) Status '
|
||||
. 'und Inhalt prüfen und dem Nutzer Bescheid geben.'
|
||||
postId: $postId,
|
||||
state: sprintf('Status von Eintrag %d ist möglicherweise schon geändert worden.', $postId),
|
||||
hint: 'Den Aufruf nicht einfach wiederholen. Zuerst mit lit/content-get (direkt: lit-content-get) '
|
||||
. 'Status und Inhalt prüfen und dem Nutzer Bescheid geben.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user