This package adds support for the Specification pattern in PHP. It helps to leverage complex specification by offloading all the tedious work. Implementing specification pattern is made simpler while leaves all logical wiring to the package.
While framework independent, you can easily integrate this package inside any PHP framework.
You need:
- PHP >= 8.0 but the latest stable version of PHP is recommended
Use composer:
composer require bakame/spec
or download the library and:
- use any other PSR-4 compatible autoloader.
- use the bundle autoloader script as shown below:
require 'path/to/spec/repo/autoload.php';
use Bakame\Specification\Chain;
$spec = Chain::one(new Rule1())
->and(new Rule2(), new Rule3())
->orNot(new Rule4());
$spec->isSatisfiedBy($subject);
"the specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic. The pattern is frequently used in the context of domain-driven design." -- wikipedia
Each rule that needs to be satisfied MUST implement the Bakame\Specification\Specification
interface.
This interface only contains one method isSatisfiedBy(mixed $subject): bool
. The method should
not throw
but if it does no mechanism MUST stop the exception from propagating outside the method.
Here's a quick example to illustrate the package usage.
First, we create a specification implementing class.
<?php
use Bakame\Specification\Specification;
final class OverDueSpecification implements Specification
{
public function __construct(
private DateTimeImmutable $date = new DateTimeImmutable('NOW', new DateTimeZone('UTC'))
) {
}
public function isSatisfiedBy(mixed $subject) : bool
{
return $subject instanceof Invoice
&& $subject->getDueDate() < $this->date;
}
}
Then using the Bakame\Specification\Chain
class and all the specifications
created, we apply all the specifications according to the business rules.
Here's how the wikipedia example is adapted using the library.
<?php
use Bakame\Specification\Chain;
$overDue = new OverDueSpecification();
$noticeSent = new NoticeSentSpecification();
$inCollection = new InCollectionSpecification();
$sendToCollection = Chain::one($overDue)
->and($noticeSent)
->andNot($inCollection);
foreach ($service->getInvoices() as $invoice) {
if ($sendToCollection->isSatisfiedBy($invoice)) {
$invoice->sendToCollection();
}
}
The Bakame\Specification\Chain
class exposes the following logical chaining methods
Logical methods | isSatisfiedBy will return true |
---|---|
Chain::and |
if the resulting the current specification and the those added are all satisfied |
Chain::or |
if at least one of the specifications is satisfied |
Chain::andNot |
if the current specification is satisfied AND the ones added are not |
Chain::orNot |
if the current specification is satisfied OR the ones added are not |
Chain::not |
will return the opposite of the current specification |
To initiate a new specification logic chain the class exposes 4 named constructors
Named constructor | returned instance |
---|---|
Chain::one |
new instance with a specification attach to it |
Chain::all |
with all specifications attach to it like Chain::and |
Chain::any |
with all specifications attach to it like Chain::or |
Chain::none |
with all specifications attach to it like Chain::not |
All the methods from the Bakame\Specification\Chain
accept variadic Bakame\Specification\Specification
implemented classes
except for the Chain::not
method which takes not parameter at all.
Creating more complex rules that you can individually test becomes trivial as do their maintenance.
To filter an array of subjects you can use the array_filter
function
<?php
$invoiceCollection = array_filter(
fn (Invoice $invoice): bool => $sendToCollection->isSatisfiedBy($invoice),
$respository->getInvoices()
);
foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
To filter a traversable structure or a generic iterator you can use the CallbackFilterIterator
class.
<?php
$invoiceCollection = new CallbackFilterIterator(
$respository->getInvoices(),
fn (Invoice $invoice): bool => $sendToCollection->isSatisfiedBy($invoice),
);
foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
The package can be used directly on collection that supports the filter
method like Doctrine
collection classes.
<?php
$invoiceCollection = $respository->getInvoices()->filter(
fn (Invoice $invoice): bool => $sendToCollection->isSatisfiedBy($invoice)
);
foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
An alternative for Laravel collections is to register a macro:
<?php
declare(strict_types=1);
use Bakame\Specification\Specification;
use Illuminate\Support\Collection;
Collection::macro('satisfies', fn (Specification $specification): Collection =>
$this->filter(
fn ($item): bool => $specification->isSatisfiedBy($item);
)
);
And then be used as described below:
$invoiceCollection = $invoices->all()->satifies($sendToCollection);
foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
Contributions are welcome and will be fully credited. Please see CONTRIBUTING and CODE OF CONDUCT for details.
The library:
- has a PHPSpec test suite
- has a coding style compliance test suite using PHP CS Fixer.
- has a code analysis compliance test suite using PHPStan and Psalm.
To run the tests, run the following command from the project folder.
$ composer test
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The package is a fork of the work of greydnls on greydnls/spec.
The MIT License (MIT). Please see License File for more information.