Skip to content

Commit

Permalink
feat(symfony): Connected StateMachine to Symfony EventDispatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
yohang committed Jan 13, 2025
1 parent ec6f7ef commit 254cb6f
Show file tree
Hide file tree
Showing 21 changed files with 188 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
tests/Extension/Symfony/fixtures/app/var/
tests/Extension/Symfony/Fixtures/app/var/
vendor/
.phpunit.result.cache
composer.lock
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
tests/Extension/Symfony/fixtures/app/var/
tests/Extension/Symfony/Fixtures/app/var/
vendor/
.phpunit.result.cache
composer.lock
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ARG DEPENDENCIES=highest
RUN set -eux; \
apk add --no-cache acl libzip; \
apk add --no-cache --virtual .build-deps ${PHPIZE_DEPS} zlib-dev libzip-dev; \
docker-php-ext-install zip; \
docker-php-ext-install zip opcache; \
pecl install pcov;\
docker-php-ext-enable pcov; \
apk del .build-deps;
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ test_all_targets:

psalm:
docker build -t yohang/finite:php-8.4 --build-arg PHP_VERSION=8.4 .
docker run -it --rm yohang/finite:php-8.4 php ./vendor/bin/psalm --show-info=true
docker run -it --rm yohang/finite:php-8.4 php ./vendor/bin/psalm --show-info=true --no-diff
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@
],
"require": {
"php": ">=8.1",
"symfony/property-access": ">=5.4,<8"
"symfony/property-access": ">=5.4,<8",
"psr/event-dispatcher": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5.40",
"symfony/var-dumper": ">=5.4,<8",
"twig/twig": "^3.4",
"vimeo/psalm": "dev-master@dev",
"symfony/http-kernel": ">=5.4,<8",
"symfony/framework-bundle": ">=5.4,<8"
"symfony/framework-bundle": ">=5.4,<8",
"symfony/event-dispatcher": ">=5.4,<8",
"symfony/stopwatch": ">=5.4,<8"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
colors="true"
bootstrap="tests/Extension/Symfony/fixtures/app/bootstrap.php"
bootstrap="tests/Extension/Symfony/Fixtures/app/bootstrap.php"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd">
<source>
<include>
Expand Down
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<psalm
errorLevel="7"
errorLevel="1"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
Expand Down
4 changes: 3 additions & 1 deletion src/Event/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Finite\Event;

abstract class Event
use Psr\EventDispatcher\StoppableEventInterface;

abstract class Event implements StoppableEventInterface
{
private bool $propagationStopped = false;

Expand Down
9 changes: 6 additions & 3 deletions src/Event/EventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

namespace Finite\Event;

class EventDispatcher
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\StoppableEventInterface;

class EventDispatcher implements EventDispatcherInterface
{
/**
* @var array<string,array<callable>>
Expand All @@ -18,7 +21,7 @@ public function addEventListener(string $eventClass, callable $listener): void
$this->listeners[$eventClass][] = $listener;
}

public function dispatch(Event $event): void
public function dispatch(object $event): void
{
if (!isset($this->listeners[get_class($event)])) {
return;
Expand All @@ -27,7 +30,7 @@ public function dispatch(Event $event): void
foreach ($this->listeners[get_class($event)] as $listener) {
$listener($event);

if ($event->isPropagationStopped()) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Reference;

final class FiniteExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$container->addDefinitions(
[
StateMachine::class => (new Definition(StateMachine::class))->setPublic(true),
StateMachine::class => (new Definition(StateMachine::class))
->setArgument('$dispatcher', new Reference('event_dispatcher'))
->setPublic(true),
TwigExtension::class => new Definition(TwigExtension::class),
]
);
Expand Down
19 changes: 9 additions & 10 deletions src/StateMachine.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
use Finite\Event\PostTransitionEvent;
use Finite\Event\PreTransitionEvent;
use Finite\Transition\TransitionInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;

class StateMachine
{
public function __construct(
private readonly EventDispatcher $dispatcher = new EventDispatcher,
private readonly EventDispatcherInterface $dispatcher = new EventDispatcher,
)
{

}

/**
Expand Down Expand Up @@ -85,13 +85,19 @@ public function getReachablesTransitions(object $object, ?string $stateClass = n
);
}

public function getDispatcher(): EventDispatcherInterface
{
return $this->dispatcher;
}

/**
* @param class-string|null $stateClass
*/
private function extractState(object $object, ?string $stateClass = null): State
{
$property = $this->extractStateProperty($object, $stateClass);

/** @psalm-suppress MixedReturnStatement */
return PropertyAccess::createPropertyAccessor()->getValue($object, $property->getName());
}

Expand All @@ -107,11 +113,8 @@ private function extractStateProperty(object $object, ?string $stateClass = null
$reflectionClass = new \ReflectionClass($object);
/** @psalm-suppress DocblockTypeContradiction */
do {
if (!$reflectionClass) {
throw new \InvalidArgumentException('Found no state on object ' . get_class($object));
}

foreach ($reflectionClass->getProperties() as $property) {
/** @var \ReflectionUnionType|\ReflectionIntersectionType|\ReflectionNamedType $reflectionType */
$reflectionType = $property->getType();
if (null === $reflectionType) {
continue;
Expand All @@ -125,10 +128,6 @@ private function extractStateProperty(object $object, ?string $stateClass = null
continue;
}

if (!$reflectionType instanceof \ReflectionNamedType) {
continue;
}

/** @var class-string $name */
$name = $reflectionType->getName();
if (!enum_exists($name)) {
Expand Down
2 changes: 2 additions & 0 deletions src/Transition/Transition.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ class Transition implements TransitionInterface
{
public function __construct(
public readonly string $name,
/** @var State[] */
public readonly array $sourceStates,
public readonly State $targetState,
/** @var array<string, string> */
public readonly array $properties = []
)
{
Expand Down
8 changes: 8 additions & 0 deletions tests/E2E/BasicGraphTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

class Article
{
public $noTypeHere = null;

public int|float $unionType = 0;

public \Traversable&\Countable $intersectionType;

public string $namedType = 'named';

private SimpleArticleState $state = SimpleArticleState::DRAFT;

private readonly \DateTimeInterface $createdAt;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

namespace Finite\Tests\Extension\Symfony\Bundle\DependencyInjection;

use Finite\Extension\Symfony\Bundle\DependencyInjection\FiniteExtension;
use Finite\Extension\Twig\FiniteExtension as TwigExtension;
use Finite\StateMachine;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class FiniteExtensionTest extends TestCase
{
public function test_it_loads_services(): void
{
$container = $this->getMockBuilder(ContainerBuilder::class)
->disableOriginalConstructor()
->getMock();

$container->expects($this->once())->method('addDefinitions')->with(
$this->logicalAnd(
$this->countOf(2),
$this->arrayHasKey(StateMachine::class),
$this->arrayHasKey(TwigExtension::class),
),
);

$extension = new FiniteExtension;
$extension->load([], $container);
}
}
11 changes: 11 additions & 0 deletions tests/Extension/Symfony/Fixtures/Model/Document.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);

namespace Finite\Tests\Extension\Symfony\Fixtures\Model;

use Finite\Tests\Extension\Symfony\Fixtures\State\DocumentState;

class Document
{
public DocumentState $state = DocumentState::DRAFT;
}
25 changes: 25 additions & 0 deletions tests/Extension/Symfony/Fixtures/State/DocumentState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);

namespace Finite\Tests\Extension\Symfony\Fixtures\State;

use Finite\State;
use Finite\Transition\Transition;

enum DocumentState: string implements State
{
case DRAFT = 'draft';
case PUBLISHED = 'published';
case REPORTED = 'reported';
case DISABLED = 'disabled';

public static function getTransitions(): array
{
return [
new Transition('publish', [self::DRAFT], self::PUBLISHED),
new Transition('clear', [self::REPORTED, self::DISABLED], self::PUBLISHED),
new Transition('report', [self::PUBLISHED], self::REPORTED),
new Transition('disable', [self::REPORTED, self::PUBLISHED], self::DISABLED),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public function registerBundles(): iterable

protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void
{
$c->prependExtensionConfig('framework', ['test' => true]);
$c->setParameter('kernel.debug', true);
$c->prependExtensionConfig('framework', ['test' => true, 'profiler' => true]);
}

public function getProjectDir(): string
Expand Down
22 changes: 19 additions & 3 deletions tests/Extension/Symfony/ServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,35 @@

namespace Finite\Tests\Extension\Symfony;

use Finite\Extension\Twig\FiniteExtension;
use Finite\Event\CanTransitionEvent;
use Finite\StateMachine;
use Finite\Tests\Extension\Symfony\Fixtures\Model\Document;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\FrameworkBundle\Test\TestContainer;
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class ServiceTest extends KernelTestCase
{
public function test_services_are_registered(): void
{
/** @var TestContainer $container */
$container = static::getContainer();

$this->assertInstanceOf(StateMachine::class, $container->get(StateMachine::class));
$this->assertInstanceOf(EventDispatcherInterface::class, $container->get(StateMachine::class)->getDispatcher());
}

public function test_it_uses_the_symfony_dispatcher(): void
{
$container = static::getContainer();

/** @var StateMachine $stateMachine */
$stateMachine = $container->get(StateMachine::class);
$stateMachine->can(new Document, 'publish');

/** @var TraceableEventDispatcher $debugDispatcher */
$debugDispatcher = $container->get('debug.event_dispatcher');

$this->assertSame(CanTransitionEvent::class, $debugDispatcher->getOrphanedEvents()[0]);
}

protected static function getKernelClass(): string
Expand Down
32 changes: 32 additions & 0 deletions tests/Extension/Twig/FiniteExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);

namespace Finite\Tests\Extension\Twig;

use Finite\Extension\Twig\FiniteExtension;
use Finite\StateMachine;
use PHPUnit\Framework\TestCase;

class FiniteExtensionTest extends TestCase
{
public function test_it_declare_twig_functions(): void
{
$object = $this->createMock(\stdClass::class);
$stateMachine = $this->createMock(StateMachine::class);

$functions = (new FiniteExtension($stateMachine))->getFunctions();

$functions = array_combine(
array_map(fn($function) => $function->getName(), $functions),
$functions,
);
$this->assertArrayHasKey('finite_can', $functions);
$this->assertArrayHasKey('finite_reachable_transitions', $functions);

$stateMachine->expects($this->once())->method('can')->with($object, 'publish')->willReturn(true);
$functions['finite_can']->getCallable()($object, 'publish');

$stateMachine->expects($this->once())->method('getReachablesTransitions')->with($object)->willReturn([]);
$functions['finite_reachable_transitions']->getCallable()($object);
}
}
Loading

0 comments on commit 254cb6f

Please sign in to comment.