Skip to content
Open
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
10 changes: 5 additions & 5 deletions src/Symfony/Action/EntrypointAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/
final class EntrypointAction
{
private static ResourceNameCollection $resourceNameCollection;
private ?ResourceNameCollection $resourceNameCollection = null;

public function __construct(
private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
Expand All @@ -41,7 +41,7 @@ public function __construct(

public function __invoke(Request $request): mixed
{
static::$resourceNameCollection = $this->resourceNameCollectionFactory->create();
$this->resourceNameCollection ??= $this->resourceNameCollectionFactory->create();
$context = [
'request' => $request,
'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION),
Expand All @@ -52,7 +52,7 @@ public function __invoke(Request $request): mixed
read: true,
serialize: true,
class: Entrypoint::class,
provider: [self::class, 'provide']
provider: [$this, 'provide']
);
$request->attributes->set('_api_operation', $operation);
$body = $this->provider->provide($operation, [], $context);
Expand All @@ -61,8 +61,8 @@ class: Entrypoint::class,
return $this->processor->process($body, $operation, [], $context);
}

public static function provide(): Entrypoint
public function provide(): Entrypoint
{
return new Entrypoint(static::$resourceNameCollection);
return new Entrypoint($this->resourceNameCollection);
}
}
71 changes: 71 additions & 0 deletions src/Symfony/Tests/Action/EntrypointActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,84 @@ class EntrypointActionTest extends TestCase
public function testGetEntrypointWithProviderProcessor(): void
{
$expected = new Entrypoint(new ResourceNameCollection(['dummies']));

$resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class);
$resourceNameCollectionFactory->method('create')->willReturn(new ResourceNameCollection(['dummies']));

$provider = $this->createMock(ProviderInterface::class);
$provider->expects($this->once())->method('provide')->willReturn($expected);

$processor = $this->createMock(ProcessorInterface::class);
$processor->expects($this->once())->method('process')->willReturnArgument(0);

$entrypoint = new EntrypointAction($resourceNameCollectionFactory, $provider, $processor);
$this->assertEquals($expected, $entrypoint(Request::create('/')));
}

public function testInvokeCachesResourceNameCollection(): void
{
$resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class);
$resourceNameCollectionFactory->expects($this->once())
->method('create')
->willReturn(new ResourceNameCollection(['Dummy']));

$provider = $this->createMock(ProviderInterface::class);
$processor = $this->createMock(ProcessorInterface::class);

$action = new EntrypointAction($resourceNameCollectionFactory, $provider, $processor);

$request = new Request();

$provider->expects($this->exactly(2))
->method('provide')
->willReturn(new Entrypoint(new ResourceNameCollection(['Dummy'])));

$processor->expects($this->exactly(2))
->method('process');

$action($request);

// Test that second call does not call factory again (lazy-loading/caching)
$action($request);
}

/**
* This test ensures that instances are isolated and don't leak state.
* In Worker mode (FrankenPHP/Swoole), static properties would cause a state leak between instances.
*/
public function testInstancesAreIsolated(): void
{
$processor = $this->createMock(ProcessorInterface::class);
$provider = $this->createMock(ProviderInterface::class);

// Instance 1: configured with ResourceA
$factory1 = $this->createMock(ResourceNameCollectionFactoryInterface::class);
$factory1->method('create')->willReturn(new ResourceNameCollection(['ResourceA']));
$action1 = new EntrypointAction($factory1, $provider, $processor);

// Instance 2: configured with ResourceB
$factory2 = $this->createMock(ResourceNameCollectionFactoryInterface::class);
$factory2->method('create')->willReturn(new ResourceNameCollection(['ResourceB']));
$action2 = new EntrypointAction($factory2, $provider, $processor);

$request = new Request();

// 1. Trigger action 1
$action1($request);
// 2. Trigger action 2 (if static were used, this would overwrite action 1's state)
$action2($request);

// Verification of isolation:
$this->assertEquals(
new ResourceNameCollection(['ResourceA']),
$action1->provide()->getResourceNameCollection(),
"Instance 1 was polluted by Instance 2 (likely due to a static property)"
);

$this->assertEquals(
new ResourceNameCollection(['ResourceB']),
$action2->provide()->getResourceNameCollection(),
"Instance 2 has incorrect state."
);
}
}
Loading