Skip to content

Commit 6cf2ab5

Browse files
KevinMartinsDevsoyuka
authored andcommitted
refactor(symfony): remove static state from EntrypointAction for Worker mode compatibility
1 parent 472ae3f commit 6cf2ab5

2 files changed

Lines changed: 76 additions & 5 deletions

File tree

src/Symfony/Action/EntrypointAction.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030
final class EntrypointAction
3131
{
32-
private static ResourceNameCollection $resourceNameCollection;
32+
private ?ResourceNameCollection $resourceNameCollection = null;
3333

3434
public function __construct(
3535
private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
@@ -41,7 +41,7 @@ public function __construct(
4141

4242
public function __invoke(Request $request): mixed
4343
{
44-
static::$resourceNameCollection = $this->resourceNameCollectionFactory->create();
44+
$this->resourceNameCollection ??= $this->resourceNameCollectionFactory->create();
4545
$context = [
4646
'request' => $request,
4747
'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION),
@@ -52,7 +52,7 @@ public function __invoke(Request $request): mixed
5252
read: true,
5353
serialize: true,
5454
class: Entrypoint::class,
55-
provider: [self::class, 'provide']
55+
provider: [$this, 'provide']
5656
);
5757
$request->attributes->set('_api_operation', $operation);
5858
$body = $this->provider->provide($operation, [], $context);
@@ -61,8 +61,8 @@ class: Entrypoint::class,
6161
return $this->processor->process($body, $operation, [], $context);
6262
}
6363

64-
public static function provide(): Entrypoint
64+
public function provide(): Entrypoint
6565
{
66-
return new Entrypoint(static::$resourceNameCollection);
66+
return new Entrypoint($this->resourceNameCollection);
6767
}
6868
}

src/Symfony/Tests/Action/EntrypointActionTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,84 @@ class EntrypointActionTest extends TestCase
3030
public function testGetEntrypointWithProviderProcessor(): void
3131
{
3232
$expected = new Entrypoint(new ResourceNameCollection(['dummies']));
33+
3334
$resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class);
3435
$resourceNameCollectionFactory->method('create')->willReturn(new ResourceNameCollection(['dummies']));
36+
3537
$provider = $this->createMock(ProviderInterface::class);
3638
$provider->expects($this->once())->method('provide')->willReturn($expected);
39+
3740
$processor = $this->createMock(ProcessorInterface::class);
3841
$processor->expects($this->once())->method('process')->willReturnArgument(0);
42+
3943
$entrypoint = new EntrypointAction($resourceNameCollectionFactory, $provider, $processor);
4044
$this->assertEquals($expected, $entrypoint(Request::create('/')));
4145
}
46+
47+
public function testInvokeCachesResourceNameCollection(): void
48+
{
49+
$resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class);
50+
$resourceNameCollectionFactory->expects($this->once())
51+
->method('create')
52+
->willReturn(new ResourceNameCollection(['Dummy']));
53+
54+
$provider = $this->createMock(ProviderInterface::class);
55+
$processor = $this->createMock(ProcessorInterface::class);
56+
57+
$action = new EntrypointAction($resourceNameCollectionFactory, $provider, $processor);
58+
59+
$request = new Request();
60+
61+
$provider->expects($this->exactly(2))
62+
->method('provide')
63+
->willReturn(new Entrypoint(new ResourceNameCollection(['Dummy'])));
64+
65+
$processor->expects($this->exactly(2))
66+
->method('process');
67+
68+
$action($request);
69+
70+
// Test that second call does not call factory again (lazy-loading/caching)
71+
$action($request);
72+
}
73+
74+
/**
75+
* This test ensures that instances are isolated and don't leak state.
76+
* In Worker mode (FrankenPHP/Swoole), static properties would cause a state leak between instances.
77+
*/
78+
public function testInstancesAreIsolated(): void
79+
{
80+
$processor = $this->createMock(ProcessorInterface::class);
81+
$provider = $this->createMock(ProviderInterface::class);
82+
83+
// Instance 1: configured with ResourceA
84+
$factory1 = $this->createMock(ResourceNameCollectionFactoryInterface::class);
85+
$factory1->method('create')->willReturn(new ResourceNameCollection(['ResourceA']));
86+
$action1 = new EntrypointAction($factory1, $provider, $processor);
87+
88+
// Instance 2: configured with ResourceB
89+
$factory2 = $this->createMock(ResourceNameCollectionFactoryInterface::class);
90+
$factory2->method('create')->willReturn(new ResourceNameCollection(['ResourceB']));
91+
$action2 = new EntrypointAction($factory2, $provider, $processor);
92+
93+
$request = new Request();
94+
95+
// 1. Trigger action 1
96+
$action1($request);
97+
// 2. Trigger action 2 (if static were used, this would overwrite action 1's state)
98+
$action2($request);
99+
100+
// Verification of isolation:
101+
$this->assertEquals(
102+
new ResourceNameCollection(['ResourceA']),
103+
$action1->provide()->getResourceNameCollection(),
104+
"Instance 1 was polluted by Instance 2 (likely due to a static property)"
105+
);
106+
107+
$this->assertEquals(
108+
new ResourceNameCollection(['ResourceB']),
109+
$action2->provide()->getResourceNameCollection(),
110+
"Instance 2 has incorrect state."
111+
);
112+
}
42113
}

0 commit comments

Comments
 (0)