Skip to content

Commit

Permalink
Add class MLL\Utils\Specification for logical combinations of predi…
Browse files Browse the repository at this point in the history
…cates
  • Loading branch information
spawnia authored Dec 31, 2024
1 parent 36552aa commit ed76cb1
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases).

## Unreleased

## v5.9.0

### Added

- Add class `MLL\Utils\Specification` for logical combinations of predicates

## v5.8.0

### Added
Expand Down
66 changes: 66 additions & 0 deletions src/Specification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php declare(strict_types=1);

namespace MLL\Utils;

/**
* Allows the logical combination of specification callables.
*
* We define specifications through a functional interface in the form `(mixed): bool`.
* This allows the usage of ad-hoc closures, first-class callables, and invokable classes.
*
* https://en.wikipedia.org/wiki/Specification_pattern
*/
class Specification
{
/**
* @template TCandidate
*
* @param callable(TCandidate): bool $specification
*
* @return callable(TCandidate): bool
*/
public static function not(callable $specification): callable
{
return fn ($value): bool => ! $specification($value);
}

/**
* @template TCandidate
*
* @param callable(TCandidate): bool ...$specifications
*
* @return callable(TCandidate): bool
*/
public static function or(callable ...$specifications): callable
{
return function ($value) use ($specifications): bool {
foreach ($specifications as $specification) {
if ($specification($value)) {
return true;
}
}

return false;
};
}

/**
* @template TCandidate
*
* @param callable(TCandidate): bool ...$specifications
*
* @return callable(TCandidate): bool
*/
public static function and(callable ...$specifications): callable
{
return function ($value) use ($specifications): bool {
foreach ($specifications as $specification) {
if (! $specification($value)) {
return false;
}
}

return true;
};
}
}
43 changes: 43 additions & 0 deletions tests/SpecificationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Tests;

use MLL\Utils\Specification;
use PHPUnit\Framework\TestCase;

final class SpecificationTest extends TestCase
{
public function testNot(): void
{
$identity = fn ($value) => $value;

self::assertTrue($identity(true));

$negatedIdentity = Specification::not($identity);
self::assertFalse($negatedIdentity(true));
}

public function testOr(): void
{
$is1 = fn ($value): bool => $value === 1;
$is2 = fn ($value): bool => $value === 2;

$is1Or2 = Specification::or($is1, $is2);
self::assertTrue($is1Or2(1));
self::assertTrue($is1Or2(2));
self::assertFalse($is1Or2(3));
}

public function testAnd(): void
{
$isPositive = fn ($value): bool => $value > 0;
$isOdd = fn ($value): bool => $value % 2 === 1;

$isPositiveAndOdd = Specification::and($isPositive, $isOdd);
self::assertTrue($isPositiveAndOdd(1));
self::assertFalse($isPositiveAndOdd(2));
self::assertFalse($isPositiveAndOdd(-1));
self::assertFalse($isPositiveAndOdd(0));
self::assertFalse($isPositiveAndOdd(-2));
}
}

0 comments on commit ed76cb1

Please sign in to comment.