Skip to content

Commit dbfa4f6

Browse files
committed
♻️ refactor(RegionBuilder): Add onEnter callback for initial state after all handlers are registered.
1 parent 5ad5818 commit dbfa4f6

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed

specs/core/region.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ features:
9393
intent: Ensures entry-triggered events execute synchronously within the same lifecycle, maintaining predictable state progression during transitions
9494
test: vendor/bin/phpunit tests/PHPUnit/Unit/Core/Region/ImmediateCascadeProcessingTest.php
9595

96+
- acceptanceCriteria: Region fires onEnter callback when entering initial state during construction
97+
intent: Ensures initialization logic executes for the starting state by triggering onEnter callbacks during region instantiation, enabling consistent state setup regardless of whether states are entered via transition or initial construction
98+
test: vendor/bin/phpunit tests/PHPUnit/Unit/Core/Region/InitialStateBootstrapTest.php
99+
96100
- name: path-generation
97101
description: Hierarchical path representation for state machines
98102
specs:

src/RegionBuilder.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,14 @@ public function build(Mesh|iterable|null $featureArgs = null, ?bool $skipMiddlew
307307
$builder = $enhance->call(new BuildParams($this, $featureArgs));
308308
}
309309

310-
return $this->buildChain->withProvider($provider)->call($builder);
310+
$region = $this->buildChain->withProvider($provider)->call($builder);
311+
312+
// Fire onEnter callback for initial state after all handlers are registered
313+
//TODO This should be done on the first trigger, not after building
314+
$events = $this->chainMail->get(Events::class);
315+
$events->onEnterState($region, $region->currentState(), new \stdClass());
316+
317+
return $region;
311318
}
312319

313320
protected function assertValidConfig(): void
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Noem\State\Tests\Unit\Core\Region;
6+
7+
use Noem\State\RegionBuilder;
8+
use PHPUnit\Framework\Attributes\Group;
9+
use PHPUnit\Framework\TestCase;
10+
use stdClass;
11+
12+
/**
13+
* Acceptance Criterion: Region fires onEnter callback when entering initial state during construction
14+
*/
15+
#[Group('region')]
16+
#[Group('event-lifecycle')]
17+
class InitialStateBootstrapTest extends TestCase
18+
{
19+
public function testOnEnterCalledForInitialState(): void
20+
{
21+
$enterCalled = false;
22+
$enteredState = null;
23+
24+
$region = (new RegionBuilder())
25+
->setStates('initial', 'next', 'final')
26+
->markInitial('initial')
27+
->onEnter('initial', function(object $t) use (&$enterCalled, &$enteredState) {
28+
$enterCalled = true;
29+
$enteredState = 'initial';
30+
})
31+
->build();
32+
33+
// onEnter callback should have been called during construction
34+
$this->assertTrue($enterCalled, 'onEnter callback must fire when region enters initial state');
35+
$this->assertEquals('initial', $enteredState, 'Callback should receive initial state');
36+
}
37+
38+
public function testOnEnterNotCalledForNonInitialStates(): void
39+
{
40+
$nextEnterCalled = false;
41+
$finalEnterCalled = false;
42+
43+
$region = (new RegionBuilder())
44+
->setStates('initial', 'next', 'final')
45+
->markInitial('initial')
46+
->onEnter('next', function(object $t) use (&$nextEnterCalled) {
47+
$nextEnterCalled = true;
48+
})
49+
->onEnter('final', function(object $t) use (&$finalEnterCalled) {
50+
$finalEnterCalled = true;
51+
})
52+
->build();
53+
54+
// Only initial state onEnter should fire during construction
55+
$this->assertFalse($nextEnterCalled, 'onEnter for non-initial states should not fire during construction');
56+
$this->assertFalse($finalEnterCalled, 'onEnter for non-initial states should not fire during construction');
57+
}
58+
59+
public function testOnEnterReceivesProperContext(): void
60+
{
61+
$receivedRegion = null;
62+
$receivedTrigger = null;
63+
64+
$region = (new RegionBuilder())
65+
->setStates('start')
66+
->onEnter('start', function(object $t) use (&$receivedTrigger) {
67+
$receivedTrigger = $t;
68+
})
69+
->build();
70+
71+
// The trigger should be an empty stdClass or similar for bootstrap
72+
$this->assertIsObject($receivedTrigger, 'onEnter callback should receive trigger object during bootstrap');
73+
}
74+
75+
public function testMultipleOnEnterCallbacksCalledForInitialState(): void
76+
{
77+
$firstCalled = false;
78+
$secondCalled = false;
79+
80+
$region = (new RegionBuilder())
81+
->setStates('initial')
82+
->onEnter('initial', function(object $t) use (&$firstCalled) {
83+
$firstCalled = true;
84+
})
85+
->onEnter('initial', function(object $t) use (&$secondCalled) {
86+
$secondCalled = true;
87+
})
88+
->build();
89+
90+
$this->assertTrue($firstCalled, 'First onEnter callback must fire during bootstrap');
91+
$this->assertTrue($secondCalled, 'Second onEnter callback must fire during bootstrap');
92+
}
93+
94+
public function testOnEnterCalledBeforeRegionIsUsable(): void
95+
{
96+
$callbackExecuted = false;
97+
98+
$region = (new RegionBuilder())
99+
->setStates('initial', 'next')
100+
->markInitial('initial')
101+
->onEnter('initial', function(object $t) use (&$callbackExecuted) {
102+
// Callback should execute during build
103+
$callbackExecuted = true;
104+
})
105+
->build();
106+
107+
$this->assertTrue($callbackExecuted, 'onEnter callback should execute during build');
108+
$this->assertEquals('initial', $region->currentState(), 'Region should be in initial state after construction');
109+
}
110+
}

0 commit comments

Comments
 (0)