diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..229ff3b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +indent_size = 4 +indent_style = space +max_line_length = 120 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes index b509bd5..a63b009 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,9 @@ -/.gitattributes export-ignore -/.github export-ignore -/.gitignore export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore +.github export-ignore +tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.php-cs-fixer.dist.ph export-ignore +phpstan.dist.neon export-ignore +phpunit.xml.dist export-ignore +README.md export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..0970b50 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [uuf6429] +custom: ['https://paypal.me/uuf6429'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 848f1e0..c3d8e0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,34 +8,37 @@ on: jobs: - build: - name: Test + Lint: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + - run: composer update --ansi --no-progress --prefer-dist --no-interaction + - run: composer run lint + + Test: strategy: + fail-fast: false matrix: - php: [ '7.4', '8.0' ] - + os: [ 'ubuntu-latest' ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + include: + - php: '8.3' + os: 'macos-latest' + - php: '8.3' + os: 'windows-latest' + runs-on: ${{ matrix.os }} steps: - - name: Set up PHP - uses: shivammathur/setup-php@v2 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - coverage: xdebug2 - - - name: Checkout code - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - - name: Download dependencies - uses: ramsey/composer-install@v1 - with: - composer-options: --no-interaction --prefer-dist --optimize-autoloader - - - name: Run tests - run: ./vendor/bin/phpunit --coverage-clover coverage.xml - - - name: Upload to Codecov + coverage: xdebug + - run: composer update --ansi --no-progress --prefer-dist --no-interaction + - run: composer run test:cover + - uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - run: bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore index 892b251..a0f7808 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -.idea/ -vendor/ -*.cache -*.iml +/vendor/ composer.lock +phpstan.neon +.php-cs-fixer.php +.php-cs-fixer.cache +coverage.xml +phpunit.xml +.phpunit.result.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..17407e6 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,14 @@ +setRiskyAllowed(true) + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setRules([ + '@PER-CS2.0' => true, + '@PER-CS2.0:risky' => true, + 'cast_spaces' => ['space' => 'none'], + ]) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__), + ); diff --git a/README.md b/README.md index 3350a2a..a5558a1 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,23 @@ This library provides some interfaces and a basic implementation of a State Engine or State Machine. ✨ **Highlights:** + - Dual functionality: - 1. Either as a basic state engine; switching to a desired state as long the transition is defined) - ([see "JiraIssueTest"](#jiraissuetest-state-engine)) - 2. Or a more sophisticated state machine; same as above but matching data for any state - ([see "TurnstileTest"](#turnstiletest-state-machine)) + 1. Either as a basic state engine; switching to a desired state as long the transition is + defined ([see "JiraIssueTest"](#jiraissuetest-state-engine)) + 2. Or a more sophisticated state machine; same as above but matching data for any + state ([see "TurnstileTest"](#turnstiletest-state-machine)) - Highly composable - everything can be replaced as desired - [PSR-14](http://www.php-fig.org/psr/psr-14/) (Event Dispatcher) compatible - Fluent builder interface ([see "From Scratch"](#from-scratch)) -- Generates PlantUML markup ([see "Examples & Testing"](#examples--testing)) +- Generates Mermaid or PlantUML markup ([see "Examples & Testing"](#-examples--testing)) ## 🔌 Installation The recommended and easiest way to install this library is through [Composer](https://getcomposer.org/): -```bash -composer require "uuf6429/state-engine-php" "^2.0" +```shell +composer require "uuf6429/state-engine-php" ``` ## 🧐 Why? @@ -54,12 +55,15 @@ There are a few key parts to how this works: ## 🚀 Usage You have the possibility to use it from scratch or plug it into your existing. There are basically three parts to it: + 1. configuring the engine (creating states and transitions) 2. using the engine (eg, in a web controller or service) 3. (optionally) handling events (with the same event dispatcher provided to the engine) A slightly different situation would be when you need to provide a list of valid transitions, for example to the user. -In this case, having the [`StateTraversion`](https://github.com/uuf6429/state-engine-php/blob/main/src/Implementation/Traits/StateTraversion.php) trait on the repository would be useful. +In this case, having the [ +`StateTraversion`](https://github.com/uuf6429/state-engine-php/blob/main/src/Implementation/Traits/StateTraversion.php) +trait on the repository would be useful. ### From Scratch @@ -135,24 +139,58 @@ $doorStateManager->changeState($doorStateMutator, new State('closed')); ## 😎 Examples & Testing -You can find some examples in this readme as well as [the tests](https://github.com/uuf6429/state-engine-php/tree/main/tests), some of which explained below. +You can find some examples in this readme as well +as [the tests](https://github.com/uuf6429/state-engine-php/tree/main/tests), some of which are explained below. ### [`JiraIssueTest`](https://github.com/uuf6429/state-engine-php/blob/main/tests/JiraIssueTest.php) State Engine This test provides a realistic example of how Jira Issue states could be set up. -The test also generates the PlantUML diagram below (embedded as an image due to GFM limitations), thanks to the [Plantable trait](https://github.com/uuf6429/state-engine-php/blob/main/src/Implementation/Traits/Plantable.php): - -![jira issue example](https://www.planttext.com/api/plantuml/svg/TPBDRiCW48JlFCKUauDV88SgZgfAlLIrymGqJ2rK31PiBENjYurfux_hpZVB370EB3tVMoF4uI9lFyOrHogA5pgKLff7qE589xgWqPRaD5cIxvPUqG_ScmnSi8ygVJjF2ZsCwrfO5a_xHbCDgHuZDNcpJZVNTWQCbUNlr1FLuBktn8w-qb0i5wuwV02AMkSHOx7K9cnR_ikaqhCEMLmqgCg1lyAg8L5Lxe8r36J0nbNvfEmwfqnNTjqyqZn5hf0IfGQCmDes8i-tDrTbZAGDr1xtb3sodpA4WTtG9rzmfeTAZpKg8vsdwmTr7QmGvtY9yJV-0W00) +The test also generates the Mermaid diagram below, thanks to +the [Mermaidable trait](https://github.com/uuf6429/state-engine-php/blob/main/src/Implementation/Traits/Mermaidable.php): + +```mermaid +stateDiagram + s1_backlog: Backlog + s2_analysis: Analysis + s3_in_dev: In Dev + s4_ready_for_dev: Ready for Dev + s5_ready_for_qa: Ready for QA + s6_ready_for_release: Ready for Release + s7_in_qa: In QA + s8_resolved: Resolved + s1_backlog --> s2_analysis: Begin analysis + s1_backlog --> s3_in_dev: Fast-track for development + s2_analysis --> s4_ready_for_dev: Analysis complete + s2_analysis --> s1_backlog: Return to backlog + s4_ready_for_dev --> s2_analysis: Need more details + s4_ready_for_dev --> s3_in_dev: Begin development + s3_in_dev --> s5_ready_for_qa: Send to QA + s3_in_dev --> s6_ready_for_release: Fast-track for release + s3_in_dev --> s4_ready_for_dev: Stop development + s5_ready_for_qa --> s7_in_qa: Begin testing + s7_in_qa --> s4_ready_for_dev: QA Failed + s7_in_qa --> s6_ready_for_release: QA Passed + s6_ready_for_release --> s8_resolved: Released + s8_resolved --> s1_backlog: Reopen +``` ### [`TurnstileTest`](https://github.com/uuf6429/state-engine-php/blob/main/tests/JiraIssueTest.php) State Machine -This test illustrates how a [state machine](https://en.wikipedia.org/wiki/Finite-state_machine) can be used to model a [turnstile gate](https://en.wikipedia.org/wiki/Turnstile). +This test illustrates how a [state machine](https://en.wikipedia.org/wiki/Finite-state_machine) can be used to model +a [turnstile gate](https://en.wikipedia.org/wiki/Turnstile). As before, here's the generated diagram: -![turnstile example](https://www.planttext.com/api/plantuml/svg/SoWkIImgAStDuUBIyCmjI2mkJapAITLKqDMrKz08W7Ej59ppC_CK2d8IarDJk90amEgGDLef1AGM5UVdAPGdvcGNAvHa5EMNfcTmSJcavgM0h040) +```mermaid +stateDiagram + s1_locked: Impassable + s2_open: Passable + s1_locked --> s2_open: Coin placed + s2_open --> s1_locked: Person walks through +``` + +Here's how the state machine definition looks like and how it could be used: -Here's how the state machine definition looks like and is used: ```php use App\Models\Turnstile; // example model that implements StateAwareInterface diff --git a/composer.json b/composer.json index 2487aa0..9b003d2 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,8 @@ { "name": "uuf6429/state-engine", - "type": "library", - "homepage": "https://github.com/uuf6429/state-engine-php", - "readme": "README.md", - "license": "MIT", "description": "A library providing interfaces and basic implementation of a State Engine or Machine", + "license": "MIT", + "type": "library", "keywords": [ "state", "engine", @@ -14,18 +12,24 @@ "workflow", "uuf6429" ], + "readme": "README.md", "authors": [ { "name": "Christian Sciberras", "email": "christian@sciberras.me" } ], + "homepage": "https://github.com/uuf6429/state-engine-php", "require": { "php": "^7.4 || ^8.0", "psr/event-dispatcher": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "ergebnis/composer-normalize": "^2.42", + "friendsofphp/php-cs-fixer": "^3.53", + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^9.5 || ^10", + "roave/security-advisories": "dev-latest" }, "autoload": { "psr-4": { @@ -36,5 +40,24 @@ "psr-4": { "uuf6429\\StateEngine\\": "tests/" } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + }, + "process-timeout": 0 + }, + "scripts": { + "lint": [ + "composer normalize --dry-run", + "composer exec phpstan -- analyse --no-progress", + "composer exec php-cs-fixer -- fix --dry-run --show-progress=none --diff" + ], + "lint:fix": [ + "composer normalize", + "composer exec php-cs-fixer -- fix --show-progress=dots --diff" + ], + "test": "phpunit ./tests/", + "test:cover": "@php -dzend_extension=php_xdebug -dxdebug.mode=coverage vendor/bin/phpunit --coverage-clover coverage.xml ./tests/" } } diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..a4fe7db --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,6 @@ +parameters: + level: 9 + treatPhpDocTypesAsCertain: true + paths: + - src + - tests diff --git a/src/Exceptions/InvalidArgumentException.php b/src/Exceptions/InvalidArgumentException.php index 797dc1a..3b8ea24 100644 --- a/src/Exceptions/InvalidArgumentException.php +++ b/src/Exceptions/InvalidArgumentException.php @@ -2,7 +2,4 @@ namespace uuf6429\StateEngine\Exceptions; -class InvalidArgumentException extends \InvalidArgumentException -{ - -} +class InvalidArgumentException extends \InvalidArgumentException {} diff --git a/src/Exceptions/RuntimeException.php b/src/Exceptions/RuntimeException.php index 53bb7e0..fd6fa4e 100644 --- a/src/Exceptions/RuntimeException.php +++ b/src/Exceptions/RuntimeException.php @@ -2,7 +2,4 @@ namespace uuf6429\StateEngine\Exceptions; -class RuntimeException extends \RuntimeException -{ - -} +class RuntimeException extends \RuntimeException {} diff --git a/src/Implementation/Builder.php b/src/Implementation/Builder.php index 83e8067..434a27e 100644 --- a/src/Implementation/Builder.php +++ b/src/Implementation/Builder.php @@ -28,10 +28,7 @@ class Builder */ protected array $transitions = []; - private function __construct() - { - - } + final private function __construct() {} public static function create(): self { @@ -41,9 +38,19 @@ public static function create(): self public static function makeStateMutator(callable $getter, callable $setter): StateAwareInterface { return new class ($getter, $setter) implements StateAwareInterface { + /** + * @var callable(): StateInterface + */ private $getter; + /** + * @var callable(StateInterface): void + */ private $setter; + /** + * @param callable(): StateInterface $getter + * @param callable(StateInterface): void $setter + */ public function __construct(callable $getter, callable $setter) { $this->getter = $getter; @@ -105,11 +112,14 @@ public function defTransition(string $oldStateName, string $newStateName, ?strin new Transition( $this->states[$oldStateName] ?? new State($oldStateName), $this->states[$newStateName] ?? new State($newStateName), - $description - ) + $description, + ), ); } + /** + * @param array $data + */ public function defDataTransition(string $oldStateName, array $data, string $newStateName, ?string $description = null): self { return $this->addTransition( @@ -117,8 +127,8 @@ public function defDataTransition(string $oldStateName, array $data, string $new $this->states[$oldStateName] ?? new State($oldStateName), $data, $this->states[$newStateName] ?? new State($newStateName), - $description - ) + $description, + ), ); } diff --git a/src/Implementation/Entities/TransitionWithData.php b/src/Implementation/Entities/TransitionWithData.php index 81c6ad8..631ec4d 100644 --- a/src/Implementation/Entities/TransitionWithData.php +++ b/src/Implementation/Entities/TransitionWithData.php @@ -6,8 +6,14 @@ class TransitionWithData extends Transition { + /** + * @var array + */ private array $data; + /** + * @param array $data + */ public function __construct(StateInterface $oldState, array $data, StateInterface $newState, ?string $description = null) { parent::__construct($oldState, $newState, $description); @@ -26,7 +32,7 @@ public function __toString(): string return sprintf( '%s (%s)', $this->getOldState()->getId(), - serialize($this->data) + serialize($this->data), ); } } diff --git a/src/Implementation/Events/StateChanged.php b/src/Implementation/Events/StateChanged.php index f12003a..45cd2dc 100644 --- a/src/Implementation/Events/StateChanged.php +++ b/src/Implementation/Events/StateChanged.php @@ -36,9 +36,9 @@ public function __toString(): string return sprintf( '%s[%s, %s->%s]', __CLASS__, - method_exists($item, '__toString') ? $item : get_class($item), + method_exists($item, '__toString') ? $item->__toString() : get_class($item), $this->getOldState(), - $item->getState() + $item->getState(), ); } } diff --git a/src/Implementation/Events/StateChanging.php b/src/Implementation/Events/StateChanging.php index ee0f916..38c7885 100644 --- a/src/Implementation/Events/StateChanging.php +++ b/src/Implementation/Events/StateChanging.php @@ -36,9 +36,9 @@ public function __toString(): string return sprintf( '%s[%s, %s->%s]', __CLASS__, - method_exists($item, '__toString') ? $item : get_class($item), + method_exists($item, '__toString') ? $item->__toString() : get_class($item), $item->getState(), - $this->getNewState() + $this->getNewState(), ); } } diff --git a/src/Implementation/Repositories/AbstractTraversable.php b/src/Implementation/Repositories/AbstractTraversable.php index 8175033..2c8f860 100644 --- a/src/Implementation/Repositories/AbstractTraversable.php +++ b/src/Implementation/Repositories/AbstractTraversable.php @@ -2,21 +2,21 @@ namespace uuf6429\StateEngine\Implementation\Repositories; -use Exception; use IteratorAggregate; -use Traversable; use uuf6429\StateEngine\Implementation\Traits\FindsTransition; -use uuf6429\StateEngine\Implementation\Traits\StateTraversion; +use uuf6429\StateEngine\Implementation\Traits\StateTraversal; +use uuf6429\StateEngine\Interfaces\TransitionInterface; use uuf6429\StateEngine\Interfaces\TransitionRepositoryInterface; +/** + * @implements IteratorAggregate + */ abstract class AbstractTraversable implements TransitionRepositoryInterface, IteratorAggregate { - use FindsTransition, StateTraversion; + use FindsTransition; + use StateTraversal; - /** - * @throws Exception - */ - public function all(): Traversable + public function all(): iterable { return $this->getIterator(); } diff --git a/src/Implementation/Repositories/ArrayRepository.php b/src/Implementation/Repositories/ArrayRepository.php index 6daa8c8..b1af04f 100644 --- a/src/Implementation/Repositories/ArrayRepository.php +++ b/src/Implementation/Repositories/ArrayRepository.php @@ -4,19 +4,30 @@ use ArrayIterator; use uuf6429\StateEngine\Implementation\Traits; +use uuf6429\StateEngine\Interfaces\TransitionInterface; class ArrayRepository extends AbstractTraversable { - use Traits\StateTraversion; + use Traits\StateTraversal; use Traits\Plantable; + use Traits\Mermaidable; + /** + * @var list + */ private array $transitions; - public function __construct(array $transitions) + /** + * @param list $transitions + */ + public function __construct(array$transitions) { $this->transitions = $transitions; } + /** + * @return ArrayIterator + */ public function getIterator(): ArrayIterator { return new ArrayIterator($this->transitions); diff --git a/src/Implementation/StateMachine.php b/src/Implementation/StateMachine.php index 06edd13..8a03bdb 100644 --- a/src/Implementation/StateMachine.php +++ b/src/Implementation/StateMachine.php @@ -10,8 +10,7 @@ class StateMachine extends AbstractEngine /** * A shortcut to avoid getting a transition. * - * @param StateAwareInterface $item - * @param array $inputData + * @param array $inputData */ public function processInput(StateAwareInterface $item, array $inputData): void { diff --git a/src/Implementation/Traits/FindsTransition.php b/src/Implementation/Traits/FindsTransition.php index 235decb..1e7e96f 100644 --- a/src/Implementation/Traits/FindsTransition.php +++ b/src/Implementation/Traits/FindsTransition.php @@ -4,13 +4,15 @@ use Exception; use uuf6429\StateEngine\Interfaces\TransitionInterface; -use uuf6429\StateEngine\Interfaces\TransitionRepositoryInterface; /** - * @mixin TransitionRepositoryInterface + * This trait searches for a transition object and returns the match, or null if no match is found. You'd typically + * `use` this in a class implementing {@see TransitionRepositoryInterface}. */ trait FindsTransition { + abstract public function all(): iterable; + /** * @throws Exception */ diff --git a/src/Implementation/Traits/Mermaidable.php b/src/Implementation/Traits/Mermaidable.php new file mode 100644 index 0000000..f4c45c8 --- /dev/null +++ b/src/Implementation/Traits/Mermaidable.php @@ -0,0 +1,80 @@ + + */ + abstract public function all(): iterable; + + public function toMermaid(): string + { + /** + * @return array + */ + $extractStateInfo = static function (TransitionInterface ...$transitions): array { + $result = []; + + foreach ($transitions as $transition) { + /** @var StateInterface $state */ + foreach ([$transition->getOldState(), $transition->getNewState()] as $state) { + $stateName = $state->getName(); + $result[$stateName] ??= [ + 'id' => sprintf('s%d_%s', count($result) + 1, preg_replace('/\W/', '_', $stateName)), + 'text' => ($state instanceof DescribableInterface) ? $state->getDescription() : $stateName, + ]; + } + } + + return $result; + }; + + /** + * @param array $stateInfo + * @return iterable + */ + $generateNodesIds = static function (array $stateInfo): iterable { + foreach ($stateInfo as ['id' => $stateId, 'text' => $stateText]) { + yield " $stateId: $stateText"; + } + }; + + /** + * @param array $stateInfo + * @return iterable + */ + $generateNodesAndEdges = static function (array $stateInfo, TransitionInterface ...$transitions): iterable { + foreach ($transitions as $transition) { + $oldStateId = $stateInfo[$transition->getOldState()->getName()]['id']; + $newStateId = $stateInfo[$transition->getNewState()->getName()]['id']; + + yield $transition instanceof DescribableInterface + ? sprintf(' %s --> %s : %s', $oldStateId, $newStateId, $transition->getDescription()) + : sprintf(' %s --> %s', $oldStateId, $newStateId); + } + }; + + $transitions = iterator_to_array($this->all()); + $stateInfo = $extractStateInfo(...$transitions); + + return implode( + PHP_EOL, + [ + 'stateDiagram', + ...$generateNodesIds($stateInfo), + ...$generateNodesAndEdges($stateInfo, ...$transitions), + ], + ); + } +} diff --git a/src/Implementation/Traits/Plantable.php b/src/Implementation/Traits/Plantable.php index d3d8299..d9d8ac3 100644 --- a/src/Implementation/Traits/Plantable.php +++ b/src/Implementation/Traits/Plantable.php @@ -7,33 +7,43 @@ use uuf6429\StateEngine\Interfaces\TransitionRepositoryInterface; /** - * This trait provides a method for generating a Plant UML diagram of the various states and transitions, assuming the - * current class is a {@see TransitionRepositoryInterface}. + * This trait provides a method for generating a Plant UML diagram of the various states and transitions. You'd + * typically `use` this in a class implementing {@see TransitionRepositoryInterface}. */ trait Plantable { + /** + * @return iterable + */ + abstract public function all(): iterable; + public function toPlantUML(): string { - $generateNodesAndEdges = static function (TransitionInterface $transition): string { - $oldText = ($oldState = $transition->getOldState()) instanceof DescribableInterface - ? $oldState->getDescription() : $oldState->getName(); - $newText = ($newState = $transition->getNewState()) instanceof DescribableInterface - ? $newState->getDescription() : $newState->getName(); - - $result = "($oldText) --> ($newText)"; + /** + * @return iterable + */ + $generateNodesAndEdges = static function (TransitionInterface ...$transitions): iterable { + foreach ($transitions as $transition) { + $oldStateText = ($oldState = $transition->getOldState()) instanceof DescribableInterface + ? $oldState->getDescription() : $oldState->getName(); + $newStateText = ($newState = $transition->getNewState()) instanceof DescribableInterface + ? $newState->getDescription() : $newState->getName(); - if ($transition instanceof DescribableInterface) { - $result .= " : {$transition->getDescription()}"; + yield $transition instanceof DescribableInterface + ? sprintf('(%s) --> (%s) : %s', $oldStateText, $newStateText, $transition->getDescription()) + : sprintf('(%s) --> (%s)', $oldStateText, $newStateText); } - - return $result; }; - /** @var $this TransitionRepositoryInterface */ - return implode(PHP_EOL, array_merge( - ['@startuml', ''], - array_map($generateNodesAndEdges, iterator_to_array($this)), - ['', '@enduml'] - )); + return implode( + PHP_EOL, + [ + '@startuml', + '', + ...$generateNodesAndEdges(...$this->all()), + '', + '@enduml', + ], + ); } } diff --git a/src/Implementation/Traits/StateTraversal.php b/src/Implementation/Traits/StateTraversal.php new file mode 100644 index 0000000..1cc5aab --- /dev/null +++ b/src/Implementation/Traits/StateTraversal.php @@ -0,0 +1,48 @@ + + */ + abstract public function all(): iterable; + + /** + * @return list + * @throws Exception + */ + public function getForwardTransitions(StateInterface $state): array + { + return array_values(array_filter( + [...$this->all()], + static function (TransitionInterface $transition) use ($state): bool { + return $transition->getOldState()->equals($state); + }, + )); + } + + /** + * @return list + * @throws Exception + */ + public function getBackwardTransitions(StateInterface $state): array + { + return array_values(array_filter( + [...$this->all()], + static function (TransitionInterface $transition) use ($state): bool { + return $transition->getNewState()->equals($state); + }, + )); + } +} diff --git a/src/Implementation/Traits/StateTraversion.php b/src/Implementation/Traits/StateTraversion.php index cc5e9fb..acf7260 100644 --- a/src/Implementation/Traits/StateTraversion.php +++ b/src/Implementation/Traits/StateTraversion.php @@ -2,42 +2,10 @@ namespace uuf6429\StateEngine\Implementation\Traits; -use Exception; -use uuf6429\StateEngine\Interfaces\StateInterface; -use uuf6429\StateEngine\Interfaces\TransitionInterface; -use uuf6429\StateEngine\Interfaces\TransitionRepositoryInterface; - /** - * Assuming the actual class is a {@see TransitionRepositoryInterface}, this trait provide methods for navigating - * between states via transitions, either forward (old state to new state) or backward (new state to old state). + * @deprecated Use {@see StateTraversal} instead. */ trait StateTraversion { - /** - * @throws Exception - */ - public function getForwardTransitions(StateInterface $state): array - { - /** @var $this TransitionRepositoryInterface */ - return array_values(array_filter( - iterator_to_array($this->all()), - static function (TransitionInterface $transition) use ($state): bool { - return $transition->getOldState()->equals($state); - } - )); - } - - /** - * @throws Exception - */ - public function getBackwardTransitions(StateInterface $state): array - { - /** @var $this TransitionRepositoryInterface */ - return array_values(array_filter( - iterator_to_array($this->all()), - static function (TransitionInterface $transition) use ($state): bool { - return $transition->getNewState()->equals($state); - } - )); - } + use StateTraversal; } diff --git a/src/Interfaces/EquatableInterface.php b/src/Interfaces/EquatableInterface.php index 28c5003..c045970 100644 --- a/src/Interfaces/EquatableInterface.php +++ b/src/Interfaces/EquatableInterface.php @@ -7,8 +7,7 @@ interface EquatableInterface /** * Returns true if this object equals the $other object. * - * @param $other - * @return bool + * @param mixed $other */ public function equals($other): bool; } diff --git a/src/Interfaces/TransitionRepositoryInterface.php b/src/Interfaces/TransitionRepositoryInterface.php index f0adc5d..a4f753c 100644 --- a/src/Interfaces/TransitionRepositoryInterface.php +++ b/src/Interfaces/TransitionRepositoryInterface.php @@ -2,8 +2,6 @@ namespace uuf6429\StateEngine\Interfaces; -use Traversable; - interface TransitionRepositoryInterface { /** @@ -17,7 +15,7 @@ public function find(TransitionInterface $search): ?TransitionInterface; /** * Returns an array or Traversable object of all transitions. * - * @return Traversable|TransitionInterface[] + * @return iterable */ - public function all(): Traversable; + public function all(): iterable; } diff --git a/tests/BuilderTest.php b/tests/BuilderTest.php index 836c1e7..4048b69 100644 --- a/tests/BuilderTest.php +++ b/tests/BuilderTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use uuf6429\StateEngine\Implementation\Builder; use uuf6429\StateEngine\Implementation\Entities\State; +use uuf6429\StateEngine\Interfaces\StateInterface; class BuilderTest extends TestCase { @@ -52,12 +53,12 @@ public function test_that_transitioning_with_mutator_works(): void { $item = new StatefulItem(new State('started')); $mutator = Builder::makeStateMutator( - static function () use ($item): State { + static function () use ($item): StateInterface { return $item->getState(); }, static function (State $newState) use ($item): void { $item->setState($newState); - } + }, ); $this->builder->getEngine()->changeState($mutator, new State('finished')); diff --git a/tests/EventTest.php b/tests/EventTest.php index 588c900..08943d4 100644 --- a/tests/EventTest.php +++ b/tests/EventTest.php @@ -2,16 +2,20 @@ namespace uuf6429\StateEngine; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; use uuf6429\StateEngine\Implementation\Builder; use uuf6429\StateEngine\Implementation\Entities\State; -use uuf6429\StateEngine\Interfaces\EngineInterface; +use uuf6429\StateEngine\Implementation\StateEngine; class EventTest extends TestCase { + /** + * @var EventDispatcherInterface&MockObject + */ private EventDispatcherInterface $dispatcher; - private EngineInterface $engine; + private StateEngine $engine; protected function setUp(): void { @@ -48,7 +52,7 @@ public function test_that_engine_triggers_events(): void 'uuf6429\StateEngine\Implementation\Events\StateChanging[StatefulItem, finished->finished]', 'uuf6429\StateEngine\Implementation\Events\StateChanged[StatefulItem, started->finished]', ], - array_map('strval', $dispatchedEvents) + array_map('strval', $dispatchedEvents), ); } } diff --git a/tests/JiraIssueTest.php b/tests/JiraIssueTest.php index a9d8a4b..50bee2d 100644 --- a/tests/JiraIssueTest.php +++ b/tests/JiraIssueTest.php @@ -59,8 +59,8 @@ public function test_that_backlog_can_transition_to_analysis_or_in_dev(): void static function (TransitionInterface $transition) { return $transition->getNewState()->getName(); }, - $transitions - ) + $transitions, + ), ); } @@ -74,8 +74,8 @@ public function test_that_ready_for_release_happens_after_fast_track_or_passing_ static function (TransitionInterface $transition) { return $transition->getOldState()->getName(); }, - $transitions - ) + $transitions, + ), ); } @@ -97,30 +97,62 @@ public function test_that_transitioning_from_in_dev_to_in_qa_is_not_allowed(): v $this->engine->changeState($item, new State('in-qa')); } - public function test_that_plant_uml_generation_works(): void + public function test_that_jira_issues_plant_uml_generation_works(): void { $this->assertEquals( [ - 0 => '@startuml', - 1 => '', - 2 => '(Backlog) --> (Analysis) : Begin analysis', - 3 => '(Backlog) --> (In Dev) : Fast-track for development', - 4 => '(Analysis) --> (Ready for Dev) : Analysis complete', - 5 => '(Analysis) --> (Backlog) : Return to backlog', - 6 => '(Ready for Dev) --> (Analysis) : Need more details', - 7 => '(Ready for Dev) --> (In Dev) : Begin development', - 8 => '(In Dev) --> (Ready for QA) : Send to QA', - 9 => '(In Dev) --> (Ready for Release) : Fast-track for release', - 10 => '(In Dev) --> (Ready for Dev) : Stop development', - 11 => '(Ready for QA) --> (In QA) : Begin testing', - 12 => '(In QA) --> (Ready for Dev) : QA Failed', - 13 => '(In QA) --> (Ready for Release) : QA Passed', - 14 => '(Ready for Release) --> (Resolved) : Released', - 15 => '(Resolved) --> (Backlog) : Reopen', - 16 => '', - 17 => '@enduml', + '@startuml', + '', + '(Backlog) --> (Analysis) : Begin analysis', + '(Backlog) --> (In Dev) : Fast-track for development', + '(Analysis) --> (Ready for Dev) : Analysis complete', + '(Analysis) --> (Backlog) : Return to backlog', + '(Ready for Dev) --> (Analysis) : Need more details', + '(Ready for Dev) --> (In Dev) : Begin development', + '(In Dev) --> (Ready for QA) : Send to QA', + '(In Dev) --> (Ready for Release) : Fast-track for release', + '(In Dev) --> (Ready for Dev) : Stop development', + '(Ready for QA) --> (In QA) : Begin testing', + '(In QA) --> (Ready for Dev) : QA Failed', + '(In QA) --> (Ready for Release) : QA Passed', + '(Ready for Release) --> (Resolved) : Released', + '(Resolved) --> (Backlog) : Reopen', + '', + '@enduml', ], - explode(PHP_EOL, $this->repository->toPlantUML()) + explode(PHP_EOL, $this->repository->toPlantUML()), + ); + } + + public function test_that_jira_issues_mermaid_generation_works(): void + { + $this->assertEquals( + [ + 'stateDiagram', + ' s1_backlog: Backlog', + ' s2_analysis: Analysis', + ' s3_in_dev: In Dev', + ' s4_ready_for_dev: Ready for Dev', + ' s5_ready_for_qa: Ready for QA', + ' s6_ready_for_release: Ready for Release', + ' s7_in_qa: In QA', + ' s8_resolved: Resolved', + ' s1_backlog --> s2_analysis : Begin analysis', + ' s1_backlog --> s3_in_dev : Fast-track for development', + ' s2_analysis --> s4_ready_for_dev : Analysis complete', + ' s2_analysis --> s1_backlog : Return to backlog', + ' s4_ready_for_dev --> s2_analysis : Need more details', + ' s4_ready_for_dev --> s3_in_dev : Begin development', + ' s3_in_dev --> s5_ready_for_qa : Send to QA', + ' s3_in_dev --> s6_ready_for_release : Fast-track for release', + ' s3_in_dev --> s4_ready_for_dev : Stop development', + ' s5_ready_for_qa --> s7_in_qa : Begin testing', + ' s7_in_qa --> s4_ready_for_dev : QA Failed', + ' s7_in_qa --> s6_ready_for_release : QA Passed', + ' s6_ready_for_release --> s8_resolved : Released', + ' s8_resolved --> s1_backlog : Reopen', + ], + explode(PHP_EOL, $this->repository->toMermaid()), ); } diff --git a/tests/TurnstileGateTest.php b/tests/TurnstileGateTest.php index 6fab1ad..993061e 100644 --- a/tests/TurnstileGateTest.php +++ b/tests/TurnstileGateTest.php @@ -45,18 +45,32 @@ public function test_turnstile_opens_after_paying(): void $this->assertSame('open', $item->getState()->getName()); } - public function test_that_plant_uml_generation_works(): void + public function test_that_turnstile_plant_uml_generation_works(): void { $this->assertEquals( [ - 0 => '@startuml', - 1 => '', - 2 => '(Impassable) --> (Passable) : Coin placed', - 3 => '(Passable) --> (Impassable) : Person walks through', - 4 => '', - 5 => '@enduml', + '@startuml', + '', + '(Impassable) --> (Passable) : Coin placed', + '(Passable) --> (Impassable) : Person walks through', + '', + '@enduml', ], - explode(PHP_EOL, $this->repository->toPlantUML()) + explode(PHP_EOL, $this->repository->toPlantUML()), + ); + } + + public function test_that_turnstile_mermaid_generation_works(): void + { + $this->assertEquals( + [ + 'stateDiagram', + ' s1_locked: Impassable', + ' s2_open: Passable', + ' s1_locked --> s2_open : Coin placed', + ' s2_open --> s1_locked : Person walks through', + ], + explode(PHP_EOL, $this->repository->toMermaid()), ); } }