Files
WP-LIT-MCP/src/Abilities/Errors.php
T

240 lines
9.2 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\Abilities;
use WP_Error;
defined('ABSPATH') || exit;
/**
* Factory for the error objects abilities return.
*
* 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
{
/**
* @param array<string, mixed> $details Extra keys for the client, e.g. fields or usages.
*/
public static function make(string $code, string $message, string $hint, int $status, array $details = []): WP_Error
{
$payload = array_merge(['code' => $code, 'message' => $message, 'hint' => $hint], $details);
$json = wp_json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return new WP_Error(
'lit_' . $code,
is_string($json) ? $json : $message,
array_merge(['status' => $status, 'hint' => $hint], $details)
);
}
public static function forbidden(
string $message,
string $hint = 'Mit den Rechten des verbundenen WordPress-Kontos nicht erlaubt; ggf. Administrator fragen.'
): WP_Error {
return self::make('forbidden', $message, $hint, 403);
}
/**
* @param array<string, string> $fields Input path => problem.
*/
public static function invalidInput(
string $message,
array $fields = [],
string $hint = 'Die unter fields genannten Eingaben korrigieren und erneut senden.'
): WP_Error {
return self::make('validation_failed', $message, $hint, 400, $fields === [] ? [] : ['fields' => $fields]);
}
public static function notFound(
string $message,
string $hint = 'ID prüfen, z. B. über lit/content-query (direkt: lit-content-query) oder lit/media-search '
. '(direkt: lit-media-search) suchen.'
): WP_Error {
return self::make('not_found', $message, $hint, 404);
}
/** $message/$hint replace the post-oriented defaults (e.g. for the menu lock menu_rev). */
public static function revMismatch(?string $message = null, ?string $hint = null): WP_Error
{
return self::make(
'rev_mismatch',
$message ?? 'Der Inhalt wurde seit dem letzten Lesen geändert; nichts wurde geschrieben.',
$hint ?? 'Neu lesen (z. B. lit/content-get, direkt: lit-content-get) und die Änderung auf dem aktuellen '
. 'Stand wiederholen.',
409
);
}
/**
* @param list<array<string, mixed>> $usages
* @param string|null $hint Replaces the default hint (force: true, for lit/media-delete).
*/
public static function inUse(string $message, array $usages, ?string $hint = null): WP_Error
{
return self::make(
'in_use',
$message,
$hint ?? 'Verwendungen prüfen; nur mit force: true trotzdem ausführen.',
409,
['usages' => $usages]
);
}
/**
* @param array<string, mixed> $details
*/
public static function confirmRequired(string $message, array $details = []): WP_Error
{
return self::make(
'confirm_required',
$message,
'Beim Nutzer nachfragen und dann mit confirm: true erneut aufrufen.',
409,
$details
);
}
public static function unsupported(string $message, string $hint): WP_Error
{
return self::make('unsupported', $message, $hint, 400);
}
/**
* Unknown Divi attribute paths (spec §5.4, §8.2). $fields names every problem of the call,
* the unknown paths with their closest known ones.
*
* @param array<string, string> $fields Input path => problem.
*/
public static function invalidAttrPath(array $fields): WP_Error
{
return self::make(
'invalid_attr_path',
'Unbekannte Attributpfade; nichts wurde gespeichert.',
'Vorschläge unter fields prüfen; alle gültigen Pfade eines Moduls liefert lit/divi-catalog '
. '(direkt: lit-divi-catalog) mit module.',
400,
['fields' => $fields]
);
}
/**
* A write failed and was rolled back completely.
*
* @param array<string, mixed> $details Extra error details (e.g. a backup that stays).
* @param string|null $hint Replaces the default hint (e.g. "re-read before retrying").
* @param string|null $state Replaces "Nichts wurde gespeichert." when the rollback left
* something behind on purpose (a backup or revision).
*/
public static function writeFailed(
string $reason,
array $details = [],
?string $hint = null,
?string $state = null
): WP_Error {
return self::make(
'write_failed',
trim('Speichern fehlgeschlagen: ' . $reason) . ' ' . ($state ?? 'Nichts wurde gespeichert.'),
$hint ?? 'Eingaben prüfen, ggf. korrigieren und erneut versuchen; tritt der Fehler wieder auf, den '
. 'Administrator informieren (Details im PHP-Error-Log der Website).',
500,
$details
);
}
/**
* The $state for writeFailed() when a clean rollback deliberately keeps something, e.g.
* "Revision 811" or "Sicherung 88": the change itself is not saved, but "Nichts wurde
* gespeichert" would be untrue.
*/
public static function notSavedKeeping(string $kept): string
{
return sprintf('Die Änderung wurde nicht gespeichert (%s bleibt erhalten).', $kept);
}
/**
* A write failed and could not be rolled back completely – something may have been saved.
* $state and $hint replace the post-oriented defaults for writes that are not about a post
* (e.g. menus); $extra is merged into the error details.
*
* @param array<string, mixed> $extra
*/
public static function partialWrite(
string $reason,
?int $postId = null,
?int $revisionId = null,
?string $state = null,
?string $hint = null,
array $extra = []
): WP_Error {
$state ??= $postId === null
? 'Möglicherweise wurde trotzdem ein Entwurf angelegt.'
: sprintf('Eintrag %d ist möglicherweise teilweise geändert bzw. angelegt worden', $postId)
. ($revisionId === null ? '.' : sprintf(' (Stand vorher: Revision %d).', $revisionId));
$details = array_merge($extra, array_filter(
['post_id' => $postId, 'revision_id' => $revisionId],
static fn (?int $id): bool => $id !== null
));
return self::make(
'partial_write',
trim('Speichern fehlgeschlagen: ' . $reason) . ' Die Änderung konnte nicht vollständig zurückgenommen '
. 'werden. ' . $state,
$hint ?? 'Den Aufruf nicht einfach wiederholen. Zuerst den aktuellen Stand prüfen (lit/content-get, '
. 'direkt: lit-content-get; bei einem neuen Entwurf lit/content-query, direkt: lit-content-query) '
. 'und ggf. mit lit/revisions-list und lit/revision-restore (über mcp-adapter-execute-ability) den '
. 'vorherigen Stand wiederherstellen; dem Nutzer Bescheid geben.',
500,
$details
);
}
/** 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(
'internal_error',
'Interner Fehler bei der Ausführung. Details stehen im PHP-Error-Log der Website.',
'Später erneut versuchen oder den Administrator der Website informieren.',
500
);
}
}