夏に引っ越した新居の日当たりがあまりにも悪く、軒並み弱った植物たちを先日会社に疎開させました。内覧に行ったのが夜だったのが失敗でした。2018年最大の失敗といっても過言ではありません。
こんにちは。バックエンドエンジニアのリョウタです。
やーっとWordPress5.0がリリースされ、実案件にも徐々にGutenbergを取り入れはじめています。旧来の「カスタムフィールドもりもり+AddQucickTagでエディターのタグを制御」のコンボから脱することができそうで嬉しいです。
魔改造3部作、栄えある頂点をとったのは、ひとつの投稿にフラグを100個くらい持たせるこの魔改造でした。
▼第3位、第2位の記事はこちら! 【2018年WordPress魔改造TOP3】第3位:特定のテンプレートだけ別テーマを使う 【2018年WordPress魔改造TOP3】第2位:カスタム投稿アーカイブページのメインクエリに分岐点を作り、前後で順序のルールを変える
第1位:WordPressでよくある「不動産サイト」をつくる
この話がバックエンドユニットにあがってきたとき、あたり一面に魔改造の香ばしさがたちこめました。
例にならって当然、「WordPressでやるって縛り外せない?」という話になるわけですが、私は内心「このまま俺に魔改造をさせてくれ……」と願っていました。
仕様・要件
よくある不動産検索サイトの体系です。
- 「バストイレ別」、「独立洗面台」、「駐輪場あり」、などで絞り込み検索ができる
- 価格の検索は「共益費込み」のオプションがある
- 「エリア」、「路線」のタクソノミーを持たせる
- 価格と面積での並び替えができる
記事用にかなり機能を省略していますが、実案件はもっと複雑でした。
アプローチ・実践
- 管理画面のUIを作るのは手間なので、カスタムフィールド系のプラグインを使用する(今回はACF)
- 機能をプラグイン化する
- ソートを軽くするためにフラグと価格、面積などを持ったテーブルを作成する
- GETパラメーターで検索条件が渡ってくるので、各キーを’add_rewrite_tag’でパラメーターに追加する。渡ってきたキーはデータ型でフィルタする
Class Realestates
{
//テーブル名
public $search_table = 'realestates';
//データ構造
public $data_structure = array(
'no' => 'int', //物件NO
'wanted' => 'bool', //募集中フラグ
'type' => 'int', //物件タイプ 0:アパート 1:マンション 2:一軒家
'floor_type' => 'int', //間取り 0:1R 1:1K 2:1DK 3:1LDK 4:2K 5:2DK 6:2LDK.......>>5LDK以上
'price' => 'int', //価格
'price_common' => 'int', //共益費
'floor_space' => 'int', //面積
'built_year' => 'date', //築年月日
'status' => 'int', //0:中古 1:新築 2:リフォーム済み
'c1' => 'bool', //バストイレ別
'c2' => 'bool', //独立洗面台
'c3' => 'bool', //室内洗濯機置場
'c4' => 'bool', //追い焚き
'c5' => 'bool', //駐輪場
'c6' => 'bool', //バイク置場
'c7' => 'bool', //フリーレント
/**
* その他諸々
*/
);
/**
* コンストラクタ
*/
public function __construct()
{
//検索用テーブルにプリフィックスをつける
global $table_prefix;
$this->search_table = $table_prefix . $this->search_table;
//ストラクチャ追加
add_action('init', array($this, 'add_search_structure'), 10, 0);
if (is_admin()) {
//投稿セーブ時に検索用テーブルに登録する
add_action('acf/save_post', array($this, 'save_meta_data'), 20);
} else {
//表示項目数
add_action('pre_get_posts', array($this, 'pre_get_posts'));
//テーブル結合
add_filter('posts_join', array($this, 'posts_join'));
//ソート順変更
add_filter('posts_orderby', array($this, 'posts_orderby'));
//条件指定
add_filter('posts_where', array($this, 'posts_where'));
}
}
/**
* ストラクチャ追加
*/
public
function add_search_structure()
{
foreach ($this->data_structure as $k => $v) {
add_rewrite_tag('%' . $k . '%', '([^&]+)');
}
add_rewrite_tag('%area%', '([^&]+)'); //エリアタクソノミー
add_rewrite_tag('%wayside%', '([^&]+)'); //路線タクソノミー
add_rewrite_tag('%include_common%', '([^&]+)'); //共益費込み
add_rewrite_tag('%sort%', '([^&]+)'); //並び順
foreach (array('price', 'floor_space') as $v) {
add_rewrite_tag('%max_' . $v . '%', '([^&]+)');
add_rewrite_tag('%min_' . $v . '%', '([^&]+)');
}
}
/**
* 投稿セーブ時に検索用テーブルに登録する
*/
public
function save_meta_data($id)
{
$post = get_post($id);
if (!empty($post) && $post->post_type == 'realestate') { //投稿タイプがrealestateの場合
//DB接続オブジェクト
global $wpdb;
//DB登録用データ配列
$data = array(
'post_id' => $id,
'post_type' => 'realestate',
);
//DB登録用にカスタムフィールドをフィルタリングしたものをセット
$data = array_merge($data, $this->filter_meta($post->ID));
//レコード存在チェック
$exist_res = $wpdb->get_var("SELECT COUNT(*) FROM " . $this->search_table . " WHERE post_id = " . $id);
if ($exist_res === "0") {
//レコードがなければINSERT
$res = $wpdb->insert(
$this->search_table,
$data
);
} else {
//レコードがあればUPDATE
$res = $wpdb->update(
$this->search_table,
$data,
array('post_id' => $post->ID)
);
}
if (!$res) {
//失敗時の処理
}
}
}
/**
* セーブ時のデータフィルタリング
*/
public function filter_meta($id)
{
//カスタムフィールド取得
$metas = get_fields($id, false);
$data = array();
//タイプによってフィルターする
foreach ($this->data_structure as $k => $type) {
$v = (!empty($metas[$k])) ? $metas[$k] : '';
if (empty($v)) {
switch ($type) {
case 'int':
$v = 0;
break;
case 'text':
$v = '';
break;
case 'date':
$v = '10000101';
break;
case 'double':
$v = 0;
break;
case 'serialize':
$v = '';
break;
case 'bool':
$v = false;
break;
}
} else {
switch ($type) {
case 'int':
$v = (int)$v;
break;
case 'text':
case 'date':
break;
case 'double':
$v = (float)$v;
break;
case 'serialize':
$v = serialize($v);
break;
case 'bool':
$v = true;
break;
}
}
$data[$k] = $v;
}
return $data;
}
/**
* タクソノミーセット
*/
public
function pre_get_posts($query)
{
#管理画面とメインクエリ以外はreturn
if (!$query->is_main_query() && is_admin()) return;
/**
* タクソノミー
*/
$tax_query = array();
foreach (array('wayside', 'area') as $taxonomy) {
if (!empty(get_query_var($taxonomy))) {
$ids = array();
foreach (get_query_var($taxonomy) as $k => $v) {
if (is_numeric($k) && $v === '1') $ids[] = $k;
}
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'id',
'operator' => 'IN',
'terms' => $ids,
);
}
}
if (count($tax_query) > 1) $tax_query['relation'] = 'AND';
//タクソノミーが指定されている場合は、tax_queryを追加する
if (!empty($tax_query)) $query->set('tax_query', $tax_query);
}
public
function posts_join($join)
{
global $wpdb;
//検索用テーブルをLEFT JOINする
$join .= ' LEFT JOIN ' . $this->search_table . ' ON ' . $wpdb->posts . '.ID = ' . $this->search_table . '.post_id ';
return $join;
}
/**
* オーダー順変更
*/
public
function posts_orderby($orderby)
{
if (!empty(get_query_var('sort'))) {
switch (get_query_var('sort')) {
//賃料が高い順
case 'higher':
$orderby = $this->search_table . '.price DESC';
break;
//賃料が安い順
case 'lower':
$orderby = $this->search_table . '.price ASC';
break;
//面積が広い順
case 'floor':
$orderby = $this->search_table . '.floor_area DESC';
break;
}
}
return $orderby;
}
/**
* 条件絞り込み
*/
public
function posts_where($where)
{
//booleanで判定できる項目
foreach ($this->data_structure as $c => $t) {
if ($t === 'bool' && !empty(get_query_var($c)) && get_query_var($c) === '1') {
$where .= " AND " . $this->search_table . "." . $c . " = '1'";
}
}
//数値がいずれかとマッチ
foreach (array('type', 'floor', 'room_type', 'status') as $c) {
if (!empty(get_query_var($c))) {
$in = array();
foreach (get_query_var($c) as $k => $v) {
if (is_numeric($k) && is_numeric($v) && $v === '1') $in[] = "'" . $k . "'";
}
if (!empty($in)) $where .= " AND " . $this->search_table . "." . $c . " IN (" . implode(',', $in) . ")";
}
}
//面積・坪数
foreach (array('floor_space') as $c) {
if (!empty(get_query_var('min_' . $c)) && is_numeric(get_query_var('min_' . $c)) && get_query_var('min_' . $c) > 0) {
$where .= " AND " . $this->search_table . "." . $c . " >= " . get_query_var('min_' . $c);
}
if (!empty(get_query_var('max_' . $c)) && is_numeric(get_query_var('max_' . $c)) && get_query_var('max_' . $c) > 0) {
$where .= " AND " . $this->search_table . "." . $c . " <= " . get_query_var('max_' . $c);
}
}
//価格 共益費込みにチェックがある場合は共益費込みで
$price_common = (!empty(get_query_var('include_common')) && get_query_var('include_common') === '1') ? ' + ' . $this->search_table . '.price_common' : '';
if (!empty(get_query_var('min_price')) && is_numeric(get_query_var('min_price')) && get_query_var('min_price') > 0) {
$where .= " AND (" . $this->search_table . ".price " . $price_common . ") >= " . get_query_var('min_price');
}
if (!empty(get_query_var('max_price')) && is_numeric(get_query_var('max_price')) && get_query_var('max_price') > 0) {
$where .= " AND (" . $this->search_table . ".price " . $price_common . ") <= " . get_query_var('max_price');
}
return $where;
}
}
「魔」ポイントを解説します
上記のコードは、機能のうち各不動産を登録する際の挙動と、一覧ページでのクエリの書き換え部分のみ記載しています。
- 記事登録時にACFで登録した値をget_fieldsで取得し、検索用テーブルに登録します。
その際、各カラムの構造に合わせて値をフォーマットします。 - 「共益費込み」や、面積での検索用に’min_’と’max_’をプリフィックスにつけたキーも登録しておきます。
- 一覧ページ・検索結果ページのメインクエリをカスマイズします。meta_queryを使わず、検索用テーブルをJOINさせることで処理を高速化させます。
あとがき
さて、ここまでお送りした魔改造三部作、いかがでしたか?
実践する機会は限りなく0に近いものばかりですが、WordPressカスタマイズの考え方の参考になったら嬉しいです。
2019年も楽しい魔改造が待っていることを願って……。それでは、リョウタでした。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。