Skip to content

Commit

Permalink
Utils created
Browse files Browse the repository at this point in the history
  • Loading branch information
slava-basko committed Dec 16, 2023
1 parent 95d81fa commit 8435e2b
Show file tree
Hide file tree
Showing 16 changed files with 324 additions and 86 deletions.
95 changes: 69 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
# Specification Pattern

Encapsulate your business decisions for readable, clear, and maintainable purposes.
Encapsulate your business decisions for readable, clear, and maintainable purposes.
In simpler words: encapsulate your business's IF's and ELSE's, and speak with clients on the same language.

Read it if you are not familiar with Specification pattern [http://www.martinfowler.com/apsupp/spec.pdf].

This library has no dependencies on any external libs and works on PHP 5.5+. Why?
This library has no dependencies on any external libs and works on PHP 5.5+. Why?
Because legacy projects still exists, and they also want some structure.


## Install

```bash
composer require slava-basko/specification-php
```


## Usage

Let's imagine that we have the specification of an Adult Person.

```php
class AdultUserSpecification extends AbstractSpecification
{
Expand All @@ -32,23 +33,26 @@ class AdultUserSpecification extends AbstractSpecification
```

Now let's check if the user is actually an adult.

```php
$adultUserSpecification = new AdultUserSpecification();
$adultUserSpecification->isSatisfiedBy(new User(14)); // false
$adultUserSpecification->isSatisfiedBy(new User(20)); // true
```

Use `TypedSpecification` decorator/wrapper if you want typed specification.

```php
$adultUserTypesSpecification = new TypedSpecification(new AdultUserSpecification(), User::class);
$adultUserTypesSpecification->isSatisfiedBy(new User(20)); // true
$adultUserTypesSpecification->isSatisfiedBy('blah'); // InvalidArgumentException will be thrown
$adultUserSpecification = new TypedSpecification(new AdultUserSpecification(), User::class);
$adultUserSpecification->isSatisfiedBy(new User(20)); // true
$adultUserSpecification->isSatisfiedBy('blah'); // InvalidArgumentException will be thrown
```


#### `TypedSpecification` VS `public function isSatisfiedBy(User $candidate)`
Of course, you can create your own specification interfaces with type hinting in `isSatisfiedBy`,

Of course, you can create your own specification interfaces with type hinting in `isSatisfiedBy`,
but sooner or later you will see a lot of interfaces that are similar by 99%.

```php
interface UserSpecification
{
Expand All @@ -71,17 +75,20 @@ interface ParcelSpecification
}
// etc.
```

Or you can use `TypedSpecification` decorator to achieve the same goal.

```php
new TypedSpecification(new SomeUserSpecification(), User::class);
new TypedSpecification(new SomeProductSpecification(), Product::class);
new TypedSpecification(new SomeCartSpecification(), Cart::class);
new TypedSpecification(new SomeParcelSpecification(), Parcel::class);
```


#### Autocompletion

Use the doc-block type hinting in your end specifications for autocompletion, like `@param User $candidate`.

```php
/**
* @param User $candidate
Expand All @@ -92,13 +99,15 @@ public function isSatisfiedBy($candidate)
return $candidate->someMethodThatWillBeAutocompletedInYourIDE();
}
```
`TypedSpecification` guaranty that `$candidate` will be an instance of `User` class,
and doc-block `@param User $candidate` helps your IDE to autocomplete `$candidate` methods.

`TypedSpecification` guaranty that `$candidate` will be an instance of `User` class,
and doc-block `@param User $candidate` helps your IDE to autocomplete `$candidate` methods.

#### Composition
This lib provides couple of useful prebuilt specifications like `NotSpecification`, `AndSpecification`,

This lib provides couple of useful builtin specifications like `NotSpecification`, `AndSpecification`,
and `OrSpecification` that helps you to group up your specifications and create a new one.

```php
$adultPersonSpecification = new AndSpecification([
new AdultSpecification(),
Expand All @@ -108,23 +117,12 @@ $adultPersonSpecification = new AndSpecification([
])
]);

$adultPersonSpecification->isSatisfiedBy($adultAlien);

$remainderUnsatisfiedSpecification = $adultPersonSpecification->remainderUnsatisfiedBy($adultAlien);

// $remainderUnsatisfiedSpecification is equal to
//
// AndSpecification([
// OrSpecification([
// MaleSpecification,
// FemaleSpecification,
// ])
// ]);
//
$adultPersonSpecification->isSatisfiedBy($adultAlien); // false
// because only AdultSpecification was satisfied; assume we know age, and we don't know alien sex.
```

Here is another example that shows how highly composable specifications could be.

```php
// Card of spades and not (two or three of spades), or (card of hearts and not (two or three of hearts))
$spec = new OrSpecification([
Expand All @@ -148,6 +146,51 @@ $spec->isSatisfiedBy(new PlayingCard(PlayingCard::SUIT_SPADES, PlayingCard::RANK
$spec->isSatisfiedBy(new PlayingCard(PlayingCard::SUIT_SPADES, PlayingCard::RANK_2)); // false
```

#### Remainders

Method `isSatisfiedBy` returns `bool`, and sometimes in case of `false` you want to know what exactly gone wrong.
Use `remainderUnsatisfiedBy` method for that. It that returns a remainder of unsatisfied specifications.

```php
$parcel = [
'value' => 20,
'destination' => 'CA'
];

$trackableParcelSpecification = new AndSpecification([
new HighValueParcelSpecification(),
new OrSpecification([
new DestinationCASpecification(),
new DestinationUSSpecification(),
])
]);

if ($trackableParcelSpecification->isSatisfiedBy($parcel)) {
// do something
} else {
$remainderSpecification = $trackableParcelSpecification->remainderUnsatisfiedBy($parcel);
// do something with $remainderSpecification
}

// $remainderSpecification is equal to
//
// AndSpecification([
// HighValueParcelSpecification
// ]);
//
// because only DestinationXX satisfied
```

You can use `Utils` to convert it to useful array.

```php
$remainder = Utils::flatten(Utils::toSnakeCase($trackableParcel->remainderUnsatisfiedBy($parcel)));
// $remainder is equal to ['high_value_parcel']
```

For example, you can use strings inside `$remainder` like `high_value_parcel` as a translation key
to show meaningful error message.

## License

Use as you want. No liability or warranty from me. Can be considered as MIT.
9 changes: 7 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"name": "slava-basko/specification-php",
"type": "library",
"description": "Encapsulate your business decisions for readable, clear, maintainable purposes.",
"keywords": ["specification", "pattern"],
"keywords": [
"specification",
"pattern"
],
"authors": [
{
"name": "Slava Basko",
Expand All @@ -18,7 +21,9 @@
"squizlabs/php_codesniffer": ">=3"
},
"autoload": {
"psr-4": { "Basko\\Specification\\": "src" }
"psr-4": {
"Basko\\Specification\\": "src"
}
},
"autoload-dev": {
"psr-4": {
Expand Down
8 changes: 7 additions & 1 deletion src/AbstractSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

abstract class AbstractSpecification implements Specification
{
/**
* @var \Basko\Specification\Specification|\Basko\Specification\Specification[]|null
*/
protected $container = null;

/**
* @param mixed $result
* @param \Basko\Specification\Specification|null $specification
Expand Down Expand Up @@ -40,8 +45,9 @@ public function __invoke($candidate)
*/
public function remainderUnsatisfiedBy($candidate)
{
if (!$this->isSatisfiedBy($candidate))
if (!$this->isSatisfiedBy($candidate)) {
return $this;
}

return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/AndSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final class AndSpecification extends GroupSpecification
*/
public function isSatisfiedBy($candidate)
{
foreach ($this->specifications as $specification) {
foreach ($this->container as $specification) {
$result = $specification->isSatisfiedBy($candidate);
$this->assertReturnType($result, $specification);
if (!$result) {
Expand Down
38 changes: 8 additions & 30 deletions src/GroupSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@

abstract class GroupSpecification extends AbstractSpecification
{
/**
* @var Specification[]
*/
protected $specifications = [];

/**
* Constructor for GroupSpecification.
*
Expand All @@ -23,7 +18,7 @@ abstract class GroupSpecification extends AbstractSpecification
*/
public function __construct($specifications)
{
$specifications = $this->flatten(\func_get_args());
$specifications = Utils::flatten(\func_get_args());

foreach ($specifications as $specification) {
if (!$specification instanceof Specification) {
Expand All @@ -35,36 +30,18 @@ public function __construct($specifications)
}
}

$this->specifications = $specifications;
}

/**
* @param array|\Traversable $list
* @return array
*/
private function flatten($list)
{
$result = [];
foreach ($list as $value) {
if (\is_array($value) || $value instanceof \Traversable) {
$result = \array_merge($result, $this->flatten($value));
} else {
$result[] = $value;
}
}

return $result;
$this->container = $specifications;
}

/**
* @param mixed $candidate
* @return \Basko\Specification\Specification|null
* @return \Basko\Specification\GroupSpecification|null
*/
public function remainderUnsatisfiedBy($candidate)
{
if ($this->isSatisfiedBy($candidate))
if ($this->isSatisfiedBy($candidate)) {
return null;
else {
} else {
// Constructs a GroupSpecification out of the specifications that have not been satisfied
return new static($this->remaindersUnsatisfiedBy($candidate));
}
Expand All @@ -79,9 +56,10 @@ public function remainderUnsatisfiedBy($candidate)
private function remaindersUnsatisfiedBy($candidate)
{
$unsatisfied = [];
foreach ($this->specifications as $specification) {
if (!$specification->isSatisfiedBy($candidate))
foreach ($this->container as $specification) {
if (!$specification->isSatisfiedBy($candidate)) {
$unsatisfied[] = $specification;
}
}

return $unsatisfied;
Expand Down
11 changes: 3 additions & 8 deletions src/NotSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@

namespace Basko\Specification;

class NotSpecification extends AbstractSpecification
final class NotSpecification extends AbstractSpecification
{
/**
* @var \Basko\Specification\Specification
*/
private $specification;

public function __construct(Specification $specification)
{
$this->specification = $specification;
$this->container = $specification;
}

/**
Expand All @@ -20,6 +15,6 @@ public function __construct(Specification $specification)
*/
public function isSatisfiedBy($candidate)
{
return !$this->specification->isSatisfiedBy($candidate);
return !$this->container->isSatisfiedBy($candidate);
}
}
2 changes: 1 addition & 1 deletion src/OrSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final class OrSpecification extends GroupSpecification
*/
public function isSatisfiedBy($candidate)
{
foreach ($this->specifications as $specification) {
foreach ($this->container as $specification) {
$result = $specification->isSatisfiedBy($candidate);
$this->assertReturnType($result, $specification);
if ($result) {
Expand Down
Loading

0 comments on commit 8435e2b

Please sign in to comment.