【Laravel】キーワード検索機能 + 都道府県プルダウン

# 0. はじめに
大阪のLaravel初学者サウナーこと、kazumakishimoto([@kazuma_dev](https://twitter.com/kazuma_dev))です！
Laravelでクエリビルダ/ローカルスコープ/ページネーションを使用した、都道府県プルダウン&キーワード検索機能の解説です！

## 0-1. 全体の流れ
> [1.都道府県プルダウン&キーワード検索機能](#1-都道府県プルダウン&キーワード検索機能)
> [2.補足](#2-補足)
> [Reference](#reference)

## 0-2. 本記事の対象者
- Laravelのキーワード検索機能
- 都道府県プルダウン機能

## 0-3. 事前準備
### configフォルダ
```php:config/pref.php
<?php

return [
    '0' => '未選択',
    '1' => '北海道',
    '2' => '青森県',
    '3' => '岩手県',
    '4' => '宮城県',
    '5' => '秋田県',
    '6' => '山形県',
    '7' => '福島県',
    '8' => '茨城県',
    '9' => '栃木県',
    '10' => '群馬県',
    '11' => '埼玉県',
    '12' => '千葉県',
    '13' => '東京都',
    '14' => '神奈川県',
    '15' => '新潟県',
    '16' => '富山県',
    '17' => '石川県',
    '18' => '福井県',
    '19' => '山梨県',
    '20' => '長野県',
    '21' => '岐阜県',
    '22' => '静岡県',
    '23' => '愛知県',
    '24' => '三重県',
    '25' => '滋賀県',
    '26' => '京都府',
    '27' => '大阪府',
    '28' => '兵庫県',
    '29' => '奈良県',
    '30' => '和歌山県',
    '31' => '鳥取県',
    '32' => '島根県',
    '33' => '岡山県',
    '34' => '広島県',
    '35' => '山口県',
    '36' => '徳島県',
    '37' => '香川県',
    '38' => '愛媛県',
    '39' => '高知県',
    '40' => '福岡県',
    '41' => '佐賀県',
    '42' => '長崎県',
    '43' => '熊本県',
    '44' => '大分県',
    '45' => '宮崎県',
    '46' => '鹿児島県',
    '47' => '沖縄県'
];
```

## 0-4. 使用画像のイメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/a0634381-2ce7-a80e-2825-01d3cf4a9e5f.png)


# 1. 都道府県プルダウン&キーワード検索機能

## 1-1. Route
```php:routes/web.php
# 検索機能
Route::get('/search', 'ArticleController@search')->name('articles.search');
```

## 1-2. Migration
```
$ php artisan make:model Article --migration
```
```php:database/migrations/XXXXXXXXXXX_create_articles_table.php
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('user_id')->unsigned();
            $table->bigInteger('pref_id')->nullable();
            $table->text('title');
            $table->text('body');
            $table->string('image')->nullable();
            $table->timestamps();
            $table->softDeletes();

            $table->foreign('user_id')
            ->references('id')
            ->on('users')
            ->onDelete('cascade');

        });
    }
```
```
$ php artisan migrate
```

## 1-3. Model
- クエリビルダの`where`メソッド('カラム名','比較演算子',"部分一致の値")

```php:app/Models/Article.php
    protected $fillable = [
        'pref_id', 'title', 'body', 'image',
    ];

    public function getPrefNameAttribute()
    {
        return config('pref.' . $this->pref_id);
    }

    public function scopeSearchFilter($query, string $search = null)
    {
        if (!$search) {
            return $query;
        }

        return $query->where('title', 'LIKE', "%{$search}%")
        ->orWhere('body', 'LIKE', "%{$search}%");
    }

    public function scopePrefFilter($query, string $pref = null)
    {
        if (!$pref) {
            return $query;
        }

        return $query->where('pref_id', $pref);
    }
```

## 1-4. Controller
```
$ php artisan make:controller ArticleController
```

- `get()`を`paginate()`に変更

```php:php:app/Http/Controllers/ArticleController.php
<?php

namespace App\Http\Controllers;

use App\Models\Article;
use App\Http\Requests\ArticleRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

//略

    // 投稿一覧
    public function index()
    {
        $articles = Article::query()
            ->with(['user', 'likes', 'tags', 'comments'])
            ->orderBy('created_at', 'desc')
            ->paginate(15);

        $data = [
            'articles' => $articles,
        ];

        return view('articles.index', $data);
    }

    // 投稿画面
    public function create()
    {
        $prefs = config('pref');

        $user = Auth::user();

        $data = [
            'user' => $user
        ];

        return view('articles.create', $data)->with(['prefs' => $prefs]);
    }

    // 投稿処理
    public function store(ArticleRequest $request, Article $article)
    {
        $article->user_id = $request->user()->id;
        $all_request = $request->all();
        $article->pref_id = $request->pref;
        $article->fill($all_request)->save();

        return redirect()->route('articles.index');
    }

    // 検索機能
    public function search(Request $request)
    {
        $articles = Article::searchFilter($request->search)
            ->prefFilter($request->pref)
            ->orderBy('created_at', 'desc')
            ->with(['user', 'likes', 'tags'])
            ->paginate(10);

        $data = [
            'articles' => $articles,
        ];

        return view('articles.index', $data);
    }
```

## 1-5. View
```php:resources/views/articles/card.blade.php
@if($article->pref_id !== 0)
<div class="ml-2 d-flex align-items-center">
    <a href="{{ route('articles.search', ['pref' => $article->pref_id]) }}" class="text-muted"><i class="fas fa-map-marker-alt p-1"></i>{{ $article->prefName }}</a>
</div>
@endif
```
```php:resources/views/articles/create.blade.php
<form method="POST" action="{{ route('articles.store', ['article' => $article]) }}" enctype="multipart/form-data">
    @method('PATCH')
    <div class="form-group">
        <label for="pref">所在地</label>
        <select class="form-control" id="pref" name="pref">
            @foreach($prefs as $key => $score)
            <option value="{{ $key }}">{{ $score }}</option>
            @endforeach
        </select>
    </div>
    <button type="submit" class="btn blue-gradient btn-block">更新する</button>
</form>
```
```php:resources/views/nav.blade.php
<form method="GET" action="{{ route('articles.search') }}" class="d-flex">
    <select class="form-control" id="pref" name="pref">
        @foreach(config('pref'); as $key => $score)
        <option value="{{ $key }}">{{ $score }}</option>
        @endforeach
    </select>
    <input class="form-control" name="search" type="text" placeholder="検索..." aria-label="Search">
    <button class="input-group-text border-0" type="submit"><i class="fas fa-search"></i></button>
</form>
```
```php:resources/views/articles/index.blade.php
<div class="paginate">
    {{ $articles->appends(request()->input())->links() }}
</div>
```


# 2. 補足
## 2-1. 開発環境（FW/ツールのバージョンなど）
|ツール|バージョン|
|:--|:--|
|Vue.js|2.6.14|
|jQuery|3.4.1|
|PHP|7.4.1|
|Laravel|6.20.43|
|MySQL|5.7.36|
|Nginx|1.18.0|
|Composer|2.0.14|
|npm|6.14.6|
|Git|2.33.1|
|Docker|20.10.11|
|docker-compose|v2.2.1|
|PHPUnit|8.0|
|CircleCI|2.1|
|heroku|7.59.4|
|MacBook Air|M1,2020|
|macOS|Monterey 12.3|
|Homebrew|3.3.8|

## 2-2. ディレクトリ構造
```
【ルートディレクトリ】
├─ .circleci
│   └─ config.yml
├─ aws / CloudFormation
│   └─ ec2.yml
├─ docker
│   └─ mysql
│   └─ nginx
│   └─ php
│   └─ phpmyadmin
├─ src
│   └─ 【Laravelのパッケージ】
│─ .env
│─ .gitignore
└─ docker-compose.yml
```

# Reference

- PHP・Laravelで検索機能を実装（備忘録） - Qiita

https://qiita.com/Yuhei_K/items/48878554709c252dad6a

- Laravel キーワード検索機能 - Qiita

https://qiita.com/S_R/items/dc4ef17209583d9ed2bb

- Laravelで検索機能とページネーションを同時につけたいとき - Qiita

https://qiita.com/n_oshiumi/items/4893abeff7e0b85ee2a2

- Laravelのページネーションが便利すぎた｜Laravel｜PHP｜開発ブログ｜株式会社Nextat（ネクスタット）

https://nextat.co.jp/staff/archives/37

- 【Laravel】all()の後にpaginateが使えない？原因と解決方法 | CODE CLUB965

https://codeclub965.com/?p=2179

- Laravelで都道府県やカテゴリなどのプルダウンと文字入力から検索機能を実装する - Qiita

https://qiita.com/masakichi_eng/items/b9948f7d14de793dc014

- Laravelチュートリアル - 汎用業務Webアプリを作る (3/4) 検索画面とページネーション - Qiita

https://qiita.com/shonansurvivors/items/d0d2e6be5103244e8eb2

- 【laravel】configフォルダを使用したプルダウンの作り方 - Qiita

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

- Laravel(+Vue.js)でSNS風Webサービスを作ろう！ | Techpit

https://www.techpit.jp/courses/11

- Laravel × CircleCI × AWSで学ぶCI/CD | Techpit

https://www.techpit.jp/courses/78