feat: content-create and content-update with schema validation
All-or-nothing writes: validation (fields, terms, featured image, parent, template) before any write; failed extras delete the new draft (create) or restore revision, old columns, fields, terms and featured image (update). content-update requires rev; Rev::ofPost covers content, title, excerpt, status and post_modified_gmt. MetaFieldSource::write() now throws on failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -4,8 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\WordPress\Schema;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Shared read/write via the post meta API.
|
||||
* Shared read/write via the post meta API. write() throws when WordPress did not
|
||||
* store a value, so callers can roll back (all or nothing, spec §8.2).
|
||||
*/
|
||||
abstract class MetaFieldSource implements FieldSource
|
||||
{
|
||||
@@ -19,20 +22,50 @@ abstract class MetaFieldSource implements FieldSource
|
||||
foreach ($values as $key => $value) {
|
||||
$key = (string) $key;
|
||||
if ($value === null) {
|
||||
delete_post_meta($postId, $key);
|
||||
$this->delete($postId, $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
$field = $definitions[$key] ?? null;
|
||||
if ($field !== null && $field->multiple && is_array($value) && array_is_list($value)) {
|
||||
delete_post_meta($postId, $key);
|
||||
$this->delete($postId, $key);
|
||||
foreach ($value as $item) {
|
||||
add_post_meta($postId, $key, wp_slash($item));
|
||||
if (add_post_meta($postId, $key, wp_slash($item)) === false) {
|
||||
throw new RuntimeException(sprintf('Meta "%s" of post %d could not be added.', $key, $postId));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
update_post_meta($postId, $key, wp_slash($value));
|
||||
// update_post_meta() also returns false when the value is unchanged – only a
|
||||
// stored value that still differs is a failure.
|
||||
if (update_post_meta($postId, $key, wp_slash($value)) === false && ! $this->holds($postId, $key, $value)) {
|
||||
throw new RuntimeException(sprintf('Meta "%s" of post %d could not be saved.', $key, $postId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function delete(int $postId, string $key): void
|
||||
{
|
||||
// delete_post_meta() returns false as well when there was nothing to delete.
|
||||
if (delete_post_meta($postId, $key) === false && metadata_exists('post', $postId, $key)) {
|
||||
throw new RuntimeException(sprintf('Meta "%s" of post %d could not be deleted.', $key, $postId));
|
||||
}
|
||||
}
|
||||
|
||||
private function holds(int $postId, string $key, mixed $value): bool
|
||||
{
|
||||
$stored = get_post_meta($postId, $key, false);
|
||||
if (! is_array($stored) || count($stored) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current = $stored[0];
|
||||
if (is_scalar($current) && is_scalar($value)) {
|
||||
// Meta is stored as text: 3 and "3", true and "1" are the same stored value.
|
||||
return (string) $current === (string) $value;
|
||||
}
|
||||
|
||||
return $current == $value; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user