WordPressの管理画面から条件分岐を設定するカスタマイズ

WordPressを使っていて「かゆい所に手が届かない」みたいなことが多々あります。その一つに「管理画面から条件分岐が使いたい」というのがあります。

管理画面で条件分岐が使えれば、「投稿」や「固定ページ」の本文、ウィジェット、カスタマイザーで設定した内容を、特定の条件下でのみ出力するといったことが可能になります。

今回は、実際に使用している、管理画面で条件分岐を設定するためのカスタマイズ方法をご紹介します。

WordPressのテンプレート側で条件分岐させるには、次のようなコードを書きます。

<?php if( is_singular( 'page' ) && !is_front_page() && is_page( array( 1, 2, 3 ) ) ): ?>
  <!-- 投稿タイプが「page (固定ページ)」で、トップページではなく、ID番号が「1」か「2」か「3」のページ -->
<?php endif; ?>

こういったコードを、「投稿」や「固定ページ」の本文に記述しても実行されず、書かれたコードは単なる文字列として、そのまま表示されるだけです。

そこで、特定の文字列を条件文として実行する仕組みが必要になります。

条件を示す文字列の記述ルールを決める

条件を示す文字列は、PHPで処理できる文字列である必要があります。そこで使用するのが、クエリ文字列 (URLパラメータ) です。

アクセスしたサイトによって、URLの途中に「?」があったりします。この「?」以降の部分をクエリ文字列と言い、「=」と「&」で区切られた、KEY1=VALUE&KEY2=VALUE&KEY3=VALUEのようなフォーマットになっています。

クエリ文字列にする最大の理由は、PHPのparse_str関数で簡単に連想配列に変換することができるためです。

条件文をクエリ文字列化するにあたって、次のような記述ルールを作りました。

  • クエリ文字列のKEYには関数名、VALUEには引数を設定する。引数がない場合は「=」を省略する。
    例: is_singular=page
  • 否定を表す場合は、キーの先頭に「!」を付ける。
    例: !is_front_page
  • 引数が配列の場合は、値を「+」で繋ぐ。
    例: is_page=1+2+3
  • 引数が複数ある場合は、引数を「,」で繋ぐ。
    例: has_term=1+2+3,category,1
  • 論理演算子の「and」や「&&」は、「&」で表す。
    例: is_singular=page&!is_front_page
  • 論理演算子の「or」や「||」は、「|」または改行 (\n) で表す。
    例: is_single|is_page または is_single\nis_page
  • 「true」「false」「null」といった真偽値やNULL値は大文字で表す。
    例: has_term=,category,NULL

この記述ルールで、先の条件文をクエリ文字列化すると、次のようになります。

is_singular=page&!is_front_page&is_page=1+2+3

これを「条件クエリ」と呼ぶことにします。

条件クエリを解析する関数を作る

次に、条件クエリをPHPで処理して「true」か「false」を返す、次のような関数を作ります。

function is_match_conditional_query( $query ) {
  if( empty( $query ) ) {
    return true;
  }

  //  全条件クエリを置換で整形
  //  整形前の例: $query = 'is_single &amp; !has_term = 1+2+3, post_tag, NULL | is_home';
  //  整形後の例: $query = 'is_single&!has_term=1+2+3,post_tag,NULL\nis_home';
  $replaces = array(
    '&amp;' => '&',
    '|'     => "\n",
    "\r\n"  => "\n",
    "\r"    => "\n",
    "\t"    => '',
    ' '     => '',
  );
  $query = str_replace( array_keys( $replaces ), array_values( $replaces ), $query );

  //  条件クエリで使用許可する条件分岐タグの関数名を列挙
  $allowed_keys = array_flip( array(
    'has_term',
    'is_front_page',
    'is_home',
    'is_page',
    'is_single',
    'is_singular',
    /* 他の条件分岐タグも必要に応じて追加 */
  ) );

  //  全条件クエリを配列化して分割
  //  分割前の例: $query = 'is_single&!has_term=1+2+3,post_tag,NULL\nis_home';
  //  分割後の例: $rows = array( 'is_single&!has_term=1+2+3,post_tag,NULL', 'is_home' );
  $rows = explode( "\n", $query );

  foreach( $rows as $row ) {
    if( empty( $row ) ) {
      continue;
    }

    //  条件クエリを連想配列化して関数名と全引数のペアに分割
    //  分割前の例: $row = 'is_single&!has_term=1+2+3,post_tag,NULL';
    //  分割後の例: $conds = array( 'is_single' => '', '!has_term' => '1 2 3,post_tag,NULL' );
    parse_str( $row, $conds );

    foreach( $conds as $cond => $params ) {

      //  論理値を設定
      //  関数名に付いた「!」を除去
      $bool = true;
      if( strpos( $cond, '!' ) === 0 ) {
        $bool = false;
        $cond = ltrim( $cond, '!' );
      }

      //  使用許可されてない条件分岐タグをスキップ
      if( !isset( $allowed_keys[ $cond ] ) ) {
        continue 2;
      }

      //  引数がある場合の処理
      if( !empty( $params ) ) {

        //  全引数を配列化して分割
        //  分割前の例: $params = '1 2 3,post_tag,NULL';
        //  分割後の例: $_params = array( '1 2 3', 'post_tag', 'NULL' );
        $_params = explode( ',', $params );

        $params = array();
        foreach( $_params as $param ) {

          //  配列化する引数の処理
          //  処理前の例: $param = '1 2 3';
          //  処理後の例: $params[] = array( '1', '2', '3' );
          if( strpos( $param, ' ' ) !== false ) {
            $params[] = explode( ' ', $param );
            continue;
          }

          //  配列化しない引数の処理
          //  処理前の例: $param = 'NULL';
          //  処理後の例: $params[] = null;
          if( $param === 'TRUE' ) {
            $params[] = true;
          } elseif( $param === 'FALSE' ) {
            $params[] = false;
          } elseif( $param === 'NULL' ) {
            $params[] = null;
          } else {
            $params[] = $param;
          }

        }#endforeach

        //  設定した論理値と可変関数がマッチしなければスキップ
        if( $bool !== (bool) $cond( ...$params ) ) {
          continue 2;
        }

      }#endif

      //  引数がない場合の処理
      //  設定した論理値と可変関数がマッチしなければスキップ
      if( $bool !== (bool) $cond() ) {
        continue 2;
      }#endif

    }#endforeach

    //  スキップが発生しなかった場合
    return true;

  }#endforeach

  //  スキップが発生した場合
  return false;
}

この関数を使うことで、文字列でしかなかった条件クエリで「true」か「false」が返るようになります。

<?php if( is_match_conditional_query( 'is_singular=page&!is_front_page&is_page=1+2+3' ) ): ?>
  <!-- 投稿タイプが「page (固定ページ)」で、トップページではなく、ID番号が「1」か「2」か「3」のページ -->
<?php endif; ?>

ウィジェットであれば、in_widget_formフックで条件クエリを入力するテキストエリアをフォームに追加し、widget_update_callbackフックで条件クエリを保存し、widget_display_callbackフックで条件クエリを解析する関数を仕込めば、細かく表示・非表示を設定できるようになります。

ウィジェットのように、条件クエリの設定とHTMLへの出力が別々であれば、これで十分ですが、「投稿」や「固定ページ」の本文、カスタマイザーといった、入力した内容がほぼそのまま出力される部分で使用する場合には、条件クエリを特定するための仕組みを用意する必要があります。

条件タグを作る

条件クエリの特定するための「条件タグ」を作ります。

これにより、かつてMicrosoftのInternet Explorerが実装していた「条件付きコメント」に似たような条件分岐が可能になります。

<!--[if IE 6]>
<style type="text/css">/* Internet Explorer 6でのみ有効になるCSSコード */</style>
<![endif]-->

HTMLのhead要素内に条件クエリを埋め込む場合は、条件付きコメントに近いフォーマットにするといいでしょう。

<!--[IF[is_singular=post&!is_single=1+2+3&has_term=4+5+6,category,NULL]]>
<meta name="test" content="投稿タイプが「post」で、投稿IDが「1」と「2」と「3」以外の、カテゴリIDが「4」か「5」か「6」に分類された投稿で出力" />
<![/IF]-->

HTMLのbody要素内に条件クエリを埋め込む場合は、data属性とhidden属性を組み合わせるといいでしょう。

<div data-if="is_singular=post&!is_single=1+2+3&has_term=4+5+6,category,NULL" hidden="hidden">
  <p>投稿タイプが「post」で、投稿IDが「1」と「2」と「3」以外の、カテゴリIDが「4」か「5」か「6」に分類された投稿で出力</p>
</div>

WordPressは、管理画面から入力されたHTMLコードを自動整形してしまうことがあります。そのため、自動整形されないフォーマットや、自動整形されたHTMLコードを再整形しやすい条件タグにする必要があります。

また、preg_match_all関数などのPHP関数で見つけやすく、置換しやすいように工夫しましょう。

条件タグによる表示・非表示の処理

WordPressでHTMLコードを出力する最終段階で、HTMLコード全体を取得し、条件タグが見つかったら、条件クエリを解析して、内容の表示・非表示の処理を行います。具体的には次のようなPHPコードになります。

add_action( 'template_redirect', function() {
  ob_start( function( $buffer ) {
    $replaces = array();

    //  '<!--[IF[条件クエリ]]>出力内容<![/IF]-->' の処理
    if( preg_match_all( '/<!--\[IF\[([^\]]*)\]\]>(.*?)<!\[\/IF\]-->/s', $buffer, $matches, PREG_SET_ORDER ) ) {
      foreach( $matches as $match ) {

        //  条件クエリを解析
        //  解析結果が「true」の場合: '<!--[IF[条件クエリ]]>出力内容<![/IF]-->' => '出力内容';
        //  解析結果が「false」の場合: '<!--[IF[条件クエリ]]>出力内容<![/IF]-->' => '';
        if( is_match_conditional_query( $match[ 1 ] ) ) {
          $replaces[ $match[ 0 ] ] = $match[ 2 ];
        } else {
          $replaces[ $match[ 0 ] ] = '';
        }

      }#endforeach
    }#endif

    //  '<div data-if="条件クエリ" hidden="hidden" 他の属性>' の処理
    if( preg_match_all( '/<div data-if="([^"]*)"(?: hidden(?:="hidden")?)?([^>]*)>/s', $buffer, $matches, PREG_SET_ORDER ) ) {
      foreach( $matches as $match ) {

        //  条件クエリを解析
        //  解析結果が「true」の場合: '<div data-if="条件クエリ" hidden="hidden" 他の属性>' => '<div 他の属性>';
        //  解析結果が「false」の場合: '<div data-if="条件クエリ" hidden="hidden" 他の属性>' => '<div hidden="hidden">';
        if( is_match_conditional_query( $match[ 1 ] ) ) {
          $replaces[ $match[ 0 ] ] = '<div'. $match[ 2 ]. '>';
        } else {
          $replaces[ $match[ 0 ] ] = '<div hidden="hidden">';
        }

      }#endforeach
    }#endif

    //  一括置換
    if( !empty( $replaces ) ) {
      $buffer = str_replace( array_keys( $replaces ), array_values( $replaces ), $buffer );
    }

    return $buffer;
  } );
}, 1000 );

今回、紹介した条件クエリや条件タグが使えると、なにかと便利です。

「かゆい所にも手が届く」WordPressのカスタマイズとして、応用してみてはいかがでしょうか?

関連記事