feat: batch media metadata updates and delete with usage check
lit/media-update validates the whole batch first (edit_post, lengths, optional modified lock) and restores already written items when a write fails midway. lit/media-delete requires confirm, refuses used media without force and reports refused deletes as write_failed. 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:
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Media;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* Writes the editable metadata of an attachment (alt text, title, caption, description) and
|
||||
* restores it from a snapshot taken before (lit/media-update rolls a batch back with it).
|
||||
* Every failure is a RuntimeException with an internal message for the PHP error log.
|
||||
*/
|
||||
final class MediaMetaWriter
|
||||
{
|
||||
public const ALT_KEY = '_wp_attachment_image_alt';
|
||||
|
||||
/** Input key => posts column. */
|
||||
public const COLUMNS = ['title' => 'post_title', 'caption' => 'post_excerpt', 'description' => 'post_content'];
|
||||
|
||||
/**
|
||||
* The current values of everything $changes will write.
|
||||
*
|
||||
* @param array<string, string> $changes Sanitized values by input key (alt, title, caption, description).
|
||||
* @return array{id: int, columns: array<string, string>, alt_changed: bool, alt: ?string}
|
||||
* alt null = there was no alt text (it is removed again on restore).
|
||||
*/
|
||||
public function snapshot(WP_Post $attachment, array $changes): array
|
||||
{
|
||||
$id = (int) $attachment->ID;
|
||||
$columns = [];
|
||||
foreach (self::columns($changes) as $column => $value) {
|
||||
$columns[$column] = (string) $attachment->{$column};
|
||||
}
|
||||
|
||||
$altChanged = array_key_exists('alt', $changes);
|
||||
$alt = $altChanged && metadata_exists('post', $id, self::ALT_KEY)
|
||||
? (string) get_post_meta($id, self::ALT_KEY, true)
|
||||
: null;
|
||||
|
||||
return ['id' => $id, 'columns' => $columns, 'alt_changed' => $altChanged, 'alt' => $alt];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $changes Sanitized values by input key.
|
||||
* @param bool $touched Set to true as soon as something may have been written.
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function apply(int $id, array $changes, bool &$touched): void
|
||||
{
|
||||
$columns = self::columns($changes);
|
||||
if ($columns !== []) {
|
||||
// Set before the call: a throwing save_post hook runs after the row was written.
|
||||
$touched = true;
|
||||
$result = wp_update_post(wp_slash(array_merge($columns, ['ID' => $id])), true);
|
||||
if ($result instanceof WP_Error || ! $result) {
|
||||
// wp_update_post() reports errors before it writes anything.
|
||||
$touched = false;
|
||||
throw new RuntimeException(sprintf(
|
||||
'Attachment %d not updated: %s',
|
||||
$id,
|
||||
$result instanceof WP_Error ? $result->get_error_message() : 'unknown error'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('alt', $changes)) {
|
||||
$touched = true;
|
||||
$this->saveAlt($id, $changes['alt']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{id: int, columns: array<string, string>, alt_changed: bool, alt: ?string} $snapshot
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function restore(array $snapshot): void
|
||||
{
|
||||
$id = $snapshot['id'];
|
||||
if ($snapshot['columns'] !== []) {
|
||||
$result = wp_update_post(wp_slash(array_merge($snapshot['columns'], ['ID' => $id])), true);
|
||||
if ($result instanceof WP_Error || ! $result) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Attachment %d not restored: %s',
|
||||
$id,
|
||||
$result instanceof WP_Error ? $result->get_error_message() : 'unknown error'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (! $snapshot['alt_changed']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($snapshot['alt'] !== null) {
|
||||
$this->saveAlt($id, $snapshot['alt']);
|
||||
} elseif (! delete_post_meta($id, self::ALT_KEY) && metadata_exists('post', $id, self::ALT_KEY)) {
|
||||
throw new RuntimeException(sprintf('Alt text of attachment %d not removed again.', $id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $changes
|
||||
* @return array<string, string> Column => value.
|
||||
*/
|
||||
private static function columns(array $changes): array
|
||||
{
|
||||
$columns = [];
|
||||
foreach (self::COLUMNS as $key => $column) {
|
||||
if (array_key_exists($key, $changes)) {
|
||||
$columns[$column] = $changes[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/** @throws RuntimeException */
|
||||
private function saveAlt(int $id, string $alt): void
|
||||
{
|
||||
// update_post_meta() is also false when the value is already stored.
|
||||
$stored = update_post_meta($id, self::ALT_KEY, wp_slash($alt)) !== false
|
||||
|| (string) get_post_meta($id, self::ALT_KEY, true) === $alt;
|
||||
if (! $stored) {
|
||||
throw new RuntimeException(sprintf('Alt text of attachment %d not saved.', $id));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user