先日(といっても2015年12月10日)、WordPressのバージョン4.4が公開された。

さっそくこのブログでもバージョン4.4に更新してみたのだが、チェックしているとコメントフォームの表示が変わっていた。

「本文」の入力欄が一番上に来てる!?

コメントフォームの設置に使われる内蔵関数 comment_form() に仕様変更があったようだが、残念ながらこの関数にフォーム内の項目の並び順を変える機能はない。

この関数はけっこうぐちゃぐちゃなタブ文字を吐き出すこともあり、いい加減うんざりしてきたので、この機会に comment_form() なんか使わずにコメントフォームを設置しちゃおう! と思い立った。

ということで、今回は俺が内蔵関数 comment_form() を紐解き、コメントフォームを自作するまでの戦いを綴る。

内蔵関数の恐怖

なんでそういうとこを勝手に変えちゃうかねぇ……と思いながら情報収集すると、どうやら今回のアップデートでの開発者肝いり(?)の変更らしい。

「最も重要なものは一番上に表示されてしかるべき」とかで、海外のフォーラムではおおむね好評らしい。

でも……海外の事情は知らないけど、少なくとも俺にとっては「名前→メアド→本文」というのが見慣れた順番なわけで。

開発者が突き付けたこの変更に、俺は甘んじて従うつもりもないぜ!

でも、多くのブログと同じように、このブログでもコメントフォームを設置するのにWordPress内蔵の関数 comment_form() を使っている。

こいつはときどき機能が追加されているが、まだ自由自在なカスタマイズにはほど遠い、というか今回勝手に変更されたフォーム内項目の並び替えにはノータッチなのだ。

しかも、こいつは吐き出すHTMLがいろいろおかしく、HTMLソースを見てみると何個、何十個ものタブ文字や改行文字でインデントがガタガタ。

抜本的に解決するには、やっぱり内蔵関数に頼らずに自力でPHPを書くしかないよねぇ……?

ただ、こういう本体機能の根幹にかかわる部分はやっぱりデリケートだから、間違えるとコメントできなくなったりするわけで。

とはいえ俺もWordPressをいろいろいじくっている間に本体ファイルの読み方にも慣れてきたし、勇気を振り絞って挑戦した次第だ。

コメントフォームを自作するにあたっての注意

上記の通り、コメント機能のような根幹となる部分はデリケートで、完全に理解するのは難しく、必要な部分に過不足があったりするとたちまちコメントができなくなったりする。

俺だってPHPがいかにしてMySQLのデータベースにコメントのデータを記録しているかなんてさっぱりわからん。

いや、今回そのような知識は必要ないのでご安心を。

でも、簡単ではないことと、この記事の真似をして問題が起こっても自分で解決することはご了承いただきたい。

※質問は受け付けるよ。

まず、コメント機能はときどき仕様変更されるけど、内蔵関数を素直に使ってる限りはその関数側で自動的に対応してくれるが、自作するとなるとそうはいかない。

今後のWordPressのアップデートにより動かなくなる可能性もあり、その際は各自で原因究明、解決を行わなければならない。

また、単に従来のHTMLをコピペして弄ったものを出力するというだけではだめ。

ログインしているかどうかや、以前コメントしたことがあるかどうかによって出力されるHTMLが変わるように、標準では設計されているので、その点も考慮する必要がある。

また、コメントフォームに関するプラグインを導入している場合、それらのプラグインで不具合が出ないように注意深く設計しなければならない。

たとえば、アクションフックはもれなく設置しなければならない。

さらに、従来のHTMLと構造が大きく変わると、CSSなどを再調整するのが大変だから、できるだけ標準の設計を踏襲しなければならない。

俺は今回、これらの点に注意して取り組んだ。

……と言いたいところだが。

この記事の内容を真似する場合の注意

俺は従来からカスタマイズしまくってたせいで、標準がどんなだったかを正確には覚えていない。

特にid名とかclass名なんかはかなり独自のものを付けてると思うから、もし従来のCSSがおかしくなったら各自でテキトーに直してくれ。

また、俺自身が無効にしているオプション設定については、未対応だったりテキトーな実装だったりするので注意。

  • 「コメントの投稿者の名前とメールアドレスの入力を必須にする」:テキトー
  • 「ユーザー登録してログインしたユーザーのみコメントをつけられるようにする」:テキトー
  • 「コメントを○階層までのスレッド (入れ子) 形式にする」:未対応

あと、俺が書いてるHTMLはHTML5で、XHTMLあたりでは構文エラーになるけど、気にしない。

俺が使ってるコード

長いけどあとで少しずつ説明するね。

<?php if (comments_open()) {
    $post_id = get_the_ID();
    $commenter = wp_get_current_commenter();
    $user = wp_get_current_user();
    $user_identity = $user->exists() ? $user->display_name : '';
    if ( get_option('require_name_email') ) {
        $span_req = '<span class="required">*</span> ';
        $input_req = ' required aria-required="true"';
    } else {
        $span_req = '';
        $input_req = '';
    }
    do_action( 'comment_form_before' ); ?>
<section id="respond" class="comment-respond">
    <h3 id="reply-title" class="comment-reply-title">こめんとする</h3>
<?php if ( get_option('comment_registration') && ! is_user_logged_in() ) { ?>
    <p class="must-log-in">コメントするには<a href="<?php echo wp_login_url(apply_filters('the_permalink', get_permalink($post_id)).'#respond'); ?>">ログイン</a>が必要だよ。</p>
<?php } else { ?>
    <form action="<?php echo site_url('/wp-comments-post.php'); ?>" method="post" id="commentform" class="comment-form" novalidate>
<?php do_action( 'comment_form_top' );
if (is_user_logged_in()) { ?>
        <p class="comment-form-author logged-in-as"><span class="comment-form-account">[ <a href="<?php echo admin_url('profile.php'); ?>" target="_blank">アカウント設定</a> - <a href="<?php echo wp_logout_url(apply_filters('the_permalink', get_permalink($post_id)).'#respond'); ?>">ログアウト</a> ]</span><!--
        --><label for="author2">お名前</label><input id="author2" type="text" value="<?php echo esc_attr($user_identity); ?>" title="<?php echo esc_attr($user_identity); ?> としてログイン中" disabled></p>
<?php do_action( 'comment_form_logged_in_after', $commenter, $user_identity );
} else { // ELSE (is_user_logged_in()) ?>
        <p class="comment-notes">本文のみ必須。でも名前も記入を推奨。メールアドレスは公開されないよ。</p>
<?php do_action( 'comment_form_before_fields' ); ?>
        <p class="comment-form-author"><span class="comment-form-account">[ <span title="ログインしてないよ">ゲストさん</span> - <a href="<?php echo wp_login_url(apply_filters('the_permalink', get_permalink($post_id)).'#respond'); ?>">ログイン</a> ]</span><!--
        --><label for="author"><?php echo $span_req; ?>お名前</label><input id="author" name="author" type="text" value="<?php echo esc_attr($commenter['comment_author']); ?>"<?php echo $input_req; ?>></p>
        <p class="comment-form-email"><label for="email"><?php echo $span_req; ?>メアド</label><input id="email" name="email" type="email" value="<?php echo esc_attr($commenter['comment_author_email']); ?>" size="60"<?php echo $input_req; ?>></p>
        <p class="comment-form-url"><label for="url">URL</label><input id="url" name="url" type="url" value="<?php echo esc_attr($commenter['comment_author_url']); ?>" size="60"></p>
<?php do_action( 'comment_form_after_fields' );
} // END ELSE (is_user_logged_in()) ?>
        <p class="comment-form-comment"><label for="comment">本文</label><textarea id="comment" name="comment" cols="50" rows="8" required aria-required="true"></textarea></p>
        <p class="comment-form-submit"><input id="submit" name="submit" type="submit" value="送信!"></p>
        <p class="form-allowed-tags">コメントに使用できるHTMLタグ:....</p>
<?php echo "\t\t\t".get_comment_id_fields()."\n"; 
if (current_user_can('unfiltered_html')) {
    echo "\t\t\t";
    wp_nonce_field( 'unfiltered-html-comment_'.$post_id, '_wp_unfiltered_html_comment_disabled', false );
    echo '<script>(function(){if(window===window.parent){document.getElementById(\'_wp_unfiltered_html_comment_disabled\').name=\'_wp_unfiltered_html_comment\';}})();</script>'."\n";
}
remove_action( 'comment_form', 'wp_comment_form_unfiltered_html_nonce' );
do_action( 'comment_form', $post_id ); ?>
    </form>
<?php } // END ELSE ( get_option('comment_registration') && ! is_user_logged_in() )
do_action( 'comment_form_after' ); ?>
<!-- / #respond --></section>
<?php } else { // ELSE (comments_open())
    do_action( 'comment_form_comments_closed' );
} ?>

青字がHTML緑字がPHPだよ。左右にスクロールして全体を表示できるよ。

コードの説明

いくつかの部分に分けて説明する。

<?php if (comments_open()) {
    $post_id = get_the_ID();
    $commenter = wp_get_current_commenter();
    $user = wp_get_current_user();
    $user_identity = $user->exists() ? $user->display_name : '';
    if ( get_option('require_name_email') ) {
        $span_req = '<span class="required">*</span> ';
        $input_req = ' required aria-required="true"';
    } else {
        $span_req = '';
        $input_req = '';
    }
    do_action( 'comment_form_before' ); ?>
<section id="respond" class="comment-respond">
    <h3 id="reply-title" class="comment-reply-title">こめんとする</h3>

1〜15行目

まず、1行目で条件分岐の関数 comments_open() を使い、そのページ(記事)でコメントが有効かどうかをチェックしている。

2〜5行目では、コード内で使う変数をあらかじめ取得している。

6〜12行目では、「コメントの投稿者の名前とメールアドレスの入力を必須にする」オプション設定が有効の場合に追加するHTMLの記述をあらかじめ設定している。

13行目ではコメントフォームの前で実行するアクションフック「 comment_form_before 」を設置している。

14行目と15行目ではコメントフォームのセクションと見出しを記述している。

<?php if ( get_option('comment_registration') && ! is_user_logged_in() ) { ?>
    <p class="must-log-in">コメントするには<a href="<?php echo wp_login_url(apply_filters('the_permalink', get_permalink($post_id)).'#respond'); ?>">ログイン</a>が必要だよ。</p>
<?php } else { ?>
    <form action="<?php echo site_url('/wp-comments-post.php'); ?>" method="post" id="commentform" class="comment-form" novalidate>
<?php do_action( 'comment_form_top' );

16〜20行目

16行目では「ユーザー登録してログインしたユーザーのみコメントを付けられるようにする」オプション設定と、ユーザーがログインしているかどうかを取得する条件分岐の関数 is_user_logged_in() を使い、ログイン必須の状態でログインしていないユーザーに対してログインを促すメッセージを表示する。

17行目が実際にメッセージを表示するHTMLで、ログインページのURLを取得するのに関数 wp_login_url() を使っている。
この関数は、引数でログイン後に戻るページのURLを指定できるので、現在のページのURLを指定している。

18行目は16行目のif文に対するelse文で、以降がコメントフォームとなる。

19行目はフォームタグの定義。

20行目ではコメントフォームの先頭で実行するアクションフック「 comment_form_top 」を設置。

if (is_user_logged_in()) { ?>
        <p class="comment-form-author logged-in-as"><span class="comment-form-account">[ <a href="<?php echo admin_url('profile.php'); ?>" target="_blank">アカウント設定</a> - <a href="<?php echo wp_logout_url(apply_filters('the_permalink', get_permalink($post_id)).'#respond'); ?>">ログアウト</a> ]</span><!--
        --><label for="author2">お名前</label><input id="author2" type="text" value="<?php echo esc_attr($user_identity); ?>" title="<?php echo esc_attr($user_identity); ?> としてログイン中" disabled></p>
<?php do_action( 'comment_form_logged_in_after', $commenter, $user_identity );

21〜24行目

21行目ではユーザーがログインしているかどうかを判定。

22〜24行目が、ログインしているユーザーに対する表示となる。
ログインしているユーザーは名前、メールアドレス、URLの入力が不要だから、これらの入力フォームのかわりに別の表示をするわけだ。

俺はちょっと凝った表示にしたくて、22行目のようなアカウント設定ページへのリンクやログアウトリンクだけじゃなく、23行目のように通常の名前入力欄と同じ形のダミーフォームを表示している。

ダミーフォームにはあらかじめユーザー名が入力されているが、 disabled 属性によって中身の変更を無効化している。

24行目では、ログインユーザーに対する表示のあとで実行するアクションフック「 comment_form_logged_in_after 」を設置。

ちなみに22行目から23行目にかけてHTMLコメントアウト <!-- --> を使っているが、これは単にHTMLを改行するとページ中に空白文字が現れてしまうので、それを防ぎつつ改行して見やすくするため。

} else { // ELSE (is_user_logged_in()) ?>
        <p class="comment-notes">本文のみ必須。でも名前も記入を推奨。メールアドレスは公開されないよ。</p>
<?php do_action( 'comment_form_before_fields' ); ?>
        <p class="comment-form-author"><span class="comment-form-account">[ <span title="ログインしてないよ">ゲストさん</span> - <a href="<?php echo wp_login_url(apply_filters('the_permalink', get_permalink($post_id)).'#respond'); ?>">ログイン</a> ]</span><!--
        --><label for="author"><?php echo $span_req; ?>お名前</label><input id="author" name="author" type="text" value="<?php echo esc_attr($commenter['comment_author']); ?>"<?php echo $input_req; ?>></p>
        <p class="comment-form-email"><label for="email"><?php echo $span_req; ?>メアド</label><input id="email" name="email" type="email" value="<?php echo esc_attr($commenter['comment_author_email']); ?>" size="60"<?php echo $input_req; ?>></p>
        <p class="comment-form-url"><label for="url">URL</label><input id="url" name="url" type="url" value="<?php echo esc_attr($commenter['comment_author_url']); ?>" size="60"></p>
<?php do_action( 'comment_form_after_fields' );
} // END ELSE (is_user_logged_in()) ?>

25〜33行目

25行目は、21行目のif文に対するelse文。
つまり、以降がログインしていないユーザー向けに名前、メールアドレス、URLの入力欄などを表示する部分になる。

26行目は、標準の comment_form() 関数の引数の配列でいうところの 'comment_notes_before' のメッセージを表示。

27行目は、フォームの各種入力欄の前で実行するアクションフック「 comment_form_before_fields 」を設置。

28〜29行目は名前の入力欄で、ログインリンク付き。

30行目はメールアドレスの入力欄。

31行目はURLの入力欄。俺は使ってないけど。

32行目は、フォームの各種入力欄の後で実行するアクションフック「 comment_form_after_fields 」を設置。
ちなみに、このブログで利用している「Captcha by BestWebSoft」プラグインはこのアクションフックによってCaptcha(文字・画像認証フォーム)を設置している。

33行目は、25行目のelse文の終わりの閉じ括弧で、以降はログインの有無にかかわらない共通項目となる。

        <p class="comment-form-comment"><label for="comment">本文</label><textarea id="comment" name="comment" cols="50" rows="8" required aria-required="true"></textarea></p>
        <p class="comment-form-submit"><input id="submit" name="submit" type="submit" value="送信!"></p>
        <p class="form-allowed-tags">コメントに使用できるHTMLタグ:....</p>

34〜36行目

34行目はコメント本文の入力欄で、入力必須項目なので required 属性やアクセシビリティに関する規格「ARIA」の aria-required 属性を指定している。

35行目はフォームの送信ボタン。

36行目は、標準の comment_form() 関数の引数の配列でいうところの 'comment_notes_after' のメッセージを表示。

<?php echo "\t\t".get_comment_id_fields()."\n"; 
if (current_user_can('unfiltered_html')) {
    echo "\t\t";
    wp_nonce_field( 'unfiltered-html-comment_'.$post_id, '_wp_unfiltered_html_comment_disabled', false );
    echo '<script>(function(){if(window===window.parent){document.getElementById(\'_wp_unfiltered_html_comment_disabled\').name=\'_wp_unfiltered_html_comment\';}})();</script>'."\n";
}
remove_action( 'comment_form', 'wp_comment_form_unfiltered_html_nonce' );
do_action( 'comment_form', $post_id ); ?>

37〜44行目

このあたりは、コメントフォームの隠しフィールドなどを記述している。
わかりにくい部分ではあるが、重要なので間違いのないように。

37行目は、ページIDに関する隠しフィールド。

38〜42行目は、 'unfiltered_html' という権限を持つユーザー(管理者とか)向けの処理だが、俺もぶっちゃけよくわかってない。
nonce というスパム・不正アクセス対策(?)の仕組みを使っているらしいが……

43行目では、38〜42行目で処理を行ったかわりにアクションフック「 comment_form 」から「 wp_comment_form_unfiltered_html_nonce 」を解除している、だったはず。

44行目では、コメントフォームの最後に実行するアクションフック「 comment_form 」を設置している。

ちなみに \t はタブ文字、 \n は改行文字のことで、HTMLソースの整形のために記述している。

    </form>
<?php } // END ELSE ( get_option('comment_registration') && ! is_user_logged_in() )
do_action( 'comment_form_after' ); ?>
<!-- / #respond --></section>

45〜48行目

45行目はフォームの終了タグ。

46行目は18行目のelse文の閉じ括弧。

47行目はコメントフォームの後で実行するアクションフック「 comment_form_after 」を設置している。

48行目はセクションの終了タグ。

<?php } else { // ELSE (comments_open())
    do_action( 'comment_form_comments_closed' );
} ?>

49〜51行目

この部分は、コメントが有効になっていないページ(投稿)向けのコード。
ここに何か、たとえば「コメントは受け付けていません」とか書いてもいいし、俺の場合はコメントが有効になっていない場合に実行するアクションフック「 comment_form_comments_closed 」だけ設置している。

49行目は1行目のif文に対するelse文。

まとめ

関数ひとつで設置できるコメントフォームだが、こうやって中身を分解して書き直してみると、仕組みがよくわかる。

そして、元の関数ではやたらややこしい処理をしてるんだな、ということが身に染みてわかる……

元の関数は282行もあるんだからな……
ほとんどは見やすくするための空行と、説明書きのコメント行だけど。

先に言った通り、この記事の方法を真似してコメントフォームを自作した場合、WordPress本体のアップデートでコメントフォームに関する仕様変更があった時には自力で対応する必要がある。

元の関数のソースを見てても、WordPressバージョン4.4での変更点が何箇所かあるからな。

まあ俺も書きっぱなしじゃなくて、覚えてたら仕様変更に対応した記事を書くかも。