Last active
April 2, 2026 01:49
-
-
Save nrctkno/93e57fd29de97a7b7d2d345d99f5fc56 to your computer and use it in GitHub Desktop.
Playing around with specification pattern
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| interface Specification | |
| { | |
| public function __invoke($candidate): bool; | |
| } | |
| abstract class AbstractSpecification implements Specification | |
| { | |
| public function and(Specification $other): Specification | |
| { | |
| return new AndSpecification($this, $other); | |
| } | |
| public function or(Specification $other): Specification | |
| { | |
| return new OrSpecification($this, $other); | |
| } | |
| public function not(): Specification | |
| { | |
| return new NotSpecification($this); | |
| } | |
| } | |
| class AndSpecification extends AbstractSpecification | |
| { | |
| private Specification $left; | |
| private Specification $right; | |
| public function __construct(Specification $left, Specification $right) | |
| { | |
| $this->left = $left; | |
| $this->right = $right; | |
| } | |
| public function __invoke($candidate): bool | |
| { | |
| return ($this->left)($candidate) | |
| && ($this->right)($candidate); | |
| } | |
| } | |
| class OrSpecification extends AbstractSpecification | |
| { | |
| private Specification $left; | |
| private Specification $right; | |
| public function __construct(Specification $left, Specification $right) | |
| { | |
| $this->left = $left; | |
| $this->right = $right; | |
| } | |
| public function __invoke($candidate): bool | |
| { | |
| return ($this->left)($candidate) | |
| || ($this->right)($candidate); | |
| } | |
| } | |
| class NotSpecification extends AbstractSpecification | |
| { | |
| private Specification $spec; | |
| public function __construct(Specification $spec) | |
| { | |
| $this->spec = $spec; | |
| } | |
| public function __invoke($candidate): bool | |
| { | |
| return !($this->spec)($candidate); | |
| } | |
| } | |
| class ClosureSpecification extends AbstractSpecification | |
| { | |
| private $predicate; | |
| public function __construct(callable $predicate) | |
| { | |
| $this->predicate = $predicate; | |
| } | |
| public function __invoke($candidate): bool | |
| { | |
| return ($this->predicate)($candidate); | |
| } | |
| } | |
| function allOf(...$specs): Specification { | |
| return array_reduce($specs, fn($acc, $s) => $acc ? $acc->and($s) : $s); | |
| } | |
| function anyOf(...$specs): Specification { | |
| return array_reduce($specs, fn($acc, $s) => $acc ? $acc->or($s) : $s); | |
| } | |
| // usage example | |
| class IsAdult extends AbstractSpecification | |
| { | |
| public function __construct( | |
| private \DateTimeInterface $now, | |
| ) { | |
| } | |
| public function __invoke($user): bool | |
| { | |
| $age = $user->birthDate->diff($this->now)->y; | |
| return $age >= 18; | |
| } | |
| } | |
| class HasEmail extends AbstractSpecification | |
| { | |
| public function __invoke($user): bool | |
| { | |
| return $user->email !== ''; | |
| } | |
| } | |
| class IsBanned extends AbstractSpecification | |
| { | |
| public function __construct( | |
| private array $bannedEmails, | |
| ) { | |
| } | |
| public function __invoke($user): bool | |
| { | |
| return in_array($user->email, $this->bannedEmails); | |
| } | |
| } | |
| class canSignup extends AbstractSpecification | |
| { | |
| public function __construct( | |
| private array $bannedEmails, | |
| private \DateTimeInterface $now, | |
| ) { | |
| } | |
| public function __invoke($user): bool | |
| { | |
| $predicate = (new IsAdult($this->now)) | |
| ->and(new HasEmail()) | |
| ->and((new IsBanned($this->bannedEmails))->not()); | |
| return $predicate($user); | |
| } | |
| } | |
| class User | |
| { | |
| public function __construct ( | |
| public \DateTimeInterface $birthDate, | |
| public bool $isEmancipated, | |
| public bool $active, | |
| public string $email, | |
| ) { | |
| } | |
| } | |
| $bannedEmails = [ | |
| 'banned@email.example' | |
| ]; | |
| $now = new \DateTimeImmutable('now'); | |
| $allowedUser = new User( | |
| new \DateTimeImmutable('1985-01-01 00:00:00'), | |
| false, | |
| true, | |
| 'allowed@email.example' | |
| ); | |
| $bannedUser = new User( | |
| new \DateTimeImmutable('1985-01-01 00:00:00'), | |
| false, | |
| true, | |
| 'banned@email.example' | |
| ); | |
| $canSignUp = new canSignup($bannedEmails, $now); | |
| var_dump($canSignUp($allowedUser)); | |
| var_dump($canSignUp($bannedUser)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment