Laravel 保守性向上の為、キーワード検索機能をTrait(トレイト)にまとめて何度も使い回す


はじめに

検索機能をモデル内に記述すると中々ファットモデルになっちゃいます。
業務で管理画面を実装する時に同じ様な検索機能を5つくらい作ってしまった事もあり、保守性や可読性が悪くなります。

そんな時に便利なのがPHPの機能であるトレイトです!!
トレイトを使えば、どのモデルでも同じ処理を使う事が出来ます。その為、色々なモデルに共通した処理を書いている場合はその処理をトレイトに記述する事で、すっきりしたコードを書く事が出来ます!!

これから検索機能をきれいにまとめますが、実務経験半年程度ですので、何か間違ってたり他にいい方法があれば教えてください!

目次

  1. テーブル設計
  2. 検索フォーム
  3. コントローラー
  4. Traitsフォルダとファイル作成
  5. Traitsファイル
  6. Postモデル
  7. 使い回す

テーブル設計

今回はQiitaの様な投稿記事を絞り込むシチュエーションにします。
投稿したユーザー(usersテーブル)と投稿した記事(postsテーブル)の2つのテーブルがある例で解説します。

検索フォーム

・value="{{ Request::get('word') }}"
検索後に検索ワードがリセットされずに検索窓にへ表示されたままとなります。

<form action="{{ route(user.post.index') }}" method="get">
    <input name="word" type="search" value="{{ Request::get('word') }}">
    <button type="submit">検索ボタン</button>
</form>

コントローラー

Postモデルで作成したsearchローカルスコープメソッドを使用しています。
下記にsearchメソッドを作成しているので、参照ください。

// PostController.php

public function search(Request $request)
{
    // 独自に作成した検索用スコープメソッド
    $posts = Post::search()

    return view('user.post.index', ['posts' => $posts]);
}

Traitsフォルダとファイル作成

appフォルダ下にTraitsフォルダを作成します。
さらにTraitsフォルダにSearch.phpファイルを作成します。

Traitsファイル

ここでは検索した複数のワードを配列に格納して、その配列をforeachで一つ一つ検索にかける処理を記述しています。このTraitファイルを様々なモデルにて共通で使用します。

// app/Traits/Search.php

<?php

namespace App\Traits;

// Requestクラスを使用するので追記
use Request;

trait Search
{

    public function getWordInArray()
    {   // 検索のワードがある場合に実行
        if (Request::query('word')) {
                    // 検索したワードの全角スペースを半角スペースに変換する。
            $words = str_replace('  ', ' ', Request::query('word'));
                    // 検索したワードをそれぞれ配列に入れる
            return explode(" ", $words);
        }
     }

    // PostControllerで使用しているsearchスコープメソッド!!
    public function scopeSearch($query)
    {
        // 検索ワードがある場合に実行
        if (Request::query('word')) {
                    // 検索ワードが格納されている配列をforeachで一つ一つ検索にかける
            foreach ($this->getWordInArray() as $word) {
                $query->where(function ($query) use ($word) {
                                    // SearchWords関数でタイトルや投稿内容を絞り込む
                                    // SearchWordsスコープメソッドは次の項目で説明しています!!
                    $query->searchWords($word);
                });
            }
        }
        return $query;
    }
}

・Request::query('word')
これは、name="word"のinputタグに入力された検索ワードを取得できます。
・str_replace('置換する部分', '置換語の文字列', 置換する対象)
今回の場合、第一引数の全角スペースを、第二引数の半角スペースに変換します。変換対象は第三引数の検索ワードです。

・explode('区切る対象', '配列に格納する文字列')
検索したワードをそれぞれ配列に格納していきます。
例えば「テスト Laravel」で検索した場合、"テスト Laravel"という文字列がリクエストで送られてきます。
今回の場合は、explodeの第一引数が半角スペースなので、半角スペースごとに文字が区切られて配列に格納されます。
その結果、['test', 'Laravel'];という配列が完成します。

※文字列の先頭と末尾にあるホワイトスペースを削除したり連続する半角スペースの削除は不要です。

Postモデル

ここでは、独自に作成したscopeSearchWordsスコープメソッドで、検索ワードと一致するtitleとcontentのレコードを絞り込んでいます。

// Postモデル

// 下記追記
use App\Traits\Search;

class Post extends Model
{
    // 下記追記
    use Search;

    // Traitで使用されているscopeSearchWordsをこちらに記述
    public function scopeSearchWords($query, $word)
    {
        if ($this->getSearchWordInArray()) 
        {
            // タイトルと検索ワードが合うもののみ取得
            $query->where('title', 'like', "%$word%")
            // 投稿内容と検索ワードが合うもののみ取得   ※orWhereを使うこと!!
                  ->orWhere('content', 'like', "%$word%");     
            return $query;
        }
    }

・orWhere()
orWhereをつけることで、検索内容のワードが「タイトル」or「投稿内容」のどちらかにマッチしていれば、そのデータを取得できる。

例:  検索ワード「テスト」  →  タイトル:「テスト」  投稿内容「test」
タイトルと検索ワードがマッチするので、取得可能。

orWhereの部分をwhereにしてしまうと、検索内容のワードと「タイトル」and「投稿内容」の両方がマッチしているデータしか取得できない。

例: 検索ワード「テスト」  →  タイトル:「テスト」  投稿内容「test」
タイトルと検索ワードはマッチしているが、投稿内容と検索ワードがマッチしないので、取得できない。

使い回す

あとは簡単で、他にも様々なワード検索を実装したい時に下記の様に記述するだけです。
例えば登録ユーザーの一覧画面で検索したい場合、先程Traitファイル内で作成したscopeSearchメソッドを使いまわします。
これだけで検索機能を使い回すことができます!!!

// PostControllerではなく、UserControllerにもsearchスコープメソッドを使い回しています。
// UserController.php

public function search(Request $request)
{
        // 別のコントローラーでもscopeSearchメソッドを使用する。
    $users = User::search()

    return view('user.user.index', ['users' => $users]);
}



モデルに下記項目を記載し、検索する対象のカラムを任意に設定するだけです。

// Userモデル

// 下記追記
use App\Traits\Search;

class User extends Model
{
    // 下記追記
    use Search;

    public function scopeSearchWords($query, $word)
    {
        if ($this->getSearchWordInArray()) 
        {
            // 名と検索ワードが合うもののみ取得
            $query->where('first_name', 'like', "%$word%")
            // 姓と検索ワードが合うもののみ取得   ※orWhereを使うこと!!
                  ->orWhere('last_name', 'like', "%$word%");     
            return $query;
        }
    }