Note
This repository is archived, but the project is not. Development continues over here: https://codeberg.org/PackageFactory/specification
Implementation of the Specification pattern for PHP
The specification pattern is a way to express business rules in a domain model using boolean logic. It is described in detail in the following document: https://www.martinfowler.com/apsupp/spec.pdf
composer require packagefactory/specification
Let's presume the following (very simplified) problem: You've got an application with a simple user registration workflow. Users can register freely, but have to verify their E-Mail address. If a user didn't verify their E-Mail address for a period of time, they shall be reminded (via E-Mail) that verification is still due.
How can this business rule be codified using the Specification pattern?
First, let's write a specification that checks if a given user has a verified E-Mail address:
usePackageFactory\Specification\Core\AbstractSpecification;
useVendor\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> */finalclass HasVerifiedEmailAddressSpecification extends AbstractSpecification
{
publicfunctionisSatisfiedBy($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($userinstanceof User);
return$user->emailAddress->isVerified;
}
}Then, let's write a specification that checks if a given user has been registered before a specific reference date:
usePackageFactory\Specification\Core\AbstractSpecification;
useVendor\Project\Domain\User;
/** * @extends AbstractSpecification<User> */finalclass HasBeenRegisteredBefore extends AbstractSpecification
{
publicfunction__construct(
privatereadonly\DateTimeImmutable$referenceDate
) {
}
publicfunctionisSatisfiedBy($user): bool
{
assert($userinstanceof User);
return$user->registrationDate->getTimestamp() < $this->referenceDate->getTimestamp();
}
}We can now use the Specification API to combine both specifications and express our business rule:
// $twoWeeksAgo is a calculated \DateTimeImmutable$needsReminderSpecification = (newHasBeenRegisteredBefore($twoWeeksAgo))
->andNot(newHasVerifiedEmailAddressSpecification());
$usersThatNeedReminder = $userRepository->findBySpecification($needsReminderSpecification);
foreach ($usersThatNeedReminderas$userThatNeedsReminder) {
$notificationService->sendReminderTo($userThatNeedsReminder);
}Each specification must implement PackageFactory\Specification\Core\SpecificationInterface. Usually, a custom specification should extend PackageFactory\Specification\Core\AbstractSpecification, which implements all methods of the SpecificationInterface except for isSatisfiedBy.
The SpecificationInterface covers the following methods:
Note on Generics: PHP does not have built-in Generics. However, there's static analysis tools like phpstan that do understand them. The
SpecificationInterfacecomes with an annotation that allows you to specify the type of$candidateyour specification is supposed to cover.Your custom specification implementation should therefore name a concrete
$candidatetype like this:/** * @extends AbstractSpecification<MyClass> */finalclass MyCustomSpecification extends AbstractSpecification { /** * @param MyClass $candidate * @return boolean */publicfunctionisSatisfiedBy($candidate): bool { // ... } }
/** * @param C $candidate * @return boolean */publicfunction isSatisfiedBy($candidate): bool;This method checks the given $candidate and returns true if it satisfies the specification and false if it doesn't.
In lieu of generics in PHP it is recommended to add a zero-cost assertion at the top of the implementation body to ensure the type of $candidate:
/** * @param MyClass $candidate * @return boolean */publicfunction isSatisfiedBy($candidate): bool;
{
assert($candidateinstanceof MyClass);
// ...
}For more on zero-cost assertions see: https://www.php.net/manual/en/function.assert.php
/** * @param SpecificationInterface<C> $other * @return SpecificationInterface<C> */publicfunction and(SpecificationInterface$other): SpecificationInterface;The result of this method is a new specification that will be satisfied by a $candidate that satisfies both the calling specification and $other.
/** * @param SpecificationInterface<C> $other * @return SpecificationInterface<C> */publicfunction andNot(SpecificationInterface$other): SpecificationInterface;The result of this method is a new specification that will be satisfied by a $candidate that satisfies the calling specification and does not satisfy $other.
/** * @param SpecificationInterface<C> $other * @return SpecificationInterface<C> */publicfunction or(SpecificationInterface$other): SpecificationInterface;The result of this method is a new specification that will be satisfied by a $candidate that satisfies either the calling specification or $other (or both).
/** * @param SpecificationInterface<C> $other * @return SpecificationInterface<C> */publicfunction orNot(SpecificationInterface$other): SpecificationInterface;The result of this method is a new specification that will be satisfied by a $candidate that either satisfies the calling specification or does not satisfy $other (or both).
/** * @return SpecificationInterface<C> */publicfunction not(): SpecificationInterface;This method negates the calling specification. That means: the result is a specification that will be satisfied by a $candidate that does not satisfy the calling specification.
We will gladly accept contributions. Please send us pull requests.
see LICENSE