Help us understand the problem. What is going on with this article?

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

TLDR

一般的にLaravelで依存注入を行う時は、ServiceProviderで処理するのが良いとされていますが、auth()のようなSessionと関わってくる物に対しては Middlewareで処理する必要があります。なぜかというと、Laravelではセッションが設置されている場所はMiddlewareですが、Middlewareが実行されるタイミングはServiceproviderが実行された後です。故に、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を作成して、そこで依存注入の結合を登録する。
ロジック自体はそんな難しくありません。もし、ユーザーがログイン済みの場合CartItemCartItemServiceを注入し、ゲストの場合はGuestCartItemGuestCartItemServiceの方を注入します。

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.phpprovidersに定義し、新しい機能が使えるようになるためphp artisan config:clearでコンフィグのキャッシュを削除しましょう。

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

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

動きますが。。。

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

"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のテーブル に入れようとしている?結合登録が正しく動いていません!なぜでしょうか?

原因を探そう!

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

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が実行されるタイミングは
Serviceproviderが実行された後です。それで、Serviceproviderの処理が実行されるタイミングはセッションが存在しません!

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

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

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エラーページが表示された。。。今回の原因はなんなんでしょう〜

"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ちゃんと作成しました!

大成功!

sencorp
幼稚園・保育園向けインターネット写真サービス「はいチーズ!」を提供しています。
https://sencorp.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした