Skip to content

Commit

Permalink
Release 0.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
othercodes authored Mar 8, 2021
2 parents 347bbb6 + 695d6b3 commit 81e8d1b
Show file tree
Hide file tree
Showing 16 changed files with 715 additions and 37 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Global owners.
* @othercodes
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/Domain/Contracts/Identifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
43 changes: 43 additions & 0 deletions src/Domain/Exceptions/StateException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace OtherCode\ComplexHeart\Domain\Exceptions;

use Exception;

/**
* Class StateException
*
* @author Unay Santisteban <usantisteban@othercode.es>
* @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.");
}
}
16 changes: 16 additions & 0 deletions src/Domain/Exceptions/StateNotFound.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace OtherCode\ComplexHeart\Domain\Exceptions;

/**
* Class StateNotFound
*
* @author Unay Santisteban <usantisteban@othercode.es>
* @package OtherCode\ComplexHeart\Domain\Exceptions
*/
class StateNotFound extends StateException
{

}
16 changes: 16 additions & 0 deletions src/Domain/Exceptions/TransitionNotAllowed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace OtherCode\ComplexHeart\Domain\Exceptions;

/**
* Class TransitionNotAllowed
*
* @author Unay Santisteban <usantisteban@othercode.es>
* @package OtherCode\ComplexHeart\Domain\Exceptions
*/
class TransitionNotAllowed extends StateException
{

}
176 changes: 176 additions & 0 deletions src/Domain/State.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php

declare(strict_types=1);

namespace OtherCode\ComplexHeart\Domain;

use OtherCode\ComplexHeart\Domain\Exceptions\StateException;
use OtherCode\ComplexHeart\Domain\ValueObjects\EnumValue;

/**
* Class State
*
* @see https://en.wikipedia.org/wiki/State_pattern
* @author Unay Santisteban <usantisteban@othercode.es>
* @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<string, callable|null>
*/
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;
}
}
19 changes: 14 additions & 5 deletions src/Domain/Traits/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace OtherCode\ComplexHeart\Domain\Traits;

use function Lambdish\Phunctional\map;

/**
* Trait HasAttributes
*
Expand Down Expand Up @@ -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}])
Expand All @@ -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])
Expand All @@ -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
);
}

/**
Expand Down
Loading

0 comments on commit 81e8d1b

Please sign in to comment.