【Laravel】検索機能を実装する（流れ）


##【概要】
作成しているポートフォリオに検索機能を実装したので、備忘録として投稿します！
細かな説明というよりは、一連の流れで記述していきます。
今回作成する機能はプルダウン検索、ラジオボタン検索、フリーワード検索（部分一致）です。
プロジェクト一覧からの検索を想定しています！

###開発環境
PHP 7.2.34
Laravel 6.20.32
Apache 2.4.6
mysql 8.0.26

###今回作成する画面
今回作成する画面の完成図はこちらです！
![検索画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1046420/9df1b2f3-201f-4f3d-3ce6-13ee0ca384bc.png)


##①migrationファイル/seederファイルを作成
❶migrationファイル
案件テーブルを作成します。

```php:××××_××_××_××××_create_projects_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\SoftDeletes;

class CreateProjectsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('projects', function (Blueprint $table) {
            $table->increments('id');
            $table->string('project_name', 64); //案件名
            $table->integer('work_location');   //勤務地
            $table->integer('unit_price');      //単価
            $table->integer('occupation');      //職種
            $table->integer('language');        //開発言語
            $table->text('work_content');       //職務内容
            $table->SoftDeletes();
            $table->timestamp('created_at')->useCurrent()->nullable();
            $table->timestamp('updated_at')->useCurrent()->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('projects');
    }
}
```

❷seederファイル
案件のseederファイルを作成します。
※work_location、occupation、language　はconfigフォルダにプルダウン項目を連想配列で作成し引っ張ってきます。
そのためkeyとなる整数を格納しています。

```php:ProjectsTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\SoftDeletes;

class ProjectsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
       DB::table('projects')->insert([
            'project_name' => '生保向け販売支援システム開発',
            'work_location' => 13,
            'unit_price' => 700000,
            'occupation' => 2,
            'language' => 1,
            'work_content' => '生命保険会社向けのシステム開発に携わっていただきます。担当工程は要件定義、基本設計、詳細設計、開発、テスト、品質管理、リリースまでを想定しております。開発担当と設計担当に分かれて作業いただく想定です。',
            'company_id' => 1,
        ]);

        DB::table('projects')->insert([
            'project_name' => 'キャリア支援サービス開発',
            'work_location' => 28,
            'unit_price' => 850000,
            'occupation' => 6,
            'language' => 5,
            'work_content' => 'キャリア支援サービスにおける主力サービスの機能開発や共通基盤システムの機能開発推進をご担当いただきます。',
            'company_id' => 2,
        ]);

        DB::table('projects')->insert([
            'project_name' => '不動産情報サイト物件検索API改修',
            'work_location' => 40,
            'unit_price' => 650000,
            'occupation' => 1,
            'language' => 2,
            'work_content' => '不動産情報サービスリニューアルに伴う、物件検索APIの改修をご担当いただきます。社内ツール・バッチアプリケーション等の改修を行います。設計から開発やテストまで幅広く担当していただきます。',
            'company_id' => 3,
        ]);

        DB::table('projects')->insert([
            'project_name' => '製造業向け電子帳票システムサーバーサイド開発',
            'work_location' => 40,
            'unit_price' => 750000,
            'occupation' => 2,
            'language' => 4,
            'work_content' => '製造業向け電子帳票システムのサーバーサイド開発をお任せします。',
            'company_id' => 4,
        ]);

        DB::table('projects')->insert([
            'project_name' => 'オンラインレッスンプラットフォームのコンセプトデザイン',
            'work_location' => 14,
            'unit_price' => 600000,
            'occupation' => 10,
            'language' => 11,
            'work_content' => 'ニュース速報アプリの特設企画ページなどのディレクションとデザイン業務を想定した募集です。ワイヤーフレームの作成や開発を意識したデザインの作成をお任せいたします。',
            'company_id' => 5,
        ]);
    }
}
```

##②Routingを作成

```php:web.php
//検索画面を表示する
Route::get('searchInput', 'ProjectsController@searchInput')->name('searchInput');
//検索結果画面を表示する
Route::get('search', 'ProjectsController@search')->name('search');
```

##③Controllerを作成
❶検索画面を表示する（searchInputメソッド）

```php:ProjectsController.php
public function searchInput(Request $request)
    {
        //Requestで送られてきた値を代入
        $search_work_location = $request->input('work_location');
        $search_occupation = $request->input('occupation');
        $search_language = $request->input('language');
        $search_unit_price = $request->input('unit_price');
        $search_keyword = $request->input('keyword');

        //session()を使用し検索条件を一時的に保存し代入（検索結果画面から検索画面に戻った時の値の保持が目的）
        $old_work_location = $request->session()->get("old_work_location");
        $old_occupation = $request->session()->get("old_occupation");
        $old_language = $request->session()->get("old_language");
        $old_unit_price = $request->session()->get("old_unit_price");
        $old_keyword = $request->session()->get("old_keyword");

        //session()で一時的に保存した値をforget()で削除
        $request->session()->forget(
        [
          'old_work_location',
          'old_occupation',
          'old_language',
          'old_unit_price',
          'old_keyword',
        ]);

        //configから各項目をそれぞれ代入
        $languages = config('language');
        $occupations = config('occupation');
        $work_locations = config('work_location');

        //view(search_input.blade.php)に変数を渡す
        $data = [
            "search_keyword" => $search_keyword,
            "languages" => $languages,
            "occupations" => $occupations,
            "work_locations" => $work_locations,
            "old_work_location" => $old_work_location,
            "old_occupation" => $old_occupation,
            "old_language" => $old_language,
            "old_unit_price" => $old_unit_price,
            "old_keyword" => $old_keyword,
        ];

        return view('search_input', $data);
    }
```

❷検索結果を表示する（searchメソッド）

```php:ProjectsController.php
public function search(Request $request)
    {
        //Requestで送られてきた値を代入
        $search_work_location = $request->input('work_location');
        $search_occupation = $request->input('occupation');
        $search_language = $request->input('language');
        $search_unit_price = $request->input('unit_price');
        $search_keyword = $request->input('keyword');

        //クエリビルダを使用し、Projectテーブルの中身を$queryに代入
        $query = Project::query();

        //Requestで値が送られてきた場合の検索条件をそれぞれ記述

        //$search_work_locatioに値があり、かつ　０（未選択）でない場合、dbのwork_location　と　$search_work_location　が一致していれば取得
        if (!is_null($search_work_location) && $search_work_location != 0) {
            $query->where('work_location', $search_work_location)->get();
        }

        if (!is_null($search_occupation) && $search_occupation != 0) {
            $query->where('occupation', $search_occupation)->get();
        }

        if (!is_null($search_language) && $search_language != 0) {
            $query->where('language', $search_language)->get();
        }

        if (!is_null($search_unit_price)) {
            //$search_unit_price以上の値のデータをget（）で取得
            $query->where('unit_price', '>=', $search_unit_price)->get();
        }

        if (!is_null($search_keyword)) {
            //全角スペースを半角に変換する
            $space_conversion = mb_convert_kana($search_keyword, 's');

            //検索ワードをスペースで区切りそれを配列に入れる
            $searched_words = preg_split('/[\s,]+/', $space_conversion, -1, PREG_SPLIT_NO_EMPTY);

            //$searched_words（配列）をforeachでまわし、db内の各項目と一部でも一致するものがあれば$queryとして保持
            //今回は、project_name と work_content を対象にしています
            foreach($searched_words as $searched_word) {
                $query->where(function ($query) use ($searched_word) {

                    /*
                    '%'で　$searched_wordを囲むことで部分一致検索を実装できる
                    self::escapeLike（）はのちに説明する「エスケープ処理」のための記述です
                    */
                    $query->where('project_name', 'like', '%' . self::escapeLike($searched_word) . '%')
                        ->orWhere('work_content', 'like', '%' . self::escapeLike($searched_word) . '%');
                });
            }
        }

        //1ページ5件でページネーションを追加　（orderBy()を使用し、projectを昇順で表示）
        $projects = $query->orderBy('id', 'asc')->paginate(5);

        $languages = config('language');
        $occupations = config('occupation');
        $work_locations = config('work_location');

        $request->session()->put("old_work_location", $search_work_location);
        $request->session()->put("old_occupation", $search_occupation);
        $request->session()->put("old_language", $search_language);
        $request->session()->put("old_unit_price", $search_unit_price);
        $request->session()->put("old_keyword", $search_keyword);
        
        $old_work_location = $request->session()->get("old_work_location");
        $old_occupation = $request->session()->get("old_occupation");
        $old_language = $request->session()->get("old_language");
        $old_unit_price = $request->session()->get("old_unit_price");
        $old_keyword = $request->session()->get("old_keyword");
        
        //こちらはview（search.blade.php）に作る「戻るボタン」の動作のための記述です
        if ($request->get('back')){
            return redirect('/searchInput')->withInput([ 
                $old_work_location, 
                $old_occupation, 
                $old_language, 
                $old_unit_price, 
                $old_keyword,
            ]);
        }

        //view(search.blade.php)に変数を渡す
        $data = [
            "search_work_location" => $search_work_location,
            "search_occupation" => $search_occupation,
            "search_language" => $search_language,
            "search_unit_price" => $search_unit_price,
            "search_keyword" => $search_keyword,
            "projects" => $projects,
            "search_keyword" => $search_keyword,
            "languages" => $languages,
            "occupations" => $occupations,
            "work_locations" => $work_locations,
            "old_work_location" => $old_work_location,
            "old_occupation" => $old_occupation,
            "old_language" => $old_language,
            "old_unit_price" => $old_unit_price,
            "old_keyword" => $old_keyword,
        ];

        return view('projects.search', $data);
    }

    /*
    こちらはエスケープ処理と言って、フリーワード検索の中で「%」や「_」などの記号をスケープしてくれます
    こうすることで、「%」や「_」も検索ワードとして検索することができるようになります
    */
    public static function escapeLike($str)
    {
        return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $str);
    }
```

##④viewを作成
まずは検索画面から作成します！
※プルダウンはconfigフォルダから引っ張ってきて作成していますが、
検索機能に直接的な関係がないため、その方法は別記事にしました。下記ご参考ください。

https://qiita.com/miyaR/items/514cbeb728bd8a35e405

❶検索画面（search_input.blade.php）

```php:search_input.blade.php
    <div class="text-center mt-4">
        <h2>検索</h2>
    </div>

    <div class="row mt-3 mb-5">
        <div class="col-md-6 offset-md-3">
            //actionに{{ route('ルート名') }}でルートを指定
            <form method="get" action="{{ route('search') }}" class="search-form">
                
            @csrf

                <div class="form-group">
                    <div class="font-weight-bold"><label for="work_location">勤務地</label></div>
                    //foreachを使用し、$work_locationsをプルダウンで表示
                    <select class="form-control" name="work_location">                  
                        @foreach($work_locations as $key => $work_location)
                            <option value="{{ $key }}" @if( isset($old_work_location) && (int)$old_work_location === $key) selected @endif>
                                {{ $work_location }}
                            </option>
                        @endforeach
                    </select>
                </div>

                <div class="form-group">
                    <div class="font-weight-bold"><label for="occupation">職種</label></div>
                    //foreachを使用し、$occupationsをプルダウンで表示
                    <select class="form-control" name="occupation">                          
                        @foreach($occupations as $key => $occupation)
                            <option value="{{ $key }}" @if( isset($old_occupation) && (int)$old_occupation === $key) selected @endif>{{ $occupation }}</option>
                        @endforeach
                    </select>
                </div>

                <div class="form-group">
                    <div class="font-weight-bold"><label for="language">言語</label></div>
                    //foreachを使用し、$languagesをプルダウンで表示
                    <select class="form-control" name="language">                          
                        @foreach($languages as $key => $language)
                            <option value="{{ $key }}" @if( isset($old_language) && (int)$old_language === $key) selected @endif>{{ $language }}</option>
                        @endforeach
                    </select>
                </div>

                <div class="form-group row">
                    <div class="col-md-4 font-weight-bold"><label for="unit_price">月額単価</label></div>

                    //ラジオボタンで、valueに対象単価金額を設定
                    <div class="col-md-6">
                        <div class="form-check">
                            <input class="form-check-input" type="radio" id="unit_price" name="unit_price" value="300000" @if(isset($old_unit_price) && (int)$old_unit_price === 300000) checked @endif>300,000~
                        </div>
                    </div>

                    <div class="col-md-4"></div>
                    <div class="col-md-6">
                        <div class="form-check">
                            <input class="form-check-input" type="radio" id="unit_price" name="unit_price" value="400000" @if(isset($old_unit_price) && (int)$old_unit_price === 400000) checked @endif>400,000~
                        </div>
                    </div>

                    <div class="col-md-4"></div>
                    <div class="col-md-6 mt-2">
                        <div class="form-check">
                            <input class="form-check-input" type="radio" id="unit_price" name="unit_price" value="500000" @if(isset($old_unit_price) && (int)$old_unit_price === 500000) checked @endif>500,000~
                        </div>
                    </div>

                    <div class="col-md-4"></div>
                    <div class="col-md-6 mt-2">
                        <div class="form-check">
                            <input class="form-check-input" type="radio" id="unit_price" name="unit_price" value="600000" @if(isset($old_unit_price) && (int)$old_unit_price === 600000) checked @endif>600,000~
                        </div>
                    </div>

                    <div class="col-md-4"></div>
                    <div class="col-md-6 mt-2">
                        <div class="form-check">
                            <input class="form-check-input" type="radio" id="unit_price" name="unit_price" value="700000" @if(isset($old_unit_price) && (int)$old_unit_price === 700000) checked @endif>700,000~
                        </div>
                    </div>

                    <div class="col-md-4"></div>
                    <div class="col-md-6 mt-2">
                        <div class="form-check">
                            <input class="form-check-input" type="radio" id="unit_price" name="unit_price" value="800000" @if(isset($old_unit_price) && (int)$old_unit_price === 800000) checked @endif>800,000~
                        </div>
                    </div>

                </div>

                //フリーワード検索
                <div class="form-group">
                    <div class="font-weight-bold"><label for="keyword">フリーワード</label></div>
                        <input type="text" name="keyword" value="{{ $search_keyword }}@if(isset($old_keyword)){{ $old_keyword }}@endif" class="form-control" placeholder="フリーワードを入力ください">
                    </div>
                </div>

                <div class="col-md-6 offset-md-3 text-center"><input type="submit" class="btn btn-secondary mt-2" value="検索"></div>
                    
            </form>

        </div>
    </div>
```

❷検索結果画面（search.blade.php）

```php:search_input.blade.php
<div class="text-center mt-4">
        <h2>検索結果</h2>

        //検索結果画面上部に検索した内容を表示するようにしています。

        @if(isset($search_work_location) && $search_work_location != 0)
            @foreach($work_locations as $key => $work_location)
                @foreach($projects as $project)
                    @if($key == $project->work_location)
                        <span>勤務地：{{$work_location}}</span>
                        @php
                            break;
                        @endphp
                    @endif
                @endforeach
            @endforeach
        @endif

        @if(isset($search_occupation) && $search_occupation != 0)
            @foreach($occupations as $key => $occupation)
                @foreach($projects as $project)
                    @if($key == $project->occupation)    
                        <span class="ml-4">職種：{{$occupation}}</span>
                        @php
                            break;
                        @endphp
                    @endif 
                @endforeach
            @endforeach
        @endif

        @if(isset($search_language) && $search_language != 0)
            @foreach($languages as $key => $language)
                @foreach($projects as $project)
                    @if($key == $project->language)
                        <span class="ml-4">言語：{{$language}}</span>
                        @php
                            break;
                        @endphp
                    @endif
                @endforeach
            @endforeach
        @endif

        @if(isset($search_unit_price))

            <span class="ml-4">月額報酬：￥{{ number_format($search_unit_price) }}〜</span>
        @endif

                
        @if(isset($search_keyword))
            <span class="ml-4">フリーワード：{{$search_keyword}}</span>
        @endif

    </div>

    //検索した条件と一致する案件があった場合、cardで表示するようにしています。

    @foreach($projects as $project)
    
    <div class="card m-5">
        <div class="m-4 row">
            <div class="col-10">

                <h4>
                    @foreach($languages as $key => $language)
                        @if($key == $project->language)
                            【{{ $language }}】
                            @php
                                break;
                            @endphp
                        @endif
                    @endforeach
                    {{ $project->project_name }}
                </h4>
                <div class="row m-1 pt-4">
                    <div class="col-xs-12">
                        @foreach($work_locations as $key => $work_location)
                            @if($key == $project->work_location)
                                <div class="ml-4">勤務地：{{ $work_location }}</div>
                                @php
                                    break;
                                @endphp
                            @endif
                        @endforeach
                    </div>

                    <div class="col-xs-12 ml-4">月額報酬：￥{{ number_format($project->unit_price) }}〜</div>
                </div>
            </div>

            //こちらは案件の詳細に飛ぶボタンですが、今回は機能しません
            <div class="text-right d-flex align-items-center">
                <div class="col-sm-12"><a href="{{ route('show') }}" type="button" class="btn btn-outline-secondary ml-4">詳細</a></div>
            </div>
    
        </div>
    </div>

    @endforeach

    <div class="pagination justify-content-center">{{ $projects->appends(request()->query())->links() }}</div>

    //もし一致する案件がなかった場合、下記文言を表示させる
    @if($projects->isEmpty())
        <div class="text-center mt-5">
            <p class="mt-4">{{ "現在、検索条件と一致する案件はございませんでした。" }}</p>
        </div>
    @endif

    <div class="row justify-content-center mb-3">
        <a class="text-decoration-none mt-2" href="/searchInput" name="back" style="color: gray;">検索画面へ戻る</a>
    </div>
```

これで検索機能は完成です！

例)  検索条件として、言語にPHP、月額報酬に500,000~、フリーワードに不動産と入力し検索ボタンを押下


![kennsakugamen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1046420/0f7cfe34-9c25-c345-98eb-21bc88fbe970.png)

検索結果：

![検索結果表示画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1046420/de6f4a46-6fe9-6ecb-458d-be4851c4cd77.png)


#終わりに
この記事では、機能の実装に関して細かな説明というよりは一連の流れを残しました。
今回ポートフォリオに検索機能を実装するにあたり、沢山の方の記事を参考にさせていただきました。
拙い記事ですが、この記事も検索機能を実装する方に少しでも役に立てばありがたいです。