diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index b231c66..f363898 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -43,5 +43,8 @@ jobs: - name: PHPStan run: bin/phpstan + - name: Composer validate + run: composer validate + # - name: Infection # run: bin/infection --formatter=progress diff --git a/composer.json b/composer.json index 17cc4a4..ea8813b 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "infection/infection": "~0.13", "phpstan/phpstan": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^10.0|^11.0|^12.0", "squizlabs/php_codesniffer": "^3.2", "symfony/cache": "^4.0|^5.0|^6.0" }, diff --git a/examples/ContextUsingCustomMetadataTest.php b/examples/ContextUsingCustomMetadataTest.php index 6b6e2d8..cd15ff1 100644 --- a/examples/ContextUsingCustomMetadataTest.php +++ b/examples/ContextUsingCustomMetadataTest.php @@ -64,7 +64,7 @@ public function test_it_should_allow_to_transit_from_pending_to_archived(): void $this->assertTrue($context->state->isInState('archived')); } - public function test_it_should_allow_to_transit_from_approved_to_published(): ContextStub + public function test_it_should_allow_to_transit_from_approved_to_published(): void { $context = new ContextStub(); $context->approve(); @@ -73,8 +73,6 @@ public function test_it_should_allow_to_transit_from_approved_to_published(): Co $context->publish(); $this->assertTrue($context->state->isInState('published')); - - return $context; } public function test_it_should_allow_to_transit_from_approved_to_archived(): void @@ -100,7 +98,7 @@ public function test_it_should_allow_to_transit_from_published_to_approved(): vo $this->assertTrue($context->state->isInState('approved')); } - public function test_it_should_allow_to_transit_from_published_to_archived(): ContextStub + public function test_it_should_allow_to_transit_from_published_to_archived(): void { $context = new ContextStub(); $context->approve(); @@ -110,8 +108,6 @@ public function test_it_should_allow_to_transit_from_published_to_archived(): Co $context->archive(); $this->assertTrue($context->state->isInState('archived')); - - return $context; } public function test_it_should_allow_to_transit_from_archived_to_pending(): void @@ -125,7 +121,7 @@ public function test_it_should_allow_to_transit_from_archived_to_pending(): void $this->assertTrue($context->state->isInState('pending')); } - public function test_it_should_allow_to_transit_from_archived_to_approved(): ContextStub + public function test_it_should_allow_to_transit_from_archived_to_approved(): void { $context = new ContextStub(); $context->discard(); @@ -134,8 +130,6 @@ public function test_it_should_allow_to_transit_from_archived_to_approved(): Con $context->unArchive(); $this->assertTrue($context->state->isInState('approved')); - - return $context; } public function test_attributes_of_pending(): void @@ -146,34 +140,35 @@ public function test_attributes_of_pending(): void $this->assertFalse($context->isVisible()); } - /** - * @param ContextStub $context - * @depends test_it_should_allow_to_transit_from_archived_to_approved - */ - public function test_attributes_of_approved(ContextStub $context): void + public function test_attributes_of_approved(): void { + $context = new ContextStub(); + $context->discard(); + $context->unArchive(); + $this->assertTrue($context->state->isInState('approved')); $this->assertTrue($context->isDraft()); $this->assertFalse($context->isVisible()); } - /** - * @param ContextStub $context - * @depends test_it_should_allow_to_transit_from_approved_to_published - */ - public function test_attributes_of_published(ContextStub $context): void + public function test_attributes_of_published(): void { + $context = new ContextStub(); + $context->approve(); + $context->publish(); + $this->assertTrue($context->state->isInState('published')); $this->assertFalse($context->isDraft()); $this->assertTrue($context->isVisible()); } - /** - * @param ContextStub $context - * @depends test_it_should_allow_to_transit_from_published_to_archived - */ - public function test_attributes_of_archived(ContextStub $context): void + public function test_attributes_of_archived(): void { + $context = new ContextStub(); + $context->approve(); + $context->publish(); + $context->archive(); + $this->assertTrue($context->state->isInState('archived')); $this->assertFalse($context->isDraft()); $this->assertFalse($context->isVisible()); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d57baea..0b8da95 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,18 +2,13 @@ + backupGlobals="false" + backupStaticProperties="false" + colors="true" + beStrictAboutOutputDuringTests="true" + failOnAllIssues="true" + bootstrap="vendor/autoload.php" +> @@ -25,9 +20,9 @@ - - - src - - + + + src + + diff --git a/src/StateMachine.php b/src/StateMachine.php index 448e388..16748c3 100644 --- a/src/StateMachine.php +++ b/src/StateMachine.php @@ -7,6 +7,7 @@ namespace Star\Component\State; +use Closure; use Star\Component\State\Callbacks\AlwaysThrowExceptionOnFailure; use Star\Component\State\Callbacks\TransitionCallback; use Star\Component\State\Event\StateEventStore; @@ -39,8 +40,11 @@ public function __construct( * @throws InvalidStateTransitionException * @throws NotFoundException */ - public function transit(string $transitionName, $context, TransitionCallback $callback = null): string - { + public function transit( + string $transitionName, + mixed $context, + ?TransitionCallback $callback = null + ): string { if (!$callback) { $callback = new AlwaysThrowExceptionOnFailure(); } @@ -101,7 +105,7 @@ public function hasAttribute(string $attribute): bool return $this->states->hasAttribute($this->currentState, $attribute); } - public function addListener(string $event, \Closure $listener): void + public function addListener(string $event, Closure $listener): void { $this->listeners->addListener($event, $listener); } diff --git a/src/StateMetadata.php b/src/StateMetadata.php index edc446a..b3310c5 100644 --- a/src/StateMetadata.php +++ b/src/StateMetadata.php @@ -39,8 +39,11 @@ private function getMachine(): StateMachine * * @return static */ - final public function transit(string $name, $context, TransitionCallback $callback = null): StateMetadata - { + final public function transit( + string $name, + $context, + ?TransitionCallback $callback = null + ): StateMetadata { $this->current = $this->getMachine()->transit($name, $context, $callback); return $this; diff --git a/tests/StateMachineTest.php b/tests/StateMachineTest.php index bcecc9a..9b4b021 100644 --- a/tests/StateMachineTest.php +++ b/tests/StateMachineTest.php @@ -7,30 +7,29 @@ namespace Star\Component\State; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Star\Component\State\Event\StateEventStore; use Star\Component\State\Event\TransitionWasFailed; use Star\Component\State\Event\TransitionWasSuccessful; use Star\Component\State\Event\TransitionWasRequested; +use Star\Component\State\Stub\EventRegistrySpy; use Star\Component\State\Transitions\OneToOneTransition; +use stdClass; +use Throwable; final class StateMachineTest extends TestCase { private TransitionRegistry $registry; private StateMachine $machine; private TestContext $context; - /** - * @var MockObject|EventRegistry - */ - private $listeners; + private EventRegistrySpy $events; public function setUp(): void { - $this->listeners = $this->createMock(EventRegistry::class); + $this->events = new EventRegistrySpy(); $this->context = new TestContext(); $this->registry = new TransitionRegistry(); - $this->machine = new StateMachine('current', $this->registry, $this->listeners); + $this->machine = new StateMachine('current', $this->registry, $this->events); } public function test_it_should_not_allow_to_transition_to_a_not_configured_transition(): void @@ -52,30 +51,22 @@ public function test_it_should_transition_from_one_state_to_the_other(): void public function test_it_should_trigger_an_event_before_any_transition(): void { - $this->listeners - ->expects($this->at(0)) - ->method('dispatch') - ->with( - StateEventStore::BEFORE_TRANSITION, - $this->isInstanceOf(TransitionWasRequested::class) - ); - $this->registry->addTransition(new OneToOneTransition('name', 'current', 'next')); $this->machine->transit('name', $this->context); + $name = StateEventStore::BEFORE_TRANSITION; + $events = $this->events->getDispatchedEvents($name); + self::assertCount(1, $events); + self::assertContainsOnlyInstancesOf(TransitionWasRequested::class, $events); } public function test_it_should_trigger_an_event_after_any_transition(): void { - $this->listeners - ->expects($this->at(1)) - ->method('dispatch') - ->with( - StateEventStore::AFTER_TRANSITION, - $this->isInstanceOf(TransitionWasSuccessful::class) - ); - $this->registry->addTransition(new OneToOneTransition('name', 'current', 'next')); $this->machine->transit('name', $this->context); + $name = StateEventStore::AFTER_TRANSITION; + $events = $this->events->getDispatchedEvents($name); + self::assertCount(1, $events); + self::assertContainsOnlyInstancesOf(TransitionWasSuccessful::class, $events); } public function test_it_should_throw_exception_with_class_context_when_transition_not_allowed(): void @@ -87,7 +78,7 @@ public function test_it_should_throw_exception_with_class_context_when_transitio $this->expectExceptionMessage( "The transition 't' is not allowed when context 'stdClass' is in state 'current'." ); - $this->machine->transit('t', new \stdClass); + $this->machine->transit('t', new stdClass); } public function test_it_should_throw_exception_with_context_as_string_when_transition_not_allowed(): void @@ -112,7 +103,7 @@ public function test_state_can_have_attribute(): void public function test_it_should_visit_the_transitions(): void { $registry = $this->createMock(StateRegistry::class); - $machine = new StateMachine('', $registry, $this->listeners); + $machine = new StateMachine('', $registry, $this->events); $visitor = $this->createMock(TransitionVisitor::class); $registry @@ -131,20 +122,18 @@ public function test_it_should_throw_exception_when_state_do_not_exists(): void public function test_it_should_dispatch_an_event_before_a_transition_has_failed(): void { - $this->listeners - ->expects($this->at(1)) - ->method('dispatch') - ->with( - StateEventStore::FAILURE_TRANSITION, - $this->isInstanceOf(TransitionWasFailed::class) - ); - $this->registry->addTransition(new OneToOneTransition('t', 'from', 'to')); try { $this->machine->transit('t', 'context'); $this->fail('An exception should have been thrown'); - } catch (InvalidStateTransitionException $exception) { + } catch (Throwable $exception) { // silence it + self::assertInstanceOf(InvalidStateTransitionException::class, $exception); } + + $name = StateEventStore::FAILURE_TRANSITION; + $events = $this->events->getDispatchedEvents($name); + self::assertCount(1, $events); + self::assertContainsOnlyInstancesOf(TransitionWasFailed::class, $events); } } diff --git a/tests/Stub/EventRegistrySpy.php b/tests/Stub/EventRegistrySpy.php new file mode 100644 index 0000000..dae3c59 --- /dev/null +++ b/tests/Stub/EventRegistrySpy.php @@ -0,0 +1,45 @@ + + */ + private array $listeners = []; + + /** + * @var array> + */ + private array $dispatches = []; + + public function dispatch(string $name, StateEvent $event): void + { + $this->dispatches[$name][] = $event; + } + + public function addListener(string $event, callable $listener): void + { + $this->listeners[$event][] = $listener; + } + + /** + * @return StateEvent[] + */ + public function getDispatchedEvents(string $event): array + { + return $this->dispatches[$event] ?? []; + } + + /** + * @return callable[] + */ + public function getListenersOfEvent(string $event): array + { + return $this->listeners[$event] ?? []; + } +} diff --git a/tests/Stub/RegistrySpy.php b/tests/Stub/RegistryBuilderSpy.php similarity index 95% rename from tests/Stub/RegistrySpy.php rename to tests/Stub/RegistryBuilderSpy.php index a61b39e..c34d806 100644 --- a/tests/Stub/RegistrySpy.php +++ b/tests/Stub/RegistryBuilderSpy.php @@ -4,7 +4,7 @@ use Star\Component\State\RegistryBuilder; -final class RegistrySpy implements RegistryBuilder +final class RegistryBuilderSpy implements RegistryBuilder { /** * @var arraygetMockBuilder(StateVisitor::class)->getMock(); - $visitor - ->expects($this->at(0)) - ->method('visitState') - ->with('from', ['attr']); - $visitor - ->expects($this->at(1)) - ->method('visitState') - ->with('to', []); + $visitor = new class implements StateVisitor + { + /** + * @var array> + */ + public array $attributes = []; + + public function visitState(string $name, array $attributes): void + { + $this->attributes[$name][] = $attributes; + } + }; $this->registry->addTransition(new OneToOneTransition('t', 'from', 'to')); $this->registry->addAttribute('from', 'attr'); $this->registry->acceptStateVisitor($visitor); + self::assertSame( + [ + 'from' => [ + 0 => [ + 0 => 'attr', + ], + ], + 'to' => [ + 0 => [], + ], + ], + $visitor->attributes + ); } } diff --git a/tests/Transitions/ManyToOneTransitionTest.php b/tests/Transitions/ManyToOneTransitionTest.php index 6dc7424..486ba79 100644 --- a/tests/Transitions/ManyToOneTransitionTest.php +++ b/tests/Transitions/ManyToOneTransitionTest.php @@ -3,7 +3,7 @@ namespace Star\Component\State\Transitions; use PHPUnit\Framework\TestCase; -use Star\Component\State\Stub\RegistrySpy; +use Star\Component\State\Stub\RegistryBuilderSpy; final class ManyToOneTransitionTest extends TestCase { @@ -21,7 +21,7 @@ public function test_it_should_have_a_name(): void public function test_it_should_register_the_from_and_to_states(): void { - $registry = new RegistrySpy(); + $registry = new RegistryBuilderSpy(); $this->transition->onRegister($registry); diff --git a/tests/Transitions/OneToOneTransitionTest.php b/tests/Transitions/OneToOneTransitionTest.php index 9111dbb..0226613 100644 --- a/tests/Transitions/OneToOneTransitionTest.php +++ b/tests/Transitions/OneToOneTransitionTest.php @@ -3,7 +3,7 @@ namespace Star\Component\State\Transitions; use PHPUnit\Framework\TestCase; -use Star\Component\State\Stub\RegistrySpy; +use Star\Component\State\Stub\RegistryBuilderSpy; final class OneToOneTransitionTest extends TestCase { @@ -21,7 +21,7 @@ public function test_it_should_have_a_name(): void public function test_it_should_register_the_from_and_to_states(): void { - $registry = new RegistrySpy(); + $registry = new RegistryBuilderSpy(); $this->transition->onRegister($registry);