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:
2026-09-26 17:40:32 +02:00
co-authored by Claude Opus 5.5
parent fb6eabf80c
commit 8e5ba9eac4
23 changed files with 358 additions and 207 deletions
@@ -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.'
);
}
}