【Laravel】「クエリパラメータに存在する場合のみ検索条件とする」をEloquentでスマートに

# 本記事における PHP と Laravel のバージョン

```bash
$ sail php -v

PHP 8.1.13 (cli) (built: Nov 26 2022 14:07:55) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.13, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.13, Copyright (c), by Zend Technologies
    with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans
```

```bash
$ sail artisan --version

Laravel Framework 9.42.2
```

# 前提

本記事では例として、都道府県名を一覧取得できるAPIを実装するものとします。
本機能はクエリパラメータを付与することができますが必須ではないため、クエリパラメータの存在有無によって検索条件を変えられるような実装にする必要があります。

**リクエストに関する仕様**

GET /api/prefectures

| パラメータ名 | 論理名 | 型式 | 必須 | 説明 |
|:-|:-|:-|:-|:-|
| prefecture_name  | 都道府県名  | string  | No  | 省略可能。<br>設定されている場合は、値に完全一致する都道府県名のみレスポンスに含める。  |

**レスポンスに関する仕様**

下表のオブジェクトを格納した配列形式のJSONを返却する。

| フィールド | 論理名 | 型式 | 説明 |
|:-|:-|:-|:-|
| name  | 都道府県名  | string  |   |
| created_at  | 登録日時  | string  | `Y-m-d H:i:s` 形式とする。  |
| updated_at  | 更新日時  | string  | `Y-m-d H:i:s` 形式とする。  |

テーブル構造、および初期データは以下の通りです。

```php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('prefectures', function (Blueprint $table) {
            $table->id();
            $table->string('name', 20)->comment('都道府県名');
            $table->timestamps();
        });
    }

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

```php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class PrefectureSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('prefectures')->insert(
            [
                'name' => '北海道'
            ],
            [
                'name' => '青森県'
            ],
            [
                'name' => '岩手県'
            ],
            [
                'name' => '宮城県'
            ],
            [
                'name' => '秋田県'
            ],
            // 中略
            [
                'name' => '宮崎県'
            ],
            [
                'name' => '鹿児島県'
            ],
            [
                'name' => '沖縄県'
            ]
        );
    }
}
```

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;

class Prefecture extends Model
{
    use HasFactory;

    protected $hidden = [
        'id'
    ];

    protected function createdAt(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => Carbon::parse($value)->format(Carbon::DEFAULT_TO_STRING_FORMAT)
        );
    }

    protected function updatedAt(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => Carbon::parse($value)->format(Carbon::DEFAULT_TO_STRING_FORMAT)
        );
    }
}
```

# Eloquent::when() を使う

`Eloquent::when()` は第１引数に指定した値が `true` の場合のみ、第２引数に指定したコールバックを実行します。

今回は以下のような関係になります。

- 第１引数 = クエリパラメータに `prefecture_name` が存在する場合。
- 第２引数 = クエリパラメータ `prefecture_name` の値を検索条件とする。

実装したコードは以下になります。
メソッドチェーンを使って記述できるため、DBアクセスの際にどのような条件を設定しているかが追いやすく、コードも簡潔です。

```php
<?php

use App\Models\Prefecture;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

// 前略

Route::get('/prefectures', function (Request $request) {
    $callback = function (Builder $builder) use ($request) {
        return $builder->where('name', '=', $request->query('prefecture_name'));
    };
    $data = Prefecture::query()
        ->when($request->has('prefecture_name'), $callback)
        ->get()
        ->all();
    return response()->json($data, 200);
});
```

# if を使う

比較のために、愚直に if で実装した場合も記述しておこうと思います。

```php
<?php

use App\Models\Prefecture;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

// 前略

Route::get('/prefectures', function (Request $request) {
    $query = Prefecture::query();
    if ($request->has('prefecture_name')) {
        $query = $query->where('name', '=', $request->query('prefecture_name'));
    }
    $data = $query->get()->all();

    return response()->json($data, 200);
});
```

一見するとシンプルになったように思えますが `$query->get()->all()` の部分で暗黙的に検索条件が追加されている場合があるため、クエリビルダの状態がどうなっているのかをイメージしながらコードを読まなければなりません。

これは簡単な例なので大したことないですが、もっとパラメータが多い場合や、長大なコードの場合は、コードを読んだだけだとどのようなクエリが発行されるかイメージしづらくなっていくと思います。

# まとめ

`Eloquent::when()` を使って特定の場合のみ条件を追加させる方法を知りました。
`Eloquent::when()` 自体は検索条件追加のメソッドではないため、第２引数に指定するコールバックの内容次第で色々なことができます。
便利な使い方を知ったら、また記事にしようと思います。