Skip to content

Commit

Permalink
Merge pull request #168 from yohang/feature/get-state-classes
Browse files Browse the repository at this point in the history
feat: Added getStateClasses() and hasState() methods on StateMachine
  • Loading branch information
yohang authored Jan 15, 2025
2 parents 5829fa8 + 91d00b7 commit fc8fdd7
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 104 deletions.
68 changes: 54 additions & 14 deletions src/StateMachine.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@ public function getReachablesTransitions(object $object, ?string $stateClass = n
);
}

/**
* @psalm-suppress MoreSpecificReturnType
* @psalm-suppress LessSpecificReturnStatement
* @return array<int, enum-string<State>>
*/
public function getStateClasses(object $object): array
{
return array_filter(
array_map(
fn(\ReflectionProperty $property): string => (string)$property->getType(),
$this->extractStateProperties($object),
),
fn (?string $name): bool => enum_exists($name),
);
}

public function hasState(object $object): bool
{
return count($this->extractStateProperties($object)) > 0;
}

public function getDispatcher(): EventDispatcherInterface
{
return $this->dispatcher;
Expand All @@ -110,6 +131,35 @@ private function extractStateProperty(object $object, ?string $stateClass = null
throw new \InvalidArgumentException(sprintf('Enum "%s" does not exists', $stateClass));
}

$properties = $this->extractStateProperties($object);
if (null !== $stateClass) {
foreach ($properties as $property) {
if ((string)($property->getType()) === $stateClass) {
return $property;
}
}

throw new \InvalidArgumentException(sprintf('Found no state on object "%s" with class "%s"', get_class($object), $stateClass));
}

if (1 === count($properties)) {
return $properties[0];
}

if (count($properties) > 1) {
throw new \InvalidArgumentException('Found multiple states on object ' . get_class($object));
}

throw new \InvalidArgumentException('Found no state on object ' . get_class($object));
}

/**
* @return array<int, \ReflectionProperty>
*/
private function extractStateProperties(object $object): array
{
$properties = [];

$reflectionClass = new \ReflectionClass($object);
/** @psalm-suppress DocblockTypeContradiction */
do {
Expand All @@ -135,23 +185,13 @@ private function extractStateProperty(object $object, ?string $stateClass = null
}

$reflectionEnum = new \ReflectionEnum($name);
/** @psalm-suppress RedundantCondition */
if (
null !== $stateClass &&
(
$reflectionEnum->getName() === $stateClass ||
(interface_exists($stateClass) && $reflectionEnum->implementsInterface($stateClass))
)
) {
return $property;
}

if (null === $stateClass && $reflectionEnum->implementsInterface(State::class)) {
return $property;
/** @psalm-suppress TypeDoesNotContainType */
if ($reflectionEnum->implementsInterface(State::class)) {
$properties[] = $property;
}
}
} while ($reflectionClass = $reflectionClass->getParentClass());

throw new \InvalidArgumentException('Found no state on object ' . get_class($object));
return $properties;
}
}
4 changes: 2 additions & 2 deletions tests/Dumper/MermaidDumperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Finite\Tests\Dumper;

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

class MermaidDumperTest extends TestCase
Expand All @@ -13,7 +13,7 @@ public function test_it_dumps(): void
$this->assertSame(
<<<MERMAID
---
title: Finite\Tests\E2E\SimpleArticleState
title: Finite\Tests\Fixtures\SimpleArticleState
---
stateDiagram-v2
draft --> published: publish
Expand Down
44 changes: 2 additions & 42 deletions tests/E2E/AlternativeGraphTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,10 @@
namespace Finite\Tests\E2E;

use Finite\StateMachine;
use Finite\Tests\Fixtures\AlternativeArticle;
use Finite\Tests\Fixtures\AlternativeArticleState;
use PHPUnit\Framework\TestCase;

class AlternativeArticle
{
private SimpleArticleState $state = SimpleArticleState::DRAFT;
private AlternativeArticleState $alternativeState = AlternativeArticleState::NEW;
private readonly \DateTimeInterface $createdAt;

public function __construct(public readonly string $title)
{
$this->createdAt = new \DateTimeImmutable;
}

public function getTitle(): string
{
return $this->title;
}

public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}

public function getState(): SimpleArticleState
{
return $this->state;
}

public function setState(SimpleArticleState $state): void
{
$this->state = $state;
}

public function getAlternativeState(): AlternativeArticleState
{
return $this->alternativeState;
}

public function setAlternativeState(AlternativeArticleState $alternativeState): void
{
$this->alternativeState = $alternativeState;
}
}

class AlternativeGraphTest extends TestCase
{
private AlternativeArticle $article;
Expand Down
42 changes: 2 additions & 40 deletions tests/E2E/BasicGraphTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,10 @@
namespace Finite\Tests\E2E;

use Finite\StateMachine;
use Finite\Tests\Fixtures\Article;
use Finite\Tests\Fixtures\SimpleArticleState;
use PHPUnit\Framework\TestCase;

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;

public function __construct(public readonly string $title)
{
$this->createdAt = new \DateTimeImmutable;
}

public function getTitle(): string
{
return $this->title;
}

public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}

public function getState(): SimpleArticleState
{
return $this->state;
}

public function setState(SimpleArticleState $state): void
{
$this->state = $state;
}
}

class BasicGraphTest extends TestCase
{
private Article $article;
Expand Down
45 changes: 45 additions & 0 deletions tests/Fixtures/AlternativeArticle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Finite\Tests\Fixtures;

class AlternativeArticle
{
private SimpleArticleState $state = SimpleArticleState::DRAFT;
private AlternativeArticleState $alternativeState = AlternativeArticleState::NEW;
private readonly \DateTimeInterface $createdAt;

public function __construct(public readonly string $title)
{
$this->createdAt = new \DateTimeImmutable;
}

public function getTitle(): string
{
return $this->title;
}

public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}

public function getState(): SimpleArticleState
{
return $this->state;
}

public function setState(SimpleArticleState $state): void
{
$this->state = $state;
}

public function getAlternativeState(): AlternativeArticleState
{
return $this->alternativeState;
}

public function setAlternativeState(AlternativeArticleState $alternativeState): void
{
$this->alternativeState = $alternativeState;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Finite\Tests\E2E;
namespace Finite\Tests\Fixtures;

use Finite\State;
use Finite\Transition\Transition;
Expand Down
43 changes: 43 additions & 0 deletions tests/Fixtures/Article.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Finite\Tests\Fixtures;

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;

public function __construct(public readonly string $title)
{
$this->createdAt = new \DateTimeImmutable;
}

public function getTitle(): string
{
return $this->title;
}

public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}

public function getState(): SimpleArticleState
{
return $this->state;
}

public function setState(SimpleArticleState $state): void
{
$this->state = $state;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Finite\Tests\E2E;
namespace Finite\Tests\Fixtures;

use Finite\State;
use Finite\Transition\Transition;
Expand Down
45 changes: 43 additions & 2 deletions tests/StateMachineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
use Finite\Event\EventDispatcher;
use Finite\Event\TransitionEvent;
use Finite\StateMachine;
use Finite\Tests\E2E\Article;
use Finite\Tests\E2E\SimpleArticleState;
use Finite\Tests\Fixtures\AlternativeArticle;
use Finite\Tests\Fixtures\AlternativeArticleState;
use Finite\Tests\Fixtures\Article;
use Finite\Tests\Fixtures\SimpleArticleState;
use PHPUnit\Framework\TestCase;
use Psr\EventDispatcher\EventDispatcherInterface;

Expand Down Expand Up @@ -114,4 +116,43 @@ public function test_it_throws_if_no_state(): void
$stateMachine = new StateMachine;
$stateMachine->can(new class extends \stdClass {}, 'transition');
}

public function test_it_throws_if_bad_state(): void
{
$this->expectException(\InvalidArgumentException::class);

$stateMachine = new StateMachine;
$stateMachine->can(new Article('test'), 'publish', AlternativeArticleState::class);
}

public function test_it_throws_if_many_state_and_none_given(): void
{
$this->expectException(\InvalidArgumentException::class);

$stateMachine = new StateMachine;
$stateMachine->can(new AlternativeArticle('test'), 'publish');
}

public function test_it_returns_class_state_classes(): void
{
$this->assertSame(
[SimpleArticleState::class],
(new StateMachine)->getStateClasses(new Article('Hi !')),
);
$this->assertSame(
[SimpleArticleState::class, AlternativeArticleState::class],
(new StateMachine)->getStateClasses(new AlternativeArticle('Hi !')),
);
$this->assertSame(
[],
(new StateMachine)->getStateClasses(new \stdClass),
);
}

public function test_it_returns_if_object_has_state(): void
{
$this->assertTrue((new StateMachine)->hasState(new Article('Hi !')));
$this->assertTrue((new StateMachine)->hasState(new AlternativeArticle('Hi !')));
$this->assertFalse((new StateMachine)->hasState(new \stdClass));
}
}
3 changes: 1 addition & 2 deletions tests/Transition/TransitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

namespace Finite\Tests\Transition;

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

Expand Down

0 comments on commit fc8fdd7

Please sign in to comment.