RitoLabo

LaravelでのADR(Action-domain-responder)実装

  • 公開:
  • 更新:
  • カテゴリ: PHP Laravel
  • タグ: Laravel,Architecture,ADR,Json,5.8

PHPフレームワークの基本といえばMVC(Model-View-Controller)ですが、LaravelではADR(Action-Domain-Responder)で実装を行う事もできます。

今回は、LaravelでのADR実装について紹介します。

アジェンダ
  1. 開発環境
  2. ADR(Action-domain-responder)
  3. LaravelでのADR実装
    1. Action
    2. Domain
    3. Responder
    4. Routing
    5. 動作確認
  4. Jsonを返すADR
    1. Action
    2. Domain
    3. Responder
    4. Routing
    5. 動作確認

開発環境

今回の開発環境については以下の通りです。

  • PHP 7.2
  • Laravel 5.8

尚、Laravelのルートディレクトリを「laravel/」とします。

ADR(Action-domain-responder)

ADR自体はソフトウェアアーキテクチャパターンの1つで、MVCの改良版という位置付けになっていますが、MVCと違うところの1つとして、1つのアクションに対してそのリクエストからレスポンスまでを定義する流れになっているところです。

Laravelに限らずCakePHPなどのPHPフレームワークもそうですが、何も考えずにMVCで実装した時に、1つのコントローラにアクションを複数定義していく形になると思います。 例えば以下のような形です。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SampleController extends Controller
{
public function index()
{

}

public function register()
{

}

public function update()
{

}

public function delete()
{

}
}

上記のように、SampleControllerにそれぞれ、「index」「register」「update」「delete」と4つのアクションが定義されています。 そして当然のようにこれらを定義していくわけですが、このパターンの場合それぞれのアクションの処理を実装していくと、最終的にコントローラが結構ファットになりがちです。 また、処理を記述する上でコンポーネント等を用いる場合に、一方のアクションでのみ使用するものがあったり、また別のコンポーネントは別のアクションでしか用いられていないなど、 1つのコントローラの中に、宣言されている全てのアクションについての記述が行われる事になり、気がついてみればまあまあカオスなコントローラが出来上がりがちです。 (なんていうか、はっきりしないしわかりにくいですよね)

対してADRの場合は、1つのアクションについてそれだけを定義していく形(1クラス1アクション)なので、シンプルなリクエスト&レスポンスの流れを作ることができます。 また、1つのクラスが持つ責務がはっきりするので、無駄がなくソースコードの見通しも良くなります。

ADRは、3つの役割から構成されます。

Action(アクション)
リクエストを受け取り、ドメインの処理結果をレスポンダに渡します。
Domain(ドメイン)
必要な処理を行い、結果を返します。ビジネスロジックはここに含まれます。
Responder(レスポンダ)
ドメインの処理結果を受け取り、必要な準備を行いレスポンスを返します。

LaravelでのADR実装

それでは実際にLaravelでADRを実装していきます。

Action

まずはアクションです。laravel/app/Http配下にActionsディレクトリを作成し、その中にSampleIndexAction.phpを作成します。

laravel
└─ app
├─ Http
│   ├─
Actions
│   │   └─
SampleIndexAction.php
laravel/app/Http/Actions/SampleIndexAction.php
<?php
declare(strict_types=1);

namespace App\Http\Actions;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Controllers\Controller;

use App\Domain\SampleIndexDomain AS Domain;

use App\Http\Responders\SampleIndexResponder AS Responder;

class SampleIndexAction extends Controller
{
protected $Domain;
protected $Responder;

public function __construct(
Domain $Domain,
Responder $Responder
)
{
$this->Domain = $Domain;
$this->Responder = $Responder;
}

/**
* @param Request $request
* @return Response
*/
public function __invoke(Request $request): Response
{
return $this->Responder->response(
$this->Domain->get()
);
}
}
  • アクションクラスではControllerクラスを継承しています。
  • コンストラクタではドメインオブジェクトとレスポンダオブジェクト(これらはこの後作成します)をコンストラクタインジェクションにて注入しています。
  • レスポンダに渡す処理をinvokeで定義しています。

__invoke()はPHPのマジックメソッドです。Laravelではルーティングの再にアクションを指定しない場合にこのマジックメソッドをコールします。 (ルーティングについてはこの後で解説します)

__invoke()の中はシンプルに、ドメインでの処理結果をレスポンダに渡しているだけになっています。

ちなみにここでは記述していませんが、もしリクエストパラメータやPOSTされてきたフォームの値をドメインの処理に渡したい場合は、ここでリクエストオブジェクトから取得して、ドメインのget()の引数に指定してあげれば渡すことができます。

Domain

次に、ドメインです。laravel/app配下にDomainディレクトリを作成し、その中にSampleIndexDomain.phpを作成します。

laravel
├─ app
│ ├─
Domain
│ │   └─
SampleIndexDomain.php
laravel/app/Domain/SampleIndexDomain.php
<?php
declare(strict_types=1);

namespace App\Domain;

class SampleIndexDomain
{
public function get()
{
/*
* 必要な処理あれこれ
* .
* .
* .
*/

return [
'name' => 'Laravel ADR Sample'
];
}
}

ここではビジネスロジックなど、必要な処理を定義します。あれこれ必要な処理を記述して、結果の値を返します。

サンプルコードがシンプル過ぎてあれですが、一応必要な処理を行ったという仮定で、nameキーを持った配列を結果という事にしてこれを返します。 もちろん、コレクションなどで返しても良くて、ここは必要に応じて実装できます。

Responder

続いてレスポンダです。laravel/app/Http配下にRespondersディレクトリを作成し、その中にSampleIndexResponder.phpを作成します。

laravel
├─ app
│ ├─ Http
│ │   ├─
Responders
│ │   │   └─
SampleIndexResponder.php
laravel/app/Http/Responders/SampleIndexResponder.php
<?php
declare(strict_types=1);

namespace App\Http\Responders;

use Illuminate\Http\Response;
use Illuminate\Contracts\View\Factory AS ViewFactory;

class SampleIndexResponder
{
protected $response;
protected $view;

public function __construct(Response $response, ViewFactory $view)
{
$this->response = $response;
$this->view = $view;
}

/**
* @param $data
* @return Response
*/
public function response($data): Response
{
if (empty($data)) {
$this->response->setStatusCode(Response::HTTP_NOT_FOUND);
$this->response->setContent(
$this->view->make('errors.404')
);
return $this->response;
}

$this->response->setContent(
$this->view->make('sample.index', ['data' => $data])
);
return $this->response;
}
}

コンストラクタではレスポンスとビューを組み立てる為のファクトリーオブジェクトをコンストラクタインジェクションにて注入しています。

responseメソッドでは、ドメインで処理した結果が無い場合にはHTTPステータスコード404と共にエラーページを表示するレスポンスを返し、 正常にデータが渡ってきた場合にはそのデータと共に指定するテンプレートをセットしたレスポンスを返しています。

尚、setStatusCode()でHTTPステータスコードをセットしない場合はデフォルトで200が返ります。なので上記コードでも成功時にはセットしていません。

Routing

最後にルーティングを行います。LaravelのルーティングはデフォルトでMVCの記述が楽になるように設定されているので、まずはこれを変更します。

laravel/app/Providers/RouteServiceProvider.php
// protected $namespace = 'App\Http\Controllers';
// ↓ 変更
protected $namespace = '';

上記の通り、デフォルトではルーティング時の名前空間にコントローラへのエイリアスが指定されているので、これを空にします。

尚、これを行うとエイリアスが外れるので、通常のコントローラへのルーティングの記述も変更する必要があります。 よって既存のアプリケーションにこれを行う場合は、他のMVCで実装済みのコントローラへのルーティングも修正する必要があるので注意が必要です。

エイリアスを外したら、ルーティングを記述します。

laravel/routes/web.php
Route::get('/sample', \App\Http\Actions\SampleIndexAction::class);
  • ルーティング先をフルネームスペースで指定します。
  • アクションのところで少し触れた通り、この記述ではアクションを指定しない形になっているので、__invoke()がコールされる事で動作するようになります。

ちなみに、MVCと共存させる場合、コントローラへのルーティングは以下のようになります。 (共存自体をおすすめしませんが、手っ取り早く試してみる場合の為に残します)

Route::post('/sample/register', '\App\Http\Controllers\SampleController@register');

動作確認

一通りの実装が完了したので、ブラウザからアクセスして確認してみます。

LaravelをADRで実装した場合の動作確認画面

リクエストからレスポンスまでをADRで実装し、表示された事が確認できました。

最初のMVCの例ではコントローラ内で「index」「register」「update」「delete」と4つのアクションを宣言していましたが、この例では「index」を実装した事になります。 つまり、コントローラからindexアクションと、それ専用に使用していたサービスなどを除去する事ができます。 残りの3つも同じ要領で実装していく事で、MVCではなくADRでのアプリケーション構築が行えます。

Jsonを返すADR

先程の例ではViewを用いて画面表示を行う場合を紹介しましたが、JSONを返すAPIの場合でももちろんADRを用いる事が可能です。 その場合は、シンプルにレスポンスをJSONで返すようにすれば実現できます。

Action

<?php
declare(strict_types=1);

namespace App\Http\Actions;

use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Http\Controllers\Controller;

use App\Domain\SampleJsonDomain AS Domain;

use App\Http\Responders\SampleJsonResponder AS Responder;

class SampleJsonAction extends Controller
{
protected $Domain;
protected $Responder;

public function __construct(Domain $Domain, Responder $Responder)
{
$this->Domain = $Domain;
$this->Responder = $Responder;
}

/**
* @param Request $request
* @return JsonResponse
*/
public function __invoke(Request $request): JsonResponse
{
return $this->Responder->response(
$this->Domain->get()
);
}
}

先程のサンプルコードとの違う点は返り値の型指定にJsonResponseオブジェクトを指定している点です。

Domain

<?php
declare(strict_types=1);

namespace App\Domain;

class SampleJsonDomain
{
public function get()
{
/*
* 必要な処理あれこれ
* .
* .
* .
*/

return [
'name' => 'Laravel ADR Sample'
];
}
}

ここはメイン処理部分なので、変更はありません。

Responder

<?php
declare(strict_types=1);

namespace App\Http\Responders;

use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\JsonResponse;

class SampleJsonResponder
{
/**
* @param $data
* @return JsonResponse
*/
public function response($data): JsonResponse
{
if (!empty($data)) {
$data = [
'status' => Response::HTTP_OK,
'data' => $data,
];
} else {
$data = [
'status' => Response::HTTP_NOT_FOUND,
'data' => [],
];
}

return response()->json($data);
}
}

ここでは、JsonResponseを返すようにしています。 そのため、responseヘルパのjson()メソッドを用いて返却するデータをjson化しています。 このメソッドを用いると、値をPHPのjson_encode()関数でjsonへ変換するのと共に、 Content-Typeヘッダーをapplication/jsonにセットしてくれます。

Routing

Route::get('/sample/json', \App\Http\Actions\SampleJsonAction::class);

動作確認

Jsonレスポンスを返すADRの動作確認

JSONを返す一連の流れをADRで実装できました。

まとめ

ADRは「1クラス1アクション」を原則としているのでクラス数は増えますが、見通しの良いシンプルな構成とソースコードによってもたらされる恩恵は大きいと思います。 また、必要によってリクエストやレスポンス部分はMVCよりも明快に指定していく事になるので、この一連の動作がどういう意図を持っているのかが、コードを共有する者の間でも伝わりやすいと思います。 ぜひ試してみてください。
サンプルコード

参考

Action Domain Responder - Paul M. Jones.
http://pmjones.io/adr/
Implementing ADR in Laravel - Martin Bean
https://martinbean.co.uk/blog/2016/10/20/implementing-adr-in-laravel/