Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

Commit be9d247

Browse files
committed
TASK: Initial Commit
0 parents  commit be9d247

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+5319
-0
lines changed

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
charset = utf-8
6+
trim_trailing_whitespace = true
7+
insert_final_newline = true
8+
indent_style = space
9+
indent_size = 4
10+
11+
[*.neon]
12+
indent_style = tab
13+
14+
[*.md]
15+
trim_trailing_whitespace = false
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Quality Assurance
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
quality-assurance:
7+
runs-on: ubuntu-22.04
8+
steps:
9+
- name: Checkout
10+
uses: actions/checkout@v2
11+
12+
- name: Setup PHP
13+
uses: shivammathur/setup-php@v2
14+
with:
15+
php-version: '8.1'
16+
tools: phpcs
17+
18+
- name: Get composer cache directory
19+
id: composercache
20+
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
21+
22+
- name: Cache dependencies
23+
uses: actions/cache@v2
24+
with:
25+
path: ${{ steps.composercache.outputs.dir }}
26+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
27+
restore-keys: ${{ runner.os }}-composer-
28+
29+
- name: Install composer dependencies
30+
run: |
31+
composer install
32+
33+
- name: Check Code Quality (PHP Code Sniffer)
34+
run: |
35+
composer lint
36+
37+
- name: Static Code Analysis (PHPStan)
38+
run: |
39+
composer lint
40+
41+
- name: Unit Tests (PHPUnit)
42+
run: |
43+
composer test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
composer.lock
3+
.phpunit.result.cache
4+
.idea

README.md

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# PackageFactory.Specification
2+
3+
> Implementation of the Specification pattern for PHP
4+
5+
The specification pattern is a way to express business rules in a domain model using boolean logic. It is described in detail in the following document: https://www.martinfowler.com/apsupp/spec.pdf
6+
7+
## Installation
8+
9+
```
10+
composer require --dev packagefactory/specification
11+
```
12+
13+
## Usage
14+
15+
### Writing a Specification
16+
17+
Let's presume the following (very simplified) problem: You've got an application with a simple user registration workflow. Users can register freely, but have to verify their E-Mail address. If a user didn't verify their E-Mail address for a period of time, they shall be reminded (via E-Mail) that verification is still due.
18+
19+
How can this business rule be codified using the Specification pattern?
20+
21+
First, let's write a specification that checks if a given user has a verified E-Mail address:
22+
23+
```php
24+
use PackageFactory\Specification\Core\AbstractSpecification;
25+
use Vendor\Project\Domain\User;
26+
27+
/**
28+
* The `@extends` annotation makes sure that static analysis tools like
29+
* phpstan understand that this specification handles `User`-objects
30+
* only:
31+
*
32+
* @extends AbstractSpecification<User>
33+
*/
34+
final class HasVerifiedEmailAddressSpecification extends AbstractSpecification
35+
{
36+
public function isSatisfiedBy($user): bool
37+
{
38+
// In lieu of generics in PHP it is recommended to add a
39+
// zero-cost assertion to ensure the type of the given value:
40+
assert($user instanceof User);
41+
42+
return $user->emailAddress->isVerified;
43+
}
44+
}
45+
```
46+
47+
Then, let's write a specification that checks if a given user has been registered before a specific reference date:
48+
49+
```php
50+
use PackageFactory\Specification\Core\AbstractSpecification;
51+
use Vendor\Project\Domain\User;
52+
53+
/**
54+
* @extends AbstractSpecification<User>
55+
*/
56+
final class HasBeenRegisteredBefore extends AbstractSpecification
57+
{
58+
public function __construct(
59+
private readonly \DateTimeImmutable $referenceDate
60+
) {
61+
}
62+
63+
public function isSatisfiedBy($user): bool
64+
{
65+
assert($user instanceof User);
66+
67+
return $user->registrationDate->getTimestamp() < $this->referenceDate->getTimestamp();
68+
}
69+
}
70+
```
71+
72+
We can now use the Specification API to combine both specifications and express our business rule:
73+
74+
```php
75+
// $twoWeeksAgo is a calculated \DateTimeImmutable
76+
$needsReminderSpecification = (new HasBeenRegisteredBefore($twoWeeksAgo))
77+
->andNot(new HasVerifiedEmailAddressSpecification());
78+
79+
$usersThatNeedReminder = $userRepository->findBySpecification($needsReminderSpecification);
80+
81+
foreach ($usersThatNeedReminder as $userThatNeedsReminder) {
82+
$notificationService->sendReminderTo($userThatNeedsReminder);
83+
}
84+
```
85+
86+
### API
87+
88+
Each specification must implement `PackageFactory\Specification\Core\SpecificationInterface`. Usually, a custom specification should extend `PackageFactory\Specification\Core\AbstractSpecification`, which implements all methods of the `SpecificationInterface` except for `isSatisfiedBy`.
89+
90+
The `SpecificationInterface` covers the following methods:
91+
92+
> **Note on Generics:** PHP does not have built-in Generics. However, there's static analysis tools like [phpstan](https://phpstan.org/) that do understand them. The `SpecificationInterface` comes with an annotation that allows you to specify the type of `$candidate` your specification is supposed to cover.
93+
>
94+
> Your custom specification implementation should therefore name a concrete `$candidate` type like this:
95+
> ```php
96+
> /**
97+
> * @extends AbstractSpecification<MyClass>
98+
> */
99+
> final class MyCustomSpecification extends AbstractSpecification
100+
> {
101+
> /**
102+
> * @param MyClass $candidate
103+
> * @return boolean
104+
> */
105+
> public function isSatisfiedBy($candidate): bool
106+
> {
107+
> // ...
108+
> }
109+
> }
110+
> ```
111+
112+
#### `isSatisfiedBy`
113+
114+
```php
115+
/**
116+
* @param C $candidate
117+
* @return boolean
118+
*/
119+
public function isSatisfiedBy($candidate): bool;
120+
```
121+
122+
This method checks the given `$candidate` and returns `true` if it satisfies the specification and `false` if it doesn't.
123+
124+
In lieu of generics in PHP it is recommended to add a zero-cost assertion at the top of the implementation body to ensure the type of `$candidate`:
125+
126+
```php
127+
/**
128+
* @param MyClass $candidate
129+
* @return boolean
130+
*/
131+
public function isSatisfiedBy($candidate): bool;
132+
{
133+
assert($candidate instanceof MyClass);
134+
135+
// ...
136+
}
137+
```
138+
139+
For more on zero-cost assertions see: https://www.php.net/manual/en/function.assert.php
140+
141+
#### `and`
142+
143+
```php
144+
/**
145+
* @param SpecificationInterface<C> $other
146+
* @return SpecificationInterface<C>
147+
*/
148+
public function and(SpecificationInterface $other): SpecificationInterface;
149+
```
150+
151+
The result of this method is a new specification that will be satisfied by a `$candidate` that satisfies both the calling specification and `$other`.
152+
153+
#### `andNot`
154+
155+
```php
156+
/**
157+
* @param SpecificationInterface<C> $other
158+
* @return SpecificationInterface<C>
159+
*/
160+
public function andNot(SpecificationInterface $other): SpecificationInterface;
161+
```
162+
163+
The result of this method is a new specification that will be satisfied by a `$candidate` that satisfies the calling specification and does not satisfy `$other`.
164+
165+
#### `or`
166+
167+
```php
168+
/**
169+
* @param SpecificationInterface<C> $other
170+
* @return SpecificationInterface<C>
171+
*/
172+
public function or(SpecificationInterface $other): SpecificationInterface;
173+
```
174+
175+
The result of this method is a new specification that will be satisfied by a `$candidate` that satisfies either the calling specification or `$other` (or both).
176+
177+
#### `orNot`
178+
179+
```php
180+
/**
181+
* @param SpecificationInterface<C> $other
182+
* @return SpecificationInterface<C>
183+
*/
184+
public function orNot(SpecificationInterface $other): SpecificationInterface;
185+
```
186+
187+
The result of this method is a new specification that will be satisfied by a `$candidate` that either satisfies the calling specification or does not satisfy `$other` (or both).
188+
189+
#### `not`
190+
191+
```php
192+
/**
193+
* @return SpecificationInterface<C>
194+
*/
195+
public function not(): SpecificationInterface;
196+
```
197+
198+
This method negates the calling specification. That means: the result is a specification that will be satisfied by a `$candidate` that does not satisfy the calling specification.
199+
200+
## Contribution
201+
202+
We will gladly accept contributions. Please send us pull requests.
203+
204+
## License
205+
206+
see [LICENSE](./LICENSE)

0 commit comments

Comments
 (0)