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:
@@ -12,6 +12,14 @@ use WP_Error;
|
||||
* mcp-adapter 0.5 turns a WP_Error into a tool result with isError: true but
|
||||
* forwards only the message text. The message therefore carries the whole
|
||||
* structured error as JSON: {code, message, hint, fields?, usages?, …} (spec §8.2).
|
||||
* clientMessage(), codeOf() and fieldsOf() read such an error back (batch results that list
|
||||
* per-item errors, merged validation errors).
|
||||
*
|
||||
* How failures travel: pure helpers (e.g. the Divi Compose classes) throw
|
||||
* InvalidArgumentException, which the caller collects into `fields`; abilities return a
|
||||
* WP_Error from this factory; writes that may have left something behind throw WriteFailed
|
||||
* (rolledBack()/partial()), which the AbilityRegistrar maps via toError(); internal steps
|
||||
* throw RuntimeException, which is logged and becomes internal_error.
|
||||
*/
|
||||
final class Errors
|
||||
{
|
||||
@@ -190,6 +198,32 @@ final class Errors
|
||||
);
|
||||
}
|
||||
|
||||
/** The client message of an error (its JSON "message"), without the JSON around it. */
|
||||
public static function clientMessage(WP_Error $error): string
|
||||
{
|
||||
$data = json_decode($error->get_error_message(), true);
|
||||
|
||||
return is_array($data) && is_string($data['message'] ?? null) ? $data['message'] : $error->get_error_message();
|
||||
}
|
||||
|
||||
/** The error code as the client sees it (without the "lit_" prefix of the WP_Error code). */
|
||||
public static function codeOf(WP_Error $error): string
|
||||
{
|
||||
return (string) preg_replace('/^lit_/', '', (string) $error->get_error_code());
|
||||
}
|
||||
|
||||
/**
|
||||
* The `fields` of a validation error (input path => problem); [] when it has none.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function fieldsOf(WP_Error $error): array
|
||||
{
|
||||
$data = $error->get_error_data();
|
||||
|
||||
return is_array($data) && is_array($data['fields'] ?? null) ? $data['fields'] : [];
|
||||
}
|
||||
|
||||
public static function internal(): WP_Error
|
||||
{
|
||||
return self::make(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Abilities;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Runs the undo steps of a failed write. Every step runs, even after an earlier one failed;
|
||||
* a step fails by throwing. The caller turns the collected messages into the log detail and
|
||||
* picks WriteFailed::rolledBack() (no failures) or WriteFailed::partial().
|
||||
*/
|
||||
final class Rollback
|
||||
{
|
||||
/**
|
||||
* @param callable(): mixed ...$steps In the order they must run.
|
||||
* @return list<string> The messages of the steps that threw, in step order. Never throws.
|
||||
*/
|
||||
public static function attempt(callable ...$steps): array
|
||||
{
|
||||
$failures = [];
|
||||
foreach ($steps as $step) {
|
||||
try {
|
||||
$step();
|
||||
} catch (Throwable $e) {
|
||||
$failures[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $failures;
|
||||
}
|
||||
}
|
||||
@@ -12,27 +12,28 @@ use WP_Error;
|
||||
* Thrown by write paths when saving failed after something may already have been
|
||||
* written. getMessage() is the internal detail for the PHP error log; $reason is the
|
||||
* client-safe German text. The AbilityRegistrar logs the detail and returns toError():
|
||||
* lit_write_failed when everything was rolled back, lit_partial_write otherwise.
|
||||
* $state/$hint customise the partial-write error for writes that are not about a post (see
|
||||
* Errors::partialWrite()); $details are added to either error. $rolledBackHint replaces the
|
||||
* default hint of the write_failed error, $rolledBackState its "Nichts wurde gespeichert."
|
||||
* (for rollbacks that deliberately keep a backup or revision).
|
||||
* lit_write_failed when everything was rolled back (rolledBack()), lit_partial_write
|
||||
* otherwise (partial()).
|
||||
*
|
||||
* $detail is the log text (default: the cause's message, else the reason); $details are
|
||||
* added to either error. For rolledBack(), $hint replaces the default hint and $state the
|
||||
* "Nichts wurde gespeichert." (for rollbacks that deliberately keep a backup or revision,
|
||||
* see Errors::notSavedKeeping()). For partial(), $state and $hint replace the post-oriented
|
||||
* defaults for writes that are not about a post (see Errors::partialWrite()).
|
||||
*/
|
||||
final class WriteFailed extends RuntimeException
|
||||
{
|
||||
/** @param array<string, mixed> $details Extra error details for the client. */
|
||||
public function __construct(
|
||||
private function __construct(
|
||||
public readonly string $reason,
|
||||
public readonly bool $rolledBack,
|
||||
public readonly ?int $postId = null,
|
||||
public readonly ?int $revisionId = null,
|
||||
?Throwable $previous = null,
|
||||
string $detail = '',
|
||||
public readonly ?string $state = null,
|
||||
public readonly ?string $hint = null,
|
||||
public readonly array $details = [],
|
||||
public readonly ?string $rolledBackHint = null,
|
||||
public readonly ?string $rolledBackState = null
|
||||
public readonly ?int $postId,
|
||||
public readonly ?int $revisionId,
|
||||
?Throwable $previous,
|
||||
string $detail,
|
||||
public readonly ?string $state,
|
||||
public readonly ?string $hint,
|
||||
public readonly array $details
|
||||
) {
|
||||
$internal = $detail !== '' ? $detail : ($previous !== null ? $previous->getMessage() : $reason);
|
||||
parent::__construct(
|
||||
@@ -42,10 +43,47 @@ final class WriteFailed extends RuntimeException
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything was restored (lit_write_failed). $postId/$revisionId only name the entry for
|
||||
* callers that log it; the client error does not carry them.
|
||||
*
|
||||
* @param array<string, mixed> $details
|
||||
*/
|
||||
public static function rolledBack(
|
||||
string $reason,
|
||||
?Throwable $cause = null,
|
||||
string $detail = '',
|
||||
?string $hint = null,
|
||||
?string $state = null,
|
||||
array $details = [],
|
||||
?int $postId = null,
|
||||
?int $revisionId = null
|
||||
): self {
|
||||
return new self($reason, true, $postId, $revisionId, $cause, $detail, $state, $hint, $details);
|
||||
}
|
||||
|
||||
/**
|
||||
* The rollback was incomplete – something may have been saved (lit_partial_write).
|
||||
*
|
||||
* @param array<string, mixed> $details
|
||||
*/
|
||||
public static function partial(
|
||||
string $reason,
|
||||
?Throwable $cause = null,
|
||||
string $detail = '',
|
||||
?int $postId = null,
|
||||
?int $revisionId = null,
|
||||
?string $state = null,
|
||||
?string $hint = null,
|
||||
array $details = []
|
||||
): self {
|
||||
return new self($reason, false, $postId, $revisionId, $cause, $detail, $state, $hint, $details);
|
||||
}
|
||||
|
||||
public function toError(): WP_Error
|
||||
{
|
||||
return $this->rolledBack
|
||||
? Errors::writeFailed($this->reason, $this->details, $this->rolledBackHint, $this->rolledBackState)
|
||||
? Errors::writeFailed($this->reason, $this->details, $this->hint, $this->state)
|
||||
: Errors::partialWrite(
|
||||
$this->reason,
|
||||
$this->postId,
|
||||
|
||||
Reference in New Issue
Block a user