Laravelのauth()による依存注入


## TLDR

一般的にLaravelで依存注入を行う時は、ServiceProviderで処理するのが良いとされていますが、`auth()`のようなSessionと関わってくる物に対しては Middlewareで処理する必要があります。なぜかというと、Laravelではセッションが設置されている場所はMiddlewareですが、Middlewareが実行されるタイミングは<b>Serviceproviderが実行された後</b>です。故に、Serviceproviderが実行される時、Sessionが存在しません。

## 課題

最近仕事で、このような課題がありました。ログイン済みのユーザーだけではなくゲストユーザーも商品をカートに入れることができる。このような機能を取り入れる。
（コードの重複や複雑さを防ぎ、一つのエンドポイントにまとめる）

## 準備

### マイグレーション

最初に商品のテーブルを見てみよう！

```products_migration.php
   public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->integer('price');
            // 色々
            $table->timestamps();
        });
    }
```

特に面白い部分はないので、CartItemsとGuestCartItemsのテーブルを見てみましょう！

```cartitems_migration.php
   public function up()
    {
        Schema::create('cart_items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('user_id')->unsigned();
            $table->bigInteger('product_id')->unsigned();
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users');
            $table->foreign('product_id')->references('id')->on('products');
        });
     }
```

```guestcartitems_migration.php
    public function up()
    {
        Schema::create('guest_cart_items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('user_cookie');　// なんやらのcookie
            $table->bigInteger('product_id')->unsigned();
            $table->timestamps();

            $table->foreign('product_id')->references('id')->on('products');
        });
    }

```

このテーブルの大きいな違いは一つしかありません！Cartitemsではuserテーブルとuser_idで紐づいているが、ゲストユーザーのテーブルは存在しないので、ブラウザーで定義されているクッキーでユーザーと紐づく必要あります。処理が重複されないため、CartItemとGuestCartItemのモデルを継承して、一つのエンドポイントにまとめれことができる！


### 継承CartItemモデル

抽象のAbstractCartItem.phpを作ってCartItemとGuestCartItemを継承するだけです（インターフェースでもいいと思います）。もうすでにLaravelのモデルに`create`の処理が実装されているので、現在AbstractCartItemを何もない条件のままにしましょう。

```AbstractCartItem.php
class AbstractCartItem extends Model
{
    //　特に処理がない
}
```

```CartItem.php
class CartItem extends AbstractCartItem
{
    protected $fillable = ['user_id', 'product_id'];
}
```

```GuestCartItem.php
class GuestCartItem extends AbstractCartItem
{
    protected $fillable = ['user_cookie', 'product_id'];
}
```

### Controller

一つのエンドポイントにするためには、一つのコントローラーにまとめる必要があります。ですが、CartItemは`user_id`を使う必要があり、GuestCartItemはクッキーから`user_cookie`を使用しなければなりません。ログイン済みのユーザーIDを取得すると、Cookieの値を取得する処理も異なり、Modelで処理する責任も無いのでサービスを使う必要になります。CartControllerにサービスを注入しましょう！

最初に、AbstractCartItemService (もしくはインターフェース)を作成して、それからGuestCartItemServiceとCartItemServiceを継承します。コンストラクターに`AbstractCartItem`をちゅうにゅうします。それぞれの作成ロジックを実装しましょう。

```AbstractCartService.php
abstract class AbstractCartItemService {
    abstract public function insert(array $params): AbstractCartItem;
}
```
```CartItemService.php
class CartItemService extends AbstractCartItemService {

    public function __construct(AbstractCartItem $cart) {
        $this->cart = $cart;
    }

    public function insert(array $params): AbstractCartItem
    {   
        return $this->cart->create([
            'user_id' => Auth::id(),
            'product_id' => $params['product_id']
        ]);
    }
}
```
```GuestCartItemService.php
class GuestCartItemService extends AbstractCartItemService {
    public function __construct(AbstractCartItem $cart) {
        $this->cart = $cart;
    }

    public function insert(array $params): AbstractCartItem
    {
        return $this->cart->create([
            'user_cookie' => Cookie::get('guest_id'),
            'product_id' => $params['product_id']
        ]);
    }
}
```
それでCartControllerにサービスを注入して、使いましょう。

```CartController.php
class CartController extends Controller
{
    public function __construct(AbstractCartItemService $service) {
         $this->service = $service;
    }

    public function store(Request $request)
    {
        return $this->service->insert($request->all());     
    }
}
```

最後にルーティングも必要です。

```web.php
    ...
    Route::post('/cartitem', 'CartController@store');
```

## 依存注入

https://readouble.com/laravel/6.x/ja/providers.html の通りServiceProviderを作成して、そこで依存注入の結合を登録する。
ロジック自体はそんな難しくありません。もし、ユーザーがログイン済みの場合`CartItem`と`CartItemService`を注入し、ゲストの場合は`GuestCartItem`と`GuestCartItemService`の方を注入します。

```CartProvider.php
    public function boot()
    {
        if (Auth::check()) {
            app()->bind(AbstractCartItemService::class, CartItemService::class);
            app()->bind(AbstractCartItem::class, CartItem::class);
        } else {
            app()->bind(AbstractCartItemService::class, GuestCartItemService::class);
            app()->bind(AbstractCartItem::class, GuestCartItem::class);
        }
    }
```
それとこのプロバイダーが使うようになるため、`config/app.php`の`providers`に定義し、新しい機能が使えるようになるため`php artisan config:clear`でコンフィグのキャッシュを削除しましょう。

```config/app.php
    'providers' => [
        ...
       App\Providers\CartProvider::class,
    ]
```

なかなかいい感じで、できてきているので、実際に実行してみましょう！

### 動きますが。。。

ゲスト・ユーザとしてGuestCartItemをきちんと入れます が。ログイン済みのユーザーの場合には、みんなが大好きな`500 | Server Error`のページが表示されます。Log見ると

```prolog
"SQLSTATE[HY000]: General error: 1364 Field 'user_cookie' doesn't have a default value 
(SQL: insert into `guest_cart_items` 
(`product_id`, `updated_at`, `created_at`) 
values (40, 2019-12-08 04:33:31, 2019-12-08 04:33:31))"
```

ええええー！？どういうこと？ログイン済なのになんで　`guest_cart_items`のテーブル　に入れようとしている？結合登録が正しく動いていません！なぜでしょうか？

### 原因を探そう！

一番手っ取り早い確認の仕方は、ログイン済みにし、`CartProvider`の`if else`に `dd()`でメッセージを出力する。

```CartProvider.php
    public function boot()
    {
        if (Auth::check()) {
            dd('Logged in');
            ...
        } else {
            dd('NOT Logged in');
            ...
        }
    }
```

実行すれば、`NOT　Logged in`が出力されます！えええええー！？どういうこと？ログイン済なのに、`Auth::check()` が `false` と返されています！ 念の為　`Auth::user()`を出力しましょう。そうすると。

`null`

と返されます。ログイン済みなのにユーザーが存在しないのはオカシイと思います。もっと調べる必要があります。

### セッションが原因だ。。。

ログイン済のユーザー情報はセッションで保持されていて、Laravelではセッションが設置されている場所は`Middleware`ですが、Middlewareが実行されるタイミングは
<b>Serviceproviderが実行された後</b>です。それで、Serviceproviderの処理が実行されるタイミングはセッションが存在しません！

セッションが設置されている後に結合登録するひつようになるので、Middlewareで依存注入結合登録をおこなうべきです。

https://readouble.com/laravel/6.x/ja/middleware.html
の通りMiddleware作成し、結合登録するロジックを書きます。それと、その機能を動くようにするには`http/Kernel.php`に<b>セッションを設定するミドルウェアの下に</b>定義して、ルートに追加しましょう。

```CartBinderMiddleware.php
    public function handle($request, Closure $next)
    {
        if (Auth::check()) {
            app()->bind(AbstractCartItemService::class, CartItemService::class);
            app()->bind(AbstractCartItem::class, CartItem::class);
        } else {
            app()->bind(AbstractCartItemService::class, GuestCartItemService::class);
            app()->bind(AbstractCartItem::class, GuestCartItem::class);
        }
    
        return $next($request);
    }
```
```app/Http/kernel.php
    protected $routeMiddleware = [
         ...
        'cartbinder' => \App\Http\Middleware\CartBinderMiddleware::class,
    ];
```
```web.php
    ...
    Route::post('/cartitem', 'CartController@store')->middleware(['cartbinder']);
```

ログインして、カートに入れようとしたら、また皆が大好きな`500`エラーページが表示された。。。今回の原因はなんなんでしょう〜

```prolog
"Target [App\Services\AbstractCartItemService] is not instantiable while building [App\Http\Controllers\CartController]."
```

コントローラーに抽象の `AbstractCartItemService`　クラスがコンストラクターに渡されているって。Controllerが作成されるタイミングはMiddleware実行される前なので、また結合が登録されていません。

Laravelはコンストラクターだけではなく、関数にも依存注入することができます。それをやってみましょう！

```CartController.php
class CartController extends Controller
{
    // storeの関数に渡す
    public function store(Request $request, AbstractCartItemService $service)
    {
        return $service->insert($request->all());        
    }
}
```

実行すると。。。。

エラーなし！CartItemちゃんと作成しました！

大成功！