【Laravel7】ユーザーをキーワード検索する

#はじめに
Laravel7の認証機能で登録したユーザーについて、キーワード検索できるページを作成します。

#環境
XAMPP環境でLaravelが使えるように設定してあります。

- Windows10 Pro 64bit
- PHP 7.3.18
- Laravel 7.12.0
- MariaDB 10.1.32

※認証機能でユーザーを登録したり、管理者を追加したりの一連の流れは過去記事をご覧ください。
- [【Laravel7でユーザー認証_1】基本のき](https://qiita.com/crosawassant/items/bf3982666b6333bcb8c8)
- [【Laravel7でユーザー認証_2】ユーザー認証を日本語化](https://qiita.com/crosawassant/items/d1f4c5c93f30afa9eb65)
- [【Laravel7でユーザー認証_3】ユーザー認証をメールアドレスからユーザー名に変更する](https://qiita.com/crosawassant/items/2464db8a0fe1c603401a)
- [【Laravel7でユーザー認証_4】パスワード変更フォームを作成する](https://qiita.com/crosawassant/items/04242c8d7a12346dcc20)
- [【Laravel7でユーザー認証_5】ユーザーを倫理削除（SoftDelete）する](https://qiita.com/crosawassant/items/a6e3e9b1aedf566b2d27)
- [【Laravel7でユーザー認証_6】ユーザーの情報を表示・変更する設定画面を作成する](https://qiita.com/crosawassant/items/d8b434f0bc98455165b4)
- [【Laravel7でユーザー認証_7】会員登録時にメール認証を行う](https://qiita.com/crosawassant/items/eed947b1e516d864fc6c)
- [【Laravel7でユーザー認証_8】メールアドレス変更時にメール認証を行う](https://qiita.com/crosawassant/items/018b29ab770c0a373bc9)
- [【Laravel7でユーザー認証_9】マルチ認証機能を使って管理者を作成する](https://qiita.com/drafts/549e7ea9615fcd9ad68e/edit)
- [【Laravel7でユーザー認証_10】マルチ認証機能で作成した管理者のパスワード変更・リセット機能を追加する](https://qiita.com/crosawassant/items/f43c4e6b4a538cf6ab29)

#要件

- 管理者のhomeに検索フォームを設置する。
- デフォルトの状態（アクセス直後）は全てのユーザーを表示する。
- 検索フォームに何も入っていないで検索した場合、全てのユーザーを表示する。
- キーワードは名前またはメールアドレスの部分一致で検索する。
- キーワードを半角または全角のスペースで区切った場合、AND検索とする。
- 検索結果は10件毎にページネーションする。

![Account_Manager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262236/8c833cb6-99b8-7542-0431-2440a2595b54.png)

#実装

##コントローラ

```php:app/Http/Controllers/Admin/HomeController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\User; 
use Illuminate\Http\Request;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:admin');
    }

    public function redirectIndex()
    {
        return redirect(route('admin.home'));
    }
    
    public function index(Request $request)
    {
        $q = $request->input('q'); //フォームの入力値を取得

        //検索キーワードが空の場合
        if (empty($q)) {
            $users = User::paginate(10);  //全ユーザーを10件/ページで表示

        //検索キーワードが入っている場合
        } else {
            $_q = str_replace('　', ' ', $q);  //全角スペースを半角に変換
            $_q = preg_replace('/\s(?=\s)/', '', $_q); //連続する半角スペースは削除
            $_q = trim($_q); //文字列の先頭と末尾にあるホワイトスペースを削除
            $_q = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $_q); //円マーク、パーセント、アンダーバーはエスケープ処理
            $keywords = array_unique(explode(' ', $_q)); //キーワードを半角スペースで配列に変換し、重複する値を削除

            $query = User::query();
            foreach($keywords as $keyword) {
                //1つのキーワードに対し、名前かメールアドレスのいずれかが一致しているユーザを抽出
                //キーワードが複数ある場合はAND検索
                $query->where(function($_query) use($keyword){
                    $_query->where('name', 'LIKE', '%'.$keyword.'%')
                           ->orwhere('email', 'LIKE', '%'.$keyword.'%');
                });
            }
            $users = $query->paginate(10); //検索結果のユーザーを10件/ページで表示
        }
        return view('admin.home', compact('users', 'q'));
    }
}
```

検索文字列に「yamada taro」と入力した場合、名前かメールアドレスに「yamada」という文字列が含まれているユーザーを抽出し、さらにその中で、名前かメールアドレスに「taro」という文字列が含まれているユーザーを抽出するという仕様になります。
この、(A or B) and (C or D) の条件での検索は、useを使ってクロージャへ変数を渡してやることで実現します。（参考：[Eloquent/QueryBuilderによるAnd検索とOr検索の複合](https://umebius.com/laravel/querybuilder%E3%81%AB%E3%82%88%E3%82%8Band%E6%A4%9C%E7%B4%A2%E3%81%A8or%E6%A4%9C%E7%B4%A2%E3%81%AE%E8%A4%87%E5%90%88/)）


###view

```php:resources/views/admin/home.blade.php
@extends('layouts.admin.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <form method="GET" action="{{ route('admin.home') }}">
              <div class="form-row">
                <div class="col">
                  <input id="q" type="text" class="form-control" name="q" value="{{ $q }}" autocomplete="q" autofocus placeholder="{{ __('Name or Email') }}">
                </div>
                <div class="col-auto">
                  <button type="submit" class="btn btn-primary">
                      {{ __('Search') }}
                  </button>
                </div>
              </div>
            </form>
        </div>

        <div class="col-md-12">
            @if (session('status'))
                <div class="alert alert-success" role="alert">
                    {{ session('status') }}
                </div>
            @endif

            @if(!empty($users))
            {{ __('SearchResult') }} : {{ __('SearchCount', ['num' => $users->total()]) }}
            <div class="card" style="margin-bottom: 20px;">
                <div class="card-header">{{ __('UserList') }}</div>

                <div class="card-body">

                    <table class="table table-hover">
                      <thead>
                        <tr>
                          <th>{{ __('ID') }}</th>
                          <th>{{ __('Name') }}</th>
                          <th>{{ __('E-Mail Address') }}</th>
                          <th>{{ __('CreateDate') }}</th>
                          <th>{{ __('UpdateDate') }}</th>
                        </tr>
                      </thead>
                      <tbody>
                      @foreach($users as $user)
                      <tr>
                        <td>{{ $user->id }}</td>
                        <td>{{ $user->name }}</td>
                        <td>{{ $user->email }}</td>
                        <td>{{ $user->created_at->format('Y/m/d') }}</td>
                        <td>{{ $user->updated_at->format('Y/m/d') }}</td>
                      </tr>
                      @endforeach
                      </tbody>
                    </table>
                </div>
            </div>

            <div class="paginate">
                {{ $users->appends(Request::only('q'))->links() }}
            </div>

            @endif

        </div>
    </div>
</div>
@endsection

```

`$users` に渡された値を、`foreach` で展開します。
`$users->total()` で結果の合計件数が表示できます。
ページネーションで2ページ目以降のページにもキーワードを引き継ぎさせたいので、 ページネーション部分は `appends` メソッドでクエリ文字列（q）を指定し `$users->appends(Request::only('q'))->links()` とします。

#参考サイト
- [【Laravel】ページネーション付き検索フォームを作成する方法](https://manablog.org/laravel-search-with-pagination/)
- [Eloquent/QueryBuilderによるAnd検索とOr検索の複合 - Laravel5開発](https://umebius.com/laravel/querybuilder%E3%81%AB%E3%82%88%E3%82%8Band%E6%A4%9C%E7%B4%A2%E3%81%A8or%E6%A4%9C%E7%B4%A2%E3%81%AE%E8%A4%87%E5%90%88/)
- [php - Laravel: Escape &quot;LIKE&quot; clause? - Stack Overflow](https://stackoverflow.com/questions/22749182/laravel-escape-like-clause)
- [Laravelでキーワード検索の配列を作成するミドルウェア - Qiita](https://qiita.com/logue/items/64c4b5545f76ef17de70)