diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..29c2cb3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Global owners. +* @othercodes diff --git a/composer.json b/composer.json index 73ebfdb..74b0caf 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,9 @@ "ramsey/uuid": "^4.1", "nesbot/carbon": "^2.40", "illuminate/collections": "^8.20", - "spatie/data-transfer-object": "^2.6" + "spatie/data-transfer-object": "^2.6", + "lambdish/phunctional": "^2.1", + "doctrine/instantiator": "^1.4" }, "require-dev": { "mockery/mockery": "^1.4", diff --git a/src/Domain/Contracts/Identifier.php b/src/Domain/Contracts/Identifier.php index 02f9286..41a1e67 100644 --- a/src/Domain/Contracts/Identifier.php +++ b/src/Domain/Contracts/Identifier.php @@ -29,4 +29,11 @@ public function value(): string; * @return bool */ public function is(Identifier $other): bool; + + /** + * Represents the id as string. + * + * @return string + */ + public function __toString(): string; } diff --git a/src/Domain/Exceptions/StateException.php b/src/Domain/Exceptions/StateException.php new file mode 100644 index 0000000..007eee2 --- /dev/null +++ b/src/Domain/Exceptions/StateException.php @@ -0,0 +1,43 @@ + + * @package OtherCode\ComplexHeart\Domain\Exceptions + */ +abstract class StateException extends Exception +{ + /** + * Create a new StateNotFound + * + * @param string $state + * @param array $valid + * + * @return StateNotFound + */ + public static function stateNotFound(string $state, array $valid): StateException + { + $valid = implode(',', $valid); + return new StateNotFound("State <{$state}> not found, must be one of: {$valid}"); + } + + /** + * Create a new TransitionNotAllowed. + * + * @param string $from + * @param string $to + * + * @return StateException + */ + public static function transitionNotAllowed(string $from, string $to): StateException + { + return new TransitionNotAllowed("Transition from <{$from}> to <{$to}> is not allowed."); + } +} \ No newline at end of file diff --git a/src/Domain/Exceptions/StateNotFound.php b/src/Domain/Exceptions/StateNotFound.php new file mode 100644 index 0000000..f166c13 --- /dev/null +++ b/src/Domain/Exceptions/StateNotFound.php @@ -0,0 +1,16 @@ + + * @package OtherCode\ComplexHeart\Domain\Exceptions + */ +class StateNotFound extends StateException +{ + +} \ No newline at end of file diff --git a/src/Domain/Exceptions/TransitionNotAllowed.php b/src/Domain/Exceptions/TransitionNotAllowed.php new file mode 100644 index 0000000..c7d13ac --- /dev/null +++ b/src/Domain/Exceptions/TransitionNotAllowed.php @@ -0,0 +1,16 @@ + + * @package OtherCode\ComplexHeart\Domain\Exceptions + */ +class TransitionNotAllowed extends StateException +{ + +} \ No newline at end of file diff --git a/src/Domain/State.php b/src/Domain/State.php new file mode 100644 index 0000000..b1da266 --- /dev/null +++ b/src/Domain/State.php @@ -0,0 +1,176 @@ + + * @package OtherCode\ComplexHeart\Domain + */ +abstract class State extends EnumValue +{ + private const DEFAULT = 'default'; + + private string $defaultState; + + /** + * Mapping of the available transitions with the transition function. + * + * @var array + */ + private array $transitions = []; + + /** + * State constructor. + * + * @param string $value + */ + public function __construct(string $value = self::DEFAULT) + { + $this->configure(); + + parent::__construct( + $value === self::DEFAULT + ? $this->defaultState + : $value + ); + } + + /** + * Configure the state machine with the specific transitions. + * + * $this->defaultState('SOME_STATE') + * ->allowTransition('SOME_STATE', 'OTHER_STATE') + * ->allowTransition('SOME_STATE', 'ANOTHER_STATE'); + */ + abstract protected function configure(): void; + + /** + * Set the default value for the state machine. + * + * @param string $state + * + * @return $this + */ + protected function defaultState(string $state): State + { + $this->defaultState = $state; + return $this; + } + + /** + * Define the allowed state transitions. + * + * @param string $from + * @param string $to + * @param callable|null $transition + * + * @return $this + * @throws StateException + */ + protected function allowTransition(string $from, string $to, ?callable $transition = null): State + { + if (!static::isValid($from)) { + throw StateException::stateNotFound($from, static::getValues()); + } + + if (!static::isValid($to)) { + throw StateException::stateNotFound($to, static::getValues()); + } + + if (is_null($transition)) { + $key = $this->getTransitionKey($from, $to); + if ($this->canCall($method = $this->getStringKey($key, 'from'))) { + // compute method using the exactly transition key: fromOneToAnother + $transition = [$this, $method]; + } elseif ($this->canCall($method = $this->getStringKey($to, 'to'))) { + // compute the method using only the $to state: toAnother + $transition = [$this, $method]; + } + } + + $this->transitions[$this->getTransitionKey($from, $to)] = $transition; + + return $this; + } + + /** + * Set the value and executed the "on{State}" method if it's available. + * + * This method is automatically invoked from the HasAttributes trait + * on set() the value property. + * + * @param string $value + * + * @return string + */ + protected function setValueValue(string $value): string + { + $onSetStateMethod = $this->getStringKey($value, 'on'); + if ($this->canCall($onSetStateMethod)) { + call_user_func_array([$this, $onSetStateMethod], []); + } + return $value; + } + + /** + * Compute the transition key using the $from and $to strings. + * + * @param string $from + * @param string $to + * + * @return string + */ + private function getTransitionKey(string $from, string $to): string + { + return $this->getStringKey("{$from}_to_{$to}"); + } + + /** + * Check if the given $from $to transition is allowed. + * + * @param string $from + * @param string $to + * + * @return bool + */ + private function isTransitionAllowed(string $from, string $to): bool + { + return array_key_exists($this->getTransitionKey($from, $to), $this->transitions); + } + + /** + * Execute the transition $from oneState $to another. + * + * @param string $to + * @param mixed ...$arguments + * + * @return $this + * @throws StateException + */ + public function transitionTo(string $to, ...$arguments): State + { + if (!static::isValid($to)) { + throw StateException::stateNotFound($to, static::getValues()); + } + + if (!$this->isTransitionAllowed($this->value, $to)) { + throw StateException::transitionNotAllowed($this->value, $to); + } + + if ($transition = $this->transitions[$this->getTransitionKey($this->value, $to)]) { + call_user_func_array($transition, $arguments); + } + + $this->set('value', $to); + + return $this; + } +} \ No newline at end of file diff --git a/src/Domain/Traits/HasAttributes.php b/src/Domain/Traits/HasAttributes.php index e62bef2..dc88cad 100644 --- a/src/Domain/Traits/HasAttributes.php +++ b/src/Domain/Traits/HasAttributes.php @@ -4,6 +4,8 @@ namespace OtherCode\ComplexHeart\Domain\Traits; +use function Lambdish\Phunctional\map; + /** * Trait HasAttributes * @@ -64,7 +66,7 @@ final protected function hydrate(iterable $source): void final protected function get(string $attribute) { if (in_array($attribute, static::attributes())) { - $method = $this->getProxyMethod('get', $attribute); + $method = $this->getStringKey($attribute, 'get', 'Value'); return ($this->canCall($method)) ? call_user_func_array([$this, $method], [$this->{$attribute}]) @@ -83,7 +85,7 @@ final protected function get(string $attribute) final protected function set(string $attribute, $value): void { if (in_array($attribute, $this->attributes())) { - $method = $this->getProxyMethod('set', $attribute); + $method = $this->getStringKey($attribute, 'set', 'Value'); $this->{$attribute} = ($this->canCall($method)) ? call_user_func_array([$this, $method], [$value]) @@ -92,19 +94,26 @@ final protected function set(string $attribute, $value): void } /** - * Return the required proxy method. + * Return the required string key. * - $prefix = 'get' * - $id = 'Name' + * - $suffix = 'Value' * will be: getNameValue * * @param string $prefix * @param string $id + * @param string $suffix * * @return string */ - protected function getProxyMethod(string $prefix, string $id): string + protected function getStringKey(string $id, string $prefix = '', string $suffix = ''): string { - return trim(lcfirst($prefix).ucfirst($id).'Value'); + return sprintf( + '%s%s%s', + $prefix, + implode('', map(fn(string $chunk) => ucfirst($chunk), explode('_', $id))), + $suffix + ); } /** diff --git a/src/Domain/Traits/HasInvariants.php b/src/Domain/Traits/HasInvariants.php index 515d410..177a902 100644 --- a/src/Domain/Traits/HasInvariants.php +++ b/src/Domain/Traits/HasInvariants.php @@ -7,6 +7,9 @@ use Exception; use OtherCode\ComplexHeart\Domain\Exceptions\InvariantViolation; +use function Lambdish\Phunctional\filter; + + /** * Trait HasInvariants * @@ -25,8 +28,9 @@ trait HasInvariants * @var array */ private array $_invariant = [ - 'exception' => InvariantViolation::class, - 'messages.fail' => "Unable to create {class} due: \n{violations}\n", + 'exception' => InvariantViolation::class, + 'handler' => 'invariantHandler', + 'messages.fail' => "Unable to create {class} due: {violations}", ]; /** @@ -38,9 +42,13 @@ final public static function invariants(): array { $invariants = []; foreach (get_class_methods(static::class) as $invariant) { - if (strpos($invariant, 'invariant') === 0) { - $invariants[$invariant] = strtolower( - preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant) + if (strpos($invariant, 'invariant') === 0 && $invariant !== 'invariants') { + $invariants[$invariant] = str_replace( + 'invariant ', + '', + strtolower( + preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant) + ) ); } } @@ -65,15 +73,23 @@ final public static function invariants(): array * $onFail function must have following signature: * fn(array) => void * + * @param callable|null $filter * @param callable|null $onFail * * @return void */ - final private function check(callable $onFail = null): void + final private function check(callable $filter = null, callable $onFail = null): void { $violations = []; - foreach (static::invariants() as $invariant => $rule) { + $invariants = filter( + is_null($filter) + ? fn(string $rule, string $invariant): bool => true + : $filter, + static::invariants() + ); + + foreach ($invariants as $invariant => $rule) { try { if (!call_user_func_array([$this, $invariant], [])) { $violations[$invariant] = $rule; @@ -85,17 +101,21 @@ final private function check(callable $onFail = null): void if (count($violations) > 0) { if (is_null($onFail)) { - $onFail = function (array $violations): void { - throw new $this->_invariant['exception']( - strtr( - $this->_invariant['messages.fail'], - [ - '{class}' => basename(str_replace('\\', '/', static::class)), - '{violations}' => implode("\n", $violations), - ] - ) - ); - }; + $onFail = (method_exists($this, $this->_invariant['handler'])) + ? function (array $violations): void { + call_user_func_array([$this, $this->_invariant['handler']], [$violations]); + } + : function (array $violations): void { + throw new $this->_invariant['exception']( + strtr( + $this->_invariant['messages.fail'], + [ + '{class}' => basename(str_replace('\\', '/', static::class)), + '{violations}' => implode(",", $violations), + ] + ) + ); + }; } $onFail($violations); diff --git a/src/Domain/Traits/IsModel.php b/src/Domain/Traits/IsModel.php index a8f0e2c..2c9b7e1 100644 --- a/src/Domain/Traits/IsModel.php +++ b/src/Domain/Traits/IsModel.php @@ -4,6 +4,10 @@ namespace OtherCode\ComplexHeart\Domain\Traits; +use RuntimeException; +use Doctrine\Instantiator\Instantiator; +use Doctrine\Instantiator\Exception\ExceptionInterface; + /** * Trait IsModel * @@ -19,10 +23,54 @@ trait IsModel * * @param array $source * @param callable|null $onFail + * + * @return static */ - protected function initialize(array $source, callable $onFail = null): void + protected function initialize(array $source, callable $onFail = null) { - $this->hydrate($source); + $this->hydrate($this->mapSource($source)); $this->check($onFail); + + return $this; + } + + /** + * Restore the instance without calling __constructor of the model. + * + * @return static + * + * @throws RuntimeException + */ + public static function wakeup() + { + try { + return (new Instantiator()) + ->instantiate(static::class) + ->initialize(func_get_args()); + } catch (ExceptionInterface $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Map the given source with the actual attributes by position, if + * the provided array is already mapped (assoc) return it directly. + * + * @param array $source + * + * @return array + */ + protected function mapSource(array $source): array + { + // check if the array is indexed or associative. + $isIndexed = fn($source): bool => [] === $source + ? false + : array_keys($source) === range(0, count($source) - 1); + + return $isIndexed($source) + // combine the attributes keys with the provided source values. + ? array_combine(array_slice(static::attributes(), 0, count($source)), $source) + // return the already mapped array source. + : $source; } } diff --git a/tests/Domain/StateTest.php b/tests/Domain/StateTest.php new file mode 100644 index 0000000..a9dab79 --- /dev/null +++ b/tests/Domain/StateTest.php @@ -0,0 +1,91 @@ + + * @package OtherCode\ComplexHeart\Tests\Domain + */ +class StateTest extends MockeryTestCase +{ + public function testShouldCreateValidStateObject(): void + { + $state = new PaymentStatus(); + $this->assertEquals(PaymentStatus::PENDING, $state->value()); + $this->assertEquals('gray', $state->color()); + } + + public function testShouldTransitionedToValidStateAndExecuteSharedTransition(): void + { + $state = new PaymentStatus(); + $state->transitionTo(PaymentStatus::PAID); + + $this->assertEquals(PaymentStatus::PAID, $state->value()); + $this->assertEquals('green', $state->color()); + $this->assertEquals(1, $state->changes()); + } + + public function testShouldTransitionedToValidStateAndExecuteSpecificTransition(): void + { + $state = new PaymentStatus(); + $state->transitionTo(PaymentStatus::CANCELLED); + + $this->assertEquals(PaymentStatus::CANCELLED, $state->value()); + $this->assertEquals('red', $state->color()); + $this->assertEquals(1, $state->changes()); + } + + public function testShouldThrowExceptionTransitionNotAllowed(): void + { + $this->expectException(TransitionNotAllowed::class); + + $state = new PaymentStatus(PaymentStatus::CANCELLED); + $state->transitionTo(PaymentStatus::PAID); + } + + public function testShouldThrowExceptionStateNotFound(): void + { + $this->expectException(StateNotFound::class); + + $state = new PaymentStatus(PaymentStatus::CANCELLED); + $state->transitionTo('NOT_VALID_STATE'); + } + + public function testShouldThrowExceptionStateNotFoundOnDefiningTransitionsFrom(): void + { + $this->expectException(StateNotFound::class); + + new class () extends State { + public const ON = 'on'; + public const OFF = 'off'; + protected function configure(): void + { + $this->allowTransition('INVALID', self::OFF); + } + }; + } + + public function testShouldThrowExceptionStateNotFoundOnDefiningTransitionsTo(): void + { + $this->expectException(StateNotFound::class); + + new class () extends State { + public const ON = 'on'; + public const OFF = 'off'; + protected function configure(): void + { + $this->allowTransition(self::ON, 'INVALID'); + } + }; + } +} diff --git a/tests/Domain/Traits/HasDomainEventTest.php b/tests/Domain/Traits/HasDomainEventTest.php index 4082b6e..bbbe5d6 100644 --- a/tests/Domain/Traits/HasDomainEventTest.php +++ b/tests/Domain/Traits/HasDomainEventTest.php @@ -11,6 +11,7 @@ use OtherCode\ComplexHeart\Domain\ValueObjects\UUIDValue; use OtherCode\ComplexHeart\Tests\Sample\Order; use OtherCode\ComplexHeart\Tests\Sample\OrderLine; +use OtherCode\ComplexHeart\Tests\Sample\ValueObjects\PaymentStatus; use OtherCode\ComplexHeart\Tests\Sample\ValueObjects\ProductName; /** @@ -28,14 +29,36 @@ public function testShouldAddAndPullDomainEvent(): void ->once() ->with(Mockery::type(Event::class)); - $o = Order::create( + $o = new Order( UUIDValue::random(), - new OrderLine(UUIDValue::random(), new ProductName('PR 1')), - new OrderLine(UUIDValue::random(), new ProductName('PR 2')), + new PaymentStatus(PaymentStatus::PENDING), + [ + new OrderLine(UUIDValue::random(), new ProductName('PR 1')), + new OrderLine(UUIDValue::random(), new ProductName('PR 2')) + ] ); $this->assertCount(1, $o->getDomainEvents()); $o->publishDomainEvents($eventBus); $this->assertCount(0, $o->getDomainEvents()); } + + public function testShouldNotAddDomainEvent(): void + { + $eventBus = Mockery::mock(EventBus::class); + $eventBus->shouldReceive('publish')->with(...[]); + + $o = Order::wakeup( + UUIDValue::random(), + new PaymentStatus(PaymentStatus::PENDING), + [ + new OrderLine(UUIDValue::random(), new ProductName('PR 1')), + new OrderLine(UUIDValue::random(), new ProductName('PR 2')) + ] + ); + + $this->assertCount(0, $o->getDomainEvents()); + $o->publishDomainEvents($eventBus); + $this->assertCount(0, $o->getDomainEvents()); + } } diff --git a/tests/Sample/Order.php b/tests/Sample/Order.php index 7f891d0..d1d554f 100644 --- a/tests/Sample/Order.php +++ b/tests/Sample/Order.php @@ -6,9 +6,11 @@ use Exception; use OtherCode\ComplexHeart\Domain\Contracts\Aggregate; +use OtherCode\ComplexHeart\Domain\Exceptions\InvariantViolation; use OtherCode\ComplexHeart\Domain\Traits\IsAggregate; use OtherCode\ComplexHeart\Domain\ValueObjects\UUIDValue as OrderId; use OtherCode\ComplexHeart\Tests\Sample\Events\OrderHasBeenCreated; +use OtherCode\ComplexHeart\Tests\Sample\ValueObjects\PaymentStatus; /** * Class Order @@ -22,34 +24,55 @@ final class Order implements Aggregate private OrderId $id; + private PaymentStatus $status; + private array $orderLines; /** * Order constructor. * * @param OrderId $id + * @param PaymentStatus $status * @param OrderLine ...$orderLines + * + * @throws Exception */ - public function __construct(OrderId $id, OrderLine ...$orderLines) + public function __construct(OrderId $id, PaymentStatus $status, array $orderLines) { - $this->initialize(['id' => $id, 'orderLines' => $orderLines]); + $this->initialize([$id, $status, $orderLines]); + $this->registerDomainEvent(new OrderHasBeenCreated($this->id()->value())); + } + + /** + * Invariant, check that all items in order line array are type of OrderLine. + * + * @return bool + * @throws InvariantViolation + */ + protected function invariantEachOrderLineMustBeTypeOfOrderLine(): bool + { + foreach ($this->orderLines as $orderLine) { + if (!($orderLine instanceof OrderLine)) { + throw new InvariantViolation("All order lines must be type of OrderLine"); + } + } + + return true; } /** * Named constructor. * * @param OrderId $id + * @param PaymentStatus $status * @param OrderLine ...$orderLines * * @return static * @throws Exception */ - public static function create(OrderId $id, OrderLine ...$orderLines): self + public static function create(OrderId $id, PaymentStatus $status, array $orderLines): self { - $order = new self($id, ...$orderLines); - $order->registerDomainEvent(new OrderHasBeenCreated($order->id()->value())); - - return $order; + return new self($id, $status, $orderLines); } /** diff --git a/tests/Sample/ValueObjects/PaymentStatus.php b/tests/Sample/ValueObjects/PaymentStatus.php new file mode 100644 index 0000000..c438de1 --- /dev/null +++ b/tests/Sample/ValueObjects/PaymentStatus.php @@ -0,0 +1,66 @@ + + * @package OtherCode\ComplexHeart\Tests\Sample\ValueObjects + */ +class PaymentStatus extends State +{ + public const PENDING = 'pending'; + public const PAID = 'paid'; + public const CANCELLED = 'cancelled'; + + private string $color; + + private int $stateChanges = 0; + + protected function configure(): void + { + $this->defaultState(self::PENDING) + ->allowTransition(self::PENDING, self::PAID) + ->allowTransition(self::PENDING, self::CANCELLED); + } + + public function color(): string + { + return $this->color; + } + + public function changes(): int + { + return $this->stateChanges; + } + + protected function toPaid(): void + { + $this->stateChanges++; + } + + protected function onPending(): void + { + $this->color = 'gray'; + } + + protected function onPaid(): void + { + $this->color = 'green'; + } + + protected function fromPendingToCancelled(): void + { + $this->stateChanges++; + } + + protected function onCancelled(): void + { + $this->color = 'red'; + } +} \ No newline at end of file diff --git a/wiki/State-Pattern.md b/wiki/State-Pattern.md new file mode 100644 index 0000000..5b6cd4d --- /dev/null +++ b/wiki/State-Pattern.md @@ -0,0 +1,135 @@ +> IMPORTANT: This feature is heavily inspired in [spatie/laravel-model-states](https://spatie.be/docs/laravel-model-states/v2/working-with-states/01-configuring-states) + +Simple implementation of [State pattern](https://en.wikipedia.org/wiki/State_pattern). + +Defining a state value in our aggregate is really easy we just need to create our Value object that defines our business +rules of our "state", por example the PaymentStatus of an order: + +```php +class PaymentStatus extends State +{ + public const PENDING = 'pending'; + public const PAID = 'paid'; + public const CANCELLED = 'cancelled'; + + protected function configure(): void + { + $this->defaultState(self::PENDING) + ->allowTransition(self::PENDING, self::PAID) + ->allowTransition(self::PENDING, self::CANCELLED); + } +} +``` + +As we can se above the definition is really simple we only need to implement the `configure()` method, defining the +default state, and the allowed state transitions using the `defaultState()` and `allowTransition()` methods. + +Now we just need to use the `transitionTo()` method to change the state of our object: + +```php +$state = new PaymentState(); +$state->value(); // pending +$state->transitionTo(PaymentState::PAID); +$state->value(); // paid +``` + +The `PaymentState` object is initialized in `pending` state as we define it in the `configure()` method. But, what +happens if we try to transition to a non-allowed state? + +```php +$state = new PaymentState(PaymentState::PAID); +$state->transitionTo(PaymentState::PENDING); + +// TransitionNotAllowed exception will be thrown. +``` + +Additionally, we can add some behavior on our object by defining some methods: + +```php +class PaymentStatus extends State +{ + public const PENDING = 'pending'; + public const PAID = 'paid'; + public const CANCELLED = 'cancelled'; + + private string $color; + + private int $stateChanges = 0; + + protected function configure(): void + { + $this->defaultState(self::PENDING) + ->allowTransition(self::PENDING, self::PAID) + ->allowTransition(self::PENDING, self::CANCELLED) + ->allowTransition(self::CANCELLED, self::PENDING) + ->allowTransition(self::PAID, self::CANCELLED); + } + + protected function onPending(): void + { + $this->color = 'gray'; + } + + protected function onPaid(): void + { + $this->color = 'green'; + } + + protected function onCancelled(): void + { + $this->color = 'red'; + } + + protected function fromPaidToCancelled(): void + { + $this->stateChanges++; + } + + protected function toPaid(): void + { + $this->stateChanges++; + } +} +``` + +- `onPendig()` this method configures the object for a specific state, so for example when the PaymentStatus is + in `paid` state, the `onPending()` method will be executed. +- `toPaid()` is executed when the transition is on going from any allowed state to `paid`, which means, the value of the + object still the old one, in this case is `pending`. Once this method ends the value is updated to the new + value `paid`. +- `fromPaidToCancelled()` same as above but this method will only be executed when the state changes from `paid` to + `cancelled`. The transition from `pending` to `cancelled` will not execute this method. + +The way to define these methods is pretty simple: + +- `on{status in camel case}`. +- `to{status in camel case}`. +- `from{stats in camel case}To{status in camel case}`. + +Finally, we can pass arbitrary arguments to the `transitionTo()` method as we can see at below: + +```php +class PaymentStatus extends State +{ + public const PENDING = 'pending'; + public const PAID = 'paid'; + public const CANCELLED = 'cancelled'; + + protected function configure(): void + { + $this->defaultState(self::PENDING) + ->allowTransition(self::PENDING, self::PAID) + ->allowTransition(self::PENDING, self::CANCELLED); + } + + //... + + protected function fromPendingToPaid($my_color): void + { + // do something with $my_color + } +} + +$state = new PaymentState(PaymentState::PENDING); +$state->transitionTo(PaymentState::PAID, 'my_color_value'); +``` diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 1521f85..54682f6 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -4,4 +4,5 @@ - [Entities](Domain-Modeling-Entities) - [Value Objects](Domain-Modeling-Value-Objects) - [Service Bus](Service-Bus) -- Criteria Pattern \ No newline at end of file +- Criteria Pattern +- [State Pattern](State-Pattern.md) \ No newline at end of file