Skip to content

Commit ed76cb1

Browse files
authored
Add class MLL\Utils\Specification for logical combinations of predicates
1 parent 36552aa commit ed76cb1

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases).
99

1010
## Unreleased
1111

12+
## v5.9.0
13+
14+
### Added
15+
16+
- Add class `MLL\Utils\Specification` for logical combinations of predicates
17+
1218
## v5.8.0
1319

1420
### Added

src/Specification.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\Utils;
4+
5+
/**
6+
* Allows the logical combination of specification callables.
7+
*
8+
* We define specifications through a functional interface in the form `(mixed): bool`.
9+
* This allows the usage of ad-hoc closures, first-class callables, and invokable classes.
10+
*
11+
* https://en.wikipedia.org/wiki/Specification_pattern
12+
*/
13+
class Specification
14+
{
15+
/**
16+
* @template TCandidate
17+
*
18+
* @param callable(TCandidate): bool $specification
19+
*
20+
* @return callable(TCandidate): bool
21+
*/
22+
public static function not(callable $specification): callable
23+
{
24+
return fn ($value): bool => ! $specification($value);
25+
}
26+
27+
/**
28+
* @template TCandidate
29+
*
30+
* @param callable(TCandidate): bool ...$specifications
31+
*
32+
* @return callable(TCandidate): bool
33+
*/
34+
public static function or(callable ...$specifications): callable
35+
{
36+
return function ($value) use ($specifications): bool {
37+
foreach ($specifications as $specification) {
38+
if ($specification($value)) {
39+
return true;
40+
}
41+
}
42+
43+
return false;
44+
};
45+
}
46+
47+
/**
48+
* @template TCandidate
49+
*
50+
* @param callable(TCandidate): bool ...$specifications
51+
*
52+
* @return callable(TCandidate): bool
53+
*/
54+
public static function and(callable ...$specifications): callable
55+
{
56+
return function ($value) use ($specifications): bool {
57+
foreach ($specifications as $specification) {
58+
if (! $specification($value)) {
59+
return false;
60+
}
61+
}
62+
63+
return true;
64+
};
65+
}
66+
}

tests/SpecificationTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\Utils\Tests;
4+
5+
use MLL\Utils\Specification;
6+
use PHPUnit\Framework\TestCase;
7+
8+
final class SpecificationTest extends TestCase
9+
{
10+
public function testNot(): void
11+
{
12+
$identity = fn ($value) => $value;
13+
14+
self::assertTrue($identity(true));
15+
16+
$negatedIdentity = Specification::not($identity);
17+
self::assertFalse($negatedIdentity(true));
18+
}
19+
20+
public function testOr(): void
21+
{
22+
$is1 = fn ($value): bool => $value === 1;
23+
$is2 = fn ($value): bool => $value === 2;
24+
25+
$is1Or2 = Specification::or($is1, $is2);
26+
self::assertTrue($is1Or2(1));
27+
self::assertTrue($is1Or2(2));
28+
self::assertFalse($is1Or2(3));
29+
}
30+
31+
public function testAnd(): void
32+
{
33+
$isPositive = fn ($value): bool => $value > 0;
34+
$isOdd = fn ($value): bool => $value % 2 === 1;
35+
36+
$isPositiveAndOdd = Specification::and($isPositive, $isOdd);
37+
self::assertTrue($isPositiveAndOdd(1));
38+
self::assertFalse($isPositiveAndOdd(2));
39+
self::assertFalse($isPositiveAndOdd(-1));
40+
self::assertFalse($isPositiveAndOdd(0));
41+
self::assertFalse($isPositiveAndOdd(-2));
42+
}
43+
}

0 commit comments

Comments
 (0)