LaravelのAPI resourceで1対1のモデルを相互に呼びあったらハングアウトした

## 概要  

Laravelのドキュメントに例としてあげられているUserモデルとPhoneモデル。  
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#one-to-one

ユーザー情報取得時には、それに紐づくPhoneモデルの情報を、  
電話情報取得時には、それに紐づくUserモデルの情報を付け加えた形で返す
シンプルなAPIをLaravelのAPI resourceを使って作りたかった。

### 期待するレスポンス

GET /api/users/1
``` json
{
  "data": {
    "id": 1,
    "name": "test",
    "phone": {
      "id": 1,
      "number": "xxx-xxxx-xxxx"
    }
  }
}
```

GET /api/phones/1
``` json
{
  "data": {
    "id": 1,
    "number": "xxx-xxxx-xxxx",
    "user": {
      "id": 1,
      "name": "test"
    }
  }
}
```

### ソース

Userモデル
``` php
class User extends Model
{
  use HasFactory;

  public function phone()
  {
    return $this->hasOne(Phone::class);
  }
}
```

Phoneモデル
``` php
class Phone extends Model
{
  use HasFactory;

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

Userリソース
``` php
class UserResource extends JsonResource
{
  public function toArray($request)
  {
    return [
      'id' => $this->id,
      'name' => $this->name,
      'phone' => new PhoneResource($this->phone),
    ];
  }
}
```

Phoneリソース
``` php
class PhoneResource extends JsonResource
{
  public function toArray($request)
  {
    return [
      'id' => $this->id,
      'number' => $this->number,
      'user' => new UserResource($this->user),
    ];
  }
}
```

Userコントローラー
``` php
class UserController extends Controller
{
  public function show(User $user)
  {
    return new UserResource($user);
  }
}
```

Phoneコントローラー
``` php
class PhoneController extends Controller
{
  public function show(Phone $phone)
  {
    return new PhoneResource($phone);
  }
}
```

APIルート
``` php
Route::apiResource('users', UserController::class);
Route::apiResource('phone', PhoneController::class);
```

### 結果と問題点

上記のソースではPostmanからAPIを呼び出すとハングアウトする。

```
GET http://localhost/api/users/1
Error: socket hang up
```

理由はAPI resouceの定義でUserモデルとPhoneモデルのリレーションの
読み込みが無限ループしていること。

例としてUserモデルを挙げるが、今回はルーティング定義時にモデルクラスを
タイプヒントすることでモデルを自動取得する方法を採用していた。

``` php
Route::apiResource('users', UserController::class);
```

これによりコントローラーのアクションメソッドでは、指定されたIDに紐づくモデルが
既に取得された状態となるが

Userコントローラー
``` php
public function show(User $user)
{
  return new UserResource($user);
}
```

実はこの時点ではUserモデルに紐づくPhoneモデルは取得されていない。  
なぜならLaravelのリレーションは遅延ロードであるため。  

このことは公式ドキュメントにも記載されている。  
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading
>プロパティとしてEloquentリレーションへアクセスすると、関連するモデルは「遅延読み込み」されます。  
つまりこれは、最初にプロパティへアクセスするまで、リレーションデータが実際にロードされないことを意味します。

なので実際にUserモデルに紐づくPhoneモデルが取得されるのは、UserリソースでのPhoneResource生成時。
これにより、以下のコメントに振っている番号のような順で呼び出しあった結果、無限ループしている。

ユーザー取得のAPIを呼び出した場合(一部抜粋)
``` php
// UserResource
return [
  'id' => $this->id,
  'name' => $this->name,
  // 1. Phoneモデルの取得
  // 3. Phoneモデルの取得
  // 5. Phoneモデルの取得.....
  'phone' => new PhoneResource($this->phone),
];

// PhoneResource
return [
  'id' => $this->id,
  'number' => $this->number,
  // 2. Userモデルの取得
  // 4. Userモデルの取得
  // 6. Userモデルの取得.....
  'user' => new UserResource($this->user),
];
```

## 解消方法
この無限ループを解消するためには、`eager load`を使う。

### eager loadに関して
eager loadの内容に関しては公式ドキュメントを参照。  
つまり都度読み込みではなく先にリレーションまで読み込んでしまう方法。  
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading

#### モデル取得方法の変更

これを行うにはモデルを取得する部分に手を入れないと行けないので、
仕方なくルーティング時にタイプヒントでモデルを自動取得する方法から、
手動で取得する方法に切り替える。
※他にやり方があるのかもしれないがみつからない。。。

APIルート
``` php
// Route::apiResource('users', UserController::class);
Route::get('/users/{id}', [UserController::class, 'show']);
// Route::apiResource('phone', PhoneController::class);
Route::get('/phones/{id}', [PhoneController::class, 'show]);
```

それに伴いコントローラーも変更
例としてUserコントローラーのみを挙げるがPhoneコントローラーの方も変更する。  

``` php
// public function show(User $user)
public function show (int $id)
{
  // これがeager load. withで指定されたリレーションをこの時点で読み込む.
  $user = User::with('phone')->get()->find($id);
  return new UserResource($user);
}
```

#### whenLoadedを使う

さらにAPI resourceにも手を加える。  
`$this->phone`ではなく`withLoaded('phone')`を使う。  
このメソッドは既にリレーションがロードされている場合に限りリレーションを返し、  
ロードされていない場合はそのプロパティごとレスポンスから削る。  
つまり、リレーションを返すか否かを`eager load`されているかに委ねる。  

例としてUserリソースのみを挙げるがPhoneリソースの方も変更する。  

Userリソース
``` php
class UserResource extends JsonResource
{
  public function toArray($request)
  {
    return [
      'id' => $this->id,
      'name' => $this->name,
      // 'phone' => new PhoneResource($this->phone),
      'phone' => new PhoneResource($this->whenLoaded('user')),
    ];
  }
}
```

これによりリレーションはコントローラでeager loadにより取得する経路だけとなり、
API resourceからリレーションを取得することがなくなるため、無限ループが解消する。  

## 参考

https://laracasts.com/discuss/channels/laravel/api-resource-infinite-loop
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading