Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add state machine dumpers #167

Merged
merged 2 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"symfony/stopwatch": ">=5.4,<8",
"symfony/twig-bundle": ">=5.4,<8",
"symfony/browser-kit": ">=5.4,<8",
"symfony/css-selector": ">=5.4,<8"
"symfony/css-selector": ">=5.4,<8",
"symfony/console": ">=5.4,<8"
},
"autoload": {
"psr-4": {
Expand Down
13 changes: 13 additions & 0 deletions src/Dumper/Dumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Finite\Dumper;

use Finite\State;

interface Dumper
{
/**
* @param enum-string<State> $stateEnum
*/
public function dump(string $stateEnum): string;
}
34 changes: 34 additions & 0 deletions src/Dumper/MermaidDumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Finite\Dumper;

use Finite\State;

class MermaidDumper implements Dumper
{
/**
* @param enum-string<State> $stateEnum
*/
public function dump(string $stateEnum): string
{
$output = [
'---',
'title: ' . $stateEnum,
'---',
'stateDiagram-v2',
];

foreach ($stateEnum::getTransitions() as $transition) {
foreach ($transition->getSourceStates() as $state) {
$output[] = sprintf(
' %s --> %s: %s',
$state->value,
$transition->getTargetState()->value,
$transition->getName()
);
}
}

return implode(PHP_EOL, $output);
}
}
6 changes: 6 additions & 0 deletions src/Extension/Symfony/Bundle/FiniteBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@

namespace Finite\Extension\Symfony\Bundle;

use Finite\Extension\Symfony\Command\DumpStateMachineCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\HttpKernel\Bundle\Bundle;

final class FiniteBundle extends Bundle
{
public function registerCommands(Application $application): void
{
$application->add(new DumpStateMachineCommand);
}
}
53 changes: 53 additions & 0 deletions src/Extension/Symfony/Command/DumpStateMachineCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Finite\Extension\Symfony\Command;

use Finite\Dumper\MermaidDumper;
use Finite\State;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

final class DumpStateMachineCommand extends Command
{
private const FORMAT_MERMAID = 'mermaid';
private const FORMATS = [self::FORMAT_MERMAID];

protected function configure(): void
{
$this
->setName('finite:state-machine:dump')
->setDescription('Dump the state machine graph into requested format')
->addArgument('state_enum', InputArgument::REQUIRED, 'The state enum to use')
->addArgument('format', InputArgument::REQUIRED, 'The format to dump the graph in');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stateEnum = (string)$input->getArgument('state_enum');

if (!(enum_exists($stateEnum) && is_subclass_of($stateEnum, State::class))) {
$io->error('The state enum "' . $stateEnum . '" does not exist.');

return self::FAILURE;
}

$format = (string)$input->getArgument('format');
switch ($format) {
case self::FORMAT_MERMAID:
/** @psalm-suppress ArgumentTypeCoercion Type is enforced upper but not detected by psalm */
$output->writeln((new MermaidDumper)->dump($stateEnum));

break;
default:
$output->writeln('Unknown format "' . $format . '". Supported formats are: ' . implode(', ', self::FORMATS));

return self::FAILURE;
}

return self::SUCCESS;
}
}
6 changes: 3 additions & 3 deletions src/Transition/Transition.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class Transition implements TransitionInterface
{
public function __construct(
public readonly string $name,
/** @var State[] */
/** @var array<int,State&\BackedEnum> */
public readonly array $sourceStates,
public readonly State $targetState,
public readonly State&\BackedEnum $targetState,
/** @var array<string, string> */
public readonly array $properties = []
)
Expand All @@ -28,7 +28,7 @@ public function getSourceStates(): array
return $this->sourceStates;
}

public function getTargetState(): State
public function getTargetState(): State&\BackedEnum
{
return $this->targetState;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Transition/TransitionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
interface TransitionInterface
{
/**
* @return State[]
* @return array<int, \BackedEnum&State>
*/
public function getSourceStates(): array;

public function getTargetState(): State;
public function getTargetState(): State&\BackedEnum;

public function process(object $object): void;

Expand Down
29 changes: 29 additions & 0 deletions tests/Dumper/MermaidDumperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Finite\Tests\Dumper;

use Finite\Dumper\MermaidDumper;
use Finite\Tests\E2E\SimpleArticleState;
use PHPUnit\Framework\TestCase;

class MermaidDumperTest extends TestCase
{
public function test_it_dumps(): void
{
$this->assertSame(
<<<MERMAID
---
title: Finite\Tests\E2E\SimpleArticleState
---
stateDiagram-v2
draft --> published: publish
reported --> published: clear
disabled --> published: clear
published --> reported: report
reported --> disabled: disable
published --> disabled: disable
MERMAID,
(new MermaidDumper)->dump(SimpleArticleState::class)
);
}
}
59 changes: 59 additions & 0 deletions tests/Extension/Symfony/Command/DumpStateMachineCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Finite\Tests\Extension\Symfony\Command;

use Finite\Tests\Extension\Symfony\Fixtures\State\DocumentState;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class DumpStateMachineCommandTest extends KernelTestCase
{
private ?CommandTester $commandTester = null;

protected function setUp(): void
{
parent::setUp();

$kernel = self::bootKernel();
$application = new Application($kernel);

$command = $application->find('finite:state-machine:dump');
$this->commandTester = new CommandTester($command);
}

public function test_it_returns_mermaid_dump(): void
{
$this->commandTester->execute([
'state_enum' => DocumentState::class,
'format' => 'mermaid',
]);

$this->commandTester->assertCommandIsSuccessful();
}

public function test_it_fails_with_unknown_state_enum(): void
{
$this->commandTester->execute([
'state_enum' => 'UnknownStateEnum',
'format' => 'mermaid',
]);

$this->assertSame(1, $this->commandTester->getStatusCode());
}

public function test_it_fails_with_unknown_format(): void
{
$this->commandTester->execute([
'state_enum' => DocumentState::class,
'format' => 'blobfish',
]);

$this->assertSame(1, $this->commandTester->getStatusCode());
}

protected static function getKernelClass(): string
{
return \AppKernel::class;
}
}
7 changes: 3 additions & 4 deletions tests/Transition/TransitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Finite\Tests\Transition;

use Finite\State;
use Finite\Tests\E2E\SimpleArticleState;
use Finite\Transition\Transition;
use PHPUnit\Framework\TestCase;

Expand All @@ -13,12 +14,10 @@ class TransitionTest extends TestCase

protected function setUp(): void
{
$targetState = $this->createMock(State::class);

$this->object = new Transition(
'name',
['source'],
$targetState,
[SimpleArticleState::DRAFT],
SimpleArticleState::PUBLISHED,
['property' => 'value', 'property2' => 'value2'],
);
}
Expand Down
Loading