この記事について
「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
Route::group(['prefix' => 'tasks', 'namespace' => 'Task', 'as' => 'task.'], function () {
Route::post('/', 'Create');
});
コントローラーには 1 アクションしか記述しないようにして、マジックメソッドである __invoke
が呼ばれるようにします。
Controller
まずは、完成した Controller の処理はこんなかんじになります。
<?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
<?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 から上のように呼ばれ、
<?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 の役目をしています。
中身は、
<?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
<?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 型のオブジェクトを返すようにしてあります。
<?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 になるんじゃないかって気がします。
もし似たようなかんじで実装されている方がいたら、ぜひご意見お聞かせいただければ、と思います