2026年6月1日

2026年6月1日

WordPressのコードスニペット管理プラグインを自作する方法

はじめに

Code Snippetsプラグインのような機能を自前で実装したい、またはfunctions.phpが肥大化したコードをプラグインとして管理したいケースがあります。カスタム投稿タイプとCodeMirrorエディタを使えば、WordPressの管理画面から安全にPHPコードを管理・実行できるシステムを構築できます。本記事では完全なスニペット管理プラグインの実装方法を解説します。

症状・原因

  • functions.phpに大量のコードが追記されて管理が困難になっている
  • プラグインやテーマを切り替えるとカスタムコードが失われる
  • コードの有効・無効を手軽に切り替える仕組みがなく、コメントアウトで管理している

解決手順

ステップ1:カスタム投稿タイプ’snippet’をスコープとステータスメタ付きで登録する

<?php
/**
 * Plugin Name: My Code Snippets Manager
 * Description: コードスニペットを管理するプラグイン
 * Version: 1.0.0
 */

// スニペット用カスタム投稿タイプを登録
add_action( 'init', 'register_snippet_post_type' );
function register_snippet_post_type() {
    register_post_type( 'snippet', array(
        'label'              => 'スニペット',
        'labels'             => array(
            'name'          => 'コードスニペット',
            'singular_name' => 'スニペット',
            'add_new_item'  => '新しいスニペットを追加',
            'edit_item'     => 'スニペットを編集',
        ),
        'public'             => false,       // フロントエンドに表示しない
        'show_ui'            => true,        // 管理画面UIを表示
        'show_in_menu'       => true,
        'supports'           => array( 'title' ), // titleのみ(contentはCodeMirror管理)
        'menu_icon'          => 'dashicons-editor-code',
        'capability_type'    => 'post',
        'capabilities'       => array(
            'publish_posts'  => 'manage_options', // 管理者のみ
            'edit_posts'     => 'manage_options',
        ),
    ) );
}

// スニペットのメタボックスを追加(スコープ・ステータス・コード)
add_action( 'add_meta_boxes', 'add_snippet_meta_boxes' );
function add_snippet_meta_boxes() {
    add_meta_box(
        'snippet_settings',
        'スニペット設定',
        'render_snippet_settings_meta_box',
        'snippet',
        'normal',
        'high'
    );
}

function render_snippet_settings_meta_box( $post ) {
    // Nonceで保存時の検証
    wp_nonce_field( 'save_snippet', 'snippet_nonce' );

    $scope  = get_post_meta( $post->ID, '_snippet_scope', true ) ?: 'global';
    $status = get_post_meta( $post->ID, '_snippet_active', true ) ?: '0';
    $code   = get_post_meta( $post->ID, '_snippet_code', true ) ?: '';
    ?>
    <table class="form-table">
        <tr>
            <th>実行スコープ</th>
            <td>
                <select name="snippet_scope">
                    <option value="global" <?php selected( $scope, 'global' ); ?>>全ページ(グローバル)</option>
                    <option value="frontend" <?php selected( $scope, 'frontend' ); ?>>フロントエンドのみ</option>
                    <option value="admin" <?php selected( $scope, 'admin' ); ?>>管理画面のみ</option>
                    <option value="once" <?php selected( $scope, 'once' ); ?>>1回だけ実行</option>
                </select>
            </td>
        </tr>
        <tr>
            <th>有効/無効</th>
            <td>
                <label>
                    <input type="checkbox" name="snippet_active" value="1" <?php checked( $status, '1' ); ?>>
                    このスニペットを有効にする
                </label>
            </td>
        </tr>
        <tr>
            <th>PHPコード</th>
            <td>
                <textarea name="snippet_code" id="snippet-code-editor" rows="20" style="width:100%;font-family:monospace;"><?php
                    echo esc_textarea( $code );
                ?></textarea>
            </td>
        </tr>
    </table>
    <?php
}

ステップ2:WP_List_Tableを拡張してスニペット一覧を管理する

<?php
// WP_List_Tableを継承したカスタムリストテーブル
if ( ! class_exists( 'WP_List_Table' ) ) {
    require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

class Snippet_List_Table extends WP_List_Table {
    public function __construct() {
        parent::__construct( array(
            'singular' => 'スニペット',
            'plural'   => 'スニペット一覧',
            'ajax'     => false,
        ) );
    }

    // テーブルのカラム定義
    public function get_columns() {
        return array(
            'cb'     => '<input type="checkbox">',
            'title'  => 'スニペット名',
            'scope'  => 'スコープ',
            'status' => 'ステータス',
            'date'   => '更新日',
        );
    }

    // ソート可能なカラム
    public function get_sortable_columns() {
        return array(
            'title' => array( 'title', false ),
            'date'  => array( 'date', true ),
        );
    }

    // データを取得
    public function prepare_items() {
        $per_page    = 20;
        $current_page = $this->get_pagenum();

        $snippets = get_posts( array(
            'post_type'      => 'snippet',
            'posts_per_page' => $per_page,
            'paged'          => $current_page,
            'post_status'    => 'any',
            'orderby'        => sanitize_text_field( $_GET['orderby'] ?? 'date' ),
            'order'          => sanitize_text_field( $_GET['order'] ?? 'DESC' ),
        ) );

        $this->set_pagination_args( array(
            'total_items' => wp_count_posts( 'snippet' )->publish,
            'per_page'    => $per_page,
        ) );

        $this->items = $snippets;
        $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
    }

    // 各行のデフォルトカラムレンダリング
    public function column_default( $item, $column_name ) {
        switch ( $column_name ) {
            case 'scope':
                $scope_labels = array(
                    'global'   => 'グローバル',
                    'frontend' => 'フロントエンド',
                    'admin'    => '管理画面',
                    'once'     => '1回のみ',
                );
                return $scope_labels[ get_post_meta( $item->ID, '_snippet_scope', true ) ] ?? '-';

            case 'status':
                $active = get_post_meta( $item->ID, '_snippet_active', true );
                return $active
                    ? '<span style="color:#00a32a;">● 有効</span>'
                    : '<span style="color:#dc3232;">○ 無効</span>';

            case 'date':
                return get_the_modified_date( 'Y-m-d H:i', $item->ID );
        }
    }
}

ステップ3:管理画面でCodeMirrorエディタを統合する

<?php
// CodeMirrorをスニペット編集ページで読み込む
add_action( 'admin_enqueue_scripts', 'enqueue_snippet_editor_scripts' );
function enqueue_snippet_editor_scripts( $hook ) {
    // スニペット投稿タイプの編集ページのみ
    global $post_type;
    if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
        return;
    }
    if ( 'snippet' !== $post_type ) {
        return;
    }

    // WordPressにバンドルされているCodeMirrorを使用
    $settings = wp_enqueue_code_editor( array(
        'type'       => 'text/x-php', // PHP構文ハイライト
        'codemirror' => array(
            'indentUnit'     => 4,
            'tabSize'        => 4,
            'lineNumbers'    => true,
            'lineWrapping'   => true,
            'matchBrackets'  => true,
            'autoCloseBrackets' => true,
            'mode'           => 'php',
            'theme'          => 'monokai', // ダークテーマ
        ),
    ) );

    // CodeMirrorをtextareaに適用するインラインスクリプト
    if ( false !== $settings ) {
        wp_add_inline_script(
            'code-editor',
            sprintf(
                'jQuery(function($){ wp.codeEditor.initialize($("#snippet-code-editor"), %s); });',
                wp_json_encode( $settings )
            )
        );
    }
}

ステップ4:スニペットをeval()でtry/catchを使って安全に実行する

<?php
// 有効なスニペットを実行するメイン処理
add_action( 'wp_loaded', 'execute_active_snippets' );
function execute_active_snippets() {
    // セーフモード: URLパラメータでスニペット実行を無効化
    // アクセス例: https://example.com/?safe_mode=1&safe_mode_key=YOUR_KEY
    if ( isset( $_GET['safe_mode'] ) && $_GET['safe_mode'] === '1' ) {
        $safe_mode_key = get_option( 'snippet_safe_mode_key', '' );
        if ( ! empty( $safe_mode_key ) && isset( $_GET['safe_mode_key'] ) && hash_equals( $safe_mode_key, $_GET['safe_mode_key'] ) ) {
            return; // 全スニペットの実行をスキップ
        }
    }

    // 有効なスニペットを全件取得
    $active_snippets = get_posts( array(
        'post_type'      => 'snippet',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
        'meta_query'     => array(
            array(
                'key'     => '_snippet_active',
                'value'   => '1',
                'compare' => '=',
            ),
        ),
    ) );

    foreach ( $active_snippets as $snippet ) {
        $scope = get_post_meta( $snippet->ID, '_snippet_scope', true );
        $code  = get_post_meta( $snippet->ID, '_snippet_code', true );

        // スコープチェック
        if ( 'frontend' === $scope && is_admin() ) {
            continue;
        }
        if ( 'admin' === $scope && ! is_admin() ) {
            continue;
        }

        // コードが空の場合はスキップ
        if ( empty( trim( $code ) ) ) {
            continue;
        }

        // try/catchでエラーを安全にキャッチ
        try {
            // PHPタグを除去(コード本体のみ実行)
            $clean_code = preg_replace( '/^<\?php\s*/i', '', $code );
            $clean_code = preg_replace( '/\?>\s*$/', '', $clean_code );

            // eval()で実行(管理者権限チェック済みのコードのみ)
            $result = eval( $clean_code ); // phpcs:ignore Squiz.PHP.Eval.Discouraged

        } catch ( \Throwable $e ) {
            // PHP 7以降はThrowableでError/Exceptionの両方をキャッチ
            $error_message = sprintf(
                'スニペット「%s」(ID: %d) でエラーが発生: %s (行 %d)',
                $snippet->post_title,
                $snippet->ID,
                $e->getMessage(),
                $e->getLine()
            );
            error_log( $error_message );

            // 管理者にはエラーを表示
            if ( current_user_can( 'manage_options' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                add_action( 'admin_notices', function() use ( $error_message ) {
                    echo '<div class="notice notice-error"><p><strong>スニペットエラー:</strong> ' . esc_html( $error_message ) . '</p></div>';
                } );
            }
        }
    }
}

ステップ5:有効・無効トグルとセーフモードを実装する

<?php
// AJAX経由でスニペットの有効・無効を切り替える
add_action( 'wp_ajax_toggle_snippet', 'ajax_toggle_snippet' );
function ajax_toggle_snippet() {
    // セキュリティチェック
    check_ajax_referer( 'toggle_snippet_nonce', 'nonce' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( '権限がありません' );
    }

    $snippet_id = intval( $_POST['snippet_id'] );
    $current    = get_post_meta( $snippet_id, '_snippet_active', true );
    $new_status = ( '1' === $current ) ? '0' : '1';

    update_post_meta( $snippet_id, '_snippet_active', $new_status );

    wp_send_json_success( array(
        'status'  => $new_status,
        'message' => '1' === $new_status ? '有効にしました' : '無効にしました',
    ) );
}

// セーフモードキーを生成・管理する設定ページ
add_action( 'admin_menu', 'add_snippet_settings_menu' );
function add_snippet_settings_menu() {
    add_submenu_page(
        'edit.php?post_type=snippet',
        'スニペット設定',
        '設定',
        'manage_options',
        'snippet-settings',
        'render_snippet_settings_page'
    );
}

function render_snippet_settings_page() {
    // セーフモードキーを生成(未設定の場合)
    $safe_mode_key = get_option( 'snippet_safe_mode_key', '' );
    if ( empty( $safe_mode_key ) ) {
        $safe_mode_key = wp_generate_password( 32, false );
        update_option( 'snippet_safe_mode_key', $safe_mode_key );
    }

    $safe_mode_url = add_query_arg( array(
        'safe_mode'     => '1',
        'safe_mode_key' => $safe_mode_key,
    ), home_url() );

    ?>
    <div class="wrap">
        <h1>スニペット設定</h1>
        <table class="form-table">
            <tr>
                <th>セーフモードURL</th>
                <td>
                    <code><?php echo esc_html( $safe_mode_url ); ?></code>
                    <p class="description">
                        スニペットでサイトが壊れた場合、このURLでアクセスすると全スニペットの実行を無効化できます。
                    </p>
                </td>
            </tr>
        </table>
    </div>
    <?php
}

注意事項

  • eval()のセキュリティ: eval()は管理者権限を持つユーザーが入力したコードのみに使用してください。一般ユーザーが入力できるフォームには絶対に使用しないこと
  • セーフモードURLの管理: セーフモードURLが第三者に知られるとスニペットを無効化されてしまいます。URLは秘密に保管してください
  • バックアップの重要性: スニペットを実行する前に必ずサイトのバックアップを取ってください。誤ったコードでサイトが壊れる可能性があります
  • PHP 8対応: PHP 8ではevalのエラーがThrowableではなくParseErrorになる場合があります。catch(\Throwable $e)で両方をキャッチしています

まとめ

カスタム投稿タイプでスニペットを管理し、CodeMirrorエディタでPHPを編集し、try/catchで安全にeval実行するという構成でCode Snippetsプラグインを自作できます。セーフモードとトグル機能を実装することで、本番環境でも安全に運用できます。関連記事:WordPressプラグイン開発の基礎

お気軽にご相談ください

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