packagefactory/specification

PHP规范模式的实现

v1.0.0 2022-11-25 16:23 UTC

This package is auto-updated.

Last update: 2024-08-28 14:37:31 UTC


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