Smart Custom Fields の設定に準じて複合検索を行う検索フォームのカスタマイズ (子テーマ)

Smart Custom Fields を使用した複合検索カスタマイズのサンプルを子テーマとして作ってみたのでメモ。

前提

今回は次のような前提で考えました。

  • 使用するフィールド:
    • テキスト、テキストエリア、ラジオ、選択、チェックボックスの5つ
    • それ以外は使用しない
  • 検索フォーム:
    • カスタムフィールドの項目によってフォームの個数や項目は自動的に増減
  • キーワード検索:
    • 標準の機能(記事タイトル・本文)は使用しない
    • 代わりにカスタムフィールドの「テキスト」または「テキストエリア」の文字列に対して行う
      • カスタムフィールドで項目が複数ある場合はOR検索
      • スペース区切りによるORやAND検索はしない
  • 項目検索:
    • キーワードのみ複数のフィールドをまたがってのOR検索だが、それ以外はAND検索
    • チェックボックスの項目に対しても検索時に選択できるのは1つの項目のみとする

テーマ

テーマは Twenty Twenty-One の子テーマとして作成。

 twentytwentyone-child/
                ├ sfc_init/
                │  └ sfc-init.php
                │
                ├ footer.php
                ├ search.php
                ├ searchform.php
                └ style.css

今回はサンプルなので最低限のファイルのみで構成しました。

style.css

/*
Theme Name: Twenty Twenty-One Child
Version: 1.5
Template: twentytwentyone
*/

style.css はテーマ認識のために用意。特に何かをするわけではありません。

sfc_init/sfc-init.php

<?php

return XXX;

XXX は Smart Custom Fields の設定が保存された投稿の投稿IDです。今回はサンプルなので安直にハードコーディングで済ませました。

footer.php

    <?php get_template_part( 'template-parts/footer/footer-widgets' ); ?>

    <?php get_template_part( 'searchform' ); ?> <!-- 追記 -->

    <footer id="colophon" class="site-footer">

こちらも安直に、 footer.php の中に検索フォームを読み込むように1行追記。

searchform.php

<?php
/**
 * The searchform.php template.
 *
 * Used any time that get_search_form() is called.
 *
 * @link https://developer.wordpress.org/reference/functions/wp_unique_id/
 * @link https://developer.wordpress.org/reference/functions/get_search_form/
 *
 * @package WordPress
 * @subpackage Twenty_Twenty_One
 * @since Twenty Twenty-One 1.0
 */

/*
 * Generate a unique ID for each form and a string containing an aria-label
 * if one was passed to get_search_form() in the args array.
 */
$twentytwentyone_unique_id = wp_unique_id( 'search-form-' );

$twentytwentyone_aria_label = ! empty( $args['aria_label'] ) ? 'aria-label="' . esc_attr( $args['aria_label'] ) . '"' : '';

$sfc_settings_id = require( __DIR__ . '/sfc_init/sfc-init.php' );

$scf_ettings = get_post_meta($sfc_settings_id, 'smart-cf-setting'); // 投稿IDは決め打ち
$scf_ettings_array = maybe_unserialize($scf_ettings)[0];
?>

<form role="search" <?php echo $twentytwentyone_aria_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. ?> method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
    <div style="display:block;width:100%;">
        <label for="<?php echo esc_attr( $twentytwentyone_unique_id ); ?>"><?php _e( 'Search&hellip;', 'twentytwentyone' ); // phpcs:ignore: WordPress.Security.EscapeOutput.UnsafePrintingFunction -- core trusts translations ?></label>
        <input type="hidden" id="<?php echo esc_attr( $twentytwentyone_unique_id ); ?>" class="search-field" value="<?php echo get_search_query(); ?>" name="s" />
    </div>
<?php
    foreach ($scf_ettings_array as $key => $val) {
?>
    <?php
        if ( $val['fields'][0]['type'] === 'select' || $val['fields'][0]['type'] === 'check' ) {
            $str = str_replace(
                [
                    "\r\n",
                    "\r",
                    "\n",
                ],
                "\n",
                $val['fields'][0]['choices']
            );
            $val_array = explode("\n", $str)
    ?>
    <div style="display:block;width:100%;">
        <label for="sfc-search-<?= esc_attr($val['fields'][0]['name']); ?>"><?= esc_attr($val['fields'][0]['label']); ?></label>
        <select name="sfc-search-<?= esc_attr($val['fields'][0]['name']); ?>" id="sfc-search-<?= esc_attr($val['fields'][0]['name']); ?>">
        <?php foreach ($val_array as $va_key => $va_val) { ?>
            <option
                value="<?= esc_attr($va_val); ?>"
                <?= $va_key === 0 ? ' selected' : ''; ?>
            >
                <?= esc_attr($va_val); ?>
            </option>
        <?php } ?>
        </select>
    </div>
    <?php
        }
        else if ( $val['fields'][0]['type'] === 'radio' ) {
            $str = str_replace(
                [
                    "\r\n",
                    "\r",
                    "\n",
                ],
                "\n",
                $val['fields'][0]['choices']
            );
            $val_array = explode("\n", $str)
    ?>
    <div style="display:block;width:100%;">
        <label for="sfc-search-<?= esc_attr($val['fields'][0]['name']); ?>_0"><?= esc_attr($val['fields'][0]['label']); ?></label>
        <?php foreach ($val_array as $va_key => $va_val) { ?>
            <label for="sfc-search-<?= esc_attr($val['fields'][0]['name']); ?>_<?= esc_attr($va_key); ?>">
                <input
                    type="radio"
                    name="sfc-search-<?= esc_attr($val['fields'][0]['name']); ?>"
                    id="sfc-search-<?= esc_attr($val['fields'][0]['name']); ?>_<?= esc_attr($va_key); ?>"
                    value="<?= esc_attr($va_val); ?>"
                    <?= $va_key === 0 ? ' checked' : ''; ?>
                ><?= esc_attr($va_val); ?>
            </label>
        <?php } ?>
    </div>
    <?php
        }
    }
    ?>
                    <div style="display:block;width:100%;">
                        <label for="sfc-search-freewords">フリーワード</label>
                        <input
                            type="search"
                            name="sfc-search-freewords"
                            id="sfc-search-freewords"
                        >
                    </div>
    <?php
    // query reset
    wp_reset_postdata();
?>
    <div style="display:block;width:100%;">
        <input type="submit" class="search-submit" value="<?php echo esc_attr_x( 'Search', 'submit button', 'twentytwentyone' ); ?>" />
    </div>
</form>

本題の検索フォーム本体。

  • 最初に $sfc_settings_id = require( __DIR__ . '/sfc_init/sfc-init.php' ); で Smart Custom Fields の設定の投稿IDを取得
    • 続けて $scf_ettings = get_post_meta($sfc_settings_id, 'smart-cf-setting'); で設定を投稿から読み込み
    • maybe_unserialize() でシリアライズされたレコードのデータから設定を配列にパース
  • パースされた設定の配列を foreach でループ
    • 選択、チェックボックスならば改行(複数考えられる改行コードを\nで統一しつつ)ごとに項目を分解してセレクトボックスとして展開。1つ目の項目は selected を付けておく
    • ラジオボタンの場合もほぼ同様の処理。1つ目の項目は checked を付けておく
  • ループ後に決め打ちでキーワード検索のテキストボックスを設置
    • 複数のフィールドにまたがる想定なので、ループで該当フィールドごとに生成するとテキストボックスが増産されてしまうため

search.php

<?php
/**
 * The template for displaying search results pages
 *
 * @link https://developer.wordpress.org/themes/basics/template-hierarchy/#search-result
 *
 * @package WordPress
 * @subpackage Twenty_Twenty_One
 * @since Twenty Twenty-One 1.0
 */

function search_param_get() {
    //GETパラメータの取得とエスケープ(ない場合はNULL)
    if(isset($_GET)) {
        $s_param = $_GET;
        array_walk_recursive($s_param, function(&$item, $key){ $item = esc_html($item); });
    }
    else {
        $s_param = [];
    }
    return $s_param;
}
function search_procedure() {
    $sfc_settings_id = require( __DIR__ . '/sfc_init/sfc-init.php' );
    $scf_ettings = get_post_meta($sfc_settings_id, 'smart-cf-setting'); // 投稿IDは決め打ち
    $scf_ettings_array = maybe_unserialize($scf_ettings)[0];

    $s_param = search_param_get();

    if($s_param !== NULL) {
        //クエリ用のパラメータを作成
        $args = [];
        $meta_query = [];
        $args_freewords = [];
        $meta_query_freewords = [];

        foreach ($scf_ettings_array as $key => $val) {
            if(array_key_exists('sfc-search-' . esc_attr($val['fields'][0]['name']), $s_param)) {
                $compare_val = '=';
                $value_val = $s_param['sfc-search-' . esc_attr($val['fields'][0]['name'])];
                if(
                    esc_attr($val['fields'][0]['type'] === 'select')
                     || esc_attr($val['fields'][0]['type'] === 'check' )
                     || esc_attr($val['fields'][0]['type'] === 'radio' )
                ) {
                    if(mb_strlen(
                        $s_param['sfc-search-' . esc_attr($val['fields'][0]['name'])],
                        'UTF-8'
                    ) > 0
                    && $s_param['sfc-search-' . esc_attr($val['fields'][0]['name'])] !== '未選択') {
                        $meta_query[] = [
                            [
                                'key' => esc_attr($val['fields'][0]['name']),
                                'value' => $value_val,
                                'type' => 'CHAR',
                                'compare' => $compare_val,
                            ],
                        ];
                    }
                }
            }
            else if(
                esc_attr($val['fields'][0]['type'] === 'text')
                || esc_attr($val['fields'][0]['type'] === 'textarea' )
            ) {
                if(
                    array_key_exists('sfc-search-freewords', $s_param)
                    && mb_strlen(
                        $s_param['sfc-search-freewords'],
                        'UTF-8'
                    ) > 0
                ) {
                    $compare_val = 'LIKE';
                    $value_val = $s_param['sfc-search-freewords'];
                    $meta_query_freewords[] = [
                        [
                            'key' => esc_attr($val['fields'][0]['name']),
                            'value' => $value_val,
                            'type' => 'CHAR',
                            'compare' => $compare_val,
                        ],
                    ];
                }
            }
        }
        if(count($meta_query) > 0 || count($meta_query_freewords) > 0) {
            $args = [
                'meta_query' => [
                    'relation' => 'AND',
                ],
            ];
            $args['meta_query'] = $args['meta_query'] + $meta_query;
        }
        if(count($meta_query_freewords) > 0) {
            $args_freewords = [
                'meta_query' => [
                    'relation' => 'OR',
                ],
            ];
            $args_freewords['meta_query'] = $args_freewords['meta_query'] + $meta_query_freewords;
            $args['meta_query'] = $args['meta_query'] + $args_freewords;
        }
        $args = $args + [
            'post_type' => 'post',
            'post_status' => 'publish',
            'order' => 'DESC',
            'orderby' => 'date',
        ];
    }
    else {
        $args = [];
    }
    //定義したargsでクエリ発行
    $the_query = new WP_Query($args);
    return $the_query;
}
$the_query = search_procedure();

$s_param = search_param_get();

get_header();

if ( $the_query->have_posts() ) {
    ?>
    <header class="page-header alignwide">
        <h1 class="page-title">
            <?php
            printf(
                /* translators: %s: Search term. */
                esc_html__( 'Results for "%s"', 'twentytwentyone' ),
                '<span class="page-description search-term">' . isset($s_param['sfc-search-freewords']) && !empty($s_param['sfc-search-freewords']) ? esc_html( $s_param['sfc-search-freewords'] ) : '' . '</span>'
            );
            ?>
        </h1>
    </header><!-- .page-header -->

    <div class="search-result-count default-max-width">
        <?php
        printf(
            esc_html(
                /* translators: %d: The number of search results. */
                _n(
                    'We found %d result for your search.',
                    'We found %d results for your search.',
                    (int) $the_query->found_posts,
                    'twentytwentyone'
                )
            ),
            (int) $the_query->found_posts
        );
        ?>
    </div><!-- .search-result-count -->
    <?php
    // Start the Loop.
    while ( $the_query->have_posts() ) {
        $the_query->the_post();

        /*
         * Include the Post-Format-specific template for the content.
         * If you want to override this in a child theme, then include a file
         * called content-___.php (where ___ is the Post Format name) and that will be used instead.
         */
        get_template_part( 'template-parts/content/content-excerpt', get_post_format() );
    } // End the loop.

    // Previous/next page navigation.
    twenty_twenty_one_the_posts_navigation();

    // If no content, include the "No posts found" template.
} else {
    get_template_part( 'template-parts/content/content-none' );
}
wp_reset_postdata();

get_footer();

こちらは検索結果ページ。

  • search_param_get(): GETパラメータを分解して検索項目を配列にする
  • search_procedure(): 検索処理
    • $sfc_settings_id = require( __DIR__ . '/sfc_init/sfc-init.php' ); で Smart Custom Fields の設定の投稿IDを取得して以下同
    • search_param_get()で検索パラメータをパース
    • foreach で検索パラメータをループ
      • meta_queryAND 条件を指定
      • 選択、チェック、ラジオの場合は該当するフィールド名の値が文字列一致しているか判定。検索フォームで「未選択」の文字列が送られてきた場合は条件判定に加えずスルーする
      • テキスト、テキストエリアの場合は文字列を LIKE 検索。 meta_query は他の項目よりも1段階深い入れ子になっており、ここでは OR 条件を指定している

大体このようなフローにしました。

検証

データの準備

Smart Custom Fields の設定
Smart Custom Fields の設定

例えばこのように Smart Custom Fields を設定します。内容は適当に、「この技術を試してみたい」というトピックを扱うものとします。

Smart Custom Fields を設定した投稿データ
Smart Custom Fields を設定した投稿データ

上述の Smart Custom Fields の項目に従って投稿を追加します。

検索フォーム

これで作成した子テーマに切り替えると……

Smart Custom Fields から生成された検索フォーム
Smart Custom Fields から生成された検索フォーム

上述の Smart Custom Fields の設定に従って、検索フォームが生成されました。

検索

いくつかのパターンで検索してみます。

検索内容1
検索内容1
検索結果1
検索結果1

きちんとヒットしました。複数の条件で絞り込まれています。

検索内容2
検索内容2
検索結果2
検索結果2

別パターン。こちらもヒットしました。

検索内容3
検索内容3
検索結果3
検索結果3

フリーワード検索。こちらもヒット。

軽く試験した感じではきちんと動作していることが確認できました。

参考

Smart Custom Fields

表示

カスタマイズ

検索フォーム

WP_Query

meta_query の複合条件検索

ANDとORを組み合わせて使うことも可。

WP_Query 内でカスタムフィールドの値を取得

WordPress で SQL を表示

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。