packagefactory / specification
PHP规范模式的实现
Requires
- php: >=8.1
Requires (Dev)
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^9.5
- squizlabs/php_codesniffer: ^3.7
README
PHP规范模式的实现
规范模式是一种使用布尔逻辑在领域模型中表达业务规则的方法。它在下述文档中有详细描述: https://martinfowler.com.cn/apsupp/spec.pdf
安装
composer require packagefactory/specification
用法
编写规范
让我们假设以下(非常简化的)问题:您有一个应用程序,具有简单的用户注册工作流程。用户可以自由注册,但必须验证他们的电子邮件地址。如果一个用户在一段时间内没有验证他们的电子邮件地址,他们将通过电子邮件收到提醒,告知验证仍然到期。
如何使用规范模式对这个业务规则进行编码?
首先,让我们编写一个规范,检查给定的用户是否有一个已验证的电子邮件地址
use PackageFactory\Specification\Core\AbstractSpecification;
use Vendor\Project\Domain\User;
/**
* The `@extends` annotation makes sure that static analysis tools like
* phpstan understand that this specification handles `User`-objects
* only:
*
* @extends AbstractSpecification<User>
*/
final class HasVerifiedEmailAddressSpecification extends AbstractSpecification
{
public function isSatisfiedBy($user): bool
{
// In lieu of generics in PHP it is recommended to add a
// zero-cost assertion to ensure the type of the given value:
assert($user instanceof User);
return $user->emailAddress->isVerified;
}
}
然后,让我们编写一个规范,检查给定的用户是否在特定的参考日期之前注册过
use PackageFactory\Specification\Core\AbstractSpecification;
use Vendor\Project\Domain\User;
/**
* @extends AbstractSpecification<User>
*/
final class HasBeenRegisteredBefore extends AbstractSpecification
{
public function __construct(
private readonly \DateTimeImmutable $referenceDate
) {
}
public function isSatisfiedBy($user): bool
{
assert($user instanceof User);
return $user->registrationDate->getTimestamp() < $this->referenceDate->getTimestamp();
}
}
现在,我们可以使用规范API组合这两个规范,并表达我们的业务规则
// $twoWeeksAgo is a calculated \DateTimeImmutable
$needsReminderSpecification = (new HasBeenRegisteredBefore($twoWeeksAgo))
->andNot(new HasVerifiedEmailAddressSpecification());
$usersThatNeedReminder = $userRepository->findBySpecification($needsReminderSpecification);
foreach ($usersThatNeedReminder as $userThatNeedsReminder) {
$notificationService->sendReminderTo($userThatNeedsReminder);
}
API
每个规范都必须实现PackageFactory\Specification\Core\SpecificationInterface
。通常,自定义规范应该扩展PackageFactory\Specification\Core\AbstractSpecification
,该类实现了SpecificationInterface
的所有方法,除了isSatisfiedBy
。
SpecificationInterface
涵盖了以下方法
关于泛型:PHP没有内置泛型。然而,存在像phpstan这样的静态分析工具可以理解它们。
SpecificationInterface
附带一个注释,允许您指定规范应该覆盖的$candidate
的类型。因此,您的自定义规范实现应命名一个具体的
$candidate
类型,如下所示/** * @extends AbstractSpecification<MyClass> */ final class MyCustomSpecification extends AbstractSpecification { /** * @param MyClass $candidate * @return boolean */ public function isSatisfiedBy($candidate): bool { // ... } }
isSatisfiedBy
/**
* @param C $candidate
* @return boolean
*/
public function isSatisfiedBy($candidate): bool;
此方法检查给定的$candidate
,如果它满足规范则返回true
,如果不满足则返回false
。
由于PHP中没有泛型,建议在实现体的顶部添加一个零成本断言,以确保$candidate
的类型
/**
* @param MyClass $candidate
* @return boolean
*/
public function isSatisfiedBy($candidate): bool;
{
assert($candidate instanceof MyClass);
// ...
}
有关零成本断言的更多信息,请参阅: https://php.ac.cn/manual/en/function.assert.php
和
/**
* @param SpecificationInterface<C> $other
* @return SpecificationInterface<C>
*/
public function and(SpecificationInterface $other): SpecificationInterface;
此方法的结果是一个新的规范,该规范将由满足调用规范和$other
的$candidate
满足。
和Not
/**
* @param SpecificationInterface<C> $other
* @return SpecificationInterface<C>
*/
public function andNot(SpecificationInterface $other): SpecificationInterface;
此方法的结果是一个新的规范,该规范将由满足调用规范但不满足$other
的$candidate
满足。
或
/**
* @param SpecificationInterface<C> $other
* @return SpecificationInterface<C>
*/
public function or(SpecificationInterface $other): SpecificationInterface;
此方法的结果是一个新的规范,该规范将由满足调用规范或$other
(或两者)的$candidate
满足。
或Not
/**
* @param SpecificationInterface<C> $other
* @return SpecificationInterface<C>
*/
public function orNot(SpecificationInterface $other): SpecificationInterface;
此方法的结果是一个新的规范,该规范将由满足调用规范或不满足$other
(或两者)的$candidate
满足。
不是
/**
* @return SpecificationInterface<C>
*/
public function not(): SpecificationInterface;
此方法否定调用规范。这意味着:结果是满足调用规范不满足的$candidate
的规范。
贡献
我们乐意接受贡献。请发送给我们pull requests。
许可
查看LICENSE