[go: up one dir, main page]
More Web Proxy on the site http://driver.im/ PHP Mentors (Posts tagged generative.programming)
1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

Practical Symfony #28: Workflowerを使ったビジネスプロセスの管理

この記事はSymfony Advent Calendar 2015 25日目の記事です。前日の記事はqcmatsuokaさんの「SpBowerBundleのキャッシュエラーを解決する | QUARTETCOM TECH BLOG」でした。


WorkflowerBPMN 2.0に準拠するPHP向けのワークフローエンジンであり、2条項BSDライセンスの下でリリースされているオープンソース製品です。Workflowerの主な用途としては、人間を中心としたビジネスプロセスをPHPアプリケーションで管理することが挙げられます。

この記事では、Workflowerを使ったビジネスプロセスの管理をSymfonyアプリケーション上で行うために必要な作業について示します。

PHPMentorsWorkflowerBundleによるSymfonyインテグレーション

PHPMentorsWorkflowerBundleはWorkflowerをSymfonyアプリケーションで使うためのインテグレーションレイヤーで、以下の機能を提供します。

  • ワークフローに対応するDIコンテナサービスの自動生成とphpmentors_workflower.process_awareタグを使ったサービスオブジェクトの自動注入
  • Symfonyセキュリティシステムを使った業務担当者(パーティシパント)の割り当てとアクセス制御
  • Doctrine ORMを使ったエンティティのための透過的なシリアライゼーション・デシリアライゼーション
  • 複数のワークフローコンテキスト(BPMNファイルが保存されたディレクトリ)

WorkflowerおよびPHPMentorsWorkflowerBundleのインストール

最初に、Composerを使ってWorkflowerおよびPHPMentorsWorkflowerBundleをプロジェクトの依存パッケージとしてインストールします。

$ composer require phpmentors/workflower "1.0.*"
$ composer require phpmentors/workflower-bundle "1.0.*"

次に、PHPMentorsWorkflowerBundleを有効にするためにAppkernelを変更します。

...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            ...
            new PHPMentors\WorkflowerBundle\PHPMentorsWorkflowerBundle(),
        );
        ...

コンフィギュレーション

続いてPHPMentorsWorkflowerBundleのコンフィギュレーションを行います。以下に例を示します。

app/config/config.yml:

phpmentors_workflower:
    serializer_service: phpmentors_workflower.base64_php_workflow_serializer
    workflow_contexts:
        app:
            definition_dir: "%kernel.root_dir%/../src/AppBundle/Resources/config/workflower"
  • serializer_service - ワークフローのインスタンスとなるPHPMentors\Workflower\Workflow\Workflowオブジェクトのシリアライズに使用するDIコンテナサービスのIDを指定します。指定されたサービスにはPHPMentors\Workflower\Persistence\WorkflowSerializerInterfaceの実装が期待されています。デフォルトはphpmentors_workflower.php_workflow_serializerです。また、シリアライズ済みのオブジェクトをBase64エンコード・デコードするphpmentors_workflower.base64_php_workflow_serializerを使うこともできます。
  • workflow_contexts - ワークフローのコンテキストID毎にdefinition_dir(BPMNファイルが保存されたディレクトリ)を指定します。

BPMNを使ったワークフローの設計

BPMN 2.0をサポートするエディターを使ってWorkflowerで動作させるワークフローを定義します。最初は開始イベント、タスク、終了イベントのみで構成されたワークフローを定義し、それでワークフローの開始から終了までの動作が確認できたら、改めてワークフロー全体を設計し、定義するとよいでしょう。このBPMNファイルの名前はワークフローIDworkflow ID)として使われます。例えばLoanRequestProcess.bpmnのような名前(キャメルケースを推奨)で保存します。分岐に使うシーケンスフローの条件式はSymfony ExpressionLanguageコンポーネントの式として評価されます。シーケンスフローの評価順は不定であるため、他の分岐先と整合的な条件式を設定する必要があることに注意してください。条件式の中では、PHPMentors\Workflower\Process\ProcessContextInterface::getProcessData()から取得される連想配列のキーを使うことができます。

以下のスクリーンショットはEclipseで利用可能なBPMNエディターであるBPMN2 Modelerのものです。

Editing the Workflow

Workflowerがサポートするワークフロー要素

WorkflowerはBPMN 2.0のワークフロー要素のうち以下のものをサポートしています。サポート外の要素はWorkflowerで動作させることができませんのでご注意ください。

  • 接続オブジェクト(connecting objects)
    • シーケンスフロー(sequence flows)
  • フローオブジェクト(flow objects)
    • アクティビティ(activities)
      • タスク(tasks)
    • イベント(events)
      • 開始イベント(start events)
      • 終了イベント(end events)
    • ゲートウェイ(gateways)
      • 排他ゲートウェイ(exclusive gateways)

Note これらの要素はBPMNによるワークフロー定義を通してクライアントと共有されるワークフロードメインモデルを構成します。このドメインモデルはドメイン駆動設計(Domain-Driven Design: DDD)が提唱するユビキタス言語Ubiquitous Language)の語彙となるものです。このようなドメインモデルを筆者はユビキタスドメインモデル(Ubiquitous Domain Model)と呼んでいます。ドメイン特化言語(Domain-Specific Language: DSL)はドメインとそのクライアントの間でユビキタスドメインモデルを媒介する役割を担っているといえるでしょう。

ワークフローのインスタンスを表すエンティティの設計

特定のワークフローのインスタンス(Workflowerではプロセスと呼ばれています)を表す永続化対象のエンティティを設計し、アプリケーションに追加します。このエンティティは通常PHPMentors\Workflower\Process\ProcessContextInterfacePHPMentors\Workflower\Persistence\WorkflowSerializableInterfaceを実装します。PHPMentors\Workflower\Process\ProcessContextInterface::getProcessData()から返される連想配列は、シーケンスフローの条件式内で展開されます。

また、アプリケーションにおける必要性(例:データベースへの問い合わせ)に応じてWorkflowオブジェクトのプロパティのスナップショットを保持するプロパティを用意するとよいでしょう。例えば、アプリケーションで特定のアクティビティに留まるプロセスをデータベースから検索する必要がある場合、現在のアクティビティを表す$currentActivityプロパティをエンティティに追加します。以下に例を示します。

...
use PHPMentors\Workflower\Persistence\WorkflowSerializableInterface;
use PHPMentors\Workflower\Process\ProcessContextInterface;
use PHPMentors\Workflower\Workflow\Workflow;
...
class LoanRequestProcess implements ProcessContextInterface, WorkflowSerializableInterface
{
    ...
    /**
     * @var Workflow
     */
    private $workflow;

    /**
     * @var string
     *
     * @Column(type="blob", name="serialized_workflow")
     */
    private $serializedWorkflow;
    ...
    /**
     * {@inheritdoc}
     */
    public function getProcessData()
    {
        return array(
            'foo' => $this->foo,
            'bar' => $this->bar,
            ...
        );
    }

    /**
     * {@inheritdoc}
     */
    public function setWorkflow(Workflow $workflow)
    {
        $this->workflow = $workflow;
    }

    /**
     * {@inheritdoc}
     */
    public function getWorkflow()
    {
        return $this->workflow;
    }

    /**
     * {@inheritdoc}
     */
    public function setSerializedWorkflow($workflow)
    {
        $this->serializedWorkflow = $workflow;
    }

    /**
     * {@inheritdoc}
     */
    public function getSerializedWorkflow()
    {
        if (is_resource($this->serializedWorkflow)) {
            return stream_get_contents($this->serializedWorkflow, -1, 0);
        } else {
            return $this->serializedWorkflow;
        }
    }
    ...

ビジネスプロセスの管理のためのドメインサービスの設計

Workflowerをプロダクションレベルで使うためには、プロセスの開始やワークアイテムの割り当て・開始・完了等を担当するドメインサービスが必要になるでしょう。特定のワークフローのプロセスとドメインサービスを結びつけるためにはPHPMentors\Workflower\Process\ProcessAwareInterfaceを実装し、そのドメインサービスのDIコンテナサービスに対して、phpmentors_workflower.process_awareタグを付与します。以下に例を示します。

...
use PHPMentors\DomainKata\Usecase\CommandUsecaseInterface;
use PHPMentors\Workflower\Process\Process;
use PHPMentors\Workflower\Process\ProcessAwareInterface;
use PHPMentors\Workflower\Process\WorkItemContextInterface;
use PHPMentors\Workflower\Workflow\Activity\ActivityInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
...
class LoanRequestProcessCompletionUsecase implements CommandUsecaseInterface, ProcessAwareInterface
{
    ...
    /**
     * @var Process
     */
    private $process;

    ...
    /**
     * {@inheritdoc}
     */
    public function setProcess(Process $process)
    {
        $this->process = $process;
    }
    ...

    /**
     * {@inheritdoc}
     */
    public function run(EntityInterface $entity)
    {
        assert($entity instanceof WorkItemContextInterface);

        $this->process->completeWorkItem($entity);
        ...

対応するサービス定義は以下のようになります。

...
app.loan_request_process_completion_usecase:
    class: "%app.loan_request_process_completion_usecase.class%"
    tags:
        - { name: phpmentors_workflower.process_aware, workflow: LoanRequestProcess, context: app }
    ...

この例のような「プロセスと操作の組み合わせ」に対応したユースケースクラスの実装は基本的ものといえますが、プロセス操作の共通性と可変性を分析し、可変性を外部に切り出すことができれば単一のクラスに操作をまとめることもできるでしょう。

ビジネスプロセスの管理

最後に、プロセスの開始やワークアイテムの割り当て・開始・完了等を実行するためのクライアント(コントローラー、コマンド、イベントリスナー等)を実装する必要があります。これらのクライアントも可変性を外部に切り出すことができるはずです。

ここまでの作業が終われば、Webインターフェイスやコマンドラインインターフェイス(Command Line Interface: CLI)からビジネスプロセスに対する一連の操作を実行できるようになります。

BPMSによるジェネレーティブプログラミングの実現に向けて

この記事では、Workflowerを使ったビジネスプロセスの管理をSymfonyアプリケーション上で行うために必要な作業について見てきました。WorkflowerおよびPHPMentorsWorkflowerBundleが提供するのはBPMN 2.0のワークフロー要素に対応するWorkflowドメインモデルと基本的なインテグレーションレイヤーに留まるため、実際にアプリケーションに組み込むためにはさらなる作業(とスキル)が要求されるでしょう。それは決して簡単なことではありません。なぜなら、それは対象ドメインに適したBPMS(Business Process Management System)あるいはBPMSフレームワークの設計に他ならないからです。

また、BPMSによるソフトウェア開発はビジネスプロセスドメインにおけるジェネレーティブプログラミング(Generative Programming)の実践といえます。PHPで利用可能なBPMSがほとんど存在しない現在、これに挑戦する者だけがその果実を手にすることができるのです。

参考

workflow bpm generative.programming dsl symfony practical.symfony multiparadigm.design ddd

Practical Symfony #27: コンパイルタイムファクトリ(Compile Time Factories)

この記事はSymfony Advent Calendar 2015 8日目の記事です。前日の記事は@__tai2__さんの「DQLのJOIN WITH構文を使えば、無用な関係を定義せずにテーブルの結合ができる」でした。


ファクトリFactories)は、オブジェクトの生成(Creation)に関するデザインパターンで、オブジェクトまたはオブジェクトグラフの組み立て方法についての知識(構成の知識)を集約するものです。Eric Evans氏(@ericevans0)の提唱するドメイン駆動設計(Domain-Driven Design: DDD)のビルディングブロックの1つとしても知られています。

論理的にはファクトリは以下のような構造を持ちます。

Factories

クラスまたはメソッドによるファクトリ

クラスまたはメソッドを使ったファクトリはPHP + Symfonyの環境における基本型といえます。その構造は上記の図と同様になります。生成されるオブジェクトのバリアントはファクトリに与える引数の違いから生じます。変数値がランタイム(runtime)にならないと決まらない場合に特に有用ですが、クライアントコードでファクトリクラスを直接使うとクライアントとファクトリが結合する点には注意が必要です。

変数の値は構成の知識ではない

バリアントが少ない場合、異なる変数値の組を持つそれぞれのメソッドを定義すればよいと思うかもしれません。しかし、この場合は構成の知識がそれぞれのメソッドに重複して存在することに留意しなければいけません。例えば、生成対象クラスに可変点が追加された場合を想像してみてください。構成の知識と変数値の組は定義されるタイミングが異なるため、異なる場所に配置される方がより適切であるといえるのです。

DIコンテナを使ったファクトリ

DIコンテナはSymfonyフレームワークを支える基盤です。構成の知識を集約するという観点から見ると、DIコンテナはファクトリの一種といえます。生成されるオブジェクトのバリアントはDIコンテナのサービス定義の違いから生じます。変数値がソースコード記述時(source time)やアプリケーションのデプロイ時(deploy time)に定まるような場合に効果を発揮します。その構造は論理的には先ほどのものと変わりませんが、物理的にはクライアントがファクトリに依存しない点で異なります。

Compile Time Factories

DIコンテナを使ったファクトリでは、変数値はSymfonyアプリケーションのキャッシュクリア時に生成されるDIコンテナクラスのメソッド内に埋め込まれます。DIコンテナの生成(コンパイル)が行われるタイミングのことを私たちはコンパイルタイムcompile time)と呼んでいます。

ドメイン特化言語として表現される可変性

Symfonyの設定ファイルapp/config/config.yml等のコードは、問題ドメインにおける可変性の表現です。そこではソフトウェアの内部的な用語ではなく、そのドメインのクライアントとソフトウェアの間で共通に使われる用語(と構造)が使われます。ドメイン特化言語(Domain-Specific Language: DSL)は解決ドメインにおける実装コンポーネントの可変性を別の形で表現したものといえます。

Note このあたりの話は書籍「ジェネレーティブプログラミング」のp.146「5.9.7 コンフィギュレーションDSL」に詳しく書かれていますので興味のある方は是非ご覧ください。

DIコンテナを使ったファクトリでオブジェクトのバリアントを扱う場合、変数値はサービス定義やパラメーターではなくconfig.ymlで記述されるのが一般的です。なぜなら、config.ymlが配置されるパスapp/config/config.ymlに表されているように変数値はドメイン(フレームワーク)ではなくアプリケーションに属するものだからです。

問題:変数値は事前にわからないためファクトリサービスを定義できない

さて、ここで1つ問題があります。変数値は事前にわからないため、ソースコードを書いているタイミングではファクトリサービスを定義できないのです。

解決:エクステンションやコンパイラーパスを使ってサービス定義を生成する

この問題はメタプログラミングを使ってDIコンテナのサービスを定義することで解決できます。Symfonyにはそのようなプログラミングを行う場所として、エクステンション(Symfony\Component\DependencyInjection\Extension\ExtensionInterface)とコンパイラーパス(Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface)があります。例としてPHPMentorsWorkflowerBundleのコードを見てみましょう。

Workflowerでは1つ以上のワークフロー定義ファイル(BPMNファイル)を持つコンテキストを1つ以上定義することができます。それぞれのコンテキストはBPMNファイルを配置するディレクトリを1つ持ちます。PHPMentors\Workflower\Definition\Bpmn2WorkflowRepositoryオブジェクトはコンテキスト毎のBPMNファイルのパスを保持し、findById()によってワークフローID(ファイル名から拡張子を取り除いたもの)に対応するPHPMentors\Workflower\Workflow\Workflowオブジェクトを返します。このPHPMentors\Workflower\Definition\Bpmn2WorkflowRepositoryのサービス定義を生成するコードは以下のようになります。

PHPMentors\WorkflowerBundle\DependencyInjection\PHPMentorsWorkflowerExtension:

<?php
...
namespace PHPMentors\WorkflowerBundle\DependencyInjection;
...
class PHPMentorsWorkflowerExtension extends Extension
{
    ...
    /**
     * @param array            $config
     * @param ContainerBuilder $container
     */
    private function transformConfigToContainer(array $config, ContainerBuilder $container)
    {
        ...
        foreach ($config['workflow_contexts'] as $workflowContextId => $workflowContext) {
            $workflowContextIdHash = sha1($workflowContextId);
            $bpmn2WorkflowRepositoryDefinition = new DefinitionDecorator('phpmentors_workflower.bpmn2_workflow_repository');
            $bpmn2WorkflowRepositoryServiceId = 'phpmentors_workflower.bpmn2_workflow_repository.'.$workflowContextIdHash;
            $container->setDefinition($bpmn2WorkflowRepositoryServiceId, $bpmn2WorkflowRepositoryDefinition);

            $definitionFiles = Finder::create()
                ->files()
                ->in($workflowContext['definition_dir'])
                ->depth('== 0')
                ->sortByName()
                ;
            foreach ($definitionFiles as $definitionFile) {
                $workflowId = Bpmn2File::getWorkflowId($definitionFile->getFilename());
                $bpmn2FileDefinition = new DefinitionDecorator('phpmentors_workflower.bpmn2_file');
                $bpmn2FileDefinition->setArguments(array($definitionFile->getPathname()));
                $bpmn2FileServiceId = 'phpmentors_workflower.bpmn2_file.'.sha1($workflowContextId.$workflowId);
                $container->setDefinition($bpmn2FileServiceId, $bpmn2FileDefinition);

                $bpmn2WorkflowRepositoryDefinition->addMethodCall('add', array(new Reference($bpmn2FileServiceId)));

                $processDefinition = new DefinitionDecorator('phpmentors_workflower.process');
                $processDefinition->setArguments(array(pathinfo($definitionFile->getFilename(), PATHINFO_FILENAME), new Reference($bpmn2WorkflowRepositoryServiceId)));
                $processServiceId = 'phpmentors_workflower.process.'.sha1($workflowContextId.$workflowId);
                $container->setDefinition($processServiceId, $processDefinition);
            }
        }
    }
    ...

エクステンションとコンパイラーパスのどちらを使うべきかはケースバイケースですが、やることは変わりません。いずれの場合においても、DIコンテナのコンパイル時にファクトリが動作するように見えることから、私はこのような解決策をコンパイルタイムファクトリCompile Time Factories)と名付け、ソフトウェアパターンとして積極的に活用しています。

おわりに

コンパイルタイムファクトリを使うと、バリアントの定義をアプリケーションに委ねつつ、構成の知識をドメイン(フレームワーク)に集約することができます。また、コンパイルタイムファクトリを使うことで、従来必要とされたファクトリクラスの多くは不要になります。それは単なる機構を超えて、関連するドメインモデルにもいくらかの影響を与えることでしょう。James Coplien氏(@jcoplien)が書籍「マルチパラダイムデザイン」で述べたように、解決ドメインの構造が問題ドメインの構造を変化させるのですから。

参考

symfony practical.symfony design pattern generative.programming

PHPカンファレス2015 PHPメンターズセミナー「モデルを設計せよ!―ドメイン駆動設計を超えて」参加レポート

2015年10月03日にPHPカンファレンス2015内で、ミニカンファレンスとして開催されたPHPメンターズセミナー「モデルを設計せよ!―ドメイン駆動設計を超えて」に参加してきました。

モデルを設計せよ!―ドメイン駆動設計を超えて

視点

PHPメンターズ 後藤秀宣 @hidenorigoto

セッションのテーマは「視点」。いくつかの実例から、参加者が視点そのものについて実際に考えることができる体験的な講演でした。後藤さんのスライドに関しては公開は無しとのことです。このセミナーでは、この「視点」という言葉が全体を通じてのテーマになっていると思います。

特に印象深いのが「コップを空にする」話です。既にたくさんの知識を身につけた修行僧が、高名な僧侶のもとに学びに行きます。修行僧がひとしきり知識を披露した後、高名な僧侶は空の茶碗にお茶をそそぎ、そそぎ続けて、茶碗からお茶が溢れてしまってもそれをとめません。

高名な僧侶は修行僧に、身につけたあふれんばかりの知識を脇において、頭のなかをいったん空にして、新しい知識を吸収する大切さを教えたかったのです。後藤さんは、いったんこれまで積み上げた知識は飲み干して、そこにこのセミナーで得られる知識をたくさん注いで帰ってもらいたいと話されていました。

フォーム検討に対する概念フォーム理論からのアプローチ

名古屋経済大学 経営学部教授 中西昌武

フォームというのは帳票(Webシステム等では画面)のことです。リレーショナルデータベースモデルにはコッド博士による理論化・体系化がなされているのに、フォームの構造に関してはそれに相当する数学的基礎がない。ならばその理論を作ってしまおうということでフォーム構造の数学的な基礎を構築されています。

帳票の裏にある論理テーブルの構造をノードとパスとみなして、その繋がり方とアクセスするための経路(これがフォームの構造として表現される)を行列として定式化できます。これにより、グラフ理論を用いて生成可能なフォームパターンを網羅的に示すことができるというような理論でした。

確かにフォームをどのように構成するかなどは、実業務の中でも経験によって作っている部分もかなりあると思うのですが、それがちゃんと数学的に定式化できて、パターンを数え上げることもできるとしたら、例えばユーザはフォームの選択肢の中から特定のフォームパターンを選んで、あとは自動的に画面が生成されるということも可能になってきます。

また、フォームを選びとるということは、背後にある論理データモデルに対するどういう見方を選びとるかということであって、その選択したものが、ちゃんと論理的に妥当なことが数学的に保証されている、ということは非常に重要なことだと感じました。

中西先生は、以前にIT勉強宴会で講演されていて、その時のレポート記事の方も参考にして下さい。

興味を持たれた方は中西先生のこれまでの論文・論考も是非ご覧ください。現在、中西先生は情報システム学会で「超上流工程における要求分析への科学的アプローチ」研究会を開催されています。研究会への参加をご希望の方は第3回勉強会のご案内をご覧ください。

ドメイン駆動設計 ~ユーザー、モデル、エンジニアの新たな関係~

株式会社フュージョンズ 杉本啓 @sugimoto_kei

ドメイン駆動設計に対する見方をいくつかの視点の切り替えで明快に整理した名演だったと思います。

エリック・エヴァンスのDDD本は示唆深く、ドメインモデルの設計に関して深い洞察を得られる本だと思うのですが、読み解くのがとても難しい本だとも思っています。杉本さんの講演(資料)を横においてエバンスのDDDを読みなおしてみるとまた新たな発見があるかもしれません。

一つ目の視点の切り替えは、ドメインモデル自体が存在する領域をどこに置くかということです。ドメインのモデルですから、字義通りに捉えるとドメイン領域に関するモデルと考えてしまいそうです。

しかし杉本さんは、ソースコード管理システム(GitとSubversion)の例を用いて、例えばコミットという概念はドメインの領域にではなくむしろ具体的なツールの側のソリューションから現れてくる概念だと説明します。そこから、ドメインモデルはドメイン(問題領域)の側から自然に立ち現われてくる(見つけだすとでも言ったらいいのでしょうか)ものではなく、むしろソリューションの中から我々が意図的に設計しようとするところから現れる解決領域のためのモデルとして捉えます。

DDD本の中で船舶/コンテナが出てくる海運事業モデルの中で、船荷証券(B/L)というモデルを見出す場面がありますが、これはモデルの問題領域を深く考察することで得られるものではなく、貿易実務の中で伝統的に開発されて普及してきた情報処理に関する事務処理パターン(船荷証券は法制度化もされています)、つまり管理過程に着目することで当然のように出てくるモデルということになります。

似たような問題領域と情報処理モデルの関係には、会計分野における複式簿記、鉄道業務におけるダイヤグラムなどがあります。

ドメインモデルは、解決領域のモデルであるというのが杉本さんの指摘だと思います。

(船荷証券に関する議論は本ブログの記事PHPメンターズ -> Practical DDD #3: モデルの深さで詳しく議論されています。)

二つ目の視点の切り替えは、このようにドメインモデルを眺める視点を移すことが、分析と設計の線引きを変えることにつながるという点です。これにより ドメイン駆動設計を、1.エンジニアリング の対象を広げながら、2.その広がったエンジニアリング活動内部での分裂を防ぐ、つまりソリューションのために作り出されたモデルと実装モデルの乖離を防ぐものだと捉えることができるようになります。

このように整理すると、各種の分析手法―分析モデルから設計モデルに落としこむ一般的なオブジェクト指向分析や、管理過程に着目して情報の流れや帳簿組織を表すデータモデルを構築する現代的なデータ指向分析と構造を比較できて面白かったです。

image

そして三つ目の視点の切り替えは、エバンスのDDD本の構成に関するものです。確かにエバンスのDDD本を頭から読んでいると、第2部においていきなり実装寄りの個別の実装パターンに入るようでおやっと思ってしまいます。

あるいは、実装者の立場からすると馴染みのある用語や実践しやすいプログラムの構成要素が出てきて、思わず飛びつきそうになります。プログラムの中にエンティティやリポジトリを登場させることがドメイン駆動設計だと間違った解釈をしてしまう場合もあるかもしれません。

この点に関して、杉本さんは、DDD本を構成するパターンランゲージを「原則のレイヤ」と「実践のレイヤ」にわけ、前者をオブジェクト指向に依存しない普遍的な部分、後者をオブジェクト指向でドメイン駆動設計を実践するためのパターン群とみることを提案しました。

そして、前者は原則的なものなので、オブジェクト指向だけではなく、データ指向分析などの様々なパラダイムの実践手法に適用することが可能になると説明します。

このような3つの視点の切り替えを通して、エバンスのDDD本を読み直すと他にも色々な発見がありそうなので、近いうちに一通り読み直してみたいと思います。

Frameworks We Live By: Design by day-to-day framework development: Multi-paradigm design in practice

PHPメンターズ 久保敦啓 @iteman

James O. Coplienのマルチパラダイムデザインは1998年に最初の版が上梓された本なので、実はエリック・エヴァンスのドメイン駆動設計よりも先発の書籍になります。

にも関わらず、問題ドメインの概念に根ざした分析/設計/実装における単一のモデルの構築を目指すという目標は同じでありながら、そこに至るための方法論に関してはCopeのマルチパラダイムデザインの方がエヴァンスのドメイン駆動設計にはなかった設計手法を明示している、というのが久保さんの主張です。

マルチパラダイムデザインでは、人間が物事を認識するときの認知モデルに基づいて問題ドメインを分析していきます。久保さんの発表で出てくる、古典的カテゴリー理論と新しいカテゴリー理論の違いは、物事を生得的で客観的な事象として捉えようとするのか、経験に基づいた具体的・身体的な認知からの拡張によって得られるものなのかという違いだと思います。

Copeはここでいう認知に関しては後者の見方を基礎にしていて、問題ドメイン分析においてそういった認知機構を元に問題をサブドメインに分割して、そうして得た対象に対して、共通性分析、可変性分析を進めていきます。ここで重視されるのはやはり「審美眼、洞察、経験」によるものであって、例えば「よく知られたサブドメインは設計の優れたスタートポイント」などを考えるとよくわかります。

そのうえで、解決ドメイン分析、ドメインモデル設計(変換分析)に移るわけですが、ここでは問題ドメインの構造と解決ドメインの構造のマッピングをしながら分析していきます。そして解決ドメインの構造で問題ドメインの構造を再定義していきます。これは、何度か繰り返されるプロセスになります。つまり、解決ドメインが提供する抽象(構造)-型、クラス、継承、リレーション、エンティティ、値、パターンといった一群の文法要素やイディオム-が問題ドメインを洗練していくのです。すなわち、解決ドメインに対して利用することのできるパラダイムを用いて問題ドメインを再構築していくのです。

これは、先ほどの杉本さんの解決領域のためのドメインモデルにも繋がっていて、マルチパラダイムデザインのドメインモデル設計(変換分析)で再定義、洗練されたドメインモデルは、エリック・エヴァンスのDDDが目指している単一のモデルと等価なものになります。

これらのマルチパラダイムデザインの議論から久保さんは一歩進んで、問題分析で目の前にあるニーズを解決する特定の分析から得られる知見よりももっと広い視野で対象とするドメインのフレームワークを作るという視点を持ち込むことで、幅広い領域に適用できる共通性を発見することが出来るといいます。

ここで言うフレームワークは一般的に知られる、SymfonyやRuby on RailsのようなWebアプリケーションフレームワークだけではなく(もちろん、それらもある特定のドメインを対象としたフレームワークです)、様々な問題領域のドメインに対して適用されるフレームワークのことを指しているのだと思います。

例えば、発表の中であったワークフローエンジンもそうですし、ルールエンジン的なものや、もっとアプリ特有のドメイン用途のフレームワークも考えていいと思います。

それらのフレームワーク開発を通してドメインについて日常的に考え指向することで、プログラマー自身が設計者となって、実装の中でドメインモデルを洗練させていくモデラーとなるというのが久保さんから一番大きく受け取ったメッセージです。最後の「Code the Domain! You are a Domain Coder!」、ドメインをコードにせよ、あなたがドメインコーダーなのだという言葉は、ドメイン駆動設計を超えて、さらにマルチパラダイムデザインを超えて、その先にあるものとしてとても希望の持てるものだと思いました。

Coding We Live By ~モデルと実装の今と未来~

PHPメンターズ 後藤秀宣 @hidenorigoto

この講演で後藤さんは、久保さんまでの講演で辿り着いたドメインモデルを設計しながら実装する、実装しながら設計するということを、もう一度興味深い実例を示して強調します。

これは、後藤さんが実際に取り組んだアプリケーションなのですが、幾つかのリストを組合せてデータを生成する機能が既存機能としてありました。それは、元ネタのリストからいったん全パターンを組み上げて「分割」するという構造になっていたそうです。業務側の人もその機能のことを「分割」と呼んでいました。それ自体はとても自然な設計で作りも悪くはなかったのですが、多数の機能追加や変更の結果、プログラムの変更がとても大変な状況になっていたようです。そこで、後藤さんが入った時に、それまでの他のアプリケーションでの経験から「部分集合を作る」「集合の直積を作る」ということとその集合演算で処理を表現できることに気付いて、実際にその演算をするためのライブラリを実装しました。そして、その集合演算をサブドメインとして切り出すことで、処理を簡素化するとともに変更に対して柔軟に対応することができるようになったということです。

このような実践を行うためには2つの条件、すなわち、既に後藤さんの頭のなかのツール箱にそういう解法に対するアイデアがあったことと、後藤さん自身がそのアイデアを実装してそれが有効であることを証明することができたということが重要であったと思います。

セミナー全体を通して「視点」の切り替えが新しい気づきや理解を促してくれて、ドメイン駆動設計を横糸に、ドメインとコードというものが強く結びついて、実装者がドメインモデラーなのだという主張をはっきりと感じ取れるようになりました。

後藤さんは「視点」の重要性を示し、中西先生は、フォーム構造という要求分析の要素にある視点(フォームを選びとるということ)から導き出される構造の数学的理論的な土台を与えてくれました。杉本さんは、ドメイン駆動設計を題材にとり、今までとは違った視点でそれを解釈し直すことで、ドメイン駆動設計をより納得行くかたちで再定義してくれました。久保さんは、そのドメイン駆動設計の先にあるマルチパラダイムデザインによる設計からさらにフレームワークによる設計への道筋を、自らの経験と成果から、これも視点を変えて設計活動を捉え直すことで、示してくれました。また、フレームワークと捉え直したドメインの設計活動の中で、実装の中でドメインモデルを設計する、実装という活動そのものが設計なのだというメッセージを提示してくれます。

そして、最後に「ドメインモデルを設計しながら実装する、ドメインモデルを実装しながら設計する」という後藤さんの言葉でセミナーは結ばれました。

セミナー全体の感想

久保さんのセッションや質疑応答の中で、ドメインエキスパートと並立する存在としてのドメインモデラーという言葉がでたのですが、ドメインモデラーは、杉本さんの言葉で言えば解決領域(情報管理過程)の専門家としてドメインエキスパートと協同してドメインモデルを作っていきます。

そのためには、特定の領域の情報管理過程、情報モデルについて精通している必要があります。みなさんも、実装者であれば何らかの分野(業務モデルとも限りません、それはソーシャルゲームの場合もあるでしょうし、組み込み領域のプログラムの場合もあると思います)についての情報モデルの専門家であることが多いと思います。その解決領域に関する知見を深めて、道具箱に使えるツールを増やしておけば、それを実装の現場で応用することが出来る場面も増えるのではないかと思いました。

当日、その後の関連ツイートについて

当日のツイートやその後の関連ツイートがPHPカンファレス2015 PHPメンターズセミナー「モデルを設計せよ!―ドメイン駆動設計を超えて」 - Togetterまとめにまとめられています。こちらも是非ご覧ください。特に最後の杉本さん(@sugimoto_kei)によるドメイン駆動設計の根底にある意図の考察は必見です!

design ddd multiparadigm.design event eric.evans james.coplien generative.programming dsl framework nakanishi.masatake sugimoto.kei practical.ddd

Practical Symfony #26: PHPMentorsPageflowerBundleを使ったページフロー定義と対話の管理

Symfony Advent Calendar 2014 (Qiita) 10日目


PHPMentorsPageflowerBundleは筆者が開発したSymfonyアプリケーション向けのページフローエンジンです。特徴としては、以下のものが挙げられます。

  • アノテーションによるページフロー定義
  • 対話の管理
  • アクセス制御されたアクション
  • 対話スコープのプロパティ
  • 対話開始直後に実行されるユーザー定義メソッド
  • 複数のブラウザーウィンドウまたはタブのサポート

PHPMentorsPageflowerBundleを使うと、コントローラーに断片的に埋め込まれたページフローに関するコードを明示的な定義で置き換えることができます。また、対話対話スコープのプロパティの導入によってコントローラーの状態管理コードを大幅を削減することができます。

では、早速コードを見てみましょう。以下はSymfony2ベースのユーザー登録サンプルのコードです。

Example\UserRegistrationBundle\Controller\UserRegistrationController:

<?php
/*
 * Copyright (c) 2012-2014 KUBO Atsuhiro <kubo@iteman.jp>,
 *               2014 YAMANE Nana <shigematsu.nana@gmail.com>,
 * All rights reserved.
 *
 * This file is part of PHPMentors_Training_Example_Symfony.
 *
 * This program and the accompanying materials are made available under
 * the terms of the BSD 2-Clause License which accompanies this
 * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause
 */

namespace Example\UserRegistrationBundle\Controller;

use PHPMentors\DomainKata\Usecase\UsecaseInterface;
use PHPMentors\PageflowerBundle\Annotation\Accept;
use PHPMentors\PageflowerBundle\Annotation\EndPage;
use PHPMentors\PageflowerBundle\Annotation\Init;
use PHPMentors\PageflowerBundle\Annotation\Page;
use PHPMentors\PageflowerBundle\Annotation\Pageflow;
use PHPMentors\PageflowerBundle\Annotation\StartPage;
use PHPMentors\PageflowerBundle\Annotation\Stateful;
use PHPMentors\PageflowerBundle\Annotation\Transition;
use PHPMentors\PageflowerBundle\Controller\ConversationalControllerInterface;
use PHPMentors\PageflowerBundle\Conversation\ConversationContext;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

use Example\UserRegistrationBundle\Entity\User;
use Example\UserRegistrationBundle\Form\Type\UserRegistrationType;

/**
 * @Route("/users/registration", service="example_user_registration.user_registration_controller")
 * @Pageflow({
 *     @StartPage({"input",
 *         @Transition("confirmation"),
 *     }),
 *     @Page({"confirmation",
 *         @Transition("success"),
 *         @Transition("input")
 *     }),
 *     @EndPage("success")
 * })
 */
class UserRegistrationController extends Controller implements ConversationalControllerInterface
{
    const VIEW_INPUT = 'ExampleUserRegistrationBundle:UserRegistration:input.html.twig';
    const VIEW_CONFIRMATION = 'ExampleUserRegistrationBundle:UserRegistration:confirmation.html.twig';
    const VIEW_SUCCESS = 'ExampleUserRegistrationBundle:UserRegistration:success.html.twig';

    /**
     * {@inheritDoc}
     */
    private $conversationContext;

    /**
     * @var User
     *
     * @Stateful
     */
    private $user;

    /**
     * {@inheritDoc}
     */
    public function setConversationContext(ConversationContext $conversationContext)
    {
        $this->conversationContext = $conversationContext;
    }

    /**
     * @Init
     */
    public function initialize()
    {
        $this->user = new User();
    }

    /**
     * @return Response
     *
     * @Route("/")
     * @Method("GET")
     * @Accept("input")
     * @Accept("confirmation")
     */
    public function inputGetAction()
    {
        if ($this->conversationContext->getConversation()->getCurrentPage()->getPageId() == 'confirmation') {
            $this->conversationContext->getConversation()->transition('input');
        }

        $form = $this->createForm(new UserRegistrationType(), $this->user, array('action' => $this->generateUrl('example_userregistration_userregistration_inputpost'), 'method' => 'POST'));

        return $this->render(self::VIEW_INPUT, array(
            'form' => $form->createView(),
        ));
    }

    /**
     * @param  Request  $request
     * @return Response
     *
     * @Route("/")
     * @Method("POST")
     * @Accept("input")
     * @Accept("confirmation")
     */
    public function inputPostAction(Request $request)
    {
        if ($this->conversationContext->getConversation()->getCurrentPage()->getPageId() == 'confirmation') {
            $this->conversationContext->getConversation()->transition('input');
        }

        $form = $this->createForm(new UserRegistrationType(), $this->user, array('action' => $this->generateUrl('example_userregistration_userregistration_inputpost'), 'method' => 'POST'));
        $form->handleRequest($request);

        if ($form->isValid()) {
            $this->conversationContext->getConversation()->transition('confirmation');

            return $this->redirect($this->conversationContext->generateUrl('example_userregistration_userregistration_confirmationget'));
        } else {
            return $this->render(self::VIEW_INPUT, array(
                'form' => $form->createView(),
            ));
        }
    }

    /**
     * @return Response
     *
     * @Route("/confirmation")
     * @Method("GET")
     * @Accept("confirmation")
     */
    public function confirmationGetAction()
    {
        $form = $this->createFormBuilder(null, array('action' => $this->generateUrl('example_userregistration_userregistration_confirmationpost'), 'method' => 'POST'))
            ->add('prev', 'submit', array('label' => '修正する'))
            ->add('next', 'submit', array('label' => '登録する'))
            ->getForm();

        return $this->render(self::VIEW_CONFIRMATION, array(
            'form' => $form->createView(),
            'user' => $this->user,
        ));
    }

    /**
     * @param  Request  $request
     * @return Response
     *
     * @Route("/confirmation")
     * @Method("POST")
     * @Accept("confirmation")
     */
    public function confirmationPostAction(Request $request)
    {
        $form = $this->createFormBuilder(null, array('action' => $this->generateUrl('example_userregistration_userregistration_confirmationpost'), 'method' => 'POST'))
            ->add('prev', 'submit', array('label' => '修正する'))
            ->add('next', 'submit', array('label' => '登録する'))
            ->getForm();
        $form->handleRequest($request);

        if ($form->isValid()) {
            if ($form->get('prev')->isClicked()) {
                return $this->redirect($this->conversationContext->generateUrl('example_userregistration_userregistration_inputget'));
            }

            if ($form->get('next')->isClicked()) {
                $this->createUserRegistrationUsecase()->run($this->user);
                $this->conversationContext->getConversation()->transition('success');

                return $this->render(self::VIEW_SUCCESS);
            }
        }

        $this->conversationContext->getConversation()->transition('input');

        return $this->render(self::VIEW_CONFIRMATION, array(
            'form' => $form->createView(),
        ));
    }

    /**
     * @return UsecaseInterface
     */
    private function createUserRegistrationUsecase()
    {
        return $this->get('example_user_registration.user_registration_usecase');
    }
}

アノテーションによるページフロー定義

...
/**
 * ...
 * @Pageflow({
 *     @StartPage({"input",
 *         @Transition("confirmation"),
 *     }),
 *     @Page({"confirmation",
 *         @Transition("success"),
 *         @Transition("input")
 *     }),
 *     @EndPage("success")
 * })
 */
...

@Pageflowアノテーションによるページフロー定義では、ページとページ間の関係を記述します。ページは1つの@StartPage0以上の@Page1つの@EndPageで構成されます。@StartPage対話が開始された後に遷移するページ(開始ページ)、@EndPageはそこに遷移すると対話が終了するページ(終了ページ)を示します。ページ間の関係は@Transitionによって記述します。

対話の管理

対話はリクエストされたURLがページフローのコントローラーである場合に自動的に開始される、ページフローのインスタンスです。1つのページフローに対して複数の対話を実行することができます。

対話は固有のID(対話ID)によって識別されます。リダイレクトURLの生成等で対話IDが埋め込まれたURLが必要な場合はController::generateUrl()の代わりにConversationContext::generateUrl()を使うことができます。また、フォームではCONVERSATION_IDフィールドが自動的に提供されます。

開発者はコントローラーの中でConversation::transition()を呼び出すことにより、カレントページを変更します。Conversation::transition()によって終了ページに遷移すると、対話は自動的に破棄されます。

アクセス制御されたアクション

...
    /**
     * ...
     * @Accept("confirmation")
     */
    public function confirmationPostAction(Request $request)
    {
    ...

@Acceptアノテーションによって、アクションを実行可能なページをホワイトリスト形式で記述します。カレントページがリストに存在しない場合、HTTPステータスコード403が返されます。

対話スコープのプロパティ

...
    /**
     * @var User
     *
     * @Stateful
     */
    private $user;
    ...

@Statefulアノテーションによって、プロパティを対話に紐付けることができます。プロパティは対話に限定されたセッション変数のように振る舞います。

対話開始直後に実行されるユーザー定義メソッド

...
    /**
     * @Init
     */
    public function initialize()
    {
        $this->user = new User();
    }
    ...

@Initアノテーションが付与されたメソッドは、対話の開始直後に自動的に実行されます。これらのメソッドは主に@Statefulが付与されたプロパティの初期化のために使われます。

複数のブラウザーウィンドウまたはタブのサポート

ブラウザーウィンドウまたはタブでそれぞれ別々の対話を実行することができます。

PHPMentorsPageflowerBundleを使いはじめるには?

PHPMentorsPageflowerBundleを使いはじめるに際には、ページフローの登録を含めた具体的なコード[Controller] ユーザー登録のページフローを実装した。 · 270b1c3 · phpmentors-jp/phpmentors-training-example-symfonyが参考になるでしょう。

参考

symfony practical.symfony dsl pageflow generative.programming

設定の仕様とは

設定の仕様をどう表現するのか、という趣旨の記事がありました。



設定はDSLである

このブログの過去記事(Pieceの中のSymfony #4: Configコンポーネント)やWEB+DB PRESS vol.75でも書いていますが、設定とはDSLです。ですから、設定の仕様というのはDSLの言語仕様として表現するのが適切です。DSLという抽象によって、内部でどのように問題が解決されるのかということと、設定の記述とが切り離されます。


PHPでの実装例(Symfony/Config利用)

以降はPHPでの例になりますが、過去記事と同じくSymfony/Configコンポーネントを利用して、同じ設定ファイルを読み込むDSLを実装してみましょう(ソース)。

src/BlogConfiguration.php クラスに、コンフィギュレーションの構文(ツリー)を定義します。

<?php
namespace Example\Config;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Respect\Validation\Validator as v;

class BlogConfiguration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('blog');

        $rootNode
            ->children()
                ->arrayNode('user_blogs')
                    ->isRequired()
                    ->requiresAtLeastOneElement()
                    ->prototype('array')
                    ->children()
                        ->scalarNode('blog_url')
                            ->info('設定したいブログのURL')
                            ->isRequired()
                            ->validate()
                                ->ifTrue(function ($value) {
                                    return !v::call(
                                        'parse_url',
                                        v::arr()->key('scheme', v::startsWith('http'))
                                            ->key('host',   v::domain())
                                    )->validate($value);
                                })
                                ->thenInvalid('ブログURLが無効: %s')
                            ->end()
                        ->end()
                        ->enumNode('permission')
                            ->info('publicなら公開、privateなら非公開')
                            ->values(['public', 'private'])
                            ->isRequired()
                        ->end()
                        ->arrayNode('can_be_edited_by')
                            ->info('編集権限を持つユーザ')
                            ->isRequired()
                            ->requiresAtLeastOneElement()
                            ->prototype('scalar')
                        ->end()
                    ->end()
                ->end()
            ->end();

        return $treeBuilder;
    }
}

ターゲットがJSONファイルなので、JSONファイル用のローダークラスを用意します(src/JsonFileLoader.php)。

元記事の例では配列がむき出しで定義されていましたが、私の例ではblog.user_blogsというルートノードを想定しています。ローダーでこの差を吸収しています。

<?php
namespace Example\Config;

use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\Loader\FileLoader;

class JsonFileLoader extends FileLoader
{
    public function load($resource, $type = null)
    {
        $path = $this->locator->locate($resource);
        $config = ['blog' => ['user_blogs' => $this->loadFile($path)]];

        if (null === $config) {
            return;
        }

        $configuration = new BlogConfiguration();
        $processor = new Processor();
        $processedConfig = $processor->processConfiguration(
            $configuration,
            $config
        );

        return $processedConfig;
    }

    public function supports($resource, $type = null)
    {
        return is_string($resource) && 'json' === pathinfo(
            $resource,
            PATHINFO_EXTENSION
        );
    }

    private function loadFile($path)
    {
        return json_decode(file_get_contents($path), true);
    }
}

これで準備完了です。このDSLを元に設定を読み込んでみましょう(bin/test_json.php)。

<?php
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;

use Example\Config\JsonFileLoader;

require_once __DIR__.'/../vendor/autoload.php';

$locator = new FileLocator(__DIR__.'/../config');

$loaderResolver = new LoaderResolver([new JsonFileLoader($locator)]);
$delegatingLoader = new DelegatingLoader($loaderResolver);

try {
    $config = $delegatingLoader->load('config.json');
    var_dump($config);
} catch (InvalidConfigurationException $e)
{
    echo "コンフィギュレーション構文エラー".PHP_EOL;
    echo $e->getMessage().PHP_EOL;
}

実行結果

$ php bin/test_json.php
array(1) {
  'user_blogs' =>
  array(3) {
    [0] =>
    array(3) {
      'blog_url' =>
      string(32) "http://shibayu36.hatenablog.com/"
      'permission' =>
      string(6) "public"
      'can_be_edited_by' =>
      array(1) {
        [0] =>
        string(10) "shiba_yu36"
      }
    }
    [1] =>
    array(3) {
      'blog_url' =>
      string(40) "http://shibayu36-private.hatenablog.com/"
      'permission' =>
      string(7) "private"
      'can_be_edited_by' =>
      array(2) {
        [0] =>
        string(10) "shiba_yu36"
        [1] =>
        string(10) "shiba_yu37"
      }
    }
    [2] =>
    array(3) {
      'blog_url' =>
      string(24) "http://blog.example.com/"
      'permission' =>
      string(6) "public"
      'can_be_edited_by' =>
      array(1) {
        [0] =>
        string(12) "example-user"
      }
    }
  }
}

コンフィギュレーションのダンプ機能を使えば、infoやisRequiredの情報は出力できます(YamlDumperかXmlDumperしかないので、以下の例はYamlDumperで出力しています)。

$ php bin/dump.php
blog:
    user_blogs:           # Required

        # 設定したいブログのURL
        blog_url:             ~ # Required

        # publicなら公開、privateなら非公開
        permission:           ~ # One of "public"; "private", Required

        # 編集権限を持つユーザ
        can_be_edited_by:     [] # Required

今回のコンフィギュレーションでは、次のようなルールも組み込んであります。

  • blog_urlには、URL形式の文字列
  • permissionには、publicかprivate

設定に想定外の文字列があると、読み込み時に次のようにエラーを検出できます。

$ php bin/test_json.php
コンフィギュレーション構文エラー
The value "hogehoge" is not allowed for path "blog.user_blogs.0.permission". Permissible values: "public", "private"

グラマー定義は設定ファイルのフォーマットには依存していないため、次のようにYAML用のローダーを用意すれば、同じDSLをYAMLで記述して読み込むこともできます(src/YamlFileLoader.php bin/test_yaml.php)。

-
    blog_url: http://shibayu36.hatenablog.com/
    permission: public
    can_be_edited_by: [ shiba_yu36 ]
-
    blog_url: http://shibayu36-private.hatenablog.com/
    permission: private
    can_be_edited_by: [ shiba_yu36, shiba_yu37 ]
-
    blog_url: http://blog.example.com/
    permission: public
    can_be_edited_by: [ example-user ]

まとめ

設定の意図は問題空間の言語であり、DSLとして表現することでその意図を明確に表せるのと同時に、内部の実装と切り離すことができます。実際のアプリケーションでは、DSLスクリプトである設定ファイルを読み込んだ後、DIコンテナ等と連携しながら内部(解決)モデルの構成処理などを行います。リポジトリとして扱いたいといった解決のための要請は、この段階で組み込んでいくわけです。


参考

configuration dsl symfony generative.programming