Web事業部実績紹介_Webサービス
Web事業部実績紹介_Webサービス
2018.12.27

【2018年WordPress魔改造TOP3】第1位:よくある「不動産サイト」をつくる

リョウタ

夏に引っ越した新居の日当たりがあまりにも悪く、軒並み弱った植物たちを先日会社に疎開させました。内覧に行ったのが夜だったのが失敗でした。2018年最大の失敗といっても過言ではありません。

こんにちは。バックエンドエンジニアのリョウタです。

やーっとWordPress5.0がリリースされ、実案件にも徐々にGutenbergを取り入れはじめています。旧来の「カスタムフィールドもりもり+AddQucickTagでエディターのタグを制御」のコンボから脱することができそうで嬉しいです。

魔改造3部作、栄えある頂点をとったのは、ひとつの投稿にフラグを100個くらい持たせるこの魔改造でした。

▼第3位、第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;
    }
}

「魔」ポイントを解説します

上記のコードは、機能のうち各不動産を登録する際の挙動と、一覧ページでのクエリの書き換え部分のみ記載しています。

  1. 記事登録時にACFで登録した値をget_fieldsで取得し、検索用テーブルに登録します。
    その際、各カラムの構造に合わせて値をフォーマットします。
  2. 「共益費込み」や、面積での検索用に’min_’と’max_’をプリフィックスにつけたキーも登録しておきます。
  3. 一覧ページ・検索結果ページのメインクエリをカスマイズします。meta_queryを使わず、検索用テーブルをJOINさせることで処理を高速化させます。

あとがき

さて、ここまでお送りした魔改造三部作、いかがでしたか?

実践する機会は限りなく0に近いものばかりですが、WordPressカスタマイズの考え方の参考になったら嬉しいです。

2019年も楽しい魔改造が待っていることを願って……。それでは、リョウタでした。

LIGにWordPressの魔改造を依頼する!