TLDR
一般的にLaravelで依存注入を行う時は、ServiceProviderで処理するのが良いとされていますが、auth()
のようなSessionと関わってくる物に対しては Middlewareで処理する必要があります。なぜかというと、Laravelではセッションが設置されている場所はMiddlewareですが、Middlewareが実行されるタイミングはServiceproviderが実行された後です。故に、Serviceproviderが実行される時、Sessionが存在しません。
課題
最近仕事で、このような課題がありました。ログイン済みのユーザーだけではなくゲストユーザーも商品をカートに入れることができる。このような機能を取り入れる。
(コードの重複や複雑さを防ぎ、一つのエンドポイントにまとめる)
準備
マイグレーション
最初に商品のテーブルを見てみよう!
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->integer('price');
// 色々
$table->timestamps();
});
}
特に面白い部分はないので、CartItemsとGuestCartItemsのテーブルを見てみましょう!
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');
});
}
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を何もない条件のままにしましょう。
class AbstractCartItem extends Model
{
// 特に処理がない
}
class CartItem extends AbstractCartItem
{
protected $fillable = ['user_id', 'product_id'];
}
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
をちゅうにゅうします。それぞれの作成ロジックを実装しましょう。
abstract class AbstractCartItemService {
abstract public function insert(array $params): AbstractCartItem;
}
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']
]);
}
}
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にサービスを注入して、使いましょう。
class CartController extends Controller
{
public function __construct(AbstractCartItemService $service) {
$this->service = $service;
}
public function store(Request $request)
{
return $this->service->insert($request->all());
}
}
最後にルーティングも必要です。
...
Route::post('/cartitem', 'CartController@store');
依存注入
https://readouble.com/laravel/6.x/ja/providers.html の通りServiceProviderを作成して、そこで依存注入の結合を登録する。
ロジック自体はそんな難しくありません。もし、ユーザーがログイン済みの場合CartItem
とCartItemService
を注入し、ゲストの場合はGuestCartItem
とGuestCartItemService
の方を注入します。
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
でコンフィグのキャッシュを削除しましょう。
'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
のテーブル に入れようとしている?結合登録が正しく動いていません!なぜでしょうか?
原因を探そう!
一番手っ取り早い確認の仕方は、ログイン済みにし、CartProvider
のif else
に dd()
でメッセージを出力する。
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
にセッションを設定するミドルウェアの下に定義して、ルートに追加しましょう。
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);
}
protected $routeMiddleware = [
...
'cartbinder' => \App\Http\Middleware\CartBinderMiddleware::class,
];
...
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はコンストラクターだけではなく、関数にも依存注入することができます。それをやってみましょう!
class CartController extends Controller
{
// storeの関数に渡す
public function store(Request $request, AbstractCartItemService $service)
{
return $service->insert($request->all());
}
}
実行すると。。。。
エラーなし!CartItemちゃんと作成しました!
大成功!