1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

wp_delete_attachment() が WP_Post を返しても、添付ファイルが消えたとは限りません【pre_delete_attachment を WordPress 7.1.2 で測った】

1
Posted at

pre_delete_attachment フィルターが添付ファイルの WP_Post を返すと、wp_delete_attachment() は何も消さずに、その WP_Post を返します。削除に成功したときの返り値も、同じ WP_Post です。返り値だけでは、消えたかどうかを区別できません。WordPress 7.1.2 で4通り測りました。

状況 返り値 get_post() ファイル(元画像+派生3枚)
1. そのまま削除 WP_Post(ID 4) null 0/4 残り
2. フィルターが $post を返す WP_Post(ID 5) ある 4/4 残り
3. フィルターが false を返す false ある 4/4 残り
4. すでに削除済みの ID をもう一度 null null 0/4 残り
返り値で判定: 成功 / 残っているかで判定: 失敗(まだある)

(2のケースを、返り値と get_post() の両方で判定した結果)

先に断っておくと、これはコアのコードとドキュメントに書いてあるとおりの動きです。wp_delete_attachment() の docblock は、返り値を「WP_Post|false|null Post data on success, false or null on failure.」としています。pre_delete_attachment(5.5.0 から)は「Filters whether an attachment deletion should take place.」で、null 以外が返ればその値をそのまま返します。ここで書くのは、それを返り値で判定していた自分のプラグインで何が起きたかと、実際に消えたかを測った結果です。

何が起きたか

自作の Rapls PDF Image Creator は、PDF のサムネイルを作り直すときに古い画像を消します。消したかどうかを、wp_delete_attachment() の返り値で判定していました。

$result = wp_delete_attachment($thumbnailId, true);
// ...
return $result !== false && $result !== null;

ほかのプラグインが pre_delete_attachment で添付ファイル自身を返すと、この判定は「消せた」になります。PDF は古い画像とのつながりを切りますが、画像はメディアライブラリに残ります。どこからも指されない画像が1枚増えます。

逆に、ほかの何かがすでにその添付ファイルを消していた場合は null が返り、「消せなかった」になります。消したいものは、もう無いのにです。

コアの該当箇所

WordPress 7.1.2 の wp-includes/post.php です。

function wp_delete_attachment( $post_id, $force_delete = false ) {
	global $wpdb;

	$post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id ) );

	if ( ! $post ) {
		return $post;
	}

	$post = get_post( $post );

	if ( 'attachment' !== $post->post_type ) {
		return false;
	}

	if ( ! $force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' !== $post->post_status ) {
		return wp_trash_post( $post_id );
	}

	$check = apply_filters( 'pre_delete_attachment', null, $post, $force_delete );
	if ( null !== $check ) {
		return $check;
	}
	// ...(ここから削除)
	return $post;
}

行が見つからなければ、$wpdb->get_row() の結果の null をそのまま返します。4のケースです。最後の return $post; は、削除の前に取得した $post なので、2のケースで返る $post と同じものです。

フィルターに渡ってくる $post を返すのは、フィルターの使い方としては素直な書き方です。削除を止めたいなら false を返すのが筋ですが、そう書かれている保証はありません。

測った環境

WordPress 7.1.2(GitHub の WordPress/WordPress から 7.1.2 のタグを取得)
PHP 8.4.21 / MariaDB 10.11.14
MEDIA_TRASH=false(既定)

添付ファイルは、GD で 800×600 の PNG を描いて wp_insert_attachment() で登録し、wp_generate_attachment_metadata() で派生サイズを作りました。この環境では元画像と派生3枚の計4ファイルになります。それぞれを wp_delete_attachment($id, true) で消し、返り値、get_post()、4ファイルの file_exists() を見ています。

2の結果:返り値も中身も同じ WP_Post

1と2は、どちらも WP_Post が返りました。中身も、削除の前に読んだ投稿そのものなので、返り値を見て区別する手がかりはありません。違いは、そのあと get_post() で引けるかどうかと、ファイルが残っているかどうかだけです。

3と4:false と null

3のように、フィルターが false を返せば false が返ります。削除を止める書き方としては、これがドキュメントの想定です。

4は、すでに無い ID を消そうとした場合です。null が返ります。docblock では null は失敗の側に入っていますが、「消したかったものが無い」という状態としては、目的は果たされています。

直し方:消えたかどうかを聞く

1.4.19 で、返り値を見るのをやめました。

private function deleteAttachment(int $attachmentId): bool
{
    wp_delete_attachment($attachmentId, true);

    return null === get_post($attachmentId);
}

消したあとで get_post() が null なら消えています。2では「消せなかった」になり、4では「消えている」になります。表の4行が、すべて実際の状態どおりに判定されます。

消せなかったときは、PDF からのつながりを切らずに残します。画像が残るなら、それを指す記録も残すほうが、あとで片付けられるからです。

なお、MEDIA_TRASH を有効にしているサイトで $force_delete を付けずに呼ぶと、wp_trash_post() の返り値が返ります。ゴミ箱に移っただけでも投稿データが返ってくるので、この場合も返り値は「消えた」を意味しません。こちらはコードを読んだだけで、測ってはいません。

あなたのプラグインで、削除や保存の成否を返り値だけで判定している場所はありませんか。

手元で試すコマンド

WordPress の直下に置いて php del.php で動きます。

<?php
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['REQUEST_URI'] = '/';
require __DIR__ . '/wp-load.php';
require_once ABSPATH . 'wp-admin/includes/image.php';

function make_attachment(string $name): int {
    $up = wp_upload_dir();
    $path = $up['path'] . "/$name.png";
    $im = imagecreatetruecolor(800, 600);
    imagefill($im, 0, 0, imagecolorallocate($im, 30, 120, 200));
    imagepng($im, $path);
    $id = wp_insert_attachment(['post_mime_type' => 'image/png', 'post_title' => $name, 'post_status' => 'inherit'], $path);
    wp_update_attachment_metadata($id, wp_generate_attachment_metadata($id, $path));
    return $id;
}

function files_of(int $id): array {
    $file = get_attached_file($id);
    $list = [$file];
    foreach ((wp_get_attachment_metadata($id)['sizes'] ?? []) as $s) {
        $list[] = dirname($file) . '/' . $s['file'];
    }
    return $list;
}

function report(string $label, int $id, $ret, array $files) {
    printf("%s 返り値=%s get_post=%s ファイル残り=%d/%d\n", $label,
        is_object($ret) ? get_class($ret) . '(ID ' . $ret->ID . ')' : var_export($ret, true),
        get_post($id) ? 'ある' : 'null',
        array_sum(array_map('file_exists', $files)), count($files));
}

$id = make_attachment('a-normal'); $f = files_of($id);
report('1 そのまま削除', $id, wp_delete_attachment($id, true), $f);

$id = make_attachment('b-filter-post'); $f = files_of($id);
$cb = fn($check, $post) => $post;
add_filter('pre_delete_attachment', $cb, 10, 2);
report('2 フィルターが $post を返す', $id, wp_delete_attachment($id, true), $f);
remove_filter('pre_delete_attachment', $cb, 10);

$id = make_attachment('c-filter-false'); $f = files_of($id);
$cb = fn() => false;
add_filter('pre_delete_attachment', $cb);
report('3 フィルターが false を返す', $id, wp_delete_attachment($id, true), $f);
remove_filter('pre_delete_attachment', $cb);

$id = make_attachment('d-already'); $f = files_of($id);
wp_delete_attachment($id, true);
report('4 すでに削除済みのIDをもう一度', $id, wp_delete_attachment($id, true), $f);

ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?