20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LaravelAdvent Calendar 2022

Day 1

Laravelの基本機能を使ってControllerをリファクタリングしてみた

Last updated at Posted at 2022-11-30

皆さん、Laravel使いこなしていますかー^ ^

AdventCalendar初投稿となります!

「Laravel Advent Calendar 2022」初日ということで、今回はControllerに書かれた多くのコードをLaravelの基本的な機能を使ってきれいに分けていきたいと思います。

若干題材が無理矢理なところや一部Laravelの機能じゃないやん!という部分もございますが大目にみてください^^;

ではいきましょう!

今回出てくるLaravelの機能

今回リファクタリングするコード

今回リファクタリングするコードは記事の更新処理をするAPIです。

AdventCalendarController.php
class AdventCalendarController extends Controller
{
    const SECRET_KEY = 'eyJpdiI6Ilh2MU5UemxVWUVkd1ZGU3FheDE0Q2c9PSIsInZhbHVlIjoiZUFMYzE1RHFFTVBIcnlmdE9DUzYraXJsVUV0eHhzdWJUNFNud0lEZzFEUT0iLCJtYWMiOiI5MmNjMGZmZDlmNWU3MTIyZTQ5YmE0Yjg3ODU5ZTA0NTBmMTU2ZDRjMjZiMWM3NTUxNDc5ZmMzY2M4MzBlMzc3IiwidGFnIjoiIn0=';

    public function update(Request $request, int $id)
    {
        $article = Article::find($id);
        
        // 自分の投稿かどうかチェック
        if ($article->user_id !== auth()->id()) {
            abort(403);
        }

        // headerにtokenを持っているかどうかチェック
        if ($request->bearerToken() !== self::SECRET_KEY) {
            abort(403);
        }

        // バリデーション
        $validator = Validator::make($request->all(), [
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string', 'max:255'],
        ]);
        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], 400);
        }

        // 登録処理
        auth()->user()->post_count = ++auth()->user()->post_count;
        auth()->user()->save();
        $request->merge(['user_id' => auth()->id()]);
        $article = Article::create($request->only(['user_id', 'title', 'content']));
        
        return [
            'id'      => $article->id,
            'title'   => $article->title,
            'content' => $article->content,
        ];
    }
}

うわ〜〜!読めなくはないけど長い!!

今回はこちらをリファクタリングしてみます!

ルートモデル結合

ルートモデル結合はupdateアクションの引数で渡ってくる$idの数字の部分に直接モデルのインスタンスを渡すものです。

(??)

実際にコードを見てみましょう。
該当部分はこちらです。

AdventCalendarController.php
public function update(Request $request, int $id)
{
    $article = Article::find($id);

    // 略
}

上記のコードでは引数でルートで渡した数字を$idとして受け取って、その下でArticleモデルのインスタンスを取得しています。

ルートモデル結合を利用すると、ここのfind()している手間がなくなります。

まずルートの記述は以下のようにします。

routes/api.php
Route::post('article/{article}', [AdventCalendarController::class, 'update']);

{article}の部分はモデル名を記載します。

そして、アクションメソッドの引数を以下のようにします。

AdventCalendarController.php
public function update(Request $request, Article $article)
{
    // 略
}

これで第二引数の$articleには直接Articleのインスタンスが入ってくるためfind()する手間が省けます。

Controllerは以下のようになります。

AdventCalendarController.php
class AdventCalendarController extends Controller
{
    const SECRET_KEY = 'eyJpdiI6Ilh2MU5UemxVWUVkd1ZGU3FheDE0Q2c9PSIsInZhbHVlIjoiZUFMYzE1RHFFTVBIcnlmdE9DUzYraXJsVUV0eHhzdWJUNFNud0lEZzFEUT0iLCJtYWMiOiI5MmNjMGZmZDlmNWU3MTIyZTQ5YmE0Yjg3ODU5ZTA0NTBmMTU2ZDRjMjZiMWM3NTUxNDc5ZmMzY2M4MzBlMzc3IiwidGFnIjoiIn0=';

    public function update(Request $request, Article $article)
    {
        // 自分の投稿かどうかチェック
        if ($article->user_id !== auth()->id()) {
            abort(403);
        }

        // headerにtokenを持っているかどうかチェック
        if ($request->bearerToken() !== self::SECRET_KEY) {
            abort(403);
        }

        // バリデーション
        $validator = Validator::make($request->all(), [
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string', 'max:255'],
        ]);
        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], 400);
        }
        
        // 更新処理
        auth()->user()->post_count = ++auth()->user()->post_count;
        auth()->user()->save();
        $article->title = $request->title;
        $article->content = $request->content;
        $article->save();

        return [
            'id'      => $article->id,
            'title'   => $article->title,
            'content' => $article->content,
        ];
    }
}

認可

続いて認可処理になります。
認可とはリソースを取得・変更して良いかの権限チェックになります。

該当部分はこちら

// 自分の投稿かどうかチェック
if ($article->user_id !== auth()->id()) {
    abort(403);
}

今回はLaravelの認可処理のGateという機能を使ってみます。

GateはApp\Providers\AuthServiceProviderクラスのbootメソッド内に定義してきます。

App\Providers\AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
    // 略

    public function boot()
    {
        $this->registerPolicies();

        // 自分の記事かどうか
        Gate::define('update-article', function (User $user, Article $article) {
            return $user->id === $article->user_id;
        });
    }

api.phpで更新のルートにこちらの認可を適用させます。
※下記の適用方法ではルートモデル結合している必要があります。

routes/api.php
Route::post('article/{article}', [AdventCalendarController::class, 'update'])->can('update-article', 'article');

これで、自分の記事でない場合は自動的に403になるようになります。
※APIで実際に使うときは、上記の書き方だとHTMLを返却してしまうため、jsonにするならapp/Exceptions/Handler.phpなどで整形処理をするなど工夫してください。

Controllerは以下のようになります。

AdventCalendarController.php
class AdventCalendarController extends Controller
{
    const SECRET_KEY = 'eyJpdiI6Ilh2MU5UemxVWUVkd1ZGU3FheDE0Q2c9PSIsInZhbHVlIjoiZUFMYzE1RHFFTVBIcnlmdE9DUzYraXJsVUV0eHhzdWJUNFNud0lEZzFEUT0iLCJtYWMiOiI5MmNjMGZmZDlmNWU3MTIyZTQ5YmE0Yjg3ODU5ZTA0NTBmMTU2ZDRjMjZiMWM3NTUxNDc5ZmMzY2M4MzBlMzc3IiwidGFnIjoiIn0=';

    public function update(Request $request, Article $article)
    {
        // headerにtokenを持っているかどうかチェック
        if ($request->bearerToken() !== self::SECRET_KEY) {
            abort(403);
        }

        // バリデーション
        $validator = Validator::make($request->all(), [
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string', 'max:255'],
        ]);
        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], 400);
        }

        // 更新処理
        auth()->user()->post_count = ++auth()->user()->post_count;
        auth()->user()->save();
        $article->title = $request->title;
        $article->content = $request->content;
        $article->save();

        return [
            'id'      => $article->id,
            'title'   => $article->title,
            'content' => $article->content,
        ];
    }
}

Middleware

MiddlewareはControllerの前もしくは後に処理をすることができます。
例えば、デフォルトでログイン認証処理やtokenチェックなどはMiddlewareで行われています。

今回のコードではtokenを持っているかどうかをMiddlewareに移してみます。

該当のコードは以下です。

AdventCalendarController.php
const SECRET_KEY = 'eyJpdiI6Ilh2MU5UemxVWUVkd1ZGU3FheDE0Q2c9PSIsInZhbHVlIjoiZUFMYzE1RHFFTVBIcnlmdE9DUzYraXJsVUV0eHhzdWJUNFNud0lEZzFEUT0iLCJtYWMiOiI5MmNjMGZmZDlmNWU3MTIyZTQ5YmE0Yjg3ODU5ZTA0NTBmMTU2ZDRjMjZiMWM3NTUxNDc5ZmMzY2M4MzBlMzc3IiwidGFnIjoiIn0=';

// headerにtokenを持っているかどうかチェック
if ($request->bearerToken() !== self::SECRET_KEY) {
    abort(403);
}

ではphp artisan make:middleware CheckBearerTokenでMiddlewareのファイルを作成します。

作成したら、以下のように記述します。

app/Http/Middleware/CheckBearerToken.php
class CheckBearerToken
{
    const SECRET_KEY = 'eyJpdiI6Ilh2MU5UemxVWUVkd1ZGU3FheDE0Q2c9PSIsInZhbHVlIjoiZUFMYzE1RHFFTVBIcnlmdE9DUzYraXJsVUV0eHhzdWJUNFNud0lEZzFEUT0iLCJtYWMiOiI5MmNjMGZmZDlmNWU3MTIyZTQ5YmE0Yjg3ODU5ZTA0NTBmMTU2ZDRjMjZiMWM3NTUxNDc5ZmMzY2M4MzBlMzc3IiwidGFnIjoiIn0=';

    public function handle(Request $request, Closure $next)
    {
        // headerにtokenを持っているかどうかチェック
        if ($request->bearerToken() !== self::SECRET_KEY) {
            abort(403);
        }
        return $next($request);
    }
}

Middlewareができたらapp/Http/Kernel.phpに以下の記述をします。

app/Http/Kernel.php
protected $routeMiddleware = [
    // 略
    'check_token' => \App\Http\Middleware\CheckBearerToken::class, // <=こちら
    ];

ルートでこのアクションメソッドにMiddlewareを適用させます。

routes/api.php
Route::post('article/{article}', [AdventCalendarController::class, 'update'])->can('update-article', 'article')->middleware('check_token');

これでBearer tokenを持っていない場合は403になります。

Controllerは以下のようになります。

AdventCalendarController.php
class AdventCalendarController extends Controller
{
    public function update(Request $request, Article $article)
    {
        // バリデーション
        $validator = Validator::make($request->all(), [
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string', 'max:255'],
        ]);
        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], 400);
        }

        // 更新処理
        auth()->user()->post_count = ++auth()->user()->post_count;
        auth()->user()->save();
        $article->title = $request->title;
        $article->content = $request->content;
        $article->save();

        return [
            'id'      => $article->id,
            'title'   => $article->title,
            'content' => $article->content,
        ];
    }
}

スッキリしてきましたね!

バリデーション

該当のコードは以下です。

AdventCalendarController.php
// バリデーション
$validator = Validator::make($request->all(), [
    'title'   => ['required', 'string', 'max:255'],
    'content' => ['required', 'string', 'max:255'],
]);
if ($validator->fails()) {
    return response()->json([
        'errors' => $validator->errors(),
    ], 400);
}

バリデーションはFormRequestに移していきます。

ではphp artisan make:request UpdateArticleRequestでFormRequestのファイルを作成します。

作成したUpdateArticleRequest.phpを以下のように記述します。

app/Http/Requests/UpdateArticleRequest.php
class UpdateArticleRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string', 'max:255'],
        ];
    }
}

あとはアクションメソッドの第一引数の型をUpdateArticleRequestに変更します。

AdventCalendarController.php
public function update(UpdateArticleRequest $request, Article $article)
{
    // 略
}

これでupdateアクションが実行される前にバリデーションチェックをしてくれるようになります。
今回はAPIなのでレスポンスもjsonになるように工夫してみましょう。

app/Http/Requests/UpdateArticleRequest.php
class UpdateArticleRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string', 'max:255'],
        ];
    }

    protected function failedValidation(Validator|\Illuminate\Contracts\Validation\Validator $validator)
    {
        $res = response()->json([
            'errors' => $validator->errors(),
        ], 400);
        throw new HttpResponseException($res);
    }
}

バリデーションまで完了しました!

Controllerは以下のようになります。

AdventCalendarController.php
class AdventCalendarController extends Controller
{
    public function update(UpdateArticleRequest $request, Article $article)
    {
        // 更新処理
        auth()->user()->post_count = ++auth()->user()->post_count;
        auth()->user()->save();
        $article->title = $request->title;
        $article->content = $request->content;
        $article->save();

        return [
            'id'      => $article->id,
            'title'   => $article->title,
            'content' => $article->content,
        ];
    }

うおーーー!だいぶ綺麗になった!もう完成やん!

と、いきたいところですがもう少しお付き合いください!!!

更新処理(ビジネスロジック)をService層に分ける

さて、更新処理の部分ですが、そこまで複雑なことはしていないですしこのままでもいいのですが、
より抽象化及び共通化するために別のファイルに移していきます。

今回はServiceというもの作ってそちらに処理を書くようにします。
※Service自体はLaravelの機能ではありません。

ビジネスロジックは人によってサービスに分けたり、モデルに分けたり、ユースケースに分けたりと様々かと思いますが各々のルールに基づいて決めてください。

app/Services/ArticleService.phpを作成し、更新処理を下記のように記述します。

app/Services/ArticleService.php
class ArticleService
{
    // ユーザーが記事を更新する
    public function updateArticleByUser(User $user, Article $article, array $params): Article
    {
        // 投稿回数をプラスする
        $user->post_count = ++$user->post_count;
        $user->save();
        return $this->update($article, $params);
    }

    // 記事を更新する
    public function update(Article $article, array $params): Article
    {
        $article->title = $params['title'];
        $article->content = $params['content'];
        $article->save();
        return $article;
    }
}

汎用高くするため、updateArticleByUser()とupdate()を作ってみました。

Controllerは以下のようになります。

AdventCalendarController.php
class AdventCalendarController extends Controller
{
    public function __construct(private ArticleService $articleService){}

    public function update(UpdateArticleRequest $request, Article $article)
    {
        $updatedArticle = $this->articleService->updateArticleByUser(auth()->user(), $article, $request->all());

        return [
            'id'      => $updatedArticle->id,
            'title'   => $updatedArticle->title,
            'content' => $updatedArticle->content,
        ];
    }
}

ArticleServiceクラスはコンストラクターインジェクションで依存解決する形にしています。

APIでなければここまでで完成です!
returen view();で返してあげればOKです。

ただ今回はAPIなのでもう一つLaravelの機能をご紹介します。さぁ次でラストいきましょう。

JSONリソース

JSONリソースはモデルの情報をreturnさせる時に便利です。

実際にコードを見てみましょう。

まずphp artisan make:resource ArticleResourceでファイルを生成します。

ファイルに以下のように記述しましょう。

app/Http/Resources/ArticleResource.php
class ArticleResource extends JsonResource
{
    public function toArray($request)
    {
                parent::withoutWrapping(); // dataでwrapしない
        return [
            'id'      => $this->id,
            'title'   => $this->title,
            'content' => $this->content,
        ];
    }
}

以下のように書くことで、レスポンスパラメータを制限したり指定のデータに整形することができます。

さて、Controllerの最終系を見てみましょう。

AdventCalendarController.php
class AdventCalendarController extends Controller
{
    public function __construct(private ArticleService $articleService){}

    public function update(UpdateArticleRequest $request, Article $article)
    {
        $updatedArticle = $this->articleService->updateMyArticle(auth()->user(), $article, $request->all());
        return new ArticleResource($updatedArticle);
    }
}

なんと、updateアクションを2行にまで減らすことができました✨

ちなみに返り値は以下のようになります。

{
    "id": 2,
    "title": "Laravelの基本機能を使ってControllerをリファクタリングしてみた",
    "content": "Hello Laravel"
}

まとめ

お疲れ様でした。
最後まで見ていただきありがとうございますm(__)m

えいやー!でAdventCalendarいれてしまいましたが無事に記事を書くことができました!

明日から続く記事も楽しみにしています!

20
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?