BEAR.SundayとDDD


アプリケーション設計

マニュアルのチュートリアルの記事などではリソースのリクエストメソッドで直接SQLを操作していますが、実際にはアプリケーションをどのように設計、実装するかはアプリケーション開発者の仕事です。

この記事はコンテキスト境界、ディレクトリ構成について考えるのフォローアップ記事で、BEAR.SundayでDDDを導入するときにどのような構成になるかを考えます。

PHP DDD Cargo Sample

PHP DDD Cargo Sampleとはエリック・エヴァンスのドメイン駆動設計で紹介されているパターンを実際に使用して作られたPHPのサンプルアプリケーションです。

このサンプルはPSR-7とPSR-15を使ったzend-expressive + Doctrine ORMで構成されていますが、これをBEAR.Sundayで利用する事を想定します。

DDD Compoentsフォルダ

まず、アプリケーションのトップディレクトリcargograph-traversalというDDDコンポーネントのフォルダを設置します。それぞれのコンポーネントはsrc/tests/ディレクトリを持っていてそれぞれプロジェクトとして独立しています。PHPの名前空間も独自に保持します。

autoloadのためにcomposer.jsonを編集します。

composer.json
"autoload": {
        "psr-4": {
            "MyVendor\\MyProject\\": "src/",
            "Codeliner\\CargoBackend\\"  : "ddd/cargo/src",
            "Codeliner\\GraphTraversalBackend\\"  : "ddd/graph-traversal/src"
        }
    }

後述するように独立したパッケージにしてもいいでしょう。

DDD src

DDDコンポーンネントのsrcフォルダは上記のようにディレクトリが分けられています。詳細はPHP DDD Cargo Sampleをご覧ください。

DDDコンポーンネントは原則、フレームワークアグノスティック、つまりBEAR.Sundayの特定の機能に依存しないように作成します。フレームワークの機能に依存してDDDを構築するのではなく、DDDコンポーネントに依存してBEAR.Sundayフレームワークを利用します。

UIレイヤー

サンプルのようにUIを全てJSで行えばBEAR.SundayはもちろんPHPからの依存をなくすことも出来ます。詳しくはJavascript UIをご覧ください。

HTTP Action

Cargoのルートを取得するHTTP Actionのコードは以下のようなものです。

final class GetCargo implements MiddlewareInterface
{
    /**
     * @var BookingService
     */
    private $bookingService;

    public function __construct(BookingService $bookingService)
    {
        $this->bookingService = $bookingService;
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
    {
        if (null === $trackingId = $request->getAttribute('trackingId')) {
            return new EmptyResponse(404);
        }

        try {
            $cargoRoutingDto = $this->bookingService->loadCargoForRouting($trackingId);

            return new JsonResponse($cargoRoutingDto->getArrayCopy());
        } catch (CargoNotFoundException $e) {
            return new EmptyResponse(404);
        }
    }
}

このアクションをBEAR.Sundayのリソースにインジェクトして利用することもできますが、Resourceのメソッドで同様の記述をする事も検討してみましょう。

上記のPSR-7/15対応のHTTP ActionはBEAR.Sundayでは以下のようになります。

final class Cargo extends ResourceObject
{
    /**
     * @var BookingService
     */
    private $bookingService;

    public function __construct(BookingService $bookingService)
    {
        $this->bookingService = $bookingService;
    }

    public function onGet(string $trackingId = null): ResourceObject
    {
        if (! $trackingId) {
            $this->code = 404;

            return $this;
        }
        try {
            $cargoRoutingDto = $this->bookingService->loadCargoForRouting($trackingId);
            $this->body = $cargoRoutingDto->getArrayCopy();

            return $this;
        } catch (CargoNotFoundException $e) {
            $this->code = 404;

            return $this;
        }
    }
}

また第3の選択肢としてBEAR.SundayをPSR7のmiddlewareでそのまま動作させることもできます。

BEAR.Sundayネイティブの記述はAPIのドキュメントの自動生成や優れたリソースキャッシュを使うか、他のフレームワークとの協調を重視してPSR対応にするかはトレードオフで、アプリケーション開発者の選択です。

終わりに

この記事で紹介したのはアプリケーションから可能な限りフレームワーク依存を取り除き、フレームワークのバージョンや密結合したコンポーネントからロックされないDDDのアプリケーションの可能性です。「BEAR.SundayでDDD」 ..ではありません。

dddフォルダ内のアプリケーションは独立したpackageとしてprivateなpackagistレポジトリに登録して複数のアプリケーション(例えば管理画面)から利用可能でしょう。分割されたアプリケーションはユースケースに応じて組み合わせるようなこともできるし、それぞれバージョンを持たして内部の互換性を担保しつつ疎にすることが出来ます。

"分散型の大きな泥だんご"になることなく、アプリケーションを分割可能にし、モノリスの良さを持ちつつ、マイクロサービスが持つメリットをかなりの程度を享受できるような構成が可能ではないでしょうか。

そういう大きな希望で今年のカレンダーを締めくくりたいと思います。作成してくれたkuma_nanaさん、執筆してくれたkaliboraさん、kawanamiyuuさん、記事を読んでくれた皆さん、BEAR.Sundayをいつも使ってくれている国内外の開発者の皆さん、今年もありがとうございました。

大好きなクマのぬいぐるみの「日曜日」は、なんにもしゃべらないし、だきついてもこない。ぼくのことをどう思ってるんだろう…

アクセル・ハッケ - クマの名前は日曜日