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

Laravel で Request, UseCase, Resource を使いコントロールフローをシンプルにする

More than 1 year has passed since last update.

この記事について

「Clean Architecture」(Robert C. Martin 著)に触発されて、ユースケースパターンを試してみたので、その記録です。

本書では UseCaseInteractor という名前になっていますが、本記事のサンプルでは UseCase としています。

あくまでも触発されただけなので、Clean Architecture に完全に沿ってるわけではないので、その点だけご注意ください、詳しくは書籍をお読みいただければ、と思います。

環境

  • PHP: 7.1.16
  • Laravel: 5.5.42

サンプルコード

「Clearn Architecture」にある Interactor についての記述を引用します。

ウェブサーバーは、ユーザーからの入力データを受け取り、左上のControllerに渡す。ControllerはプレインオールドなJavaオブジェクトにデータを詰め込み、InputBoundaryを経由して、UseCaseInteractorに渡す。UseCaseInteractorは、そのデータを解釈し、Entitiesのダンスを制御する。また、DataAccessInterfaceを使用して、Entitiesが使うデータをDatabaseからメモリに持ってくる。それが終わると、UseCaseInteractorはEntitiesからデータを収集し、OutputDataをプレインオールドなJavaオブジェクトとして生成する。OutputDataは、OutputBoundaryインターフェイスを経由して、Presenterに渡される。
Clean Architecture より

ちょっと長いですが、

リクエスト → Controller → InputBoundary → UseCaseInteractor → OutputData → OutputBoundary → Presenter → レスポンス

となるように、クラス構成をつくる、ということです。それぞれ Adapter を介して、入出力データを次の層に最適化された形式に変換してやる、というのがみそです。

本記事では、簡略化して、

リクエスト → Request → Controller → UseCase → OutputData → Resource → レスポンス

となるように構成しました。

違いは、リクエストデータの変換を Controller に渡ってきたあとにやるか、渡ってくる前にやるか、という点です(Laravel では、Request オブジェクトがメソッドインジェクションで渡ってくるので、前に変換することができます)。

お題

簡単なTODOアプリケーションで、タスク生成の API を実装します。

主な登場人物

モデル: タスク(Task)、ユーザー(User)

ユースケース: タスクの作成

入力と出力はいずれも JSON 形式であるとします。

Routing

エンドポイント: POST /api/tasks

api.php
Route::group(['prefix' => 'tasks', 'namespace' => 'Task', 'as' => 'task.'], function () {
    Route::post('/', 'Create');
});

コントローラーには 1 アクションしか記述しないようにして、マジックメソッドである __invoke が呼ばれるようにします。

Controller

まずは、完成した Controller の処理はこんなかんじになります。

Create.php
<?php

namespace App\Http\Controllers\Task;

use App\Http\Controllers\Controller;
use App\Http\Requests\Task\Create as Request;
use App\UseCases\Task\Create as UseCase;
use App\Http\Resources\TaskResource;

class Create extends Controller
{
    public function __invoke(Request $request, UseCase $useCase)
    {
        // 外部から渡されるデータを取得する
        $task = $request->makeTask();
        // ユースケースを実行し、レスポンスの元になるデータを受け取る
        $created = $useCase->invoke($task);
        // Event や Job が必要ならここに書く
        // TaskCreated::dispatch($created);
        // レスポンスを返す
        return new TaskResource($created);
    }
}

$request->makeTask() というのが見慣れない使い方かもしれません。

Request

Create.php
<?php

namespace App\Http\Requests\Task;

use App\Models\Task;
use Illuminate\Foundation\Http\FormRequest;

class Create extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'subject'  => 'required|max:255',
            'priority' => 'required|in:' . implode(',', Task\Priority::values()),
            'due_to'   => 'nullable|date',
        ];
    }

    public function makeTask(): Task
    {
        $task = new Task(
            ['created_by' => $this->user()->id] + $this->validated()
        );

        return $task;
    }
}

エンティティの生成と他のエンティティの関連付けを Request クラスでやっていて、これが InputBoundary の役目をしています。ユースケースに必要なドメインのオブジェクトを渡せるようにします。

UseCase

続いて UseCase の方は、

// ユースケースを実行し、レスポンスの元になるデータを受け取る
$created = $useCase->invoke($task);

Controller から上のように呼ばれ、

Create.php
<?php

namespace App\UseCases\Task;

use App\Models\Task;

class Create
{
    public function invoke(Task $task): Task
    {
        $task->save();
        // 他にも処理がある場合はここに色々書く
        return $task;
    }
}

こちらも公開メソッドは invoke ただひとつで、「タスク生成」のユースケースに関わる処理しか入れません。

(最初は、ここでも __invoke マジックメソッドを使いましたが、IDE でジャンプできなくなるので、 invoke というメソッドにしました)

入出力データは、ドメインの型であることが必要です。

ちなみに、UseCase は POPO ですが、コンストラクタインジェクションが使えるので、

public function __construct(HogeService $service)

のように、Service クラスをバインドすることができます。

Resource

Eloquent: API Resources - Laravel - The PHP Framework For Web Artisans

最後に、レスポンスに返す前に、エンティティの中身を選別して返す Resource クラスを使って、レスポンスを最適化します。

return new TaskResource($created);

賢いのは、渡されたエンティティが create されたものであれば、 それを識別して HTTP ステータス 201 を返してくれることです。

これが OutputBoundary の役目をしています。

中身は、

TaskResource.php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class TaskResource extends Resource
{
    public function toArray($request)
    {
        return [
            'id'       => $this->id,
            'subject'  => $this->subject,
            'priority' => $this->priority,
        ];
    }
}

Laravel で Model::toArray() の返り値にどのプロパティを含めるか、というのは、 $with$hidden$append を使うことが多いと思いますが、ユースケースに応じてさらにそこから必要なデータのみを抜き出したり、追加のプロパティを加えたりすることができます。

例として、上の3つのプロパティのみ返すようにしてありますが、他にも色々便利機能があるので、興味のある方は公式ドキュメントを読んでみてください。

余談: Enum について

Task::priority は Enum で実装してみたので、ついでにご紹介します。

myclabs/php-enum: PHP Enum implementation inspired from SplEnum

Priority.php
<?php

namespace App\Models\Task;

use MyCLabs\Enum\Enum;

/**
 * Class Priority
 * @package App\Models\Task
 *
 * @method static Priority HIGH()
 * @method static Priority NORMAL()
 * @method static Priority LOW()
 */
class Priority extends Enum
{
    const HIGH = 'high';
    const NORMAL = 'normal';
    const LOW = 'low';
}

ドメインレイヤー内ではこの型で扱えるように、モデル側では、以下のように Priority 型のオブジェクトを返すようにしてあります。

Task.php
<?php

namespace App\Models;

use App\Models\Task\Priority;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

/**
 * Class Task
 * @package App\Models
 *
 * @property int id
 * @property string subject
 * @property Priority priority
 * @property Carbon due_to
 * @property User creator
 * @property User assignee
 */
class Task extends Model
{
    protected $fillable = ['subject', 'priority', 'due_to', 'created_by'];
    protected $dates = ['due_to'];

    public function creator()
    {
        return $this->belongsTo(User::class, 'created_by');
    }

    public function assignee()
    {
        return $this->belongsTo(User::class, 'assigned_to');
    }

    public function getPriorityAttribute($value)
    {
        return new Priority($value);
    }
}

まとめ

  • 入出力のアダプタの役割を Request, Resource にやらせて、ドメインロジックを UseCase に閉じ込めることで、Controller がスリムに保たれる
  • TaskService のような大きくなりがちな Service クラスをつくらなくてよくなる(共通の処理があれば別のクラスにしましょう)
  • 入出力のデータをドメインの型に揃えることで、受け渡し時に型の恩恵を受けられるようになる

ちょっとアイデアを拝借しただけですが、ちょっとでも Clean になるんじゃないかって気がします。

もし似たようなかんじで実装されている方がいたら、ぜひご意見お聞かせいただければ、と思います :bow:

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
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