wp_insert_post()で実装!WordPress独自の投稿フォームプラグイン作成術

2018.12.26

WordPressで独自の投稿フォームをプラグイン化する方法

WordPressの管理画面外から記事を投稿したい、と考えたことはありませんか?例えば、会員専用の投稿フォームをWebサイトのフロントエンドに設置したい場合などです。この記事では、WordPressの標準関数 wp_insert_post() を活用し、独自の投稿フォーム機能を持つプラグインを作成する手順を、コードを交えて具体的に解説します。

記事投稿の核となる関数「wp_insert_post()」

wp_insert_post() は、WordPressの記事(投稿や固定ページなど)をデータベースに挿入・更新するための非常に強力な関数です。この関数を使うことで、管理画面を介さずにプログラムから直接記事を操作できます。

基本的な使い方は、投稿したい内容を配列($post)にまとめ、引数として渡すだけです。オプションの第2引数 $wp_errortrue に設定すると、エラー発生時に WP_Error オブジェクトを返してくれます。

$my_post = array(
    'ID'           => [ <投稿 ID> ], // 既存の投稿を更新する場合に指定
    'post_content' => [ <文字列> ], // 投稿の全文(必須)
    'post_title'   => [ <文字列> ], // 投稿のタイトル(必須)
    'post_status'  => [ 'publish' | 'draft' | 'pending' ... ], // 投稿ステータス
    'post_name'    => [ <文字列> ], // 投稿のスラッグ
    'post_type'    => [ 'post' | 'page' | 'カスタム投稿タイプ' ], // 投稿タイプ
    'post_category'=> [ array(<カテゴリー ID>, ...) ], // カテゴリーIDの配列
    'tags_input'   => [ 'タグ1, タグ2, ...' | array ] // タグ
);

// 投稿を実行
wp_insert_post( $my_post, $wp_error );

post_titlepost_content は、最低限必要な引数です。

プラグイン実装までの3ステップ

この関数を使って、ショートコードで呼び出せる投稿フォームプラグインを作成します。主な実装ステップは以下の3つです。

ステップ1. 投稿フォームのHTMLを作成する

まずは、記事データを送信するための基本的なHTMLフォームを用意します。method="POST" でデータを送信するのが一般的です。

<form action="[送信するURL]" method="POST">
    <!-- タイトル入力 -->
    <input type="text" name="post_title" placeholder="ここにタイトルを入力">
    <!-- 本文入力 -->
    <textarea name="post_content"></textarea>
    
    <button type="submit">送信する</button>
</form>

ステップ2. セキュリティ対策:「nonce」の設定

フォームからデータベースを操作する際は、セキュリティ対策が不可欠です。特に、不正なリクエスト(CSRF:クロスサイト・リクエスト・フォージェリ攻撃など)を防ぐために「nonce(ノンス)」と呼ばれる一時的な認証キーを使用します。

WordPressでは wp_nonce_field() という便利な関数を使うだけで、簡単にnonceをフォームに組み込めます。

<form action="[送信するURL]" method="POST">
    <?php 
        // nonceフィールドをフォーム内に追加
        wp_nonce_field('outside_postform', 'nonce_outside_postform'); 
    ?>

    <!-- 入力フィールドの設定 -->
    <input type="text" name="post_title" placeholder="ここにタイトルを入力">
    <textarea name="post_content"></textarea>

    <button type="submit">送信する</button>
</form>

データを受け取った側では、wp_verify_nonce() 関数を使って、設定したnonceが正しいかを必ずチェックします。

ちなみに、wp_insert_post() は、内部でデータのサニタイズ(無害化処理)を行ってくれますが、nonceによる認証チェックは別途必ず実行しましょう。

ステップ3. 重複投稿を防ぐリダイレクト処理

フォームを送信した後、ユーザーがブラウザの「更新(リロード)」ボタンを押すと、同じデータが再度POSTされ、記事が二重に投稿されてしまう問題があります。

これを防ぐため、wp_insert_post() で処理が完了したら、wp_redirect() を使って同じページにリダイレクトさせます。このとき、URLのパラメータ(GETパラメータ)に処理結果のメッセージを付与するのがポイントです。

// (run_insert_post関数内を想定)

// ... wp_insert_post() の処理 ...

if ($insert_status) {
    $mesg = '投稿完了';
} else {
    $mesg = '投稿失敗';
}

// ...

// 現在のページのURLにステータスを付与してリダイレクト
$url = add_query_arg(array('outside_postform_func_status' => $mesg), get_the_permalink($this->this_post->ID));
wp_redirect($url);
exit;

アラート表示とURLのクリーンアップ

リダイレクト先(つまり同じページ)では、URLパラメータをチェックし、JavaScript(alert())を実行します。アラートを表示した後は、location.href を使ってURLから不要なパラメータを取り除いた状態に再度遷移させると、見た目がクリーンになります。

// アラート後にページの遷移
public function run_alert()
{
    $link = get_the_permalink($this->this_post->ID);
    $insert_status = urldecode($this->get_data['outside_postform_func_status']);
    
    // JavaScriptを出力
    print <<< EOT
    <script>
        alert("{$insert_status}");
        location.href = "{$link}"; // パラメータを除去したURLに遷移
    </script>
EOT;
}

このリダイレクト処理(run_insert_post)はHTMLが出力される前に実行する必要があるため get_header フックを、JavaScriptの出力(run_alert)は <head> タグ内に記述したいため wp_head フックを利用します。

投稿フォームプラグインの全コード

ここまでの内容をまとめ、ショートコードで投稿フォームを呼び出せるプラグイン(クラス形式)の全コードです。

<?php
/*
Plugin Name: Outside Postform
Description: 外部フォームから投稿を可能にするプラグイン
Version: 1.0
Author: (あなたの名前)
*/

// クラスのインスタンスを作成
$outside_postform = new OutsidePostform();

class OutsidePostform
{
    private $plugin_path;
    private $post_data;
    private $get_data;
    private $this_post; // リダイレクト元の投稿オブジェクトを保持

    // コンストラクタ:フックの設定など
    public function __construct()
    {
        $this->plugin_path = WP_PLUGIN_URL . '/' . str_replace(basename(__FILE__), "", plugin_basename(__FILE__));
        
        // ショートコード [outside_postform] を登録
        add_shortcode('outside_postform', array($this, 'outside_postform_func'));
        
        // 必要なCSSを読み込む
        add_action('wp_enqueue_scripts', array($this, 'theme_name_scripts'));
        
        // (おまけ:CSVのアップロードを許可する場合)
        add_filter('upload_mimes', array($this, 'custom_mime_types'));

        // --- POSTデータの処理 ---
        $args = array(
            'nonce_outside_postform' => FILTER_SANITIZE_ENCODED
        );
        $this->post_data = filter_input_array(INPUT_POST, $args);
        
        // nonceが送信されてきた場合
        if ($this->post_data['nonce_outside_postform']) {
            // HTML出力前に記事登録処理を実行
            add_action('get_header', array($this, 'run_insert_post'));
        }

        // --- GETデータの処理(リダイレクト後) ---
        $args = array(
            'outside_postform_func_status' => FILTER_SANITIZE_ENCODED
        );
        $this->get_data = filter_input_array(INPUT_GET, $args);
        
        // ステータスパラメータがURLにある場合
        if ($this->get_data['outside_postform_func_status']) {
            // wp_headでアラート用のJSを実行
            add_action('wp_head', array($this, 'run_alert'));
        }
    }

    // (おまけ:CSVのMIMEタイプを許可)
    public function custom_mime_types($mimes)
    {
        $mimes['csv'] = 'text/csv';
        return $mimes;
    }

    // ショートコード [outside_postform] の中身
    public function outside_postform_func()
    {
        $form = '';
        // ログインユーザーのみにフォームを表示
        if (is_user_logged_in()) {
            $nonce = wp_nonce_field('outside_postform', 'nonce_outside_postform', true, false);
            $action_url = $_SERVER['REQUEST_URI'];

            // --- 投稿タイプ一覧を取得 ---
            $select_option = '';
            $args = array(
                'public' => true,
            );
            $post_types = get_post_types($args, 'names');
            unset($post_types['attachment']); // attachment(メディア)は除外
            foreach ($post_types as $v) {
                $select_option .= '<option>' . $v . '</option>';
            }
            unset($v);

            // --- カテゴリーとタグのリストを取得 ---
            $cats_li = $this->get_post_category_terms('category');
            $tags_li = $this->get_post_category_terms('post_tag');

            // --- フォームHTML(ヒアドキュメント) ---
            $form = <<< EOT
            <form id="outside_postform_func" action="{$action_url}" method="POST">
                {$nonce}
                <div>
                    <input type="number" name="ID" value="" placeholder="ID">
                    <span class="sub">※修正の場合はIDを入力する</span>
                </div>
                <div>
                    <input type="text" name="post_title" value="" placeholder="ここにタイトルを入力" required>
                </div>
                <div>
                    <input type="text" name="post_name" value="" placeholder="スラッグ">
                </div>
                <div>
                    <span class="heading">投稿タイプ</span>
                    <select name="post_type">{$select_option}</select>
                </div>
                <div>
                    <span class="heading">カテゴリー</span>
                    {$cats_li}
                </div>
                <div>
                    <span class="heading">タグ</span>
                    {$tags_li}
                </div>
                <div>
                    <textarea name="post_content" placeholder="本文を入力"></textarea>
                </div>
                <button class="button button-large button-primary" type="submit">送信する</button>
            </form>
EOT;
        } else {
            $form = '<p>投稿フォームを表示するにはログインが必要です。</p>';
        }
        return $form;
    }

    // カテゴリーやタグのリスト(チェックボックス)を生成
    private function get_post_category_terms($taxonomies)
    {
        $checkbox_li = '<ul class="terms_li">';
        $args = array(
            'hide_empty' => false,
        );
        $cats = get_terms($taxonomies, $args);
        foreach ($cats as $v) {
            $checkbox_li .= <<< EOT
                <li>
                    <label><input type="checkbox" name="{$taxonomies}[]" value="{$v->term_id}">{$v->name}</label>
                </li>
EOT;
        }
        unset($v);
        $checkbox_li .= '</ul>';
        return $checkbox_li;
    }

    // 外部CSSファイルの読み込み
    public function theme_name_scripts()
    {
        // プラグインディレクトリ内の 'assets/css/style.css' を読み込む
        wp_enqueue_style('style-name', $this->plugin_path . 'assets/css/style.css');
    }

    // 記事の挿入・更新処理を実行
    public function run_insert_post()
    {
        // ログインユーザーか確認
        if (is_user_logged_in()) {
            $nonce = $this->post_data['nonce_outside_postform'];
            
            // nonceを検証
            if (wp_verify_nonce($nonce, 'outside_postform')) {
                
                // 投稿データ($my_post配列)の準備
                $my_post = array(
                    'post_status'   => 'publish', // とりあえず公開
                    'post_title'    => $_POST['post_title'],
                    'post_name'     => $_POST['post_name'],
                    'post_type'     => $_POST['post_type'],
                    'post_content'  => $_POST['post_content'],
                    'post_category' => $_POST['category'], // name="category[]" で送信された配列
                    'tags_input'    => $this->insert_post_tag() // タグ配列を整形
                );

                // IDがあり、かつその投稿が存在する場合(=編集の場合)
                if (isset($_POST['ID']) && get_post($_POST['ID'])) {
                    $my_post['ID'] = $_POST['ID'];
                }

                // WordPressデータベースに挿入(または更新)
                $insert_status = wp_insert_post($my_post);

                if ($insert_status) {
                    $mesg = '投稿完了';
                } else {
                    $mesg = '投稿失敗';
                }
            } else {
                $mesg = '不正な投稿';
            }

            // リダイレクト処理
            global $post;
            $this->this_post = $post; // 現在の投稿オブジェクトを保持
            $url = add_query_arg(array('outside_postform_func_status' => $mesg), get_the_permalink($this->this_post->ID));
            wp_redirect($url);
            exit;
        }
    }

    // 送信されたタグIDの配列を、カンマ区切りのタグ名文字列に変換
    private function insert_post_tag()
    {
        $tag_array = array();
        if (is_array($_POST['post_tag'])) {
            foreach ($_POST['post_tag'] as $v) {
                $tag_data = get_tag($v); // タグIDからタグ情報を取得
                $tag_array[] = $tag_data->name;
            }
            unset($v);
        }
        return implode(",", $tag_array); // "タグA,タグB" の形式に
    }

    // アラート表示後にパラメータを除去してリダイレクト
    public function run_alert()
    {
        // $this_post は run_insert_post() でセットされたもの
        $link = get_the_permalink($this->this_post->ID); 
        $insert_status = urldecode($this->get_data['outside_postform_func_status']);
        
        print <<< EOT
        <script>
            alert("{$insert_status}");
            location.href = "{$link}";
        </script>
EOT;
    }
}
?>

プラグインの使用方法

上記のコードを outside-postform.php といったファイル名で保存し、WordPressの wp-content/plugins/ ディレクトリにアップロードして有効化します。

その後、投稿フォームを表示したい固定ページや投稿の編集画面(ブロックエディタまたはクラシックエディタ)で、以下のショートコードを貼り付けます。

[outside_postform]

なお、このプラグインはセキュリティのため、WordPressにログインしているユーザーにのみフォームを表示する仕様になっています。

よくある質問(FAQ)

Q1. ログインしていない人(ゲスト)にも投稿フォームを使わせたいです。
A1. コード内の is_user_logged_in() によるチェックを外すことで技術的には可能ですが、セキュリティリスクが非常に高くなります。スパム投稿(ボットによる自動投稿)の格好の的になるため、Google reCAPTCHAの導入など、別途厳重なスパム対策が必須となります。
Q2. 記事と一緒にカスタムフィールドも登録・更新したいです。
A2. wp_insert_post() はカスタムフィールドを直接扱えません。wp_insert_post() を実行した後、返り値として取得した投稿ID($insert_status に入ります)を使い、update_post_meta($insert_status, 'フィールド名', $value); のように関数を追加で実行する必要があります。
Q3. タイトルが未入力の場合、「タイトルは必須です」とエラーを出したいです。
A3. run_insert_post() 関数内で、wp_insert_post() を実行する前に $_POST['post_title'] が空かどうかをチェックします。もし空だった場合は $mesg = 'タイトルは必須です'; のようにエラーメッセージを設定し、wp_insert_post() を実行せずにリダイレクト処理に進むように分岐させます。

CONTACT

webサイト制作、デザインに関するご相談、御見積のご依頼など、弊社へのお問い合わせはこちら