2026年10月1日

2026年10月1日

WordPressでカスタムWP-CLIコマンドを追加する方法

はじめに

「WP-CLIに独自のバッチ処理コマンドを追加したい」「大量の投稿を一括処理するCLIスクリプトを作りたい」「デプロイ時に自動実行するセットアップコマンドを実装したい」——WP_CLI::add_command()でカスタムWP-CLIコマンドを実装できます。

症状・原因

Webブラウザ経由の管理画面操作では大量データの処理やタイムアウトの問題が発生します。WP-CLIカスタムコマンドを使うことで、サーバー上でタイムアウトなしにバッチ処理を実行できます。

解決手順

ステップ1:シンプルなカスタムコマンドを作成する

// includes/class-my-cli-commands.php
if ( ! defined( 'ABSPATH' ) ) exit;
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) return;

/**
 * カスタムサイト管理コマンド
 */
class My_CLI_Commands {

    /**
     * サイトの状態を確認する
     *
     * ## OPTIONS
     *
     * [--verbose]
     * : 詳細な情報を表示する
     *
     * ## EXAMPLES
     *
     *   wp mysite status
     *   wp mysite status --verbose
     *
     * @when after_wp_load
     */
    public function status( array $args, array $assoc_args ): void {
        $verbose = WP_CLI\Utils\get_flag_value( $assoc_args, 'verbose', false );

        WP_CLI::line( '=== サイト状態チェック ===' );

        // WordPressバージョン
        WP_CLI::line( 'WordPress: ' . get_bloginfo( 'version' ) );

        // アクティブテーマ
        $theme = wp_get_theme();
        WP_CLI::line( 'テーマ: ' . $theme->get( 'Name' ) . ' v' . $theme->get( 'Version' ) );

        // 有効プラグイン数
        $active_plugins = get_option( 'active_plugins', [] );
        WP_CLI::line( '有効プラグイン数: ' . count( $active_plugins ) );

        if ( $verbose ) {
            foreach ( $active_plugins as $plugin ) {
                $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
                WP_CLI::line( '  - ' . $data['Name'] . ' v' . $data['Version'] );
            }
        }

        // 投稿数
        $post_counts = wp_count_posts();
        WP_CLI::line( sprintf(
            '投稿数: 公開=%d / 下書き=%d',
            $post_counts->publish,
            $post_counts->draft
        ) );

        WP_CLI::success( 'チェック完了' );
    }

    /**
     * 古いメタデータを一括削除する
     *
     * ## OPTIONS
     *
     * <meta_key>
     * : 削除するメタキー名
     *
     * [--dry-run]
     * : 実際には削除せずにシミュレーションを実行する
     *
     * [--batch-size=<number>]
     * : 一度に処理する件数(デフォルト: 100)
     * ---
     * default: 100
     * ---
     *
     * ## EXAMPLES
     *
     *   wp mysite clean-meta _old_cache --dry-run
     *   wp mysite clean-meta _old_cache --batch-size=500
     *
     * @when after_wp_load
     */
    public function clean_meta( array $args, array $assoc_args ): void {
        $meta_key   = $args[0];
        $dry_run    = WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );
        $batch_size = (int) WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 );

        if ( $dry_run ) {
            WP_CLI::warning( 'ドライランモード: 実際の削除は行いません' );
        }

        global $wpdb;

        // 対象件数を確認
        $count = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s",
            $meta_key
        ) );

        if ( 0 === $count ) {
            WP_CLI::warning( "メタキー '{$meta_key}' は見つかりませんでした。" );
            return;
        }

        WP_CLI::line( "削除対象: {$count} 件 (meta_key={$meta_key})" );

        if ( $dry_run ) {
            WP_CLI::success( 'ドライラン完了。実行する場合は --dry-run を外してください。' );
            return;
        }

        // 確認プロンプト
        WP_CLI::confirm( "{$count} 件のメタデータを削除しますか?" );

        // プログレスバー付きで処理
        $progress = WP_CLI\Utils\make_progress_bar( "削除中", $count );
        $deleted  = 0;
        $offset   = 0;

        while ( $offset < $count ) {
            $ids = $wpdb->get_col( $wpdb->prepare(
                "SELECT meta_id FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT %d",
                $meta_key,
                $batch_size
            ) );

            if ( empty( $ids ) ) {
                break;
            }

            $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
            $wpdb->query( $wpdb->prepare(
                "DELETE FROM {$wpdb->postmeta} WHERE meta_id IN ({$placeholders})",
                ...$ids
            ) );

            $deleted += count( $ids );
            $progress->tick( count( $ids ) );

            // メモリ解放
            $wpdb->flush();
            gc_collect_cycles();
        }

        $progress->finish();
        WP_CLI::success( "{$deleted} 件のメタデータを削除しました。" );
    }
}

ステップ2:コマンドをWP-CLIに登録する

// my-cli-plugin.php: プラグインのメインファイル
if ( defined( 'WP_CLI' ) && WP_CLI ) {
    require_once plugin_dir_path( __FILE__ ) . 'includes/class-my-cli-commands.php';

    // コマンドを登録: wp mysite <サブコマンド>
    WP_CLI::add_command( 'mysite', 'My_CLI_Commands' );
}

// または特定のメソッドだけをトップレベルコマンドとして登録
WP_CLI::add_command( 'mysite-status', function(): void {
    WP_CLI::line( 'シンプルなコマンド' );
    WP_CLI::success( '完了' );
} );

ステップ3:投稿の一括処理コマンドを実装する

// includes/class-my-cli-commands.php に追加

/**
 * 投稿のカスタムメタを一括更新する
 *
 * ## OPTIONS
 *
 * [--post-type=<post-type>]
 * : 対象の投稿タイプ(デフォルト: post)
 * ---
 * default: post
 * ---
 *
 * [--status=<status>]
 * : 対象の投稿ステータス(デフォルト: publish)
 * ---
 * default: publish
 * ---
 *
 * ## EXAMPLES
 *
 *   wp mysite update-reading-time
 *   wp mysite update-reading-time --post-type=news --status=any
 *
 * @when after_wp_load
 */
public function update_reading_time( array $args, array $assoc_args ): void {
    $post_type = WP_CLI\Utils\get_flag_value( $assoc_args, 'post-type', 'post' );
    $status    = WP_CLI\Utils\get_flag_value( $assoc_args, 'status', 'publish' );

    $query = new WP_Query( [
        'post_type'      => $post_type,
        'post_status'    => $status,
        'posts_per_page' => -1,
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ] );

    $post_ids = $query->posts;
    $total    = count( $post_ids );

    if ( 0 === $total ) {
        WP_CLI::warning( '対象の投稿が見つかりませんでした。' );
        return;
    }

    WP_CLI::line( "対象: {$total} 件" );
    $progress = WP_CLI\Utils\make_progress_bar( '読了時間を計算中', $total );
    $updated  = 0;

    foreach ( $post_ids as $post_id ) {
        $content       = get_post_field( 'post_content', $post_id );
        $text          = wp_strip_all_tags( $content );
        $char_count    = mb_strlen( $text );
        $reading_time  = max( 1, (int) ceil( $char_count / 400 ) ); // 日本語: 約400字/分

        update_post_meta( $post_id, '_reading_time', $reading_time );
        $updated++;
        $progress->tick();

        // N件ごとにWP_Queryをリセット
        if ( $updated % 100 === 0 ) {
            stop_the_insanity(); // ループ後のデータクリア(関数は下で定義)
        }
    }

    $progress->finish();
    WP_CLI::success( "{$updated} 件の読了時間を更新しました。" );
}

/**
 * メモリ解放のヘルパー関数(大量処理時に必須)
 */
function stop_the_insanity(): void {
    global $wpdb, $wp_object_cache;
    $wpdb->queries = [];

    if ( is_object( $wp_object_cache ) ) {
        $wp_object_cache->group_ops      = [];
        $wp_object_cache->stats          = [];
        $wp_object_cache->memcache_debug = [];
        $wp_object_cache->cache          = [];
    }
}

ステップ4:コマンドでテーブル形式の出力を使う

/**
 * 投稿の統計情報を表示する
 *
 * ## EXAMPLES
 *
 *   wp mysite stats
 *   wp mysite stats --format=json
 *
 * @when after_wp_load
 */
public function stats( array $args, array $assoc_args ): void {
    $format = WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' );

    $post_types = get_post_types( [ 'public' => true ], 'objects' );
    $data = [];

    foreach ( $post_types as $post_type ) {
        $counts = wp_count_posts( $post_type->name );
        $data[] = [
            '投稿タイプ' => $post_type->label,
            'スラッグ'   => $post_type->name,
            '公開'       => $counts->publish ?? 0,
            '下書き'     => $counts->draft   ?? 0,
            '合計'       => array_sum( (array) $counts ),
        ];
    }

    // テーブル形式・JSON・CSVなどで出力
    WP_CLI\Utils\format_items( $format, $data, array_keys( $data[0] ) );
}

ステップ5:wp-cli.ymlでコマンドの設定を共有する

# wp-cli.yml: プロジェクトルートに配置
path: /var/www/html
url: https://example.com

# カスタムコマンドの自動読み込み
require:
  - includes/class-my-cli-commands.php

# コマンドのデフォルト値
mysite update-reading-time:
  post-type: post
  status: publish

# エイリアス
@production:
  ssh: user@example.com
  path: /var/www/html

@staging:
  ssh: user@staging.example.com
  path: /var/www/staging
# 使用例
wp mysite status
wp mysite status --verbose
wp mysite clean-meta _old_cache --dry-run
wp mysite clean-meta _old_cache --batch-size=500
wp mysite update-reading-time --post-type=news
wp mysite stats
wp mysite stats --format=json

# リモートサーバーで実行
wp @production mysite status
wp @production mysite update-reading-time

注意事項

  • WP_CLI::add_command()は必ずif ( defined('WP_CLI') && WP_CLI )の条件分岐の中で呼び出してください。Web経由でもファイルが読み込まれる場合、クラスが存在せずエラーになります。
  • 大量データを処理する場合はstop_the_insanity()等でメモリを定期的に解放してください。1万件以上の処理ではメモリ不足でプロセスが終了することがあります。
  • WP_CLI::confirm()は--yesフラグで自動承認できます(CI/CDパイプラインでの自動化に便利)。

まとめ

WP-CLIカスタムコマンドの実装は「WP_CLI定数を確認してからクラスを読み込み→クラスのメソッドにDOCBLOCKでコマンド説明とオプションを記述→WP_CLI::add_command()でコマンド名とクラスを登録→WP_CLI\Utils\make_progress_bar()でプログレスバーを表示→大量処理時はバッチサイズを設定してメモリを定期解放→wp-cli.ymlでデフォルト値とエイリアスを設定」の流れで整備します。関連記事:WordPressのCronジョブを設定する方法、WordPressのトランジェントAPIでキャッシュを実装する方法。

お気軽にご相談ください

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