🐣

学んだ機能を活用してControllerをリファクタリングしてみる Part.1(Laravel)

2021/03/17に公開2

Controllerはフロント側からリクエストが来たときに最初に処理を行う場所で、
Controllerに記述する処理をなるべく少なくすることで、再利用性が高まり高品質なシステムが出来上がります。

今回はこれまでに学んだ機能を用いて、Controllerをスッキリきれいにするリファクタリングについて紹介したいと思います。

Controllerをスッキリさせることを主目的としているため、用いている機能の詳細説明など細かい点の説明は行いません。

リファクタリングで用いる機能はこちらです

  • FormRequest
  • リクエストパラメータの一括取得
  • Job/Queue

今回のリファクタリング対象とするItemControllerのstoreメソッドでは下記の処理を行います。

  1. バリデーションを行う
  2. itemsテーブルにいれる
  3. 商品が追加された旨をメールで通知する

そしてメソッドの中身はこちらです。

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use App\Models\Item;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(Request $request)
    {
        Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'price' => 'required|integer',
            'decription' => 'required|string'
        ]);
	
        $name = $request->input('name');
        $price = $request->input('price');
        $description = $request->input('description');

        $item = Item::create([
            'name' => $name,
            'price' => $price,
            'description' => $description
        ]);

        $email = Auth::user()->email;
        Mail::to($email)->send(new ItemCreateMail($item->name));

        return redirect('/home');
    }
}

こちらをこれからリファクタリングしていきます。

バリデーションをFormRequestに書く

バリデーションを直接Controllerに書いているところを、FormRequestに移します。

FormRequestの詳細に関してはこちらで解説しているのであまりよく知らない方はご覧ください
https://zenn.dev/naoki_oshiumi/articles/1efe6dedec251a

FormRequestの作成

まずはFormRequestのファイルを作成します。

$ php artisan make:request ItemCreateRequest

コマンドを実行すると、 app/Http/Requests/ItemCreateRequest が出来ました!

産まれたてのFormRequestはこの状態です。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ItemCreateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

こちらの authorize をtrueにして、rules の中にバリデーションルールを書いていきます

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ItemCreateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'price' => 'required|integer',
            'decription' => 'required|string'
        ];
    }
}

Controllerの修正

まずは先程作成したItemCreateRequestを使用します。

ItemCreateRequestを使用する

App\Http\Requests\ItemCreateRequest; をインポートし、
storeメソッドの引数に指定することで、ItemCreateRequestを使用します。

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use App\Models\Item;
use Illuminate\Http\Request; // 削除
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\ItemCreateRequest; // 追記
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request) // RequestからItemCreateRequestに変更
    {
        Validator::make($request->all(), [

もともと定義していたバリデーションを削除する

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator; // 削除
use App\Models\Item;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\ItemCreateRequest;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        Validator::make($request->all(), [ // 削除
            'name' => 'required|string|max:255',  // 削除
            'price' => 'required|integer', // 削除
            'decription' => 'required|string' // 削除
        ]);
	
        $name = $request->input('name');
        $price = $request->input('price');
        $description = $request->input('description');
 

FormReuest適用後のControllerはこうなる

FormRequest適用後はこのようなコントローラーになります!

最初に比べて少しすっきりしましたね!

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Item;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\ItemCreateRequest;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        $name = $request->input('name');
        $price = $request->input('price');
        $description = $request->input('description');

        $item = Item::create([
            'name' => $name,
            'price' => $price,
            'description' => $description
        ]);

        $email = Auth::user()->email;
        Mail::to($email)->send(new ItemCreateMail($item->name));

        return redirect('/home');
    }
}

でもまだまだこれ以上にすっきりできます!
では次もみていきましょう!

リクエストパラメータを一括取得してそのままcreateに渡す

現在はリクエストパラメータをinputを用いて、1つずつ使用しているところを only を用いてまとめて受け取っちゃいます。

リクエストパラメータの受け取り方法については下記URLで解説しています。
allメソッドを使わないほうが良い理由など書いていますのでぜひご覧ください。
https://zenn.dev/naoki_oshiumi/articles/b542f7daca7a95

リクエストパラメータを一括取得する

リクエストパラメータを only で一括取得するためにはこのように変更します。

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Item;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\ItemCreateRequest;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        $name = $request->input('name'); // 削除
        $price = $request->input('price'); // 削除
        $description = $request->input('description'); // 削除
	
	$attributes = $request->only(['name', 'price', 'description']); // 追記

        $item = Item::create([
            'name' => $name,
            'price' => $price,
            'description' => $description
        ]);

        $email = Auth::user()->email;
        Mail::to($email)->send(new ItemCreateMail($item->name));

        return redirect('/home');
    }
}

このように onlyの引数を配列で渡してあげると、リクエストパラメータを二次元配列で取得できるようになります。

createにそのまま渡す

二次元配列で受け取れるので、そのままcreateに渡しちゃいます。

コードはこのようになります。

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Item;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\ItemCreateRequest;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        $attributes = $request->only(['name', 'price', 'description']);

        $item = Item::create($attributes);

        $email = Auth::user()->email;
        Mail::to($email)->send(new ItemCreateMail($item->name));

        return redirect('/home');
    }
}

めちゃくちゃ簡潔になりましたね!!

認証中のユーザーは$request->user() で受け取れる

現在はわざわざ $user = Auth::user() としてユーザーを取得していますが、 $request->user() で認証中のユーザーを取得できます。

なので、このように変更しちゃいましょう!

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Item;
use Illuminate\Support\Facades\Auth;  // 削除
use App\Http\Requests\ItemCreateRequest;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        $attributes = $request->only(['name', 'price', 'description']);

        $item = Item::create($attributes);

        $user = Auth::user(); // 削除
        Mail::to($request->user())->send(new ItemCreateMail($item->name)); // 変更

        return redirect('/home');
    }
}

これで現在の最新のコードはこのようになります。

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Http\Requests\ItemCreateRequest;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        $attributes = $request->only(['name', 'price', 'description']);

        $item = Item::create($attributes);

        Mail::to($request->user()->email)->send(new ItemCreateMail($item->name));

        return redirect('/home');
    }
}

Job/Queueを使用して非同期処理を行う

さて、ここまでで十分簡潔なコードになりましたが、最後に非同期処理の実装をしてみます。

今回は、Itemの作成やメールの処理を非同期処理とすることで、レスポンスの速度が早くなります。

Job/Queueについてはこちらで解説していますので、あまり良くご存知でない方はこちらをご参照ください。
https://zenn.dev/naoki_oshiumi/articles/d8194a94e900f7

Jobを作成する

コマンドでJobを作成します。

$ php artisan make:job CreateItemJob

コマンドを実行すると、 app/Jobs/CreateItemJob.php が出来ました!

産まれたてのCreateItemJobはこのような感じです。

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class CreateItemJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        //
    }
}

こちらのhandleの中に、非同期で処理したい処理内容を記述します。
今回はItemの作成とメール送信を非同期で処理したいので、それを持ってきます。

<?php

namespace App\Jobs;

use App\Mail\ItemCreateMail; // 追記
use App\Models\Item; // 追記
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail; // 追記

class CreateItemJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $email; // 追記
    private $attributes; // 追記

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(string $email, array $attributes) // 変更
    {
        $this->email = $email; // 追記
        $this->attributes = $attributes; // 追記
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $item = Item::create($this->attributes); // 追記

        Mail::to($this->email)->send(new ItemCreateMail($item->name)); // 追記
    }
}

JobにメールアドレスとItemの配列を渡すと、Item作成とメール送信を行うという処理になっています。

Controllerのコードを変更する

JobをControllerの中で使用するために、コードをこのように変更します。

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Item;  // 削除
use App\Http\Requests\ItemCreateRequest;
use App\Mail\ItemCreateMail; // 削除
use Illuminate\Support\Facades\Mail;  // 削除

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        $attributes = $request->only(['name', 'price', 'description']);

        $item = Item::create($attributes); // 削除

        Mail::to($request->user()->email)->send(new ItemCreateMail($item->name)); // 削除
	
	CreateItemJob::dispatch($request->user()->email, $attributes); // 追記

        return redirect('/home');
    }
}

最終的にControllerはこんなにコンパクトになる

こちらが Before

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use App\Models\Item;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Mail\ItemCreateMail;
use Illuminate\Support\Facades\Mail;

class ItemController extends Controller
{
    public function store(Request $request)
    {
        Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'price' => 'required|integer',
            'decription' => 'required|string'
        ]);
	
        $name = $request->input('name');
        $price = $request->input('price');
        $description = $request->input('description');

        $item = Item::create([
            'name' => $name,
            'price' => $price,
            'description' => $description
        ]);

        $email = Auth::user()->email;
        Mail::to($email)->send(new ItemCreateMail($item->name));

        return redirect('/home');
    }
}

こちらが After です。

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Http\Requests\ItemCreateRequest;

class ItemController extends Controller
{
    public function store(ItemCreateRequest $request)
    {
        $attributes = $request->only(['name', 'price', 'description']);

	CreateItemJob::dispatch($request->user()->email, $attributes);

        return redirect('/home');
    }
}

いかがでしょうか? こんなに記述が少なくなるもんなんですね!

おまけ: Observerを使用するとこうなる

Observerはモデルのイベントの変更をキャッチできます。

ご存知でない方はこちらで解説しているので、ぜひご覧ください
https://zenn.dev/naoki_oshiumi/articles/9c4b7d8f580342

Controllerの中身はこれ以上コンパクトにはならないのですが、
Observerを使用するとこんなこともできますよーという紹介です。

今回は Itemが作成されるとメールが送信されるという要件だとしてObserverを使用していきます。

Observerを作成する

まずはコマンドでObserverを作成します。

 php artisan make:observer ItemObserver --model=Item

すると、 app/Observers/ItemObserver.php が作成されました。

産まれたてのObserverはこのような感じです。

<?php

namespace App\Observers;

use App\Models\Item;

class ItemObserver
{
    /**
     * Handle the Item "created" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function created(Item $item)
    {
        //
    }

    /**
     * Handle the Item "updated" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function updated(Item $item)
    {
        //
    }

    /**
     * Handle the Item "deleted" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function deleted(Item $item)
    {
        //
    }

    /**
     * Handle the Item "restored" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function restored(Item $item)
    {
        //
    }

    /**
     * Handle the Item "force deleted" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function forceDeleted(Item $item)
    {
        //
    }
}

こちらの created メソッドの部分に、メールを送信する処理を書いてあげます。

CreateItemJobに書いたメール送信部分をそのまま持ってきてあげればOKですね。

<?php

namespace App\Observers;

use App\Models\Item;
use App\Mail\ItemCreateMail; // 追記
use Illuminate\Support\Facades\Mail; // 追記
use Illuminate\Support\Facades\Auth; // 追記

class ItemObserver
{
    /**
     * Handle the Item "created" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function created(Item $item)
    {
        Mail::to(Auth::user()->email)->send(new ItemCreateMail($item->name));  // 追記、変更
    }

    /**
     * Handle the Item "updated" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function updated(Item $item)
    {
        //
    }

    /**
     * Handle the Item "deleted" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function deleted(Item $item)
    {
        //
    }

    /**
     * Handle the Item "restored" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function restored(Item $item)
    {
        //
    }

    /**
     * Handle the Item "force deleted" event.
     *
     * @param  \App\Models\Item  $item
     * @return void
     */
    public function forceDeleted(Item $item)
    {
        //
    }
}

Observerに処理を移行したら、結局認証中のユーザーを指定するのに Auth::user() を使用しなければならなくなるという(笑)

そして、AppServiceProvider の中のbootメソッドに、Observerを登録します。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;
use App\Models\Item; // 追記
use App\Observers\ItemObserver; // 追記

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        item::observe(ItemObserver::class); // 追記
    }
}

これでItemが作成されたらメールが送信されるようになるのでJobからはメール送信を削除しておいてくださいね。

<?php

namespace App\Jobs;

use App\Mail\ItemCreateMail; // 削除
use App\Models\Item;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail; // 削除

class CreateItemJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $email;
    private $attributes;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(string $email, array $attributes)
    {
        $this->email = $email;
        $this->attributes = $attributes;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Item::create($this->attributes);

        Mail::to($this->email)->send(new ItemCreateMail); // 削除
    }
}

終わりに

テキストだと一部分かりにくい部分があるかと思いますが、
Controllerを超コンパクトにするリファクタリング術を紹介してみました。

記事内で疑問におもったこと、質問、ミス指摘などはコメント欄からどうぞ!

ではまた!

Discussion

yuyu

いつも勉強になる記事ありがとうございます!
Controllerの記述はいつもちょっと多くなりがちで考え物だったんですが、鴛海さんのおかげで新たな視点が生まれました!