Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: bakame-php/spec
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2.0.0
Choose a base ref
...
head repository: bakame-php/spec
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 8 commits
  • 12 files changed
  • 1 contributor

Commits on May 16, 2022

  1. Improve documentation

    nyamsprod committed May 16, 2022
    Copy the full SHA
    aa404c9 View commit details

Commits on May 17, 2022

  1. Copy the full SHA
    4e066f2 View commit details
  2. Improve interface docblock

    nyamsprod committed May 17, 2022
    Copy the full SHA
    59ec13b View commit details
  3. Improve test suite

    nyamsprod committed May 17, 2022
    Copy the full SHA
    4145b2c View commit details

Commits on Sep 4, 2022

  1. Copy the full SHA
    4b8f6f5 View commit details

Commits on Jan 23, 2023

  1. Copy the full SHA
    edf5d4f View commit details
  2. Resolved merge conflict

    nyamsprod committed Jan 23, 2023
    Copy the full SHA
    64ffc47 View commit details
  3. Copy the full SHA
    0ea74b4 View commit details
Showing with 167 additions and 44 deletions.
  1. +4 −7 .github/workflows/build.yml
  2. +100 −17 README.md
  3. +5 −5 composer.json
  4. +1 −3 src/All.php
  5. +13 −0 src/AllSpec.php
  6. +1 −3 src/Any.php
  7. +14 −0 src/AnySpec.php
  8. +5 −5 src/Chain.php
  9. +9 −0 src/ChainSpec.php
  10. +1 −3 src/None.php
  11. +13 −0 src/NoneSpec.php
  12. +1 −1 src/Specification.php
11 changes: 4 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -11,12 +11,8 @@ jobs:
continue-on-error: true
strategy:
matrix:
php: ['8.0', '8.1']
php: ['8.0', '8.1', '8.2']
stability: [prefer-stable]
include:
- php: '8.1'
flags: "--ignore-platform-req=php"
stability: prefer-stable
steps:
- name: Checkout code
uses: actions/checkout@v2
@@ -52,5 +48,6 @@ jobs:
- name: Run PHPStan static analysis
run: composer phpstan

- name: Run coding style check
run: composer phpcs
- name: Run Coding style rules
run: composer phpcs:fix
if: ${{ matrix.php == '8.1'}}
117 changes: 100 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Spec
# Complex Specifications Pattern in PHP

Implementing the Specification pattern in PHP
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.

[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
While framework independent, you can easily integrate this package inside any PHP framework.

[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build](https://github.com/bakame-php/spec/workflows/build/badge.svg)](https://github.com/bakame-php/spec/actions?query=workflow%3A%22build%22)
[![Latest Version](https://img.shields.io/github/release/bakame-php/spec.svg?style=flat-square)](https://github.com/bakame-php/spec/releases)
[![Total Downloads](https://img.shields.io/packagist/dt/bakame/spec.svg?style=flat-square)](https://packagist.org/packages/bakame/spec)
@@ -32,7 +36,7 @@ or download the library and:
~~~php
require 'path/to/spec/repo/autoload.php';

use Bakame\Http\Specification\Chain;
use Bakame\Specification\Chain;

$spec = Chain::one(new Rule1())
->and(new Rule2(), new Rule3())
@@ -49,12 +53,17 @@ 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](https://en.wikipedia.org/wiki/Specification_pattern)

How do I use it?
Usage
------------

Each rule that needs to be validated MUST implement the `Bakame\Specification\Specification` interface.
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. First, create a specification implementing class.
Here's a quick example to illustrate the package usage.

First, we create a specification implementing class.

~~~php
<?php
@@ -76,9 +85,8 @@ final class OverDueSpecification implements Specification
}
~~~

Then using the decorator/builder class `Bakame\Specification\Chain` and all the specifications
created, it becomes straightforward to logically apply all the specification
as expected by your business rules.
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](https://en.wikipedia.org/wiki/Specification_pattern#Example_of_use) is adapted using the library.

@@ -91,21 +99,18 @@ $overDue = new OverDueSpecification();
$noticeSent = new NoticeSentSpecification();
$inCollection = new InCollectionSpecification();

// example of specification pattern logic chaining
$sendToCollection = Chain::one($overDue)
->and($noticeSent)
->andNot($inCollection);

$invoiceCollection = $service->getInvoices();

foreach ($invoiceCollection as $invoice) {
if ($sendToCollection->isSatisfiedBy($invoice)) {
foreach ($service->getInvoices() as $invoice) {
if ($sendToCollection->isSatisfiedBy($invoice)) {
$invoice->sendToCollection();
}
}
~~~

The `Bakame\Specification\Chain` class exposes the following logical methods
The `Bakame\Specification\Chain` class exposes the following logical chaining methods

| Logical methods | `isSatisfiedBy` will return true |
|-----------------|-----------------------------------------------------------------------------------|
@@ -125,10 +130,88 @@ To initiate a new specification logic chain the class exposes 4 named constructo
| `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.
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.

Tips on how to validate a list of subject.
----

### Array

To filter an array of subjects you can use the `array_filter` function

```php
<?php
$invoiceCollection = array_filter(
fn (Invoice $invoice): bool => $sendToCollection->isSatisfiedBy($invoice),
$respository->getInvoices()
);

foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
```

### Traversable

To filter a traversable structure or a generic iterator you can use the `CallbackFilterIterator` class.

```php
<?php
$invoiceCollection = new CallbackFilterIterator(
$respository->getInvoices(),
fn (Invoice $invoice): bool => $sendToCollection->isSatisfiedBy($invoice),
);

foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
```

### Collections

The package can be used directly on collection that supports the `filter` method like `Doctrine` collection classes.

```php
<?php
$invoiceCollection = $respository->getInvoices()->filter(
fn (Invoice $invoice): bool => $sendToCollection->isSatisfiedBy($invoice)
);

foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
```

### Collection Macro

An alternative for Laravel collections is to register a macro:

```php
<?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:

```php
$invoiceCollection = $invoices->all()->satifies($sendToCollection);
foreach ($invoiceCollection as $invoice) {
$invoice->sendToCollection();
}
```

Contributing
-------

10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
@@ -33,15 +33,15 @@
"require-dev": {
"friends-of-phpspec/phpspec-code-coverage": "^v6.1.0",
"friendsofphp/php-cs-fixer": "^v3.8.0",
"phpspec/phpspec": "^7.2.0",
"phpstan/phpstan": "^1.6.8",
"phpstan/phpstan-strict-rules": "^1.2.3",
"vimeo/psalm": "^4.23.0"
"phpspec/phpspec": "^7.3.0",
"phpstan/phpstan": "^1.9.14",
"phpstan/phpstan-strict-rules": "^1.4.5",
"vimeo/psalm": "^5.1.0"
},
"scripts": {
"phpcs": "php-cs-fixer fix -vvv --diff --dry-run --allow-risky=yes --ansi",
"phpcs:fix": "php-cs-fixer fix -vvv --allow-risky=yes --ansi",
"phpstan": "phpstan analyse -c phpstan.neon --ansi",
"phpstan": "phpstan analyse -c phpstan.neon --xdebug --ansi",
"psalm": "psalm --show-info=true",
"phpspec": " XDEBUG_MODE=coverage phpspec run",
"test": [
4 changes: 1 addition & 3 deletions src/All.php
Original file line number Diff line number Diff line change
@@ -31,9 +31,7 @@ public function count(): int
*/
public function getIterator(): Iterator
{
foreach ($this as $specification) {
yield $specification;
}
yield from $this->specifications;
}

public function isSatisfiedBy(mixed $subject): bool
13 changes: 13 additions & 0 deletions src/AllSpec.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@

namespace Bakame\Specification;

use Countable;
use IteratorAggregate;
use PhpSpec\ObjectBehavior;

final class AllSpec extends ObjectBehavior
@@ -23,6 +25,17 @@ public function it_implements_the_specification_interface(): void
$this->shouldImplement(Specification::class);
}

public function it_implements_the_count_interface(): void
{
$this->shouldImplement(Countable::class);
$this->count()->shouldBe(2);
}

public function it_implements_the_iterator_aggregate_interface(): void
{
$this->shouldImplement(IteratorAggregate::class);
}

public function it_will_pass_with_two_true_values(Specification $spec1, Specification $spec2): void
{
$spec1->isSatisfiedBy('anything')->willReturn(true);
4 changes: 1 addition & 3 deletions src/Any.php
Original file line number Diff line number Diff line change
@@ -31,9 +31,7 @@ public function count(): int
*/
public function getIterator(): Iterator
{
foreach ($this as $specification) {
yield $specification;
}
yield from $this->specifications;
}

public function isSatisfiedBy(mixed $subject): bool
14 changes: 14 additions & 0 deletions src/AnySpec.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@

namespace Bakame\Specification;

use Countable;
use IteratorAggregate;
use PhpSpec\ObjectBehavior;

final class AnySpec extends ObjectBehavior
@@ -23,6 +25,18 @@ public function it_implements_the_specification_interface(): void
$this->shouldImplement(Specification::class);
}

public function it_implements_the_count_interface(): void
{
$this->shouldImplement(Countable::class);
$this->count()->shouldBe(2);
}

public function it_implements_the_iterator_aggregate_interface(): void
{
$this->shouldImplement(IteratorAggregate::class);
}


public function it_will_pass_with_two_true_values(Specification $spec1, Specification $spec2): void
{
$spec1->isSatisfiedBy('anything')->willReturn(true);
10 changes: 5 additions & 5 deletions src/Chain.php
Original file line number Diff line number Diff line change
@@ -42,26 +42,26 @@ public function isSatisfiedBy(mixed $subject): bool

public function and(Specification ...$specification): Composite
{
return new self(new All($this->specification, ...$specification));
return self::all($this->specification, ...$specification);
}

public function andNot(Specification ...$specification): Composite
{
return new self(new All($this->specification, new None(...$specification)));
return self::all($this->specification, new None(...$specification));
}

public function or(Specification ...$specification): Composite
{
return new self(new Any($this->specification, ...$specification));
return self::any($this->specification, ...$specification);
}

public function orNot(Specification ...$specification): Composite
{
return new self(new Any($this->specification, new None(...$specification)));
return self::any($this->specification, new None(...$specification));
}

public function not(): Composite
{
return new self(new None($this->specification));
return self::none($this->specification);
}
}
9 changes: 9 additions & 0 deletions src/ChainSpec.php
Original file line number Diff line number Diff line change
@@ -57,4 +57,13 @@ public function it_can_composite_with_logicial_not(Specification $spec): void
{
$this->not()->specification()->shouldBeLike(new None($spec->getWrappedObject()));
}

public function it_can_check_specification_validity(Specification $spec, Specification $spec2): void
{
$spec->isSatisfiedBy('anything')->willReturn(true);
$spec2->isSatisfiedBy('anything')->willReturn(false);

$this->andNot($spec2)->specification()->shouldBeLike(new All($spec->getWrappedObject(), new None($spec2->getWrappedObject())));
$this->isSatisfiedBy('anything')->shouldEqual(true);
}
}
4 changes: 1 addition & 3 deletions src/None.php
Original file line number Diff line number Diff line change
@@ -31,9 +31,7 @@ public function count(): int
*/
public function getIterator(): Iterator
{
foreach ($this as $specification) {
yield $specification;
}
yield from $this->specifications;
}

public function isSatisfiedBy(mixed $subject): bool
13 changes: 13 additions & 0 deletions src/NoneSpec.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@

namespace Bakame\Specification;

use Countable;
use IteratorAggregate;
use PhpSpec\ObjectBehavior;

final class NoneSpec extends ObjectBehavior
@@ -23,6 +25,17 @@ public function it_implements_the_specification_interface(): void
$this->shouldImplement(Specification::class);
}

public function it_implements_the_count_interface(): void
{
$this->shouldImplement(Countable::class);
$this->count()->shouldBe(1);
}

public function it_implements_the_iterator_aggregate_interface(Specification $spec): void
{
$this->shouldImplement(IteratorAggregate::class);
}

public function it_will_pass_with_a_false(Specification $spec): void
{
$spec->isSatisfiedBy('anything')->willReturn(false);
2 changes: 1 addition & 1 deletion src/Specification.php
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
interface Specification
{
/**
* @param mixed $subject the item that needs to be validated
* Returns true if the subject satisfies the specification or false otherwise.
*/
public function isSatisfiedBy(mixed $subject): bool;
}