Skip to content
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: 3 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@ jobs:
- name: PHPStan
run: bin/phpstan

- name: Composer validate
run: composer validate

# - name: Infection
# run: bin/infection --formatter=progress
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
43 changes: 19 additions & 24 deletions examples/ContextUsingCustomMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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());
Expand Down
29 changes: 12 additions & 17 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@

<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit
backupGlobals = "false"
backupStaticAttributes = "false"
colors = "true"
convertErrorsToExceptions = "true"
convertNoticesToExceptions = "true"
convertWarningsToExceptions = "true"
convertDeprecationsToExceptions = "true"
beStrictAboutOutputDuringTests = "true"
beStrictAboutTestsThatDoNotTestAnything="true"
failOnRisky="true"
failOnWarning="true"
bootstrap = "vendor/autoload.php" >
backupGlobals="false"
backupStaticProperties="false"
colors="true"
beStrictAboutOutputDuringTests="true"
failOnAllIssues="true"
bootstrap="vendor/autoload.php"
>

<testsuites>
<testsuite name="main">
Expand All @@ -25,9 +20,9 @@
</testsuite>
</testsuites>

<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>
10 changes: 7 additions & 3 deletions src/StateMachine.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
}
Expand Down
7 changes: 5 additions & 2 deletions src/StateMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 23 additions & 34 deletions tests/StateMachineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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);
}
}
45 changes: 45 additions & 0 deletions tests/Stub/EventRegistrySpy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types=1);

namespace Star\Component\State\Stub;

use Star\Component\State\Event\StateEvent;
use Star\Component\State\EventRegistry;

final class EventRegistrySpy implements EventRegistry
{
/**
* @var array<string, callable[]>
*/
private array $listeners = [];

/**
* @var array<string, array<int, StateEvent>>
*/
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] ?? [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Star\Component\State\RegistryBuilder;

final class RegistrySpy implements RegistryBuilder
final class RegistryBuilderSpy implements RegistryBuilder
{
/**
* @var array<string, array{
Expand Down
Loading