wordpressのウィジェット「最近の投稿」にサムネイルを表示させる

前回の続きで、今回はもう少しまともに使えるウィジェットを作ってみます。

wordpressで最初から用意されているウィジェットの一つに「最近の投稿」があります。

f:id:fullstack-baby:20190920160934p:plain

ウィジェットのタイトルと、何件表示するか、投稿日を表示するか、だけを設定できるシンプルな作りです。

フロント画面で表示させるとこのようになります。

f:id:fullstack-baby:20190920171454p:plain
最近の投稿のウィジェットの表示

サムネイルとかを表示させてもうちょいリッチにしたいですよね。通常wordpressのコアをカスタマイズするにはフックを探すのが王道なのですが、ソースコードを見ると使えそうなフックが用意されていませんでした。

仕方がないのでWP_Widget_Recent_Postsというクラスを継承して、新しいウィジェットを作ることにしましょう。WP_Widget_Recent_Postsの継承だと、コンストラクタのオーバーライドが出来なかったので、WP_Widget_Recent_Postsを参考にしつつ、WP_Widgetを継承したウィジェットを作っていくことにします。

ちなみに、皆がやりたいと思うことは大体プラグイン化されています。急ぎの人はこちらを使って感想聞かせてください。

WP_Widgetを継承してMy_Recent_Postsクラスを作成する

<?php
/**
 * WP_Widgetを継承して、サムネイル表示できる最近の投稿ウィジェットを作る
 */
class My_Recent_Posts extends WP_Widget {

    /**
    * WP_Widget_Recent_Postsとidや名前が被らないように注意して、親のコンストラクタ関数に引数を渡します
    */
    public function __construct() {
        $widget_ops = array(
            'classname'                   => 'widget-recent-entries-with-thumbnail',
            'description'                 => __( 'Your site&#8217;s most recent Posts.' ),
            'customize_selective_refresh' => true,
        );
        parent::__construct( 'recent-posts-with-thumbnail', __( 'Recent Posts With Thumbnail' ), $widget_ops );
        $this->alt_option_name = 'widget_recent_entries_with_thumbnail';
    }

    /**
    * フロントサイドの表示部分です
    * サムネイルと記事タイトルを表示できるようにします
    *
    * @param array $args     Display arguments including 'before_title', 'after_title',
    *                        'before_widget', and 'after_widget'.
    * @param array $instance Settings for the current Recent Posts widget instance.
    */
    public function widget( $args, $instance ) {
        if ( ! isset( $args['widget_id'] ) ) {
            $args['widget_id'] = $this->id;
        }
        $title = ( ! empty( $instance['title'] ) ) ? $instance['title'] : __( 'Recent Posts' );
        /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
        $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
        $number = ( ! empty( $instance['number'] ) ) ? absint( $instance['number'] ) : 5;
        if ( ! $number ) {
            $number = 5;
        }
        $show_date = isset( $instance['show_date'] ) ? $instance['show_date'] : false;

        /**
        * カスタムポイント
        * ウィジェット設定で「サムネイルを表示」にチェックが入っていればtrue
        */
        $show_thumbnail = isset( $instance['show_thumbnail'] ) ? $instance['show_thumbnail'] : false;

        /**
        * Filters the arguments for the Recent Posts widget.
        *
        * @see WP_Query::get_posts()
        *
        * @param array $args     An array of arguments used to retrieve the recent posts.
        * @param array $instance Array of settings for the current widget.
        */
        $r = new WP_Query(
            apply_filters(
                'widget_posts_args',
                array(
                    'posts_per_page'      => $number,
                    'no_found_rows'       => true,
                    'post_status'         => 'publish',
                    'ignore_sticky_posts' => true,
                ),
                $instance
            )
        );
        if ( ! $r->have_posts() ) {
            return;
        }
        ?>
        <?php echo $args['before_widget']; ?>
        <?php
        if ( $title ) {
            echo $args['before_title'] . $title . $args['after_title'];
        }
        ?>
        <ul>
            <?php foreach ( $r->posts as $recent_post ) : ?>
                <?php
                $post_title   = get_the_title( $recent_post->ID );
                $title        = ( ! empty( $post_title ) ) ? $post_title : __( '(no title)' );
                $aria_current = '';
                if ( get_queried_object_id() === $recent_post->ID ) {
                    $aria_current = ' aria-current="page"';
                }
                ?>
                <li>
                    <?php
                    /**
                    * カスタムポイント
                    * テンプレートタグを使用してサムネイルを表示させます。
                    * cssでデザイン調整しやすいように適当なクラス名を与えていますが任意です。
                    */
                    ?>
                    <?php if ($show_thumbnail): ?>
                        <div class="recent-post-thumbnail-wrap">
                            <?php echo get_the_post_thumbnail( $recent_post->ID, 'post-thumbnail', array('class' => 'recent-post-thumbnail') ); ?>
                        </div>
                    <?php endif; ?>

                    <div class="recent-post-body">
                        <a href="<?php the_permalink( $recent_post->ID ); ?>"<?php echo $aria_current; ?>><?php echo $title; ?></a>
                        <?php if ( $show_date ) : ?>
                            <span class="post-date"><?php echo get_the_date( '', $recent_post->ID ); ?></span>
                        <?php endif; ?>
                    </div>
                </li>
            <?php endforeach; ?>
        </ul>
        <?php
        echo $args['after_widget'];
    }

    /**
    * サムネイル表示切り替え用に、show_thumbnailというフラグを保存させるようにします。
    *
    * @param array $new_instance New settings for this instance as input by the user via
    *                            WP_Widget::form().
    * @param array $old_instance Old settings for this instance.
    * @return array Updated settings to save.
    */
    public function update( $new_instance, $old_instance ) {
        $instance              = $old_instance;
        $instance['title']     = sanitize_text_field( $new_instance['title'] );
        $instance['number']    = (int) $new_instance['number'];
        $instance['show_date'] = isset( $new_instance['show_date'] ) ? (bool) $new_instance['show_date'] : false;
        $instance['show_thumbnail'] = isset( $new_instance['show_thumbnail'] ) ? (bool) $new_instance['show_thumbnail'] : false;
        return $instance;
    }
    /**
    * サムネイル表示を切り替えるチェックボックスを表示させます
    *
    * @param array $instance Current settings.
    */
    public function form( $instance ) {
        $title     = isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : '';
        $number    = isset( $instance['number'] ) ? absint( $instance['number'] ) : 5;
        $show_date = isset( $instance['show_date'] ) ? (bool) $instance['show_date'] : false;
        $show_thumbnail = isset( $instance['show_thumbnail'] ) ? (bool) $instance['show_thumbnail'] : false;
        ?>
        <p><label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
        <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo $title; ?>" /></p>

        <p><label for="<?php echo $this->get_field_id( 'number' ); ?>"><?php _e( 'Number of posts to show:' ); ?></label>
        <input class="tiny-text" id="<?php echo $this->get_field_id( 'number' ); ?>" name="<?php echo $this->get_field_name( 'number' ); ?>" type="number" step="1" min="1" value="<?php echo $number; ?>" size="3" /></p>

        <p><input class="checkbox" type="checkbox"<?php checked( $show_date ); ?> id="<?php echo $this->get_field_id( 'show_date' ); ?>" name="<?php echo $this->get_field_name( 'show_date' ); ?>" />
        <label for="<?php echo $this->get_field_id( 'show_date' ); ?>"><?php _e( 'Display post date?' ); ?></label></p>

        <p><input class="checkbox" type="checkbox"<?php checked( $show_thumbnail ); ?> id="<?php echo $this->get_field_id( 'show_thumbnail' ); ?>" name="<?php echo $this->get_field_name( 'show_thumbnail' ); ?>" />
        <label for="<?php echo $this->get_field_id( 'show_thumbnail' ); ?>"><?php _e( 'Display thumbnail?' ); ?></label></p>
        <?php
    }
}

WP_Widget_Recent_Postsのソースコードをコピペして、必要な部分だけ加筆修正しました。

結果を確認する

管理画面

f:id:fullstack-baby:20190920165239p:plain
今回作ったウィジェットの設定画面

サムネイルを切り替えるためのチェックボックスが表示されました。翻訳をあてていないため英語表記のままです。

フロント画面

f:id:fullstack-baby:20190920180212p:plain
サムネイルが表示された

見た目を整えるために以下のcssをあてています。

.widget-recent-entries-with-thumbnail li {
    display: flex;
}
.widget-recent-entries-with-thumbnail .recent-post-thumbnail {
    width: 100px;
    height: auto;
    margin-right: 0.5rem;
}

以上です。よきwordpressライフを!

wordpressで独自のカスタムウィジェットを表示させるゾ

wordpressで独自のwidgetを表示させるには以下の手順を踏みます。

①WP_Widgetクラスを拡張したクラスを作成する ②作成したクラスをwordpressに登録する ③ウィジェットエリアを登録してテンプレートで表示する

それでは順番に見ていきましょう。

WP_Widgetクラスを拡張したクラスを作成する

今回は公式ドキュメントを参考に、My_Widgetという名前で、管理画面で入力したタイトル名をウィジェットタイトルとして表示するウィジェットを作成します。

functions.phpに書いてもいいのですが、見通しが良いようにテーマ直下にwidgetsという新しいディレクトリを作って、その中にmy_widget.phpというファイルを作成します。

<?php
// widgets/my_widget.php
class My_Widget extends WP_Widget {

    /**
    * WordPress でウィジェットを登録
    */
    function __construct() {
        parent::__construct(
            'my_widget', // Base ID
            __( 'My Widget!!', 'text_domain' ), // Name
            array( 'description' => __( 'サンプルのウィジェットです。', 'text_domain' )) // Args
        );
    }

    /**
    * ウィジェットのフロントエンド表示
    *
    * @see WP_Widget::widget()
    *
    * @param array $args     ウィジェットの引数
    * @param array $instance データベースの保存値
    */
    public function widget( $args, $instance ) {
        echo $args['before_widget'];
        if ( ! empty( $instance['title'] ) ) {
            echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ). $args['after_title'];
        }
        echo __( '管理画面で入力したタイトルが表示されましたか?', 'text_domain' );
        echo $args['after_widget'];
    }

    /**
    * バックエンドのウィジェットフォーム
    *
    * @see WP_Widget::form()
    *
    * @param array $instance データベースからの前回保存された値
    */
    public function form( $instance ) {
        $title = ! empty( $instance['title'] ) ? $instance['title'] : __( '新しいタイトル', 'text_domain' );
        ?>
        <p>
        <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'タイトル:' ); ?></label>
        <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
        </p>
        <?php
    }

    /**
    * ウィジェットフォームの値を保存用にサニタイズ
    *
    * @see WP_Widget::update()
    *
    * @param array $new_instance 保存用に送信された値
    * @param array $old_instance データベースからの以前保存された値
    *
    * @return array 保存される更新された安全な値
    */
    public function update( $new_instance, $old_instance ) {
        $instance = array();
        $instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';

        return $instance;
    }

}

作成したクラスをwordpressに登録する

作成したウィジェットを登録するには、'widgets_init'アクションフックでregister_widget()をコールします。

<?php
// functions.php
require get_theme_file_path(  'widgets/my_widget.php'  );
add_action( 'widgets_init', function(){
     register_widget( 'My_Widget' );
}); 

こうすると、先ほど作成した「My Widget!!」という名前のウィジェットが、「外観」→「ウィジェット」の一覧に表示されます。

f:id:fullstack-baby:20190920155422p:plain
My Widget!!が一覧に表示された

ウィジェットエリアを登録してテンプレートで表示する

ウィジェットエリアとは、作成したウィジェットを配置する場所になります。ウィジェットエリアを使うには、register_sidebar()という関数で登録し、dynamic_sidebar()というテンプレートタグでテンプレートファイル上の任意の場所に表示させます。

ウィジェットエリアの登録

このウィジェットエリアはいくつでも登録することができますし、多くの配布されているテーマには既にいくつかのウィジェットエリアが登録済みです。試しにTwenty Seventeenのテーマを有効化してウィジェットエリアを確認してみます。

f:id:fullstack-baby:20190920133504p:plain
Twenty Seventeenウィジェットエリア

ブログサイドバー、フッター1、フッター2、という三つのウィジェットエリアが確認できますね。

このウィジェットエリアをそのまま使ってもいいですし、エリアの使い分けをしたい、他の場所にもウィジェットを表示させたい、などという場合は新しいウィジェットエリアを登録することもできます。試しにフッター3という名前のウィジェットエリアを追加してみましょう。

<?php
// functions.phpとかに書く
add_action( 'widgets_init', function() {
    register_sidebar( array(
        'name' => 'フッター3',
        'id' => 'footer-3',
        'before_widget' => '<div>',
        'after_widget' => '</div>',
        'before_title' => '<h4>',
        'after_title' => '</h4>',
    ) );
} );

f:id:fullstack-baby:20190920151210p:plain
'フッター3' ウィジェットエリアが追加された

ウィジェットエリアの表示

今作ったフッター3というウィジェットエリアを表示させたい場所でdynamic_sidebar()を実行します。関数名が紛らわしいんですが、別にsidebar.php以外にも、テンプレートファイル内の好きな場所でこの関数を使用してウィジェットを表示させることができます。ちなみに、is_active_sidebar('footer-3')というのは、'footer-3'というidのウィジェットエリアが登録されているかを判定する条件分岐タグです。

<?php if ( is_active_sidebar( 'footer-3' ) ) : ?>
    <div id="footer-3" class="footer-3 widget-area" role="complementary">
                 <?php // register_sidebar()で決めたdを引数に与える  ?>
        <?php dynamic_sidebar( 'footer-1' ); ?>
    </div>
<?php endif; ?>

できましたでしょうか?

次回はもうちょっと使い道のあるウィジェットを実際に作成してみます!よきwordpressライフを!

【詳解】wp_enqueue_scriptでjavascriptファイルを読み込む

今日はwordpressスクリプトファイルをインクルードする方法について紹介します。

wp_enqueue_scriptの使い方やwp_register_scriptとの違い、wordpressでよくあるjsまわりのトラブルシューティングなどについてお伝えできればと思います。

【事前知識】htmlにjavascriptの外部ファイルを読み込む方法

インクルード vs インライン

Google Analyticsのトラッキングコードを自身のブログに張り付けたことがある人も多いと思いますが、中身はただのjavascriptです。 実際に見てみましょう。

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=************"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', '************');
</script>

このトラッキングコードには二つの<script></script>タグがあります。

一つ目は、scriptタグのsrc属性にurlが書かれていてタグの中身は空っぽです。これがいわゆるインクルードと呼ばれる方法で、src属性の先にあるjavascriptファイルを読み込んで実行します。(asyncについてはここでは触れません。)

二つ目はインラインと呼ばれる方法で、scriptタグの中に直接javascriptを書いて動かすことができます。

どっちを使うのがいいんだろう?と悩む方もいるかと思いますが、基本的にjavascriptはインラインで記述せず、jsファイルに書いてインクルードする方法が好まれます。

理由はいっぱいあるかと思いますが、ここでは三つほど挙げます。

まず、htmlはhtmlファイルに、javascriptはjsファイルに分けて書くことで、複数人での分業がやりやすくなります。あと、管理もしやすくなります。どういうことかと言うと、元の開発者と別の人がソースのメンテナンスを任された時に、ファイルを分けて書いておけば、javascriptの修正が必要になった時にjsファイルから該当の処理を探せば済みます。実は探していた処理がインラインで書かれていた、というオチは実際しばしばあるのですが、発見までにかなり時間がかかることもあり担当者泣かせだったりします。

次に、外部ファイルをインクルードする場合、ブラウザキャッシュのメリットを享受できます。最初にウェブページを読み込み、同一ページを2回目以降見ると表示が早くなる感覚ありますよね?あれはブラウザがページ表示に利用するcssやjsファイルなどのリソースをローカルに一定期間保存してくれていて、そいつがあればローカルのものを読み込んでくれるようになるためです。一々サーバへリクエストする必要がなくなる分、ページの表示が早くなります。一方でインラインの場合には通常このキャッシュが使用できません。

最後に、これは副次的な効果ですが、外部ファイルにjavascriptを書くと(インラインで場当たり的に書くよりは)機能ごとにモジュール化を意識した書き方になりやすいです。

以上のとおり書きましたが、実際の開発現場では、例えばGoogle Analyticsのトラッキングコードのように、外部サービスによってインラインでの挿入を指示されることもありますし、このページだけちょっとだけjavascript使いたいんだよな、みたいな時にはインラインで書いちゃうこともあります。あまり固く考えすぎずにケースバイケースで運用しましょう。

ヘッダーで読み込むか、フッターで読み込むか

javascriptファイルを<head></head>タグの中と</body>の直前のどちらで読み込むかはよく議論されますが、一般的には</body>の直前にで読み込むことが推奨されます。本題ではないので詳しくは書きませんが、<head>で読み込むと、jsファイルの転送が完了するまでページの表示を遅延させてしまうためです。</body>の直前に書けば、以後に続くDOMがないのでページ表示は遅延しません。

どういう書き方が遅延を引き起こすかについて詳しくはgoogleサイトに初回ペイントの遅延を引き起こすリソースを使用しないとかを読んでみてください。

wordpressスクリプトファイルをインクルードするには

wordpressでは、テンプレートファイル(header.phpとかfooter.phpとか)に直接<script src="****"></script>を書くことは推奨されていません。代わりにwp_enqueue_script()を使います。

使い方

wp_enqueue_script( string $handle, string $src = '', array $deps = array(), string|bool|null $ver = false, bool $in_footer = false );

<?php 
// 使用例
wp_enqueue_script( 'my-script',  get_stylesheet_directory_uri() . '/my-script.js', array(), false, false);

第一引数のみが必須の引数になります。

第一引数$handleは、スクリプトファイルを識別するためのハンドル名で、任意の名前を付けることができます。ただ、wordpressが予約しているハンドル名や、有効化されたプラグインで使用されているハンドル名と重複すると不具合の原因になるので、my-などのように接頭辞を付けることをおすすめします。

第二引数には、スクリプトファイルのurlかパスを与えられます。これを省略する場合、事前にwp_register_script()で登録しておく必要があります。

第三引数には、依存するスクリプトファイルのハンドル名を配列で与えられます。配列に含まれるスクリプトファイルは、ここで指定したスクリプトファイルよりも前に読み込まれます。例えばmy-script.jsがjQueryを使用したスクリプトである場合、このファイルよりも前にjQuery本体を読み込んでおかないと動きません。そうした時に、第三引数にarray('jquery')という配列を与えてあげるとそれが保証されます。ちなみに、ここでの'jquery'というのはwordpressが定義済みjQueryのハンドル名です。

第四引数はファイルパスにクエリストリングとして追加される文字列です。javascriptファイルに変更を加えた時に、キャッシュされた古いファイルが呼ばれないように、ファイルパスを新しくしたい時に使用します。

第五引数はこのスクリプトファイルをヘッダーで読み込むか、フッターで読み込むかを指定します。デフォルトではヘッダーでのインクルードですが、引数にtrueを与えるとフッターでインクルードされるようになります。

wp_enqueue_scriptとwp_register_scriptの違い

上の使用例は、実は以下のように書いても同義です。

<?php 
//  元の例
wp_enqueue_script( 'my-script',  get_stylesheet_directory_uri() . '/my-script.js', array(), false, false);

// こう書いても同じ
wp_register_script( 'my-script',  get_stylesheet_directory_uri() . '/my-script.js', array(), false, false);
wp_enqueue_script('my-script');

wp_register_script()でハンドル名を登録するだけしておき、wp_enqueue_script()が登録済みのハンドル名を使ってエンキューします。

こう見るとwp_register_script()いらないじゃん、と思うかもしれませんし、実際テーマ開発していてこれを使う機会って少ないんですが、もし自分で依存関係があるスクリプトファイルを複数作った場合、wp_register_script()を使えば明示的に読み込み順序を指定出来て便利です。

<?php 
//  child.jsはparent.jsに依存する
wp_register_script( 'my-parent',  get_stylesheet_directory_uri() . '/parent.js', array(), false, false);
wp_enqueue_script( 'my-child',  get_stylesheet_directory_uri() . '/child.js', array('my-parent'), false, false);

wp_enqueue_scriptはどこで実行するか

'wp_enqueue_scripts'というアクションフックが用意されているので、特に理由がない限りここで実行しましょう。

<?php
// テーマ開発の場合、functions.phpとかに書く
add_action( 'wp_enqueue_scripts', 'enqueue_my_scripts' );
function enqueue_my_scripts() {
    wp_enqueue_script( 'my-script',  get_stylesheet_directory_uri() . '/my-script.js', array(), false, false);    
}

トラブルシューティング

最後に、wordpressでよくあるjsまわりのトラブルシューティングについてまとめておきます。

$ is not defined

jQueryを使う時に、通常$という名の変数でjQueryオブジェクトにアクセスしますが、wordpressでインクルードされるjQuerynoConflict()が有効化されていて、$という変数を使えません。

このエラーを回避するには、以下のようにするのが一般的です。

jQuery(document).ready(function($) {
 // この関数の中では、$()をjQuery()のエイリアスとして利用できます。
 // DOM readyのタイミングでこの無名関数が実行されます。 
});

jQuery(function($) {
    // このような書き方をしても上と同じです。
});

(function($) {
 // DOM readyを待たずに処理を実行させたい場合には、このように即時実行関数を使う方法があります。
 // この関数の中では、$()をjQuery()のエイリアスとして利用できます。
})(jQuery);

jQuery本体をフッターにインクルードしたいのにヘッダーに出てきちゃう!

冒頭でスクリプトファイルはフッター読み込みの方が好ましいと言いました。

ふむふむ、じゃあこうすればいいんだな、と思って以下のようなコードを書いても、jQueryがヘッダーでインクルードされてしまうと思います。

<?php
// 第三引数で依存先にjqueryを指定
// 第五引数でフッター読み込みを指定
wp_enqueue_script( 'my-script',  get_stylesheet_directory_uri() . '/script.js', array('jquery'), false, true);

これは、wordpressが登録済みのjQueryを使っているためです。このjQuery$in_footer == falsewp_register_script()されているので依存元をフッター読み込みにしても、jQueryはヘッダーに来てしまいます。

これを解決するためにはwp_deregister_script()で登録を外し、新しくwp_register_script()で登録し直します。

<?php
add_action('wp_enqueue_scripts', 'enqueue_my_scripts');
function enqueue_my_scripts() {
 //登録済みのjQueryを一回解除する
 wp_deregister_script( 'jquery' );

 // 同じハンドル名で、フッター読み込みに変えて再登録する
 wp_register_script( 'jquery', includes_url( '/js/jquery/jquery.js' ), array(), false, true );
 
 // 続けて使用したいスクリプトファイルをエンキューしていく
}

もしこれをやっても相変わらずjQueryがヘッダーに来てしまう場合、考えられる原因として、プラグインが独自のスクリプトファイルをjquery依存でヘッダーにインクルードしている可能性があります。こうなると、プラグインがどこまでアクションフックを用意してくれているか次第です。もし上手くプラグインの設定を変えられない場合、諦めましょう

jQuery is not defined

jQueryを使用したスクリプトファイルをインクルードする前に、jQuery本体がインクルードされていないようです。まずはwp_enqueue_script()の第三引数にarray('jquery')を与えて問題が解決するか確認してみてください。

見落としがちなのがヘッダー読み込みとフッター読み込みの混在による問題です。例えば以下のケース。

<?php
//登録済みのjQueryを一回解除する
wp_deregister_script( 'jquery' );

// 同じハンドル名で、フッター読み込みに変えて再登録する
wp_register_script( 'jquery', includes_url( '/js/jquery/jquery.js' ), array(), false, true );

// foo.jsとbar.jsはともにjQueryに依存していることとする
// foo.jsはフッターで読み込まれるように指定したが、bar.jsはうっかり指定を忘れてヘッダー(デフォルト)で読み込まれてた!
wp_enqueue_script( 'my-foo',  get_stylesheet_directory_uri() . '/foo.js', array('jquery'), false, true);
wp_enqueue_script( 'my-bar',  get_stylesheet_directory_uri() . '/bar.js');

こう書いた場合、jQuery本体とfoo.jsは</body>タグの直前でインクルードされますが、bar.jsは<head></head>の中でインクルードされます。jQueryより先にbar.jsが読み込まれることになるので、jQuery is not definedとなってしまいます。

このようなケアレスミスは意外とあるので、引数は省略せずに全て書くことをおすすめします。

jQueryが重複して読み込まれるんですけど!

これは特異なケースですが起こり得ます。以下の例を見てください。

<?php
add_action('wp_enqueue_scripts', 'enqueue_my_scripts');
function enqueue_my_scripts() {
 // cdnでjQueryを読み込んだ!ハンドル名は'jQuery', wordpressの内蔵jQueryのハンドル名は'jquery'なので微妙に異なります。
 wp_enqueue_script('jQuery', 'https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js', array(), false, true);

 // 続けて使用したいスクリプトファイルをエンキューしていく
}

CDNjQuery'jQuery'というハンドル名でエンキューしました。

これだけでは特に問題ないのですが、この条件に加えて、例えばContact Form 7という有名なプラグインを有効化しているとどうでしょうか?wordpress内蔵のjQueryと、CDNjQueryが二つインクルードされてしまうと思います。

これは、Contact Form 7のプラグイン内でスクリプトファイルがwp_enqueue_script()されていて、そこで'jquery'というハンドル名、つまりwordpress内蔵のjQueryを依存先に指定しているためです。

'jquery''jQuery'、ハンドル名の取り扱いはケースセンシティブのようで、今回別々のハンドル名で依存先をそれぞれ指定している状況となったため、jQueryが二つ読み込まれてしまったということになります。

では、CDN版のハンドル名を'jquery'にすればいいのかということで実験してみたのですが、この場合jQueryが重複してインクルードされる現象は解消されたものの、同じハンドル名でスクリプトファイルを登録した場合、先にContact Form 7が依存先として指定しているwordpress内蔵のjQueryが優先されるようで、CDNjQueryを読み込んでくれませんでした...

<?php
add_action('wp_enqueue_scripts', 'enqueue_my_scripts');
function enqueue_my_scripts() {
 // Contact Form 7を有効化している状況を仮定します
 // Contact Form 7は内部で、ハンドル名'jquery'と'jquery-form'に依存するスクリプトファイルをwp_enqueue_scriptしてます
 // 今回は'jquery-form'については触れません

 // wordpressで予約されている'jquery'というハンドル名でエンキューすれば、ソースをCDNに上書きできるのではと期待する
 wp_enqueue_script('jquery', 'https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js', array(), false, true);

 // 結果、Contact Form 7の依存先で指定されているwordpress内蔵のjQueryが優先され、上書きは出来なかった
}

ならばということで試してみる価値があるのが、一旦wordpressの内蔵jQuerywp_deregister_script()して、CDNの方を再登録するという方法です。Contact Form 7の場合これで上手くいきました。

<?php
add_action('wp_enqueue_scripts', 'enqueue_my_scripts');
function enqueue_my_scripts() {
 // Contact Form 7を有効化している状況を仮定します
 // Contact Form 7は内部で、ハンドル名'jquery'と'jquery-form'に依存するスクリプトファイルをwp_enqueue_scriptしてます
 // 今回は'jquery-form'については触れません

 // 一旦wordpress内蔵のjQueryをderegisterする
 wp_deregister_script('jquery');

 // wordpressで予約されている'jquery'というハンドル名で再度登録する
 wp_register_script('jquery', 'https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js', array(), false, true);

 // 結果、CDNのjQueryがフッターでインクルードされた!
}

読んでいただきありがとうございました!よきwordpressライフを!

wordpressのフックをマスターしてカスタマイズの幅を広げる

wordpressはもともとCMSですが、フックを理解して使いこなせればプラグインを自分仕様にカスタマイズしたり、オリジナルのプラグインを作成して、CMSに留まらないウェブアプリケーション、例を挙げると不動産の検索サイト、SNS、ホテルや美容室などの予約システム、マッチングシステムといったものまで作ることができちゃいます。

つまりwordpressのフックを制すものが世界を制すといっても過言ではないのです!

すいませんそれはさすがに過言ですが、フックを理解して使えるようになれば初級者から中級者にジャンプアップできることは間違いないので、この記事でしっかりフックについて学んでください!

wordpressのフックの種類

wordpressをインストールしてみた方はわかると思うんですが、インストールした直後からもう立派なCMSに仕上がっていてちゃんと動きますよね。そうです、もう出来上がっちゃってるわけです。なのでこの完成品をそのまま使うことももちろんOKなのですが、wordpressはデフォルトの動作を自由にカスタマイズできるポイントをたくさん残してくれています。そうしたカスタマイズポイントがフックだと思ってください。

wordpressのフックには2種類あります。それがフィルターフックとアクションフックです。この違いについて、まずは本家プラグイン API - WordPress Codex 日本語版から引用してみます。

  1. アクション: アクションは、実行中の特定のポイントもしくは特定のイベント発生時に WordPress のコアが起動させるフックです。アクション API を使用して、これらのポイントで実行中の PHP 関数を一つ以上指定することができます。
  2. フィルター: フィルターは、データベースに追加する前やブラウザのスクリーンに送り出す前にさまざまなタイプのテキストを改造するために WordPress が起動させるフックです。プラグインは、フィルター API を使用して、これらのタイミングで特定のタイプのテキストを改造するために一つ以上の PHP 関数の実行を指定することがきます。

今はまだよく分からなくても大丈夫です。現時点で押さえておくべきことは、

  • 共通点:フック(カスタマイズポイント)で実行する関数(コールバック)を用意する
  • 相違点:アクションフックは追加の処理を実行するためのポイント、フィルターフックはテキストを改造するためのポイント

ということです。

フィルターフックとは

フィルターフックとは、デフォルトの動作の途中でテキストを改造するためコールバックを実行するポイントになります。用意されているフィルターフックの探し方は、apply_filters()またはapply_filters_ref_array()がコールされている場所を見付けることです。

例えば、wordpressのテンプレートタグthe_content()を例に見てみましょう。

<?php
// wp-includes/post-template.php
//////  中略 ///////
function the_content( $more_link_text = null, $strip_teaser = false ) {
        $content = get_the_content( $more_link_text, $strip_teaser );

        /**
         * Filters the post content.
         *
         * @since 0.71
         *
         * @param string $content Content of the current post.
         */
        $content = apply_filters( 'the_content', $content );
        $content = str_replace( ']]>', ']]&gt;', $content );
        echo $content;
}

$content = apply_filters( 'the_content', $content )という部分を注目してください。の第一引数'the_content'がフィルターフック名となります。

apply_filters()は、第一引数のフィルターフック名に登録されているコールバック(あとで出てくるadd_filter()で追加された関数)を探し、登録があればそのコールバックを実行し、コールバックが返す値をreturnします。apply_filters()の第二引数(この場合だと$content)が、コールバックに改造を許すテキストです。

フックに関数が登録されていない場合、$contentは改造されずそのままreturnされます。

フックを利用してこの$contentを改造したい場合は以下のようにします。

<?php
add_filter('the_content', 'add_cta_text', 10, 1);
function add_cta_text($content) {
 $content = $content . '文末にCTAを追加してみる';
 return $content;
}

// こう無名関数を使って書いてもいい
add_filter('the_content', function($content) {
 $content = $content . '文末にCTAを追加してみる';
 return $content;
}, 10, 1);

?>

このように、add_filter()という関数を、テーマ内のfunctions.phpとかプラグインとかの適切な場所に書きます。第一引数にはその関数を登録したいフック名を記述し、第二引数には実行させたい関数(コールバック)を与えます。

第二引数のコールバックの引数には、apply_filters()側の第二引数(と、第三引数以降があればそれら)が与えられます。フィルターフックの目的はテキストを改造することと説明しました。なので、上の例のとおり、コールバックは改造後のテキストをreturnする必要があることに注意してください。

このようにしてフィルターフックにコールバックを登録しておくと、apply_filters()が呼ばれた時に登録された関数されます。コールバックはいくつでも登録可能で、複数のコールバックが登録されている場合、add_filter()の第3引数の数字を優先順位にして、順次実行され値が改造されていきます。詳しくはリファレンスをご確認ください。

アクションフックとは

アクションフックとは、任意のコールバックを実行させられるポイントでフィルターフックとよく似た動きをします。用意されているアクションフックの探し方は、do_action()またはdo_action_ref_array()がコールされている場所を見付けることです。

代表的なアクションフック'wp_head'を例にその使い方を見ていきましょう。

wordpressでテーマ開発をやり始めると、

header.phpにはwp_head()を必ず書きましょう

という説明を頻繁に見ると思います。

wp_head()はwp-includes/general-template.phpに定義されていて、その中身はdo_action()をコールするだけです。

<?php
// wp-includes/general-template.php
function wp_head() {
 /**
 * Prints scripts or data in the head tag on the front end.
 *
 * @since 1.5.0
 */
 do_action( 'wp_head' );
}

do_action()の第一引数'wp_head'がアクションフック名です。このアクションフックに、あとで説明するadd_action()で実行させたいコールバックを登録させます。

wordpressはデフォルトでこの'wp_head'に色々なコールバックを登録しているので、テンプレート内でwp_head()を実行しないと登録済みのコールバックたちが実行されず、デフォルトで予定された結果が得られません。なので必ずwp_head()をheader.php(の<head></head>の中)に書きましょうと言われているのです。

ではこのアクションフックに適当なコールバックを登録してみましょう。

<?php
// functions.php
add_action('wp_head', 'add_custom_styles');
function add_custom_styles() {
 echo "<meta name='robots' content='noindex,follow' />\n";
}

// 無名関数を使ってこう書いてもいい
add_action('wp_head', function() {
 echo "<meta name='robots' content='noindex,follow' />\n";
});

add_custom_stylesというコールバックは、noindex, followのメタタグを出力します。このコールバックはwp_head()を実行した場所で、do_action('wp_head')によって実行されます。

フィルターフックとアクションフックの違い

フィルターフックもアクションフックも、コールバックを登録できるポイントという点で非常によく似ています。

実は、add_action()は内部でadd_filter()を呼んでいるだけなので、コールバックの登録に関してはほぼ、どころか完全に一緒なのです!

<?php
// wp-includes/plugin.php#405
function add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
 return add_filter( $tag, $function_to_add, $priority, $accepted_args );
}

二つのフックの違いは、コールバックを実行する側apply_filters()do_action()だけです。そしてその違いも単純で、

  • apply_filters()は値をreturnする
  • do_action()returnしない

という違いしかありません。

適切なフックの探し方

wordpressにはこうしたアクションフック、フィルターフックが覚えきれないぐらい用意されています。また、プラグインも独自のフックを用意してくれています。

フックが多いということは、それだけ処理をカスタマイズしてオレオレ仕様にできるということです。どうです、wordpressのカスタマイズ欲が沸いてきましたか?

しかしながら、これだけフックあるとどのフックを使えばいいか迷う時もあります。そんな時に、適切なフックを探す方法をいくつかお知らせします。

ドキュメントを見る

まずは公式を読みましょう。

めちゃくちゃ沢山あるので全部を今理解する必要はありません。逆引き的な感じ使うといいと思います。

関数から辿る

フィルターフックを探す時メインのtipsです。フィルターフックを探すということは何らかの関数の出力を変更したい、というニーズがあるはずです。たとえばthe_content()で本文の内容を変更したい、とか。そうした時は怖がらずthe_content()の定義を確認しに行きましょう。

<?php
// wp-includes/post-template.php#237
function the_content( $more_link_text = null, $strip_teaser = false ) {
 $content = get_the_content( $more_link_text, $strip_teaser );
    
 /**
 * Filters the post content.
 *
 * @since 0.71
 *
 * @param string $content Content of the current post.
 */
 $content = apply_filters( 'the_content', $content );
 $content = str_replace( ']]>', ']]&gt;', $content );
 echo $content;
}

ほらありました。wordpressのコアで用意されているフックには、このようにコメントでアノテーションを付けてくれているので、ぱっと見でどこにフックがあるか分かりやすいです。

アクションフックの実行順序から調べる

アクションフックはフィルターフックよりも当たりをつけるのが難しいです。何かやりたい処理をどこかで実行できればそれでいいので、フィルターフックのように「このフックを使わないとダメ」とは限らないからです。また、やりたい処理の内容によっては、そのアクションフックでは早すぎる、遅すぎる、ということも起こり得ます。

これを理解するにはwordpressのヘッダーがどういう順番で処理されるかを知っておく必要がありますが、かなりマニアックなので、取り合えず「アクションフック 順番」などと検索して、この記事のように誰かがまとめてくれた記事を見て、ここら辺かな、と当たりをつけるといいと思います。

フックでよくあるトラブル

最後に、フックを利用としてよくあるトラブルと確認ポイントをまとめておきます。他のもこんなトラブルがあるよ、という方がいればコメントください。

フィルターフックで値を変更したいけど反映されない!

以下の点を確認してみましょう。

自作のコールバック内でreturnを忘れてませんか?

フィルターフックでは、コールバックでreturnした値が返されます。

フィルターフックと思いきや、アクションフックにコールバックを登録してませんか?

アクションフックは値を返さないため、コールバックでreturnしても無駄です。周辺にapply_filters()がないか探してみましょう。

アクションフックに登録したコールバックが実行されない

コールバックが実行された後にコールバックを登録している

フックを利用するためには、①add_action()でコールバックを登録、②do_action()でコールバックを実行、順番になってないといけません。

しかし、例えばあるフックに登録したコールバックの中で、更にadd_action()する、みたいな処理を書くと、①と②の順番が逆になって実行されない、ということが起こり得ます。

まずは本当にコールバックが呼ばれていないか確認するため、コールバック内でdie('test')などと書いて動作確認してみるのが良いと思います。そして、やはり呼ばれていないということであればdo_action()が過ぎた後にadd_action()している可能性が高いので、アクションフックの実行順序を調べて、もう少し早い場所でadd_action()してみましょう。

worpdressでよく見る$postとは何なのか

wordpressで調べ物をしていると、よく$postという変数を目にすると思います。何となくコピペで使っている方も多いんじゃないかと思いますが、もうちょっと理解したいよ、という方のために記事を書こうと思います。$postの中身は何なのか、どこからやってくるのか、そして例えばループの中で使用する$post->post_contentthe_content()との違いは、などについてのお話です。

$postとは何か

$postはwordpressのコアで定義されるグローバル変数の一種です。グローバル変数というのは、グローバルスコープの変数という意味で、簡単に言うとどこからでもアクセスできる変数です。ただし、関数の中で使用された変数はデフォルトでローカルスコープの変数と扱われるので、関数の中でグローバル変数を利用するには以下のようにします。

<?php
$ foo = 'this is global variable'

function bar() {
  global $foo;  // グローバル変数の$fooを使うと宣言する
  echo $foo;
}

公式ドキュメントを見ると、

$post (オブジェクト) – 現在の投稿オブジェクト。

と書かれています。投稿オブジェクトとは何なのでしょうか?

投稿オブジェクトとは

wordpressの投稿オブジェクト($postの中身)は、実は使用する文脈によって個別投稿だったり、固定ページだったり、添付ファイルだったりします。これを理解するためには、投稿タイプとwordpressのデータベースについて少しだけ知っておく必要があります。

wordpressにはデフォルトで5つの投稿タイプが用意されています。

  • post(個別記事)
  • page(固定ページ)
  • revision(リビジョン)
  • attachment(添付ファイル)
  • nav_menu_item(ナビゲーションメニュー)

個別記事(一般的に投稿と言われるとこれを指す)も固定ページも添付ファイルも、各投稿タイプの通称と言えます。

ではそれぞれの投稿タイプのデータはどこに保存されているのでしょうか?

http://wpdocs.osdn.jp/wiki/images/WP3.9.4-ERD.png

これはwordpressのER図ですが、その中央に「wp_posts」というテーブルがあり、その中に「post_type」というカラムがあります。これが投稿タイプを表す部分です。

wordpressは各投稿タイプのデータを別々のテーブルに格納するわけでななく、全部wp_postsにぶち込みpostオブジェクトとして扱います。そして、wp_posts.post_typeに'post', 'page', 'revision', 'attachment', 'nav_menu_item'などの投稿タイプ名を保存しておき、どの投稿タイプのオブジェクトなのかを判別する仕組みになってます。

つまり、個別記事も固定ページも添付ファイルもwp_postsから取り出されたpostオブジェクトです。なので、$postの中身は個別記事(投稿タイプ: 'post')の場合もあれば、固定ページ(投稿タイプ: 'page')の場合もあり、文脈によって異なるのです。ちなにpostオブジェクトの投稿タイプを判別するにはis_single($post), is_page($post), is_attachment($post)などの条件判定タグが利用できます。

$postはどこでセットされるのか

$postグローバル変数なのでどこでも使えるかというと実はそうではありません。というか、アクセスはできるもののnullの場合があります。

そもそもグローバル変数$postがどこで最初に定義されるかというと、wordpressのヘッダーで呼ばれるWPクラスのregister_globals()というメソッド内で定義されます。

<?php
// wp-includes/class-wp.php
class WP {
 // 省略
 public function register_globals() {
  global $wp_query;
  // Extract updated query vars back into global namespace.
  foreach ( (array) $wp_query->query_vars as $key => $value ) {
   $GLOBALS[ $key ] = $value;
  }
  $GLOBALS['query_string'] = $this->query_string;
  $GLOBALS['posts']        = & $wp_query->posts;
  $GLOBALS['post']         = isset( $wp_query->post ) ? $wp_query->post : null;
  $GLOBALS['request']      = $wp_query->request;
  if ( $wp_query->is_single() || $wp_query->is_page() ) {
   $GLOBALS['more']   = 1;
         $GLOBALS['single'] = 1;
  }
  if ( $wp_query->is_author() && isset( $wp_query->post ) ) {
   $GLOBALS['authordata'] = get_userdata( $wp_query->post->post_author );
  }
 }
 // 省略
}

isset( $wp_query->post ) ? $wp_query->post : nullとあることから、この段階では$post === nullの場合があると分かります。

$wp_queryというのはWP_Queryクラスのインスタンスで、リクエストされたページからクエリを生成して必要なpostオブジェクトを取得してくるやつです。

例えばアーカイブページなどの記事一覧ページをリクエストした場合、$wp_queryは複数の投稿データを取得するはずですよね。$wp_query->postは一つの投稿データを表すプロパティなので、投稿データが複数ヒットしたのでこの時点では$postはnullの状態になります。

ではそのような場合、いつ$postに投稿データが入れられるのかというと「ループの中」です。テンプレートで呪文のように書いてるアレです。

<?php
if ( have_posts() ) {
    while ( have_posts() ) {
        the_post(); 
        //
        // 投稿がここに表示される
        //
    } // end while
} // end if

このthe_post()$wp_query->the_post()$wp_queryも実はグローバル変数なのです。)のラッパー関数で、その実態を見てみると、

<?php
// wp-includes/class-wp-query.php
class WP_Query {
  // 省略
    public function the_post() {
        global $post;
        $this->in_the_loop = true;
        if ( $this->current_post == -1 ) { // loop has just started
            /**
            * Fires once the loop is started.
            *
            * @since 2.0.0
            *
            * @param WP_Query $this The WP_Query instance (passed by reference).
            */
            do_action_ref_array( 'loop_start', array( &$this ) );
        }
        $post = $this->next_post();
        $this->setup_postdata( $post );
    }
  // 省略
}

ここでグローバル変数の$postを宣言して、$post = $this->next_post()で次の投稿オブジェクトがセットされていることが分かります。ということで、ループの中では、グローバル変数$postには現在の投稿データが入っているということが理解できました。

グローバル変数とテンプレートタグのどちらを使うべきか

よくwordpressの解説記事とかで、$post->post_contentのように$postオブジェクトのプロパティに直接アクセスしているものを見かけます。一方でwordpressには投稿本文を取得するthe_content()というテンプレートタグも用意されています。どちらを使うべきでしょうか?

結論を言うと、なるべくthe_content()を使うべきです。the_content()グローバル変数$postからデータを取り出しているのですが、若干違いがあります。the_content()はwp-includes/post-template.phpで定義されているのでそれを見てみましょう。

<?php
// wp-includes/post-template.php
//////  中略 ///////
function the_content( $more_link_text = null, $strip_teaser = false ) {
        $content = get_the_content( $more_link_text, $strip_teaser );

        /**
         * Filters the post content.
         *
         * @since 0.71
         *
         * @param string $content Content of the current post.
         */
        $content = apply_filters( 'the_content', $content );
        $content = str_replace( ']]>', ']]&gt;', $content );
        echo $content;
}

$content = apply_filters( 'the_content', $content )とあるように、取得した本文が'the_content'というフィルターフックを通過していることがわかります。

wordpressではこうしたフックが至る所で仕込まれていて、誰でもフックを利用して出力内容をカスタマイズすることができます。そして、現在使用中のプラグインでもこうしたフックを利用して場合があります。もしthe_content()を使わず、$post->post_contentで本文を出力すると、プラグインで予定されていたフックを通過せず、予期した結果が得られない、などの可能性が考えられます。

こうしたことはthe_content()に限らず、色々なテンプレートタグであり得ます。なので、欲しい値を出力するテンプレートタグが用意されている場合には、他に理由がない限りテンプレートタグを使用した方がいいです。

テンプレートタグの解説については【初級・中級】wordpressのテンプレートタグをTwenty Nineteenを見ながら解説するをご覧ください。

よきwordpressライフを!

【初級・中級】wordpressのテンプレートタグをTwenty Nineteenを見ながら解説する

wordpressの勉強を始めるとすぐ、ループとかテンプレートタグとかいう見慣れない言葉に出くわして訳わかんないよ、と思う方も多い思います。 今日はこのうちテンプレートタグについてできるだけ丁寧に説明し、次回ループについて見てみたいと思います。

テンプレートタグとは

テンプレートタグとは、wordpressのコアで用意されている関数のうち、テンプレートファイル内で使用することを意図して、HTMLを出力したり、カテゴリやタグなどで条件分岐したり、部分テンプレートを読み込んだりするものをいいます。要するに関数です。 ここからはwordpress公式の最新テーマTwenty Nineteenを例に具体的に見ていきましょう。

// single.php
<?php
get_header();
?>
    <section id="primary" class="content-area">
        <main id="main" class="site-main">

            <?php
            /* Start the Loop */
            while ( have_posts() ) :
                the_post();
                get_template_part( 'template-parts/content/content', 'single' );
                if ( is_singular( 'attachment' ) ) {
                                    // 中略
                                } elseif ( is_singular( 'post' ) ) {
                                    // 中略
                }
                // 中略
            endwhile; // End of the loop.
            ?>
        </main><!-- #main -->
    </section><!-- #primary -->

<?php
get_footer();

一般テンプレートタグ

一般テンプレートタグとは、主にテンプレートファイル内で使用し、HTMLの出力を生成することを目的とした関数群です。一般テンプレートタグの代表的なものにthe_title()とか、the_content()とかがあります。その名のとおり、それぞれタイトルとコンテンツ(本文)を出力するための関数です。single.phpは個別投稿を表示するためのテンプレートなので、これらのテンプレートタグが書かれていそうなものですが、見てもありませんね。

このテーマでは、template-parts/content/content-single.phpという投稿表示用の部分テンプレートが用意されていて、あとで説明するget_template_part()というインクルードタグによって挿入されていますのがわかります。content-single.phpを見てみると、

// template-parts/content/content-single.php
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
    <?php if ( ! twentynineteen_can_show_post_thumbnail() ) : ?>
    <header class="entry-header">
        <?php get_template_part( 'template-parts/header/entry', 'header' ); ?>
    </header>
    <?php endif; ?>

    <div class="entry-content">
        <?php
        the_content(
            sprintf(
                wp_kses(
                    /* translators: %s: Name of current post. Only visible to screen readers */
                    __( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'twentynineteen' ),
                    array(
                        'span' => array(
                            'class' => array(),
                        ),
                    )
                ),
                get_the_title()
            )
        );
                // 中略
        ?>
    </div><!-- .entry-content -->
        <!-- 中略 -->
</article><!-- #post-${ID} -->

10行目でthe_content()が呼ばれていますね。the_title()は見当たりませんが、これはさらに部分テンプレートを辿って行った先(template-parts/header/entry-header.php)に書かれています。

余談ですが、single.phpは個別記事を表示するための使われるのに対して、page.phpは固定ページを表示するテンプレートです。Twenty Nineteenのpage.phpを見るとsingle.phpとよく似たつくりになっていて、部分テンプレートを辿っていくとthe_title(), the_content()という記述が見付かると思います。個別記事と固定ページはworpdress上別々の扱いなのに、何故同じテンプレートメソッドが使えるのか疑問に思った方は感がいいです。気になる方はこちらの記事を読んでみてください。

worpdressでよく見る$postとは何なのか

もう一つ余談ですが、template-parts/content/content-single.phpの中で、the_title()と良く似たget_the_title()という関数が出てきました。これはwordpressで良くある関数のペアで、ざっくり言うと、theから始まる関数は実行した場所で出力を行うのに対して、get_theと続く関数は値を取得するだけで出力は行いません。またその他の違いとして、the_title()the_content()などはループの中でしか使えないのに対し、get_the_content()get_the_title()は、引数に投稿オブジェクトを与えてあげればループの外でも機能します。しかしながら、例えばthe_guid()get_the_guid()はどちらもループ外で使用できちゃう、など一般化しにくいところがwordpressの覚えにくいところだったりします。

条件分岐タグ

先ほど、投稿はsingle.php、固定ページはpage.phpを利用すると書きましたが、実はsingular.phpという名前のテンプレート1つでどちらも表示できてしまいます。テンプレート階層について深く知りたい方は、wordpressのテンプレート階層を参照してください。

では、singular.phpを使うとして、固定ページの場合と投稿の場合で一部の表示を出し分けるにはどうすればいいでしょうか?

そういう時に使えるのが条件分岐タグです。条件分岐タグはTRUE/FALSEを返す定義済みの関数で、僕も全部把握してないぐらい沢山あります。困ったときは公式ドキュメントを見るといいです。

wordpressの条件分岐タグ一覧

今回の場合、is_singular()を使えば、以下のような感じに表示を出し分けることができます。

<?php
// singular.php内で投稿タイプによって表示を出し分ける例
if ( is_singular('page') ) {
 echo 'これは固定ページです'
} elseif ( is_singular('post') ) {
 echo 'これは個別投稿ページです'
}

Twenty Nineteenのsingle.phpでもis_singular()が使われていますね。single.phpは個別記事の表示に使われるのですが、アップロード画像なども一種の投稿とみなされてsingle.phpが使われる場合があるのです。なので、現在表示しているのが画像などのattachment投稿タイプか、個別記事(post投稿タイプ)かで表示を出し分ける処理をしています。

なお、条件分岐タグもループの中でしか使えないものがいくつかあるので、ループ外に書いて期待どおりに動かない場合は公式ドキュメントに立ち返ってください。

それと、条件分岐タグはテンプレートタグの一部の扱いですが、実際のところテンプレート内だけじゃなく、function.phpとかで定義した関数の中(フックに登録して処理を書き換える場合など)で使うこともよくあります。ループ内でしか使えない条件分岐タグも、ループ内のテンプレートタグに仕込まれたフックでなら使うことができます。(ちょっと何言ってるかわからないですね) 詳しくはwordpressのフックをマスターしてカスタマイズの幅を広げるをお読みください。

インクルードタグ

例えば、トップページでも個別記事のページでも共通のヘッダーとフッターを表示したいとします。single.phpとhome.phpに同じヘッダー・フッターコードを書くけば実現できますが面倒臭いです。

複数のページで使い回すことが多いヘッダーやヘッダーは部分テンプレートに書いておき、必要な場所でその部分テンプレートを挿入することができれば楽ですよね。

これはphpに元々備わっているinclude()でも実現できますが、wordpressにはもっと簡単に部分テンプレートを読み込める関数(インクルードタグ)が用意されています。

  • get_header()
  • get_footer()
  • get_sidebar()
  • get_template_part()
  • get_search_form()
  • comments_template()

get_header(), get_footer(), get_sidebar()は似たような使い方で、引数を与えないで呼び出した場合、それぞれテーマディレクトリ内のheader.php, footer.php, sidebar.phpを探し、見付かればそのファイルをインクルードします。見付からない場合はwordpressコアに用意されているデフォルトテンプレートが使われるらしいです。

これらの関数は文字列の引数を一つ受け取ることができて、例えばget_header('first')とすると、テーマディレクトリ内のheader-first.phpを探してインクルードしてくれます。ページによってヘッダーやフッターのデザインを変えたり、サイドバーのコンテンツを切り替えたい場合に使ったりします。

次に、Twenty Nineteenのsingle.phpにも登場したget_template_part()は色々な使い方ができますが、一つの投稿表示部分を部分テンプレートに切り出して、ループ内でインクルードするという使い方が多く見られます。

なお、get_template_part()railsのpartialなどと違って、部分テンプレート内で使用するローカル変数を渡すことはできません。 どうしても変数を渡したい場合、別の方法を試しましょう。

次回はループについて説明します!よきwordpressライフを!