Skip to content

Commit 81e8d1b

Browse files
authored
Release 0.4.0
2 parents 347bbb6 + 695d6b3 commit 81e8d1b

File tree

16 files changed

+715
-37
lines changed

16 files changed

+715
-37
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Global owners.
2+
* @othercodes

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"ramsey/uuid": "^4.1",
1717
"nesbot/carbon": "^2.40",
1818
"illuminate/collections": "^8.20",
19-
"spatie/data-transfer-object": "^2.6"
19+
"spatie/data-transfer-object": "^2.6",
20+
"lambdish/phunctional": "^2.1",
21+
"doctrine/instantiator": "^1.4"
2022
},
2123
"require-dev": {
2224
"mockery/mockery": "^1.4",

src/Domain/Contracts/Identifier.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ public function value(): string;
2929
* @return bool
3030
*/
3131
public function is(Identifier $other): bool;
32+
33+
/**
34+
* Represents the id as string.
35+
*
36+
* @return string
37+
*/
38+
public function __toString(): string;
3239
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OtherCode\ComplexHeart\Domain\Exceptions;
6+
7+
use Exception;
8+
9+
/**
10+
* Class StateException
11+
*
12+
* @author Unay Santisteban <[email protected]>
13+
* @package OtherCode\ComplexHeart\Domain\Exceptions
14+
*/
15+
abstract class StateException extends Exception
16+
{
17+
/**
18+
* Create a new StateNotFound
19+
*
20+
* @param string $state
21+
* @param array $valid
22+
*
23+
* @return StateNotFound
24+
*/
25+
public static function stateNotFound(string $state, array $valid): StateException
26+
{
27+
$valid = implode(',', $valid);
28+
return new StateNotFound("State <{$state}> not found, must be one of: {$valid}");
29+
}
30+
31+
/**
32+
* Create a new TransitionNotAllowed.
33+
*
34+
* @param string $from
35+
* @param string $to
36+
*
37+
* @return StateException
38+
*/
39+
public static function transitionNotAllowed(string $from, string $to): StateException
40+
{
41+
return new TransitionNotAllowed("Transition from <{$from}> to <{$to}> is not allowed.");
42+
}
43+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OtherCode\ComplexHeart\Domain\Exceptions;
6+
7+
/**
8+
* Class StateNotFound
9+
*
10+
* @author Unay Santisteban <[email protected]>
11+
* @package OtherCode\ComplexHeart\Domain\Exceptions
12+
*/
13+
class StateNotFound extends StateException
14+
{
15+
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OtherCode\ComplexHeart\Domain\Exceptions;
6+
7+
/**
8+
* Class TransitionNotAllowed
9+
*
10+
* @author Unay Santisteban <[email protected]>
11+
* @package OtherCode\ComplexHeart\Domain\Exceptions
12+
*/
13+
class TransitionNotAllowed extends StateException
14+
{
15+
16+
}

src/Domain/State.php

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OtherCode\ComplexHeart\Domain;
6+
7+
use OtherCode\ComplexHeart\Domain\Exceptions\StateException;
8+
use OtherCode\ComplexHeart\Domain\ValueObjects\EnumValue;
9+
10+
/**
11+
* Class State
12+
*
13+
* @see https://en.wikipedia.org/wiki/State_pattern
14+
* @author Unay Santisteban <[email protected]>
15+
* @package OtherCode\ComplexHeart\Domain
16+
*/
17+
abstract class State extends EnumValue
18+
{
19+
private const DEFAULT = 'default';
20+
21+
private string $defaultState;
22+
23+
/**
24+
* Mapping of the available transitions with the transition function.
25+
*
26+
* @var array<string, callable|null>
27+
*/
28+
private array $transitions = [];
29+
30+
/**
31+
* State constructor.
32+
*
33+
* @param string $value
34+
*/
35+
public function __construct(string $value = self::DEFAULT)
36+
{
37+
$this->configure();
38+
39+
parent::__construct(
40+
$value === self::DEFAULT
41+
? $this->defaultState
42+
: $value
43+
);
44+
}
45+
46+
/**
47+
* Configure the state machine with the specific transitions.
48+
*
49+
* $this->defaultState('SOME_STATE')
50+
* ->allowTransition('SOME_STATE', 'OTHER_STATE')
51+
* ->allowTransition('SOME_STATE', 'ANOTHER_STATE');
52+
*/
53+
abstract protected function configure(): void;
54+
55+
/**
56+
* Set the default value for the state machine.
57+
*
58+
* @param string $state
59+
*
60+
* @return $this
61+
*/
62+
protected function defaultState(string $state): State
63+
{
64+
$this->defaultState = $state;
65+
return $this;
66+
}
67+
68+
/**
69+
* Define the allowed state transitions.
70+
*
71+
* @param string $from
72+
* @param string $to
73+
* @param callable|null $transition
74+
*
75+
* @return $this
76+
* @throws StateException
77+
*/
78+
protected function allowTransition(string $from, string $to, ?callable $transition = null): State
79+
{
80+
if (!static::isValid($from)) {
81+
throw StateException::stateNotFound($from, static::getValues());
82+
}
83+
84+
if (!static::isValid($to)) {
85+
throw StateException::stateNotFound($to, static::getValues());
86+
}
87+
88+
if (is_null($transition)) {
89+
$key = $this->getTransitionKey($from, $to);
90+
if ($this->canCall($method = $this->getStringKey($key, 'from'))) {
91+
// compute method using the exactly transition key: fromOneToAnother
92+
$transition = [$this, $method];
93+
} elseif ($this->canCall($method = $this->getStringKey($to, 'to'))) {
94+
// compute the method using only the $to state: toAnother
95+
$transition = [$this, $method];
96+
}
97+
}
98+
99+
$this->transitions[$this->getTransitionKey($from, $to)] = $transition;
100+
101+
return $this;
102+
}
103+
104+
/**
105+
* Set the value and executed the "on{State}" method if it's available.
106+
*
107+
* This method is automatically invoked from the HasAttributes trait
108+
* on set() the value property.
109+
*
110+
* @param string $value
111+
*
112+
* @return string
113+
*/
114+
protected function setValueValue(string $value): string
115+
{
116+
$onSetStateMethod = $this->getStringKey($value, 'on');
117+
if ($this->canCall($onSetStateMethod)) {
118+
call_user_func_array([$this, $onSetStateMethod], []);
119+
}
120+
return $value;
121+
}
122+
123+
/**
124+
* Compute the transition key using the $from and $to strings.
125+
*
126+
* @param string $from
127+
* @param string $to
128+
*
129+
* @return string
130+
*/
131+
private function getTransitionKey(string $from, string $to): string
132+
{
133+
return $this->getStringKey("{$from}_to_{$to}");
134+
}
135+
136+
/**
137+
* Check if the given $from $to transition is allowed.
138+
*
139+
* @param string $from
140+
* @param string $to
141+
*
142+
* @return bool
143+
*/
144+
private function isTransitionAllowed(string $from, string $to): bool
145+
{
146+
return array_key_exists($this->getTransitionKey($from, $to), $this->transitions);
147+
}
148+
149+
/**
150+
* Execute the transition $from oneState $to another.
151+
*
152+
* @param string $to
153+
* @param mixed ...$arguments
154+
*
155+
* @return $this
156+
* @throws StateException
157+
*/
158+
public function transitionTo(string $to, ...$arguments): State
159+
{
160+
if (!static::isValid($to)) {
161+
throw StateException::stateNotFound($to, static::getValues());
162+
}
163+
164+
if (!$this->isTransitionAllowed($this->value, $to)) {
165+
throw StateException::transitionNotAllowed($this->value, $to);
166+
}
167+
168+
if ($transition = $this->transitions[$this->getTransitionKey($this->value, $to)]) {
169+
call_user_func_array($transition, $arguments);
170+
}
171+
172+
$this->set('value', $to);
173+
174+
return $this;
175+
}
176+
}

src/Domain/Traits/HasAttributes.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace OtherCode\ComplexHeart\Domain\Traits;
66

7+
use function Lambdish\Phunctional\map;
8+
79
/**
810
* Trait HasAttributes
911
*
@@ -64,7 +66,7 @@ final protected function hydrate(iterable $source): void
6466
final protected function get(string $attribute)
6567
{
6668
if (in_array($attribute, static::attributes())) {
67-
$method = $this->getProxyMethod('get', $attribute);
69+
$method = $this->getStringKey($attribute, 'get', 'Value');
6870

6971
return ($this->canCall($method))
7072
? call_user_func_array([$this, $method], [$this->{$attribute}])
@@ -83,7 +85,7 @@ final protected function get(string $attribute)
8385
final protected function set(string $attribute, $value): void
8486
{
8587
if (in_array($attribute, $this->attributes())) {
86-
$method = $this->getProxyMethod('set', $attribute);
88+
$method = $this->getStringKey($attribute, 'set', 'Value');
8789

8890
$this->{$attribute} = ($this->canCall($method))
8991
? call_user_func_array([$this, $method], [$value])
@@ -92,19 +94,26 @@ final protected function set(string $attribute, $value): void
9294
}
9395

9496
/**
95-
* Return the required proxy method.
97+
* Return the required string key.
9698
* - $prefix = 'get'
9799
* - $id = 'Name'
100+
* - $suffix = 'Value'
98101
* will be: getNameValue
99102
*
100103
* @param string $prefix
101104
* @param string $id
105+
* @param string $suffix
102106
*
103107
* @return string
104108
*/
105-
protected function getProxyMethod(string $prefix, string $id): string
109+
protected function getStringKey(string $id, string $prefix = '', string $suffix = ''): string
106110
{
107-
return trim(lcfirst($prefix).ucfirst($id).'Value');
111+
return sprintf(
112+
'%s%s%s',
113+
$prefix,
114+
implode('', map(fn(string $chunk) => ucfirst($chunk), explode('_', $id))),
115+
$suffix
116+
);
108117
}
109118

110119
/**

0 commit comments

Comments
 (0)