【WordPress】ウィジェットを自作したくて調べたこと・作り方【画像付き最新記事】

アイキャッチ:地平線と花

新しいブログテーマを作るにあたり、「サイドバーに最新記事を出してぇなぁ」と考えていました。
WordPressのデフォルトのウィジェットでも最新記事を表示することは可能ですが、こちらはアイキャッチがなくタイトルのみ。
タイトルだけよりも、絵があったほうが見栄えがいい。

「無ければ作ればいい」という精神で、アイキャッチ付きで表示できるウィジェットを作ったので、その解説をしていきます。
プラグインを使わず、かつ仕組みを理解して自作したい方にどうぞ。

期待する動作・完成イメージ

挿絵:ウィジェットの完成イメージ
  • ウィジェットとして登録、並び替え、設定ができる
  • 最新記事を指定したn件表示する
  • 最新記事のアイキャッチも同時に表示する

言ってしまえば「デフォルトのウィジェットにアイキャッチが付いてる」だけなんですが。
口で言うのが簡単でも、実装しなきゃ実現しないのがプログラミング。

簡単な流れ

挿絵:完成に向けたフローチャート

完成までの流れをフローチャートに起こしました。
コンストラクタで親クラスを継承する以外に、form関数・update関数・widget関数を定義していきます。

ウィジェット開発の前に

functions.phpの記述

自作したウィジェットは、functions.phpに記述します。
ですが、functions.phpは色んな用途のコードが入り混じって複雑になるため、別のファイルで管理することが好ましいと思います。

私の場合、libディレクトリとwidets.phpというファイルを作成して、functions.php側から呼び出しています。

# functions.php
require_once get_template_directory() . '/lib/widgets.php';

ウィジェットの登録

ウィジェットを段階的に実装していきますが、その前に「ウィジェットとして登録する」必要があります。
このコードがないと、ウィジェットが出来上がっていても管理画面には表示されません。
widgets.php(またはfunctions.php)に忘れずに追記します。

# ウィジェットを登録する
# ここでは「ImgNewPostWidget」とする

function theme_register_widget() {
    register_widget( 'ImgNewPostWidget' );
}
add_action( 'widgets_init', 'theme_register_widget' );

register_widget関数は、追加したいプラグインのクラス名をパラメータとして渡します。

段階的に実装する

クラス関数の順番は、個人的にはHTML出力を行うwidget関数を最後にしています。

まず、ダッシュボード(管理画面)のウィジェットに表示できること。
次に、ダッシュボードのウィジェットで設定を反映できること。
最後に、登録したウィジェットをWebページで表示するためのコード。

この順番で解説するのが好ましいと判断しました。

STEP1.コンストラクタ

ここでの目的は、ダッシュボードのページでウィジェットが表示されることです(Fig1)。
たったそれだけですが、追加するウィジェットに必要な設定の入力フォームの実装につながります。

Fig1.管理画面に載った自作のウィジェット
Fig1.管理画面に載った自作のウィジェット

クラスを継承し、コンストラクタ内で初期化処理を記述します。
他のform関数、update関数、widget関数は空っぽのままです。

コンストラクタの記述

WordPress Codexのサンプルでは、コンストラクタをまとめて記述しているコードがあります。

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

最後の行でまとめてclassnamedescriptionの配列を記述すると雑多になるため、配列名だけの記述で済むように先にパラメータを処理しています。
親クラスであるWP_Widgetのコンストラクタを呼び出して、ここで記述した値を渡しています。

/* クラス名は先頭が大文字 */
class ImgNewPostWidget extends WP_Widget {
  /**
  * 初期化処理・コンストラクタ
  */
  public function __construct() {
 
    // ウィジェットの最低限の初期値
    $widget_options = array(
      'classname'                     => 'widget-imgnewpost',
      'description'                   => '自作:画像つきの最新記事',
      'customize_selective_refresh'   => true,
    );
    // 操作用の設定値
    $control_options = array();
 
    // 親クラスのコンストラクタをコール
   // 例)parent::__construct( $id_base, $name, $widget_options, $control_options )
    parent::__construct( 'widget-imgnewpost', '自作:画像つき最新記事', $widget_options, $control_options );
  }

$widget_options

classnameはウィジェットに割り当てられるHTMLのクラス名です。descriptionは、文字通り「説明」ですが、ダッシュボード上では「ウィジェットの名前」とも言えます。

customize_selective_refreshは、テーマカスタマイザーでの変更をリアルタイムに適用するもののようです。
できた方が便利だと思うので”true”を指定しています。
(これに関しては、以下の理由で明示せずとも良さそうですが……)

※ウィジェットによっては、コンストラクターで明示的に ‘customize_selective_refresh’ => true と指定しているものもありますが、このような指定がなくとも WP_Widgetから継承したウィジェットならこの設定により自動的に customize_selective_refresh が有効化されるようです

$control_options

$control_optionsは、ダッシュボード上で展開した際の幅や高さを指定することができます。
高さも幅も、プリセットされたウィジェットの幅で充分だと思ったため、空の配列を指定しています。

また、id_baseというキーを持っているようですが、英語版のリファレンスまでさかのぼったところ、「同じウィジェットを複数使うときに区別するために必要となる」感じです

$options   (array) (Optional) Array or string of control options.

  • ‘height’ (int) Never used. Default 200.
  • ‘width’ (int) Width of the fully expanded control form (but try hard to use the default width). Default 250.
  • ‘id_base’ (int|string) Required for multi-widgets, i.e widgets that allow multiple instances such as the text widget. The widget id will end up looking like {$id_base}-{$unique_number}.

Default value: array()

さて、これでガワだけとはいえ、ダッシュボード上に表示され、他のウィジェットと同じようにドラッグアンドドロップできるものができました。
次のステップで、ウィジェットのタイトルや、ウィジェット独自の設定を入力できるフォームを追加していきます。

STEP2.クラス関数その1:form関数

挿絵:ウィジェットの管理画面にフォームを実装する

※この項目が一番長いですが、やっていることは難しくありません。

form関数では、ウィジェットエリアで表示する名前のように、ユーザが設定を入力可能なフォームを実装します(Fig4)。
(ウィジェットが持つ設定のことをオプションと呼びます)

この「最新記事をアイキャッチ付きで表示するウィジェット」には、他のウィジェットと同じように任意の名前を付けられるフォームと、記事をいくつ表示するかを指定するフォームを持たせます。

基本的にはダッシュボードで表示するためのHTML(とPHP)を書きますが、例えば表示件数が定義されていないとエラーの原因になるため、指定した初期値を読み込むようにします。

Fig4.form関数を実装するフローチャート
Fig4.form関数を実装するフローチャート

設定を入力するフォームの実装

HTMLに慣れた人、またはWordPressの自作テーマで検索フォームを作ったことがある人なら、labelタグとinputタグを簡単に扱えるでしょう。
他のウィジェットを見るとわかるように、基本的にラベル(見出し)とフォームがセットになっています。
複雑な点といえば、PHPのコードを含むので「改行しないと読みづらい」ことでしょうか。

<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>

フォームのIDや名前を動的に取得する

form関数の引数$instanceからIDや名前を引っ張ってきています。
labelタグのfor属性は、セットとなるフォームのIDを示します。・・・(※1)

そのため、inputタグのIDも同じ値を取るために $this->get_field_id( 'title' ) でecho出力されています。
また、inputタグのname属性は「フォームの名前」、value属性は「フォームの初期値」を示し、それぞれをechoで出力しています。・・・(※2)

class=”widefat”とは

「input」「textarea」タグに”widefat”というクラスが指定されていますが、これは要素を横幅一杯に表示するCSSプロパティが設定されています。WordPressで用意されているクラスです。また、各フォームはpタグで囲うと綺麗に整います

後から出てきますが、WordPressには数値のような小さい値を扱うフォームにはtiny-textというクラスが当てられています。

表示件数には整数を扱うフォームを。

挿絵:表示件数のフォームを"number"で作り直したもの

他の解説を読んでこのウィジェットを試作したとき、フォームはテキストフォームで作っていました。
しかし、この解説を書くために「デフォルトのウィジェットのコードを見ればいいじゃん」と気付き、デフォルトのウィジェットが「タイプ属性にnumberを指定している」ことを突き止めました。・・・(※3)
(オープンソースというから、てっきりGithubからtar.gzを取ってこなきゃいけのかと思ってた。まさかWebで閲覧できるとは。)

「文字列を整数にキャストするの面倒くせーな」とか思ってたんですが、これなら全角の無効化や入力必須の仕様を実現できそうです。・・・(※4)
(※キャストする=変数の型を変換する)

ついでに、非現実的な数値は入れられないようにします。
(最低でも1件。最大で15件くらいにしてみましょう。)

表示件数の入力フォームの実装は、次のように書くことが出来ます。

<p>
  <?php /* 表示する投稿数 */ ?>
  <label for="<?php echo $this->get_field_id( 'limit' ); ?>">表示する投稿数</label>
  <input type="number"
    class="tiny-text"
    id="<?php echo $this->get_field_id( 'limit' ); ?>"
    name="<?php echo $this->get_field_name( 'limit' ); ?>"
    value="<?php echo esc_attr( $instance['limit'] ); ?>"
    min="1" max="15" step="1" size="3" required />
</p>
<?php
}

※tiny-textクラスにはwidth:45pxが指定されています。
これにより、デフォルト(35px)より少し横幅が大きくなっています。

利用したinputタグの属性は以下のものです。

  • min…最小値
  • max…最大値
  • step…刻み幅と増減のUI(スピナー:▲▼ボタン)小数点の入力不可(初期値:1)
  • required…入力必須

スピナーで最小値は1、最大値は15でストップします。
また、全角の数字は入力しても消えます。
半角の少数は入力できますが、(update関数を実装すると)ボタンは「保存しました」に変わりません。ちゃんと無効になっています。
空白の場合、入力必須の条件を満たしていないので、少数と同じように保存ボタンが効きません。

エルビス演算子で無害化と初期値のセットを同時に行う

WordPressデフォルトのウィジェットでは、無害化(サニタイズ)と初期値の宣言を同時に行っています。

public function form( $instance ) {
    $title = isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : '';
    $number = isset( $instance['number'] ) ? absint( $instance['number'] ) : 5;

タイトルの無害化にはesc_attr関数、表示件数の無害化にはabsint関数を使っています。

「はてな記号による分岐(?:)」はエルビス演算子(あのエルビス・プレスリーが由来)と呼ぶらしい(PHP5.3で登場)。
記号ってググるのが大変なので、読み方は載せるべき派。

$instanceとは?

以上を踏まえると、form関数は次のように書くことができます。

/**
   * 管理画面のウィジェット設定フォーム
   *
   * @param array $instance   現在のオプション値が渡される。
   */
  public function form( $instance ) {
    $title = isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : '';
    $limit = isset( $instance['limit'] ) ? absint( $instance['limit'] ) : 5;
  ?>
  <p>
    <?php /* タイトル */ ?>
    <label for="<?php echo $this->get_field_id( 'title' ); ?>">タイトル</label>
    <input type="text"
        class="widefat"
        id="<?php echo $this->get_field_id( 'title' ); ?>"
        name="<?php echo $this->get_field_name( 'title' ); ?>"
        value ="<?php echo $title; ?>" />
  </p>
  <p>
    <?php /* 表示する投稿数 */ ?>
    <label for="<?php echo $this->get_field_id( 'limit' ); ?>">表示件数</label>
    <input type="number"
        class="tiny-text"
        id="<?php echo $this->get_field_id( 'limit' ); ?>"
        name="<?php echo $this->get_field_name( 'limit' ); ?>"
        value="<?php echo $limit; ?>"
        min="1" max="15" step="1" size="3" required />
  </p>
  <?php
  }

STEP3.クラス関数その2:update関数

update関数は、ウィジェットの設定を変更するような入力があった際に、その値を無害化して保存する過程を担います。
この関数が空だと、フォームに入力しても保存されません(定義した初期値が呼ばれます)。
値を無害化するデータが二つということもあり、他の関数と比べて処理が少ないです(Fig6)。

Fig6.update関数を実装するフローチャート
Fig6.update関数を実装するフローチャート

Codexのサンプルで、基本的な形を見てみましょう。
Documentコメントの方が長いという驚くかもしれない。

/**
 * ウィジェットフォームの値を保存用にサニタイズ
 *
 * @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;
}

$old_instanceの使いみち

Codexサンプルコードでは、$old_instanceを使っていませんが、他所のコードを見てみると、関数の冒頭で$old_instanceを保存しています。

$instance = $old_instance;

おそらく、新しく入力した値が適切でない場合に、すでに保存されている値を保持するためだと思われます。
空の配列を宣言しているのは、初回で保存されていない場合を想定しているのかもしれません。
(具体的な理由を見つけられなかった。)

ウィジェットタイトルの無害化

Codexのウィジェットタイトルの無害化にはPHPのstrip_tags関数が使われています。
デフォルトのウィジェットではWordPressのsanitize_text_field関数が使われています。

この関数は、指定した文字列 (str) から全ての NUL バイトと HTML および PHP タグを取り除きます。 この関数は、fgetss() 関数と同じタグ除去アルゴリズムを使用します。

ユーザーが入力、またはデータベースから取得した文字列を無害化します。無効な UTF-8 をチェックし、独立した ‘<‘ 文字をエンティティーへ変換し、タグをすべて除去し、改行・タブ・余分な空白を削除し、オクテット(’%’ に続く 2 桁の 16 進数)を除去します。

うーん、sanitize_text_field関数の方が用途にあってるから採用しよう。

表示件数の無害化

form関数にて、type属性に数値しか扱えない”number”を指定しました。
そのおかげで、数字(string)を数値(integer)に変換する処理は不要です。
それでも、デフォルトのウィジェットでは、int型への型変換が行われています。
データ検証を優先して処理をしているのでしょうか?
(表示件数の初期値は数値として「5」を指定したため、文字列である心配は要らないでしょう。)

update関数のコードはこれだけです。
二つのオプション値を無害化するだけなので短いです。

/**
 * ウィジェットオプションのデータ検証/無害化
 *
 * @param array $new_instance   新しいオプション値
 * @param array $old_instance   以前のオプション値
 *
 * @return array データ検証/無害化した値を返す
 */
public function update( $new_instance, $old_instance ) {
  // 一時的に以前のオプションを別変数に退避
  $instance = $old_instance;

  // タイトル値を無害化(サニタイズ)
  $instance['title'] = sanitize_text_field( $new_instance['title'] );

  // 投稿数の検証
  $instance['limit'] = is_numeric( $new_instance['limit'] ) ? $new_instance['limit'] : 5;

  return $instance;
}

STEP4.クラス関数その3:widget関数

いよいよ、ウィジェットの個性が出る段階です。
実際にWebページで表示する視覚的な要素は、PHPを織り混ぜたHTMLが担います(Fig7)。

Fig7.widget関数を実装するフローチャート
Fig7.widget関数を実装するフローチャート

ウィジェットを構成するHTMLタグたち

widget関数で主となるのは、Webページで表示される内容です。
before_widgetafter_widgetbefore_titleafter_titleが登場し、ウィジェットを構成します。
WordPressのテーマを作る上で、functions.phpにダイナミックサイバー(ウィジェットエリア)を記述したことがある人なら見たことがあると思います。
これらの値は、$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'];
}

ウィジェットという一つの塊は、まずbefore_widgetでHTMLを吐き出すところから始まります。
次にタイトルの前にbefore_title、ウィジェットのタイトルが来て、after_widgetで閉じます。
そうすればウィジェット本体の出番。
最後に、after_widgetを出力して終了です。

設定した値を受け取る処理

掲載されているコードによっては、ここで渡された値が空かどうかを判定しています。
無害化・サニタイズする処理は行われてきましたが、「カラかどうか」「カラなら値をセットする」という処理は、この段階でも行うようです。
(初期値としてform関数で宣言した自分としては、違和感がありますが。)

デフォルトのウィジェットのコードを読んでみると、バグを起こさないために、absint関数で絶対値かつ整数を取っています。
次の行では、false(ゼロ)の場合に、それでは表示件数の意味がないので初期値「5」を代入しています(ゼロは整数)。
form関数で最小値を1に指定し、少数の入力を不可にしているのですが、「データを取り出すときも検証する」のが大事ということでしょうか。

public function widget( $args, $instance ) {
(略)
  $number = ( ! empty( $instance['number'] ) ) ? absint( $instance['number'] ) : 5;
  if ( ! $number ) {
    $number = 5;

サブループで最新記事とアイキャッチを表示する

最新記事の表示には、WP_Queryに条件を指定しています。
以前書いた「関連記事を実装する」記事でも扱ったパラメータを使います。

$query_args = array(
    'posts_per_page'      => $limit,
    'post_type'           => 'post',
    'ignore_sticky_posts' => 1,
);
$my_query = new WP_Query( $query_args );

表示件数は、’posts_per_page‘パラメータに$instance['limit']を検証した$limitを渡しています。
新しい順に並べるには、’order’パラメータの初期値’DESC(降順)‘と、'orderby‘パラメータの初期値’post_date(日付)‘で実現できます。
「先頭に固定した記事を除外する」’ignore_sticky_posts‘パラメータを有効にしていますが、記事の固定をしない人は記述を省いてもいいと思います。

以上のパラメータをもとにサブループを作成すれば、最新記事を取り出すことができます。
アイキャッチはループの中でthe_post_thumbnails関数を使って呼び出しています。
呼び出すアイキャッチはCSSで縮小した同じ画像です。

widget関数のコードはHTMLを出力する部分が含まれていて長くなっています。

/**
 * ウィジェットの内容をWebページに出力(HTML表示)
 *
 * @param array $args       register_sidebar()で設定したウィジェットの開始/終了タグ、タイトルの開始/終了タグなどが渡される。
 * @param array $instance   データベースの保存値。
 */
public function widget( $args, $instance ) {

  // ウィジェットのオプション「タイトル(title)」を取得
  $title = empty( $instance['title'] ) ? '' : $instance['title'];

  // ウィジェットのオプション「表示する投稿数(limit)」を取得
  $limit = ( ! empty( $instance['limit'] ) ) ? absint( $instance['limit'] ) : 5;
  // ゼロ(整数)を認めない
  if( ! $limit){
    $limit = 5;
  }

  // ウィジェット開始タグ(<div>など)
  echo $args['before_widget'];
  if ( ! empty( $title ) ) {
      // タイトルの値をタイトル開始/終了タグで囲んで出力
      //<h4>最近の投稿</h4>
      echo $args['before_title'] . $title . $args['after_title'];
  }

  // queryオブジェクト
  $query_args = array(
    'posts_per_page'      => $limit,
    'post_type'           => 'post',
    'ignore_sticky_posts' => 1,
  );
  $my_query = new WP_Query( $query_args );

  /* 出力するHTML */
  if( $my_query -> have_posts() ):
  ?>
  <div class="recent-posts">
    <?php while( $my_query -> have_posts() ): $my_query -> the_post(); ?>
    <article>
      <a class="flex-container" href="<?php the_permalink(); ?>">
        <figure>
          <?php
          if( has_post_thumbnail() ):
            the_post_thumbnail('large');
          else: ?>
          <img src="<?php echo esc_url( get_template_directory_uri() ); ?>/images/noimage.png" />
        <?php endif; ?>
        </figure>
        <h2><?php the_title(); ?></h2>
      </a>
    </article>
    <?php endwhile; ?>
  </div>
  <?php endif;
    wp_reset_postdata();
    echo $args['after_widget'];
}

さいごに

当初は他所様の解説でウィジェットを作ってたんですが、デフォルトウィジェットのコードの存在を知ったときに気づいてしまいました。
「これ、アイキャッチを表示するところ以外は同じなんじゃね?」と。

まぁウィジェットを作るハードルを下げるために解説を書こうとして、あれやこれやと調べてまわったのはかなり勉強になりました。
それをほぼほぼ載せたものだから、長い記事になってしまったけど。

タイムスタンプ