1. Home
  2. PHP
  3. Laravel
  4. LaravelでのADR(Action-domain-responder)実装

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

  • 公開日
  • 更新日
  • カテゴリ:Laravel
  • タグ:Laravel,Architecture,ADR,Json,5.8
LaravelでのADR(Action-domain-responder)実装

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

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

Contents

  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. 動作確認
    6. 参考

開発環境

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

  • 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');

動作確認

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

リクエストからレスポンスまでを 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 で実装できました。

まとめ

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

参考

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級