Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

Repository files navigation

Note

This repository is archived, but the project is not. Development continues over here: https://codeberg.org/PackageFactory/specification

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

Installation

composer require packagefactory/specification

Usage

Writing a 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);
}

API

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 SpecificationInterface comes with an annotation that allows you to specify the type of $candidate your specification is supposed to cover.

Your custom specification implementation should therefore name a concrete $candidate type like this:

/** * @extends AbstractSpecification<MyClass> */finalclass MyCustomSpecification extends AbstractSpecification
{
/** * @param MyClass $candidate * @return boolean */publicfunctionisSatisfiedBy($candidate): bool
{
// ...
}
}

isSatisfiedBy

/** * @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

and

/** * @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.

andNot

/** * @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.

or

/** * @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).

orNot

/** * @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).

not

/** * @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.

Contribution

We will gladly accept contributions. Please send us pull requests.

License

see LICENSE

About

Implementation of the Specification pattern for PHP

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages