DDD,PHP - 在哪里执行验证?
DDD, PHP - where to perform the validation?
我最近开始玩 DDD。今天,我在将验证逻辑放入我的应用程序时遇到了问题。我不确定我应该选择哪一层。我在互联网上搜索,找不到解决我问题的统一解决方案。
让我们考虑以下示例。用户实体由 ValueObjects 表示,例如 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;
}
}
非业务逻辑相关的验证由 ValueObjects 执行。这很好。
我在设置业务逻辑规则验证时遇到问题。
假设我们需要让年满 18 岁的用户拥有自己的电子邮件地址怎么办?
我们必须检查今天的年龄,如果不正常则抛出异常。
我应该把它放在哪里?
- 实体 - 在构造函数中创建用户实体时检查它?
- 命令 - 在执行 Insert/Update/whatever 命令时检查它?我在我的项目中使用战术师,所以它应该是一份工作
- 命令
- 命令处理程序
负责与存储库检查数据的验证器放在哪里?
喜欢电子邮件的唯一性。我读到了规范模式。直接在Command Handler中使用可以吗?
最后但并非最不重要的一点。
如何与UI验证集成?
我上面描述的所有内容都是关于域级别的验证。但是让我们考虑从 REST 服务器处理程序执行命令。我的 REST API 客户希望我 return 在输入数据错误的情况下出现问题的完整信息。我想要 return 一个带有错误描述的字段列表。
我实际上可以将所有命令准备包装在 try 块中并监听验证类型的异常,但主要问题是它会给我有关 单个错误 的信息,直到第一个异常。
这是否意味着我必须在控制器级别复制我的验证逻辑(即使用 zend-inputfilter - 我正在使用 ZF2/3)?听起来前后矛盾...
提前致谢。
我会尽力一一回答您的问题,另外在这里和那里给我两分钱,以及 我 将如何解决这些问题。
Non business logic related validation is performed by ValueObjects
实际上 ValueObjects 代表您业务领域的概念,因此这些验证实际上也是业务逻辑验证。
Entity - check it while creating User entity, in the constructor?
是的,在我看来,您应该尝试将这种行为尽可能深入地添加到聚合中。如果将其放入命令或命令处理程序中,则会失去内聚性,并且业务逻辑会泄漏到应用程序层中。我什至会走得更远。问问自己,您的模型中是否存在未明确表示的隐藏概念。在您的情况下,AdultUser
和 UnderagedUser
(它们 可以 都实现 UserInterface
)实际上 有 不同的行为。在这些情况下,我总是努力明确地对此进行建模。
Like email uniqueness. I read about the Specification pattern. Is it ok, if I use it directly in Command Handler?
如果您希望能够将复杂查询与逻辑运算符结合起来(尤其是对于读取模型),规范模式非常有用。就您而言,我认为这是一种矫枉过正。在 UserRepositoryInterface
中添加一个简单的 containsUserForEmail($emailValueObject)
方法并从用例中调用它就可以了。
<?php
$userRepository
->containsUserForEmail($emailValueObject)
->hasOrThrow(new EmailIsAlreadyRegistered($emailValueObject));
How to integrate it with UI validation?
所以首先应该对相关字段进行客户端验证。让以正确的方式使用您的系统变得容易,以错误的方式使用它变得困难。
当然还需要服务器端验证。我们目前使用模式验证方法,我们有一个中央模式注册表,我们从中获取给定有效负载的模式,然后可以根据该 JSON 模式验证 JSON 有效负载。如果失败,我们 return 序列化 ValidationErrors
object。我们还通过 Content-Type: application/json; profile=https://some.schema.url/v1/user#
header 告诉客户端如何构建有效负载。
之上构建 RESTful API 的好文章
只是为了扩展 tPl0ch 所说的内容,因为我发现它很有帮助...虽然我已经很多年没有进入 PHP 堆栈,但无论如何,这主要是理论讨论。
在 DDD 的实际应用中面临的一个更大的问题是验证问题。传统逻辑会规定验证必须存在于某个地方,它确实应该存在于任何地方。当将其应用于 DDD 时,可能最让人们绊倒的是域的质量永远不会 "in an invalid state"。 CQRS 已经远远解决了这个问题,您正在使用命令。
就我个人而言,我这样做的方式是,命令是改变状态的唯一方式。即使我需要为复杂的操作创建域服务,也只有命令才能完成工作。传统的命令处理程序将针对聚合调度命令并将聚合置于过渡状态。所有这些都是相当标准的,但我另外将验证转换的责任委托给命令本身,因为它们也已经包含业务逻辑。例如,如果我正在创建一个新帐户,并且我需要名字、姓氏和电子邮件地址,我应该在命令中验证它是否存在,然后再尝试通过命令处理程序。因此,我的每个命令处理程序不仅具有命令意识,而且还具有命令验证器。
此验证程序可确保命令的状态不会危及域,这使我能够验证命令本身,并且在某个点上我不会因必须在基础架构中的某处进行验证而产生额外费用,或者执行。由于我必须改变状态的唯一方法是完全在命令中,所以我不会将任何逻辑直接放入域对象本身。这并不是说领域模型贫血,实际上远非如此。有一个假设,如果您不在域对象本身中进行验证,那么该域会立即变得贫乏。但是,聚合需要公开设置这些值的方法——通常是通过一种方法——并且转换命令以将这些值提供给该方法。您看到的一种半常见方法是将逻辑放入 属性 设置器中,但由于您一次只设置一个 属性,因此您可以更轻松地将聚合留在无效状态。如果您将命令视为为了将该状态更改为单个操作而被验证,您会看到该命令是聚合的逻辑扩展(并且从代码组织的角度来看,它非常接近(如果不是在)聚合)。
由于此时我只处理命令验证,因此我通常也会进行持久性验证。本质上,就在聚合被持久化之前,聚合的整个状态将被立即验证。最终目标是让命令持久化,这意味着我将在每个聚合中拥有一个持久性验证器,但命令验证器的数量与我拥有的命令一样多。那个单一的持久性验证器将提供绝对可靠的验证,即命令没有以违反总体域问题的方式改变聚合。它还将意识到单个聚合可以具有多个有效的过渡状态,这是不容易在命令中捕获的。对于多个状态,我的意思是聚合可能对持久性有效,就像 "insert" 对持久性一样,但可能对 "update" 操作无效。最简单的例子是我无法更新或删除尚未持久化的聚合。
所有这些都可以在我自己的实现中浮出水面 UI。 UI 将数据传递给应用程序服务,应用程序服务将创建命令,并将在我的处理程序上调用 "Validate" 方法,该方法将 return 命令中的任何验证失败没有执行它。如果存在验证错误,应用程序服务可以让步给控制器,return处理它发现的任何验证错误,并允许它们浮出水面。此外,预提交,可以发送数据,按照相同的路径进行验证,并且 return 那些验证错误而无需实际提交数据。这是两全其美的。如果用户提供无效输入,命令违规可能经常发生。另一方面,持久性违规应该很少发生,如果有的话,在测试之外。这意味着命令正在以域不支持的方式改变状态。
最后,post-命令的验证,应用服务可以执行它。我构建自己的基础架构的方式是命令处理程序知道命令是否在执行前立即得到验证。如果不是,命令处理程序将执行 "Validate" 方法公开的相同验证。然而,不同之处在于它将作为例外出现。此时的目标是停止执行,因为无效命令无法进入域。
虽然样本在 Java(同样,不是我选择的平台),但我强烈推荐 Vaughn Vernon 的 "Implementing Domain-Driven Design"。它确实吸收了 Evans 的 material 中的许多概念以及 DDD 范式的进步,例如 CQRS+ES。至少对我而言,Vernon 书中的 material 也是 "DDD Series" 书籍的一部分,它改变了我从根本上接近 DDD 的方式,就像 Blue Book 向我介绍它一样。
我最近开始玩 DDD。今天,我在将验证逻辑放入我的应用程序时遇到了问题。我不确定我应该选择哪一层。我在互联网上搜索,找不到解决我问题的统一解决方案。
让我们考虑以下示例。用户实体由 ValueObjects 表示,例如 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;
}
}
非业务逻辑相关的验证由 ValueObjects 执行。这很好。 我在设置业务逻辑规则验证时遇到问题。
假设我们需要让年满 18 岁的用户拥有自己的电子邮件地址怎么办? 我们必须检查今天的年龄,如果不正常则抛出异常。
我应该把它放在哪里?
- 实体 - 在构造函数中创建用户实体时检查它?
- 命令 - 在执行 Insert/Update/whatever 命令时检查它?我在我的项目中使用战术师,所以它应该是一份工作
- 命令
- 命令处理程序
负责与存储库检查数据的验证器放在哪里?
喜欢电子邮件的唯一性。我读到了规范模式。直接在Command Handler中使用可以吗?
最后但并非最不重要的一点。
如何与UI验证集成?
我上面描述的所有内容都是关于域级别的验证。但是让我们考虑从 REST 服务器处理程序执行命令。我的 REST API 客户希望我 return 在输入数据错误的情况下出现问题的完整信息。我想要 return 一个带有错误描述的字段列表。 我实际上可以将所有命令准备包装在 try 块中并监听验证类型的异常,但主要问题是它会给我有关 单个错误 的信息,直到第一个异常。 这是否意味着我必须在控制器级别复制我的验证逻辑(即使用 zend-inputfilter - 我正在使用 ZF2/3)?听起来前后矛盾...
提前致谢。
我会尽力一一回答您的问题,另外在这里和那里给我两分钱,以及 我 将如何解决这些问题。
Non business logic related validation is performed by ValueObjects
实际上 ValueObjects 代表您业务领域的概念,因此这些验证实际上也是业务逻辑验证。
Entity - check it while creating User entity, in the constructor?
是的,在我看来,您应该尝试将这种行为尽可能深入地添加到聚合中。如果将其放入命令或命令处理程序中,则会失去内聚性,并且业务逻辑会泄漏到应用程序层中。我什至会走得更远。问问自己,您的模型中是否存在未明确表示的隐藏概念。在您的情况下,AdultUser
和 UnderagedUser
(它们 可以 都实现 UserInterface
)实际上 有 不同的行为。在这些情况下,我总是努力明确地对此进行建模。
Like email uniqueness. I read about the Specification pattern. Is it ok, if I use it directly in Command Handler?
如果您希望能够将复杂查询与逻辑运算符结合起来(尤其是对于读取模型),规范模式非常有用。就您而言,我认为这是一种矫枉过正。在 UserRepositoryInterface
中添加一个简单的 containsUserForEmail($emailValueObject)
方法并从用例中调用它就可以了。
<?php
$userRepository
->containsUserForEmail($emailValueObject)
->hasOrThrow(new EmailIsAlreadyRegistered($emailValueObject));
How to integrate it with UI validation?
所以首先应该对相关字段进行客户端验证。让以正确的方式使用您的系统变得容易,以错误的方式使用它变得困难。
当然还需要服务器端验证。我们目前使用模式验证方法,我们有一个中央模式注册表,我们从中获取给定有效负载的模式,然后可以根据该 JSON 模式验证 JSON 有效负载。如果失败,我们 return 序列化 ValidationErrors
object。我们还通过 Content-Type: application/json; profile=https://some.schema.url/v1/user#
header 告诉客户端如何构建有效负载。
只是为了扩展 tPl0ch 所说的内容,因为我发现它很有帮助...虽然我已经很多年没有进入 PHP 堆栈,但无论如何,这主要是理论讨论。
在 DDD 的实际应用中面临的一个更大的问题是验证问题。传统逻辑会规定验证必须存在于某个地方,它确实应该存在于任何地方。当将其应用于 DDD 时,可能最让人们绊倒的是域的质量永远不会 "in an invalid state"。 CQRS 已经远远解决了这个问题,您正在使用命令。
就我个人而言,我这样做的方式是,命令是改变状态的唯一方式。即使我需要为复杂的操作创建域服务,也只有命令才能完成工作。传统的命令处理程序将针对聚合调度命令并将聚合置于过渡状态。所有这些都是相当标准的,但我另外将验证转换的责任委托给命令本身,因为它们也已经包含业务逻辑。例如,如果我正在创建一个新帐户,并且我需要名字、姓氏和电子邮件地址,我应该在命令中验证它是否存在,然后再尝试通过命令处理程序。因此,我的每个命令处理程序不仅具有命令意识,而且还具有命令验证器。
此验证程序可确保命令的状态不会危及域,这使我能够验证命令本身,并且在某个点上我不会因必须在基础架构中的某处进行验证而产生额外费用,或者执行。由于我必须改变状态的唯一方法是完全在命令中,所以我不会将任何逻辑直接放入域对象本身。这并不是说领域模型贫血,实际上远非如此。有一个假设,如果您不在域对象本身中进行验证,那么该域会立即变得贫乏。但是,聚合需要公开设置这些值的方法——通常是通过一种方法——并且转换命令以将这些值提供给该方法。您看到的一种半常见方法是将逻辑放入 属性 设置器中,但由于您一次只设置一个 属性,因此您可以更轻松地将聚合留在无效状态。如果您将命令视为为了将该状态更改为单个操作而被验证,您会看到该命令是聚合的逻辑扩展(并且从代码组织的角度来看,它非常接近(如果不是在)聚合)。
由于此时我只处理命令验证,因此我通常也会进行持久性验证。本质上,就在聚合被持久化之前,聚合的整个状态将被立即验证。最终目标是让命令持久化,这意味着我将在每个聚合中拥有一个持久性验证器,但命令验证器的数量与我拥有的命令一样多。那个单一的持久性验证器将提供绝对可靠的验证,即命令没有以违反总体域问题的方式改变聚合。它还将意识到单个聚合可以具有多个有效的过渡状态,这是不容易在命令中捕获的。对于多个状态,我的意思是聚合可能对持久性有效,就像 "insert" 对持久性一样,但可能对 "update" 操作无效。最简单的例子是我无法更新或删除尚未持久化的聚合。
所有这些都可以在我自己的实现中浮出水面 UI。 UI 将数据传递给应用程序服务,应用程序服务将创建命令,并将在我的处理程序上调用 "Validate" 方法,该方法将 return 命令中的任何验证失败没有执行它。如果存在验证错误,应用程序服务可以让步给控制器,return处理它发现的任何验证错误,并允许它们浮出水面。此外,预提交,可以发送数据,按照相同的路径进行验证,并且 return 那些验证错误而无需实际提交数据。这是两全其美的。如果用户提供无效输入,命令违规可能经常发生。另一方面,持久性违规应该很少发生,如果有的话,在测试之外。这意味着命令正在以域不支持的方式改变状态。
最后,post-命令的验证,应用服务可以执行它。我构建自己的基础架构的方式是命令处理程序知道命令是否在执行前立即得到验证。如果不是,命令处理程序将执行 "Validate" 方法公开的相同验证。然而,不同之处在于它将作为例外出现。此时的目标是停止执行,因为无效命令无法进入域。
虽然样本在 Java(同样,不是我选择的平台),但我强烈推荐 Vaughn Vernon 的 "Implementing Domain-Driven Design"。它确实吸收了 Evans 的 material 中的许多概念以及 DDD 范式的进步,例如 CQRS+ES。至少对我而言,Vernon 书中的 material 也是 "DDD Series" 书籍的一部分,它改变了我从根本上接近 DDD 的方式,就像 Blue Book 向我介绍它一样。