3.9. Specification
3.9.1. Purpose
Builds a clear specification of business rules, where objects can be
checked against. The composite specification class has one method called
isSatisfiedBy
that returns either true or false depending on whether
the given object satisfies the specification.
3.9.2. Examples
3.9.3. UML Diagram
3.9.4. Code
You can also find this code on GitHub
Item.php
1<?php
2
3declare(strict_types=1);
4
5namespace DesignPatterns\Behavioral\Specification;
6
7class Item
8{
9 public function __construct(private float $price)
10 {
11 }
12
13 public function getPrice(): float
14 {
15 return $this->price;
16 }
17}
Specification.php
1<?php
2
3declare(strict_types=1);
4
5namespace DesignPatterns\Behavioral\Specification;
6
7interface Specification
8{
9 public function isSatisfiedBy(Item $item): bool;
10}
OrSpecification.php
1<?php
2
3declare(strict_types=1);
4
5namespace DesignPatterns\Behavioral\Specification;
6
7class OrSpecification implements Specification
8{
9 /**
10 * @var Specification[]
11 */
12 private array $specifications;
13
14 /**
15 * @param Specification[] $specifications
16 */
17 public function __construct(Specification ...$specifications)
18 {
19 $this->specifications = $specifications;
20 }
21
22 /*
23 * if at least one specification is true, return true, else return false
24 */
25 public function isSatisfiedBy(Item $item): bool
26 {
27 foreach ($this->specifications as $specification) {
28 if ($specification->isSatisfiedBy($item)) {
29 return true;
30 }
31 }
32
33 return false;
34 }
35}
PriceSpecification.php
1<?php
2
3declare(strict_types=1);
4
5namespace DesignPatterns\Behavioral\Specification;
6
7class PriceSpecification implements Specification
8{
9 public function __construct(private ?float $minPrice, private ?float $maxPrice)
10 {
11 }
12
13 public function isSatisfiedBy(Item $item): bool
14 {
15 if ($this->maxPrice !== null && $item->getPrice() > $this->maxPrice) {
16 return false;
17 }
18
19 if ($this->minPrice !== null && $item->getPrice() < $this->minPrice) {
20 return false;
21 }
22
23 return true;
24 }
25}
AndSpecification.php
1<?php
2
3declare(strict_types=1);
4
5namespace DesignPatterns\Behavioral\Specification;
6
7class AndSpecification implements Specification
8{
9 /**
10 * @var Specification[]
11 */
12 private array $specifications;
13
14 /**
15 * @param Specification[] $specifications
16 */
17 public function __construct(Specification ...$specifications)
18 {
19 $this->specifications = $specifications;
20 }
21
22 /**
23 * if at least one specification is false, return false, else return true.
24 */
25 public function isSatisfiedBy(Item $item): bool
26 {
27 foreach ($this->specifications as $specification) {
28 if (!$specification->isSatisfiedBy($item)) {
29 return false;
30 }
31 }
32
33 return true;
34 }
35}
NotSpecification.php
1<?php
2
3declare(strict_types=1);
4
5namespace DesignPatterns\Behavioral\Specification;
6
7class NotSpecification implements Specification
8{
9 public function __construct(private Specification $specification)
10 {
11 }
12
13 public function isSatisfiedBy(Item $item): bool
14 {
15 return !$this->specification->isSatisfiedBy($item);
16 }
17}
3.9.5. Test
Tests/SpecificationTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace DesignPatterns\Behavioral\Specification\Tests;
6
7use DesignPatterns\Behavioral\Specification\Item;
8use DesignPatterns\Behavioral\Specification\NotSpecification;
9use DesignPatterns\Behavioral\Specification\OrSpecification;
10use DesignPatterns\Behavioral\Specification\AndSpecification;
11use DesignPatterns\Behavioral\Specification\PriceSpecification;
12use PHPUnit\Framework\TestCase;
13
14class SpecificationTest extends TestCase
15{
16 public function testCanOr()
17 {
18 $spec1 = new PriceSpecification(50, 99);
19 $spec2 = new PriceSpecification(101, 200);
20
21 $orSpec = new OrSpecification($spec1, $spec2);
22
23 $this->assertFalse($orSpec->isSatisfiedBy(new Item(100)));
24 $this->assertTrue($orSpec->isSatisfiedBy(new Item(51)));
25 $this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
26 }
27
28 public function testCanAnd()
29 {
30 $spec1 = new PriceSpecification(50, 100);
31 $spec2 = new PriceSpecification(80, 200);
32
33 $andSpec = new AndSpecification($spec1, $spec2);
34
35 $this->assertFalse($andSpec->isSatisfiedBy(new Item(150)));
36 $this->assertFalse($andSpec->isSatisfiedBy(new Item(1)));
37 $this->assertFalse($andSpec->isSatisfiedBy(new Item(51)));
38 $this->assertTrue($andSpec->isSatisfiedBy(new Item(100)));
39 }
40
41 public function testCanNot()
42 {
43 $spec1 = new PriceSpecification(50, 100);
44 $notSpec = new NotSpecification($spec1);
45
46 $this->assertTrue($notSpec->isSatisfiedBy(new Item(150)));
47 $this->assertFalse($notSpec->isSatisfiedBy(new Item(50)));
48 }
49}