Laravelの個人的プラクティス

## 最初に
数ヶ月Laravelを使った開発を行ってきました。
その中で、今後実装する上で気をつけたい点が溜まってきたので、備忘録として残しておきます。
※ 用途はReactのバックエンドのAPIサーバーで、[Bladeテンプレート](https://readouble.com/laravel/8.x/ja/blade.html)は使っておりません。

ここに記載されてあるポイントはこちらのボイラープレートに反映されています。

https://github.com/PaykeDeveloper/payke-hinagata-backend


## サービスコンテナを使用する
サービスコンテナとは、以下の事を指します。

https://readouble.com/laravel/9.x/ja/container.html

コントローラーにロジックが乗ったり、モデルにそぐわないメソッドが実装されるのを防ぐ為に
最初からこういったレイヤーを導入すべきでした。

このサービスコンテナを導入する事で、他のメリットも享受できます。
サービスコンテナはDI(Depedency Injection)によってインスタンスが生成されるので、テストの際に簡単にモックに入れ替える事ができます。

また、サービスコンテナはコントローラーに限らず他のクラス、例えばジョブクラスからもDI出来るので、再利用しやすいです。

https://github.com/PaykeDeveloper/payke-hinagata-backend/blob/55493aff27e8af02339b618fe3ab792871a584de/app/Jobs/Sample/CreateProject.php


## レスポンスはAPIリソースを使用する
APIリソースとは、以下の事を指します。

https://readouble.com/laravel/9.x/ja/eloquent-resources.html

以下のドキュメントにある様に、Eloquentモデルやそのコレクションを直接返す事ができますが、
この方法が避けるべきだと考えます。

https://readouble.com/laravel/8.x/ja/responses.html

少し極端ですが、以下の様な実装がされているとレスポンスに`user`が含まれたり含まれなかったりします。

``` php
class Phone extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
```

``` php
Route::get('/phones/{phone}', function (Phone $phone) {
    if (rand(0, 1)) {
        $phone->user;
    }
    return $phone;
});
```

`$phone->user`にアクセスしていない場合は以下の様になりますが、
```json
{
  "id": 1,
  "user_id": 1,
  "created_at": "2022-07-08T02:45:56.000000Z",
  "updated_at": "2022-07-08T02:45:56.000000Z"
}
```

`$phone->user`にアクセスした場合は`user`の中身もレスポンスに含まれてしまいます。
```json
{
  "id": 1,
  "user_id": 1,
  "created_at": "2022-07-08T02:45:56.000000Z",
  "updated_at": "2022-07-08T02:45:56.000000Z",
  "user": {
    "id": 1,
    "name": "Mr. Kameron Barrows",
    "email": "boehm.lia@example.com",
    "email_verified_at": "2022-07-08T02:45:55.000000Z",
    "created_at": "2022-07-08T02:45:55.000000Z",
    "updated_at": "2022-07-08T02:45:55.000000Z"
  }
}
```

`$hidden`に設定すれば、勝手にレスポンスに含まれるという問題は解決できますが、
追加で開発を進める内に設定が漏れる可能性があり、また漏れても気がつかない恐れがあります。
``` php
class Phone extends Model
{
    protected $hidden = [
        'user',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
```



## N+1対応を入れる
`Model::preventLazyLoading()`を使って、
最初から開発環境でN+1が発生した際にはエラーが発生する様にすべきでした。

https://github.com/PaykeDeveloper/payke-hinagata-backend/blob/55493aff27e8af02339b618fe3ab792871a584de/app/Providers/AppServiceProvider.php#L19

また、対応方針として`with`もしくは`loadMissing`を使うことを推奨します。
`loadMissing`は、未ロードの場合のみロードするので、不要なロードを防ぐ事ができます。

そこで、`loadMissing`を「コントローラー」「サービスコンテナ」「APIリソース」のどこで実装するかという問題がありますが、基本的に必要なタイミングの直前で実装するのが良いと考えます。

例えば、APIのレスポンスとして関連するモデルが必要な場合は、APIリソースで実装します。
APIのレスポンスとしてのみ必要にも関わらず、サービスコンテナで実装してしまうと
バッチからそのサービスコンテナを呼び出すと不要なロードになります。

では、APIリソースでの実装箇所ですが、`JsonResource::toResponse`、`ResourceCollection::toResponse`メソッドをオーバーライドして実装します。
`JsonResource::toArray`で実装してしまうと、`JsonResource::collection`を使用した際に何度もロードが呼び出される事になります。

https://github.com/PaykeDeveloper/payke-hinagata-backend/blob/55493aff27e8af02339b618fe3ab792871a584de/app/Http/Resources/Division/DivisionCollection.php

https://github.com/PaykeDeveloper/payke-hinagata-backend/blob/55493aff27e8af02339b618fe3ab792871a584de/app/Http/Resources/Division/DivisionResource.php


## 権限チェックのポリシーはコントローラーと紐づける
ポリシーとは、以下の事を指します。

https://readouble.com/laravel/9.x/ja/authorization.html

上記ドキュメントにもある様に、ポリシーはモデルと1対1の関係が前提になっている様です。
ただ、同じモデルでもAPIのエンドポイントによって別のポリシーを適用したいケースが発生しました。

例えば、`Phone`モデルを扱う用途の異なるAPIエンドポイント(コントローラー)が2つあり
1つは管理者用のポリシーを、1つは一般ユーザー用のポリシーを適用したケースです。

| エンドポイント | コントローラー | ポリシー |
|---|---|---|
| /api/phones/ | App\Http\Controllers\Admin\PhoneController.php | App\Policies\Admin\PhonePolicy.php |
| /aip/user/phones/ | App\Http\Controllers\User\PhoneController.php  | App\Policies\User\PhonePolicy.php |

`Gate::guessPolicyNamesUsing`を実装することで、プロジェクトに合ったロジックを適用する事ができます。

https://github.com/PaykeDeveloper/payke-hinagata-backend/blob/55493aff27e8af02339b618fe3ab792871a584de/app/Providers/AuthServiceProvider.php#L46

## 最後に
まだまだ日々発見の多いLaravelですが、RailsやDjangoなどと比較しても魅力的なフレームワークです。
Qiitaを含め、皆様の知見もキャッチアップしながら、より良い実装を目指していきたいと思います。