fix: typed write failures, template rollback, parent and term cleanup
WriteFailed maps to lit_write_failed / lit_partial_write (partial writes are logged with result partial); old template read before the update; row marker set before wp_update_post; created terms deleted on rollback; parent must be a readable non-descendant of a hierarchical type; featured image must be readable; snapshot keeps empty values; meta comparison after sanitize_meta; follow-up failures (listener, change log) only add warnings. 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:
@@ -39,7 +39,8 @@ abstract class MetaFieldSource implements FieldSource
|
||||
|
||||
// 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)) {
|
||||
$updated = update_post_meta($postId, $key, wp_slash($value));
|
||||
if ($updated === false && ! $this->holds($postId, $postType, $key, $value)) {
|
||||
throw new RuntimeException(sprintf('Meta "%s" of post %d could not be saved.', $key, $postId));
|
||||
}
|
||||
}
|
||||
@@ -53,8 +54,14 @@ abstract class MetaFieldSource implements FieldSource
|
||||
}
|
||||
}
|
||||
|
||||
private function holds(int $postId, string $key, mixed $value): bool
|
||||
/**
|
||||
* Whether the stored value already is what update_post_meta() would have stored –
|
||||
* compared after sanitize_meta(), like WordPress does, so registered sanitize
|
||||
* callbacks do not look like failures.
|
||||
*/
|
||||
private function holds(int $postId, string $postType, string $key, mixed $value): bool
|
||||
{
|
||||
$value = sanitize_meta($key, $value, 'post', $postType);
|
||||
$stored = get_post_meta($postId, $key, false);
|
||||
if (! is_array($stored) || count($stored) !== 1) {
|
||||
return false;
|
||||
|
||||
@@ -49,31 +49,69 @@ final class TermAssigner
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the post's terms per taxonomy, creating terms given by name. If this
|
||||
* fails, the terms it created in this call are deleted again before it throws.
|
||||
*
|
||||
* @param array<string, list<int|string>> $resolved Output of resolve().
|
||||
* @return array<int, string> Created terms: term ID => taxonomy (for a later rollback).
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function assign(int $postId, array $resolved): void
|
||||
public function assign(int $postId, array $resolved): array
|
||||
{
|
||||
foreach ($resolved as $taxonomy => $items) {
|
||||
$ids = [];
|
||||
foreach ($items as $item) {
|
||||
if (is_int($item)) {
|
||||
$ids[] = $item;
|
||||
continue;
|
||||
$created = [];
|
||||
try {
|
||||
foreach ($resolved as $taxonomy => $items) {
|
||||
$ids = [];
|
||||
foreach ($items as $item) {
|
||||
if (is_int($item)) {
|
||||
$ids[] = $item;
|
||||
continue;
|
||||
}
|
||||
|
||||
$term = wp_insert_term($item, $taxonomy);
|
||||
if (is_wp_error($term)) {
|
||||
throw new RuntimeException('Term could not be created: ' . $term->get_error_message());
|
||||
}
|
||||
$ids[] = (int) $term['term_id'];
|
||||
$created[(int) $term['term_id']] = $taxonomy;
|
||||
}
|
||||
|
||||
$created = wp_insert_term($item, $taxonomy);
|
||||
if (is_wp_error($created)) {
|
||||
throw new RuntimeException('Term could not be created: ' . $created->get_error_message());
|
||||
$result = wp_set_object_terms($postId, $ids, $taxonomy, false);
|
||||
if (is_wp_error($result)) {
|
||||
throw new RuntimeException('Terms could not be assigned: ' . $result->get_error_message());
|
||||
}
|
||||
$ids[] = (int) $created['term_id'];
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
try {
|
||||
$this->deleteTerms($created);
|
||||
} catch (RuntimeException $cleanup) {
|
||||
throw new RuntimeException($e->getMessage() . '; ' . $cleanup->getMessage(), 0, $e);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$result = wp_set_object_terms($postId, $ids, $taxonomy, false);
|
||||
if (is_wp_error($result)) {
|
||||
throw new RuntimeException('Terms could not be assigned: ' . $result->get_error_message());
|
||||
return $created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes terms (e.g. the ones assign() created). A term that no longer exists is fine.
|
||||
*
|
||||
* @param array<int, string> $terms Term ID => taxonomy.
|
||||
* @throws RuntimeException Naming every term that could not be deleted.
|
||||
*/
|
||||
public function deleteTerms(array $terms): void
|
||||
{
|
||||
$failed = [];
|
||||
foreach ($terms as $termId => $taxonomy) {
|
||||
$result = wp_delete_term((int) $termId, $taxonomy);
|
||||
if ($result !== true && $result !== false) {
|
||||
$failed[] = sprintf('%s %d', $taxonomy, $termId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($failed !== []) {
|
||||
throw new RuntimeException('Created terms could not be deleted: ' . implode(', ', $failed));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user