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

WEB-4061: Test senaryo datalarının v4'te kullanılabilir olmasının sağlanması #64

10 changes: 10 additions & 0 deletions src/Behavior/EventBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
public function __construct(
public null|string|Optional $type = null,
public null|array|Optional $payload = null,
#[WithoutValidation]

Check failure on line 41 in src/Behavior/EventBehavior.php

View workflow job for this annotation

GitHub Actions / larastan

Attribute class Spatie\LaravelData\Attributes\WithoutValidation does not have the parameter target.
bool $isTransactional = null,
#[WithoutValidation]

Check failure on line 43 in src/Behavior/EventBehavior.php

View workflow job for this annotation

GitHub Actions / larastan

Attribute class Spatie\LaravelData\Attributes\WithoutValidation does not have the parameter target.
mixed $actor = null,
public int|Optional $version = 1,

Expand Down Expand Up @@ -83,4 +83,14 @@
{
return $this->actor;
}

/**
* Retrieves the scenario value from the payload.
*
* @return string|null The scenario value if available, otherwise null.
*/
public function getScenario(): ?string
{
return $this->payload['scenario'] ?? null;
}
}
95 changes: 88 additions & 7 deletions src/Definition/MachineDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class MachineDefinition
/** The initial state definition for this machine definition. */
public ?StateDefinition $initialStateDefinition = null;

/** Indicates whether the scenario is enabled. */
public bool $scenariosEnabled = false;

// endregion

// region Constructor
Expand All @@ -75,9 +78,18 @@ private function __construct(
public ?array $behavior,
public string $id,
public ?string $version,
public ?array $scenarios,
public string $delimiter = self::STATE_DELIMITER,
) {
$this->scenariosEnabled = isset($this->config['scenarios_enabled']) && $this->config['scenarios_enabled'] === true;

$this->root = $this->createRootStateDefinition($config);

// Checks if the scenario is enabled, and if true, creates scenario state definitions.
if ($this->scenariosEnabled) {
$this->createScenarioStateDefinitions();
}

$this->root->initializeTransitions();

$this->stateDefinitions = $this->root->stateDefinitions;
Expand Down Expand Up @@ -107,12 +119,14 @@ private function __construct(
public static function define(
array $config = null,
array $behavior = null,
array $scenarios = null,
): self {
return new self(
config: $config ?? null,
behavior: array_merge(self::initializeEmptyBehavior(), $behavior ?? []),
id: $config['id'] ?? self::DEFAULT_ID,
version: $config['version'] ?? null,
scenarios: $scenarios,
delimiter: $config['delimiter'] ?? self::STATE_DELIMITER,
);
}
Expand Down Expand Up @@ -159,15 +173,38 @@ protected function createRootStateDefinition(?array $config): StateDefinition
);
}

/**
* Creates scenario state definitions based on the defined scenarios.
*
* This method iterates through the specified scenarios and creates StateDefinition objects
* for each, with the provided states configuration.
*/
protected function createScenarioStateDefinitions(): void
{
if (!empty($this->scenarios)) {
foreach ($this->scenarios as $name => $scenarios) {
$parentStateDefinition = reset($this->idMap);
$state = new StateDefinition(
config: ['states' => $scenarios],
options: [
'parent' => $parentStateDefinition,
'machine' => $this,
'key' => $name,
]
);

$state->initializeTransitions();
}
}
}

/**
* Build the initial state for the machine.
*
* @return ?State The initial state of the machine.
*/
public function getInitialState(): ?State
public function getInitialState(EventBehavior|array $event = null): ?State
{
$initialStateDefinition = $this->root->findInitialStateDefinition();

if (is_null($this->initialStateDefinition)) {
return null;
}
Expand All @@ -179,6 +216,9 @@ public function getInitialState(): ?State
currentStateDefinition: $this->initialStateDefinition,
);

$initialState = $this->getScenarioStateIfAvailable(state: $initialState, eventBehavior: $eventBehavior ?? null);
$this->initialStateDefinition = $initialState->currentStateDefinition;

// Record the internal machine init event.
$initialState->setInternalEventBehavior(type: InternalEvent::MACHINE_START);

Expand All @@ -194,8 +234,8 @@ public function getInitialState(): ?State
eventBehavior: $initialState->currentEventBehavior,
);

if ($initialStateDefinition?->transitionDefinitions !== null) {
foreach ($initialStateDefinition->transitionDefinitions as $transition) {
if ($this->initialStateDefinition?->transitionDefinitions !== null) {
foreach ($this->initialStateDefinition->transitionDefinitions as $transition) {
if ($transition->isAlways === true) {
return $this->transition(
event: [
Expand Down Expand Up @@ -227,6 +267,40 @@ public function getInitialState(): ?State
return $initialState;
}

/**
* Retrieves the scenario state if scenario is enabled and available; otherwise, returns the current state.
*
* @param State $state The current state.
* @param EventBehavior|array|null $eventBehavior The optional event behavior or event data.
*
* @return State|null The scenario state if scenario is enabled and found, otherwise returns the current state.
*/
public function getScenarioStateIfAvailable(State $state, EventBehavior|array $eventBehavior = null): ?State
{
if ($this->scenariosEnabled === false) {
return $state;
}

if ($eventBehavior !== null) {
// Initialize the event and validate it
$eventBehavior = $this->initializeEvent($eventBehavior, $state);
if ($eventBehavior->getScenario() !== null) {
$state->context->set('scenario', $eventBehavior->getScenario());
}
}

$scenarioStateKey = str_replace($this->id, $this->id.$this->delimiter.$state->context->get('scenario'), $state->currentStateDefinition->id);
if ($state->context->has('scenario') && isset($this->idMap[$scenarioStateKey])) {
return $this->buildCurrentState(
context: $state->context,
currentStateDefinition: $this->idMap[$scenarioStateKey],
eventBehavior: $eventBehavior
);
}

return $state;
}

/**
* Builds the current state of the state machine.
*
Expand Down Expand Up @@ -484,8 +558,12 @@ public function transition(
EventBehavior|array $event,
State $state = null
): State {
// Use the initial state if no state is provided
$state ??= $this->getInitialState();
if ($state !== null) {
$state = $this->getScenarioStateIfAvailable(state: $state, eventBehavior: $event);
} else {
// Use the initial state if no state is provided
$state = $this->getInitialState(event: $event);
}

$currentStateDefinition = $this->getCurrentStateDefinition($state);

Expand Down Expand Up @@ -549,6 +627,9 @@ public function transition(
$newState = $state
->setCurrentStateDefinition($targetStateDefinition ?? $currentStateDefinition);

// Get scenario state if exists
$newState = $this->getScenarioStateIfAvailable(state: $newState, eventBehavior: $eventBehavior);

// Record state enter event
$state->setInternalEventBehavior(
type: InternalEvent::STATE_ENTER,
Expand Down
25 changes: 25 additions & 0 deletions tests/ScenarioTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

use Tarfinlabs\EventMachine\Actor\State;
use Tarfinlabs\EventMachine\Definition\MachineDefinition;
use Tarfinlabs\EventMachine\Tests\Stubs\Machines\MachineWithScenarios;

it('scenarios run if scenarios enabled', function (): void {
$machine = MachineWithScenarios::create();

$state = $machine->send(['type' => 'EVENT_B', 'payload' => ['scenario' => 'test']]);

expect($state)
->toBeInstanceOf(State::class)
->and($state->value)->toBe([MachineDefinition::DEFAULT_ID.MachineDefinition::STATE_DELIMITER.'test.stateC'])
->and($state->context->count)->toBe(0);

$state = $machine->send('EVENT_D');

expect($state)
->toBeInstanceOf(State::class)
->and($state->value)->toBe([MachineDefinition::DEFAULT_ID.MachineDefinition::STATE_DELIMITER.'test.stateA'])
->and($state->context->count)->toBe(-1);
});
79 changes: 79 additions & 0 deletions tests/Stubs/Machines/MachineWithScenarios.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Tarfinlabs\EventMachine\Tests\Stubs\Machines;

use Tarfinlabs\EventMachine\Actor\Machine;
use Tarfinlabs\EventMachine\ContextManager;
use Tarfinlabs\EventMachine\Definition\MachineDefinition;

class MachineWithScenarios extends Machine
{
public static function definition(): MachineDefinition
{
return MachineDefinition::define(
config: [
'initial' => 'stateA',
'scenarios_enabled' => true,
'context' => [
'count' => 1,
],
'states' => [
'stateA' => [
'on' => [
'EVENT_B' => [
'target' => 'stateB',
'actions' => 'incrementAction',
],
],
],
'stateB' => [
'on' => [
'EVENT_C' => 'stateC',
],
],
'stateC' => [
'on' => [
'EVENT_D' => [
'target' => 'stateD',
'actions' => 'incrementAction',
],
],
],
'stateD' => [],
],
],
behavior: [
'actions' => [
'incrementAction' => function (ContextManager $context): void {
$context->set('count', $context->get('count') + 1);
},
'decrementAction' => function (ContextManager $context): void {
$context->set('count', $context->get('count') - 1);
},
],
],
scenarios: [
'test' => [
'stateA' => [
'on' => [
'EVENT_B' => [
'target' => 'stateC',
'actions' => 'decrementAction',
],
],
],
'stateC' => [
'on' => [
'EVENT_D' => [
'target' => 'stateA',
'actions' => 'decrementAction',
],
],
],
],
]
);
}
}
Loading