Skip to content

Instantly share code, notes, and snippets.

@nrctkno
Last active April 2, 2026 01:49
Show Gist options
  • Select an option

  • Save nrctkno/93e57fd29de97a7b7d2d345d99f5fc56 to your computer and use it in GitHub Desktop.

Select an option

Save nrctkno/93e57fd29de97a7b7d2d345d99f5fc56 to your computer and use it in GitHub Desktop.
Playing around with specification pattern
<?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