DDD、PHP - 在哪里执行验证?

hop*_*sey 6 php validation domain-driven-design model domain-object

我最近开始玩 DDD。今天我在我的应用程序中放置验证逻辑时遇到问题。我不确定我应该选择哪一层。我在互联网上搜索,找不到解决我问题的统一解决方案。

让我们考虑以下示例。用户实体由值对象表示,例如 id (UUID)、年龄和电子邮件地址。

final class User
{
    /**
     * @var \UserId
     */
    private $userId;

    /**
     * @var \DateTimeImmutable
     */
    private $dateOfBirth;

    /**
     * @var \EmailAddress
     */
    private $emailAddress;


    /**
     * User constructor.
     * @param UserId $userId
     * @param DateTimeImmutable $dateOfBirth
     * @param EmailAddress $emailAddress
     */
    public function __construct(UserId $userId, DateTimeImmutable $dateOfBirth, EmailAddress $emailAddress)
    {
        $this->userId = $userId;
        $this->dateOfBirth = $dateOfBirth;
        $this->emailAddress = $emailAddress;
    }
}
Run Code Online (Sandbox Code Playgroud)

与业务逻辑无关的验证由 ValueObjects 执行。没关系。我在放置业务逻辑规则验证时遇到了麻烦。

假设,如果我们需要让年满 18 岁的用户拥有自己的电子邮件地址,该怎么办?我们必须检查今天的年龄,如果不正常则抛出异常。

我应该把它放在哪里?

  • 实体 - 在构造函数中创建用户实体时检查它?
  • 命令 - 在执行插入/更新/任何命令时检查它?我在我的项目中使用战术家,所以它应该是一份工作
    • 命令
    • 命令处理程序

在哪里放置负责检查存储库数据的验证器?

就像电子邮件的唯一性一样。我阅读了规范模式。可以吗,如果我直接在命令处理程序中使用它?

最后但并非最不重要。

如何将其与 UI 验证集成?

我上面描述的所有内容都是关于域级别的验证。但是让我们考虑从 REST 服务器处理程序执行命令。我的 REST API 客户端希望我在输入数据错误的情况下返回有关出错的完整信息。我想返回带有错误描述的字段列表。我实际上可以在 try 块中包装所有命令准备并侦听验证类型异常,但主要问题是它会为我提供有关单个错误的信息,直到第一个异常。这是否意味着我必须在控制器级别(即使用 zend-inputfilter - 我使用 ZF2/3)复制我的验证逻辑?听起来很矛盾...

先感谢您。

Tho*_*och 6

我将尝试一一回答您的问题,并另外给我两分钱,以及我将如何解决这些问题。

非业务逻辑相关的验证由 ValueObjects 执行

实际上 ValueObjects 代表来自您的业务领域的概念,因此这些验证实际上也是业务逻辑验证。

实体 - 在构造函数中创建用户实体时检查它?

是的,在我看来,您应该尝试在聚合中尽可能深地添加这种行为。如果你把它放在命令或命令处理程序中,你就会失去凝聚力,业务逻辑会泄漏到应用程序层。我什至会走得更远。问问自己这个问题,你的模型中是否有没有明确表达的隐藏概念。在您的情况下, anAdultUser和 an UnderagedUser(它们都可以实现 a UserInterface)实际上具有不同的行为。在这些情况下,我总是努力明确地对此进行建模。

就像电子邮件的唯一性一样。我阅读了规范模式。可以吗,如果我直接在命令处理程序中使用它?

如果您希望能够将复杂查询与逻辑运算符(尤其是读取模型)结合起来,则规范模式非常好。在你的情况下,我认为这是一种矫枉过正。在用例中添加一个简单的containsUserForEmail($emailValueObject)方法UserRepositoryInterface并从用例中调用它就可以了。

<?php
$userRepository
    ->containsUserForEmail($emailValueObject)
    ->hasOrThrow(new EmailIsAlreadyRegistered($emailValueObject));
Run Code Online (Sandbox Code Playgroud)

如何将其与 UI 验证集成?

因此,首先应该对相关字段进行客户端验证。使以正确的方式使用系统变得容易,而以错误的方式使用系统变得困难。

当然,仍然需要进行服务器端验证。我们目前使用模式验证方法,其中我们有一个中央模式注册表,我们从中获取给定有效负载的模式,然后可以根据该 JSON 模式验证 JSON 有效负载。如果失败,我们返回一个序列化的ValidationErrors对象。我们还通过Content-Type: application/json; profile=https://some.schema.url/v1/user#标头告诉客户端它如何构建有效的有效负载。

您可以在此处和此处找到有关如何在 CQRS 架构之上构建 RESTful API 的一些不错的文章。

  • 当您说“我们在命令中使用存储库”时,我希望您指的是命令处理程序。不应将存储库注入命令中。命令是一种特殊的 DTO(或消息),它携带用户意图的语义,并且应该只包含完成请求所需的数据。是的,在您从存储库加载聚合的处理程序中,调用`Invoice::addItems($invoiceItems)` 方法(可能由于业务规则而收到异常)并最后将聚合保存在单个事务边界内聚合代表。 (3认同)