Skip to content

Commit d7d3678

Browse files
fix(symfony): reset the Mercure publisher between requests
PublishMercureUpdatesListener collects the created, updated and deleted resources in onFlush and publishes them in postFlush. When a flush fails, postFlush is never dispatched and the collected objects stay in the listener, so a long-running worker could publish them during another request. Make the listener resettable and tag it kernel.reset.
1 parent 359c13b commit d7d3678

5 files changed

Lines changed: 83 additions & 4 deletions

File tree

‎src/Symfony/Bundle/Resources/config/doctrine_odm_mercure_publisher.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@
3333
'%api_platform.mercure.include_type%',
3434
])
3535
->tag('doctrine_mongodb.odm.event_listener', ['event' => 'onFlush'])
36-
->tag('doctrine_mongodb.odm.event_listener', ['event' => 'postFlush']);
36+
->tag('doctrine_mongodb.odm.event_listener', ['event' => 'postFlush'])
37+
->tag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore']);
3738
};

‎src/Symfony/Bundle/Resources/config/doctrine_orm_mercure_publisher.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@
3333
'%api_platform.mercure.include_type%',
3434
])
3535
->tag('doctrine.event_listener', ['event' => 'onFlush'])
36-
->tag('doctrine.event_listener', ['event' => 'postFlush']);
36+
->tag('doctrine.event_listener', ['event' => 'postFlush'])
37+
->tag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore']);
3738
};

‎src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@
3636
use Symfony\Component\Mercure\Update;
3737
use Symfony\Component\Messenger\MessageBusInterface;
3838
use Symfony\Component\Serializer\SerializerInterface;
39+
use Symfony\Contracts\Service\ResetInterface;
3940

4041
/**
4142
* Publishes resources updates to the Mercure hub.
4243
*
4344
* @author Kévin Dunglas <dunglas@gmail.com>
4445
*/
45-
final class PublishMercureUpdatesListener
46+
final class PublishMercureUpdatesListener implements ResetInterface
4647
{
4748
use DispatchTrait;
4849
use ResourceClassInfoTrait;
@@ -149,7 +150,7 @@ public function postFlush(): void
149150
}
150151
}
151152

152-
private function reset(): void
153+
public function reset(): void
153154
{
154155
$this->createdObjects = [];
155156
$this->updatedObjects = [];

‎src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,16 @@ public function testMcpProviderChainIsNotSecuredWithoutSecurityBundle(): void
470470
$this->assertContainerHasService('api_platform.mcp.state_provider.parameter_validator');
471471
}
472472

473+
public function testMercurePublisherIsReset(): void
474+
{
475+
$config = self::DEFAULT_CONFIG;
476+
$config['api_platform']['doctrine']['enabled'] = true;
477+
$config['api_platform']['mercure']['enabled'] = true;
478+
(new ApiPlatformExtension())->load($config, $this->container);
479+
480+
$this->assertSame([['method' => 'reset', 'on_invalid' => 'ignore']], $this->container->getDefinition('api_platform.doctrine.orm.listener.mercure.publish')->getTag('kernel.reset'));
481+
}
482+
473483
public function testItRegistersMetadataConfiguration(): void
474484
{
475485
$config = self::DEFAULT_CONFIG;

‎src/Symfony/Tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,59 @@ public function testPublishUpdate(): void
172172
$this->assertEquals([null, null, null, null, null, 10, null], $retry);
173173
}
174174

175+
public function testResetDropsTheUpdatesCollectedByAFailedFlush(): void
176+
{
177+
$failed = new Dummy();
178+
$failed->setId(1);
179+
$published = new Dummy();
180+
$published->setId(2);
181+
182+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
183+
$resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class);
184+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
185+
186+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
187+
$iriConverterProphecy->getIriFromResource($failed, Argument::cetera())->shouldNotBeCalled();
188+
$iriConverterProphecy->getIriFromResource($published, UrlGeneratorInterface::ABS_URL, Argument::any())->willReturn('http://example.com/dummies/2')->shouldBeCalled();
189+
190+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
191+
$resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [(new ApiResource())->withOperations(new Operations([
192+
'get' => (new Get())->withShortName('Dummy')->withMercure(['hub' => 'managed', 'enable_async_update' => false]),
193+
]))]));
194+
195+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
196+
$serializerProphecy->serialize($published, 'jsonld', [])->willReturn('2');
197+
198+
$topics = [];
199+
$managedHub = $this->createMockHub(static function (Update $update) use (&$topics): string {
200+
$topics = array_merge($topics, $update->getTopics());
201+
202+
return 'id';
203+
});
204+
205+
$listener = new PublishMercureUpdatesListener(
206+
$resourceClassResolverProphecy->reveal(),
207+
$iriConverterProphecy->reveal(),
208+
$resourceMetadataFactoryProphecy->reveal(),
209+
$serializerProphecy->reveal(),
210+
['jsonld' => ['application/ld+json']],
211+
null,
212+
new HubRegistry($this->createStub(HubInterface::class), [
213+
'managed' => $managedHub,
214+
]),
215+
);
216+
217+
// the commit of the first flush fails: Doctrine dispatches onFlush, but never postFlush
218+
$listener->onFlush($this->createOnFlushEventArgs([$failed]));
219+
// between two requests (or two messages), the services resetter resets the listener
220+
$listener->reset();
221+
222+
$listener->onFlush($this->createOnFlushEventArgs([$published]));
223+
$listener->postFlush();
224+
225+
$this->assertSame(['http://example.com/dummies/2'], $topics);
226+
}
227+
175228
public function testPublishUpdateMultipleTopicsUsingExpressionLanguage(): void
176229
{
177230
$mercure = [
@@ -444,6 +497,19 @@ public function testPublishUpdateWithMultipleResources(): void
444497
$this->assertEquals('{"public":1}', $data[1]);
445498
}
446499

500+
private function createOnFlushEventArgs(array $insertions): OnFlushEventArgs
501+
{
502+
$uowProphecy = $this->prophesize(UnitOfWork::class);
503+
$uowProphecy->getScheduledEntityInsertions()->willReturn($insertions);
504+
$uowProphecy->getScheduledEntityUpdates()->willReturn([]);
505+
$uowProphecy->getScheduledEntityDeletions()->willReturn([]);
506+
507+
$emProphecy = $this->prophesize(EntityManagerInterface::class);
508+
$emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal());
509+
510+
return new OnFlushEventArgs($emProphecy->reveal());
511+
}
512+
447513
private function createMockHub(callable $callable): HubInterface
448514
{
449515
return new MockHub('https://mercure.demo/.well-known/mercure', new StaticTokenProvider('x'), $callable);

0 commit comments

Comments
 (0)