2026年9月7日

2026年9月7日

WP-CLIのカスタムコマンドを作る方法|WordPressのCLIスクリプト開発入門

WP-CLI には、自分で作った処理を wp site-tools ... のようなコマンドとして追加する仕組みがあります。この記事では、WP_CLI::add_command() でカスタムコマンドを登録し、引数とオプションの定義、表形式・CSV での出力、確認プロンプトと --dry-run、大量データのバッチ処理までを、そのまま使えるサンプルで解説します。管理画面では時間がかかる一括処理や、定期的に流したい保守作業をスクリプト化したい人向けの内容です。

WP-CLI のインストールや基本コマンドの使い方は済んでいる前提で進めます。

カスタムコマンドを作るメリット

一度きりの作業なら wp eval-file script.php で PHP ファイルを実行するだけでも済みます。それでもコマンドとして作る価値があるのは、次のような場面です。

  • 同じ処理を何度も、または cron や CI から定期的に実行したい
  • 対象の投稿タイプや件数などを、実行するたびに引数で変えたい
  • wp help で使い方を表示でき、他の人にも引き継げる形にしたい
  • 書き込み前の確認や、試し実行(dry-run)を組み込みたい

WP-CLI は WordPress を読み込んだ状態でコマンドを実行するので、get_posts() や update_post_meta() などのコア関数をそのまま使えます。PHP の実行時間制限(max_execution_time)も、通常は CLI では適用されないため、管理画面ではタイムアウトする件数でも処理できます。

コードを置く場所

カスタムコマンドは、プラグインか mu-plugin(wp-content/mu-plugins/)に書きます。テーマの functions.php にも書けますが、テーマを切り替えるとコマンドが消えるので、保守用のコマンドはテーマから分けておくのがおすすめです。

ポイントは、WP-CLI から実行されたときだけクラスを読み込むことです。WP_CLI 定数で判定すれば、通常のページ表示には影響しません。

最初のコマンド:アイキャッチ未設定の投稿を一覧表示する

まずは読み取りだけを行う安全なコマンドから作ります。wp-content/mu-plugins/site-tools-cli.php として保存します。

<?php
/**
 * Plugin Name: Site Tools CLI
 * Description: 保守用の WP-CLI コマンドを追加します。
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( defined( 'WP_CLI' ) && WP_CLI ) {

	class Site_Tools_CLI_Command {

		/**
		 * アイキャッチ画像が設定されていない公開済みの投稿を一覧表示します。
		 *
		 * ## OPTIONS
		 *
		 * [--post_type=<post_type>]
		 * : 対象の投稿タイプ。
		 * ---
		 * default: post
		 * ---
		 *
		 * [--format=<format>]
		 * : 出力形式。
		 * ---
		 * default: table
		 * options:
		 *   - table
		 *   - csv
		 *   - json
		 * ---
		 *
		 * ## EXAMPLES
		 *
		 *     wp site-tools missing-thumbnails
		 *     wp site-tools missing-thumbnails --post_type=page --format=csv > missing.csv
		 *
		 * @subcommand missing-thumbnails
		 */
		public function missing_thumbnails( $args, $assoc_args ) {
			$post_type = $assoc_args['post_type'];

			if ( ! post_type_exists( $post_type ) ) {
				WP_CLI::error( "投稿タイプ {$post_type} は存在しません。" );
			}

			$ids = get_posts(
				array(
					'post_type'      => $post_type,
					'post_status'    => 'publish',
					'posts_per_page' => -1,
					'fields'         => 'ids',
					'no_found_rows'  => true,
					'meta_query'     => array(
						array(
							'key'     => '_thumbnail_id',
							'compare' => 'NOT EXISTS',
						),
					),
				)
			);

			if ( empty( $ids ) ) {
				WP_CLI::success( 'アイキャッチ未設定の投稿はありません。' );
				return;
			}

			$items = array();
			foreach ( $ids as $id ) {
				$items[] = array(
					'ID'    => $id,
					'title' => get_the_title( $id ),
					'url'   => get_permalink( $id ),
				);
			}

			WP_CLI\Utils\format_items( $assoc_args['format'], $items, array( 'ID', 'title', 'url' ) );
		}
	}

	WP_CLI::add_command( 'site-tools', 'Site_Tools_CLI_Command' );
}

保存したら、次のコマンドで使い方と実行結果を確認します。

wp help site-tools missing-thumbnails
wp site-tools missing-thumbnails --format=csv

docblock がそのまま仕様になる

WP-CLI は、メソッドの上に書いたコメント(docblock)を読み取って、引数の定義とヘルプを作ります。## OPTIONS の書き方には決まりがあります。

  • <name>:必須の位置引数
  • [<name>]:省略できる位置引数
  • --name=<value>:必須のオプション
  • [--name=<value>]:省略できるオプション
  • [--flag]:値を取らないフラグ

--- で囲んだ部分には default や options を書けます。options を指定すると、それ以外の値を渡したときに WP-CLI がエラーで止めてくれるので、自分で入力チェックを書く手間が減ります。docblock の書式を崩すと引数が正しく解釈されないので、インデントや空行の位置はサンプルに合わせてください。

メソッド名はそのままサブコマンド名になりますが、PHP のメソッド名にはハイフンが使えません。@subcommand missing-thumbnails を付けると、missing_thumbnails メソッドを missing-thumbnails という名前で呼べるようになります。

出力はメッセージ用の関数を使い分ける

echo で出力する代わりに、WP-CLI の関数を使います。

  • WP_CLI::log():通常のメッセージ
  • WP_CLI::success():成功の報告
  • WP_CLI::warning():処理は続けるが注意が必要な場合
  • WP_CLI::error():エラーを表示して終了(終了コード1)

WP_CLI::error() で止めると終了コードが0以外になるため、シェルスクリプトや CI から呼んだときに失敗を検知できます。WP_CLI\Utils\format_items() は、同じデータを table・csv・json で出し分けてくれるので、結果を表計算ソフトに渡したいときに便利です。

書き込みを伴うコマンド:確認と dry-run を組み込む

次は、指定したカテゴリーの投稿にカスタムフィールドを一括で設定するコマンドです。データを書き換える処理は、取り返しがつくように作るのが大切です。実行の前にはデータベースのバックアップ(wp db export backup.sql など)を取ってください。

先ほどのクラスに、次のメソッドを追加します。

		/**
		 * 指定カテゴリーの投稿に、カスタムフィールドを一括で設定します。
		 *
		 * ## OPTIONS
		 *
		 * <category>
		 * : カテゴリーのスラッグ。
		 *
		 * --key=<meta_key>
		 * : 設定するメタキー。
		 *
		 * --value=<meta_value>
		 * : 設定する値。
		 *
		 * [--dry-run]
		 * : 書き込まずに、対象件数だけを表示します。
		 *
		 * [--yes]
		 * : 確認のプロンプトを省略します。
		 *
		 * ## EXAMPLES
		 *
		 *     wp site-tools set-meta news --key=campaign --value=autumn --dry-run
		 *
		 * @subcommand set-meta
		 */
		public function set_meta( $args, $assoc_args ) {
			list( $category ) = $args;
			$key     = trim( $assoc_args['key'] );
			$value   = sanitize_text_field( $assoc_args['value'] );
			$dry_run = WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );

			if ( '' === $key || is_protected_meta( $key, 'post' ) ) {
				WP_CLI::error( 'このメタキーは指定できません(空、または _ で始まる保護されたキー)。' );
			}

			$term = get_term_by( 'slug', $category, 'category' );
			if ( ! $term ) {
				WP_CLI::error( "カテゴリー {$category} が見つかりません。" );
			}

			$query_args = array(
				'post_type'      => 'post',
				'post_status'    => 'any',
				'cat'            => $term->term_id,
				'fields'         => 'ids',
				'posts_per_page' => 100,
				'orderby'        => 'ID',
				'order'          => 'ASC',
			);

			$count_query = new WP_Query( array_merge( $query_args, array( 'posts_per_page' => 1 ) ) );
			$total       = (int) $count_query->found_posts;

			if ( 0 === $total ) {
				WP_CLI::success( '対象の投稿はありません。' );
				return;
			}

			if ( $dry_run ) {
				WP_CLI::log( "dry-run: {$total} 件の投稿に {$key} = {$value} を設定します(書き込みはしていません)。" );
				return;
			}

			WP_CLI::confirm( "{$total} 件の投稿を更新します。よろしいですか?", $assoc_args );

			$progress = WP_CLI\Utils\make_progress_bar( '更新中', $total );
			$updated  = 0;
			$paged    = 1;

			do {
				$query = new WP_Query( array_merge( $query_args, array( 'paged' => $paged ) ) );

				foreach ( $query->posts as $post_id ) {
					if ( false !== update_post_meta( $post_id, $key, $value ) ) {
						$updated++;
					}
					$progress->tick();
				}

				$paged++;
			} while ( $paged <= $query->max_num_pages );

			$progress->finish();
			WP_CLI::success( "{$updated} 件を更新しました(変更のなかった投稿は数えていません)。" );
		}

このコードで工夫している点を説明します。

dry-run を先に用意する:--dry-run を付けると件数だけを表示して終わります。本番ではまず dry-run で対象件数を見て、想定どおりかを確かめてから本実行します。

確認プロンプト:WP_CLI::confirm() は「よろしいですか? [y/n]」と尋ね、y 以外なら処理を止めます。--yes が渡されている場合は確認を飛ばすので、cron から実行するときは --yes を付けます。

100件ずつ処理する:posts_per_page => -1 で全件を一度に読み込むと、件数が多いサイトではメモリが足りなくなります。100件ずつページを送って処理し、進捗バー(make_progress_bar())で残りを表示しています。今回は更新しても検索条件(カテゴリー)が変わらないのでページ送りで問題ありませんが、処理によって条件から外れる投稿を扱う場合は、ページ送りだと取りこぼしが出ます。その場合は毎回1ページ目を取り直すか、先に全件の ID だけを取得しておく方法を選びます。

入力のチェック:CLI は管理者が使う前提とはいえ、値は sanitize_text_field() で整えています。_thumbnail_id のようなアンダースコアで始まる保護されたメタキーは is_protected_meta() で弾き、内部用のデータを誤って書き換えないようにしています。

実行時に知っておきたいオプション

自作のコマンドにも、WP-CLI 共通のオプションがそのまま使えます。

  • --path=/path/to/wordpress:WordPress のディレクトリの外から実行する
  • --url=https://example.com/osaka/:マルチサイトで対象のサイトを指定する
  • --user=admin:特定のユーザーとして実行する(権限が関わる処理で使います)
  • --debug:読み込みの順序やエラーの詳細を表示する

コマンドが「見つからない」と言われる場合は、まず wp cli has-command "site-tools set-meta" で登録されているかを確かめ、次にプラグインが有効か(mu-plugins に置いた場合はファイルの場所)、WP_CLI の判定の中に add_command() があるかを確認します。PHP の文法エラーがあると WordPress の読み込み自体が失敗するので、--debug の出力も参考にしてください。

他のサイトでも使い回したいとき

複数のサイトで同じコマンドを使うなら、プラグインとして配布するか、WP-CLI のパッケージにする方法があります。パッケージの雛形は wp scaffold package で作れますが、このコマンドは別途 scaffold-package-command を導入する必要があります(2026年10月現在)。最初は mu-plugin で作り、使い方が固まってからパッケージ化を検討する順番で十分です。

まとめ

  • カスタムコマンドは WP_CLI::add_command() で登録し、defined( 'WP_CLI' ) && WP_CLI の中で読み込む
  • docblock の ## OPTIONS が引数の定義とヘルプを兼ね、default や options で入力チェックもできる
  • 出力は WP_CLI::log() / success() / warning() / error() を使い分け、結果は format_items() で表や CSV に
  • 書き込む処理には dry-run と WP_CLI::confirm() を入れ、実行前にデータベースをバックアップする
  • 大量のデータはバッチに分けて処理し、マルチサイトでは --url で対象を指定する

お気軽にご相談ください

お見積りへ お問い合わせへ
▲