Laravelコーディングよくやる集2022

:::note
この記事は「[【マイスター・ギルド】本物のAdvent Calendar 2022](https://adventar.org/calendars/7794)」18日目の記事です。
:::


LaravelでWebアプリケーションを開発しているんですが、これは毎回書きそうだなーというのがいくつかあったので、それをまとめてみました。開発はまだまだ途中なので、これから変化していく可能性もありますが。

# グローバルスコープ

グローバルスコ―プを使って`DBのデータ + 何らかの条件付き`を表現します。それを使えば疑似的[^globalscope]に条件付きのモデルが定義できます。

例えば`User`モデルがあり、利用者が閲覧可能かどうかを`is_visible`で表したデータがあるとき、

```php
class VisibleUser extends User
{
    protected $table = 'users';

    protected $hidden = [
        'is_visible',  // 持つ必要が無い
    ];

    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(function (Builder $builder) {
            // 公開中のもののみという条件を付与
            $builder->where('is_visible', true);
        });
    }
}
```

とする事で「利用者が閲覧可能なデータのみに絞られた`VisibleUser`」を定義できます。

利用者に閲覧可能なユーザーの情報のみが表示できる`/public/users/{user}`というルーティングがあった時に、モデルバインディングで

```php
Route::get('/public/users/{user}', function (VisibleUser $user) {
    return $user;
});
```

としてしまえば、`is_visible=falase`なUserを表示しようとしても404エラーに出来ます。


[^globalscope]: グローバルスコープは DB から取得時のSQLに`'is_visible' = true`を付与するだけ。DB から取得したデータの`is_visible`を`true`に上書きすることは可能なので、モデルが常に`is_visible=true`であることを保証するわけではないことに注意。

# カスタムクエリビルダ

ユースケースから呼び出す検索条件は、`Illuminate\Database\Eloquent\Builder`を拡張したカスタムクエリビルダに書くようにします。

```php
use Illuminate\Database\Eloquent\Builder;

class UserQueryBuilder extends Builder
{
    public function nameIs(string $val): static
    {
        return $this->where('name', '=', $val);
    }
}
```

というカスタムクエリビルダを定義し、モデルで利用するクエリに差し替えます。

```php
class User extends Model
{
    ...

    public static function query(): UserQueryBuilder
    {
        return parent::query();
    }

    public function newEloquentBuilder($query): UserQueryBuilder
    {
        return new UserQueryBuilder($query);
    }
}
```

こうするとユースケースで

```php
User::query()->nameIs('山田')->orderBy('id');
```

と呼ぶことが出来ます。

# トランザクション

コントローラーでトランザクション制御を行います。

```php
public function create(CreateRequest $request): JsonResponse
{
    $input = $request->validated();
    DB::transaction(function () use ($input) {
        $this->usecase->create($input);
    });
    return response()->json();
}
```

`DB::transaction`でユースケースの処理を囲っておき、成功時はコミット、失敗（エラー発生）時はロールバックを自動でされるようにしておきます。

| 要トランザクション | トランザクション不要 |
| ------------------ | -------------------- |
| 登録、更新、削除   | 取得                 |

処理の種類とトランザクションの要・不要は上の通りで、ユースケースに「要」の処理が含まれるならトランザクションで囲うようにします。

# エラーログ

```php
Log::error(...);
```

で Laravel のログ出力先に吐き出します。実際にエラーが起きた際のログは、基本的には何もしなくてもLaravelが勝手に出してくれるんですが、`try-catch`してエラーを投げなおさない場合には、

```php
try{
    ...
} catch (Exception $e) {
    Log::error($e);
    ...
}
```

として手動でエラーを出力する必要が出てきます。

# メッセージ

# 多言語対応・メッセージ管理

画面へ表示されるメッセージを書く場合、多言語対応を見据えて`__`ヘルパを使っておきます。
https://readouble.com/laravel/9.x/ja/localization.html#retrieving-translation-strings

```php
__('登録に成功しました。')
```

`__`ヘルパを使っておくと`lang/`ディレクトリ下で

```php
'登録に成功しました。' => 'registration successful',
```

を定義すれば、configの言語設定に従って別言語化されます。

あるいは`__`ヘルパには定数を渡すようにして、`lang/ja/`下で定数と表示するメッセージの対応を書くようにすれば、`lang`フォルダ内に表示メッセージを集めて管理ができます。

```php
__('message.welcome')
```
（定数を`.`で区切ることで階層化が可能です）
```php:lang/ja/message.php
`welcome` => 'ようこそ'
```

## 画面へのフラッシュ表示（1度だけ表示）

[laracast/flash](https://github.com/laracasts/flash)を利用します。
成功メッセージは

```php
Flash::success(...);
```

で出力し、

エラーメッセージは

```php
Flash::error(...);
```

で出力します。表示させる側の blade では

```php
@include('flash::message')
```

を読ませるだけです。成功メッセージに対しては`alert-success`、失敗メッセージには`alert-danger`のクラスが付いた DOM が生成されるので、文字色を変えるなどのスタイルを当てます。

# 通知

Notification を使います。

イベント発火から通知までの流れは以下のようにします。

## イベント発火

ユースケースでイベントを発火します（`Registered`がイベントです）。この時リスナー処理で必要になる情報をイベントに渡しておきます（DB 登録したモデルとか）。

```php
event(new Registered($user));
```

## イベントとリスナーの対応付け

`Providers/EventServiceProvider`に定義しておいたイベントとリスナーの対応づけから、リスナー（`SendRegisteredEmail`）が呼び出されます。

```php
protected $listen = [
    Registered::class => [
        SendRegisteredEmail::class,
    ],
];
```

## リスナー処理

リスナーでイベント発火時に渡したモデル（DB 登録したモデル）を参照し、そのモデルに定義されている通知用のメソッド（`sendRegisteredNotification`）を呼び出します。リスナ―には`ShouldQueue`インターフェースを使うようにし、後々非同期処理されるようにします。

```php
class SendRegisteredEmail implements ShouldQueue
{
    public function handle(Registered $event)
    {
        $event->user->sendRegisteredNotification();
    }
}
```

## イベント通知

通知用のメソッド内にて`notify`メソッドを呼び、引数に通知クラス（ここの`Registered`は通知クラス）を渡します。このモデルには`Notifiable`トレイトが使用されている事が前提です。

```php
class User extends Model
{
    use Notifiable;

    public function sendRegisteredNotification()
    {
        $this->notify(new Registered());
    }
}
```

## 通知処理

通知クラスでは、通知処理（送信する情報の構築）を書きます。

```php
class Registered extends Notification
{
    ...

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject(__('登録が完了しました'))
            ->markdown('mails.registered');
    }
}
```

上記はメール通知処理ですが、SMS通知やSlack通知などもここに実装出来ます。

# テスト

## テスト用DB

- phpunit の設定で、`testing`という開発とは別の DB を使用する設定にします。
- `RefreshDatabase`トレイトを使い、DB はテストケース毎にリフレッシュされるようにします。
- あらかじめデータが入った DB が必要な場合は、テストコードの前半で必要なデータを作成します。

## テストケース

`@dataProvider`アノテーションを使い、TDT（テーブル駆動テスト）っぽくなるようにします。

```php
    /**
     * @dataProvider registDataProvider
     */
    public function test_regist($input, $seed, $expected, $expectedException)
    {
        ...
    }

    public function registDataProvider()
    {
        return [
            '正常系' => [
                [
                    'name' => 'Test User',
                    'email' => 'test@m-gild.com',
                    'password' => 'password',
                ], // 引数の$inputにあたります
                RegistTestSeeder::class, // 引数の$seedにあたります
                'test@m-gild.com', // 引数の$expectedにあたります
                null, // 引数の$expectedExceptionにあたります
            ],
            '異常系' => [
                [], // 引数の$inputにあたります
                RegistTestSeeder::class, // 引数の$seedにあたります
                '', // 引数の$expectedにあたります
                Exception::class, // 引数の$expectedExceptionにあたります
            ]
        ];
    }
```

上記のようにすると、`正常系`と`異常系`がテストケースとなり、test_regist が2度回ります。その際に test_regist に渡される引数は、配列として書いた順に渡されます。

## モック

Usecase をテストしたい場合、それに依存している Model や Service はモック化し、テストに影響を与えないようにします。
UT の`setUp`関数でテスト対象の初期化処理を書くときにモックを渡し、準備をします。

```php
    protected function setUp(): void
    {
        parent::setUp();
        $this->mock = Mockery::mock('App\Models\User'); // モック化したいクラスのパスを渡します
        // UserUsecaseがテスト対象です
        $this->usecase = new UserUsecase(
            $this->mock,
        );
    }
```

テストコードの前半でモックのメソッドをダミーなもので定義します。定義していないメソッドが呼び出されるとエラーになります。

`User`モデルの`create`というメソッドが使われる Usecase をテストする場合は、

```php
        $receive = $this->mock->shouldReceive('create')
            ->once() // 一度だけシミュレーションするという宣言
            ->with($input); // 引数
        if (is_null($mockException)) {
            // エラーを期待しない場合
            $receive->andReturn($mockUser); // 応答をダミーに置き換えます
        } else {
            // エラーを期待する場合
            $receive->andThrow($mockException); // 異常時のエラーの発生をダミーに置き換えます
        }

        $this->usecase->regist($input);  // テスト対象。この中でUser::createが呼ばれています
```

となります。