From 66c6f2a7fc32bb49dda6cd5cb3ca38fe9a537181 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 25 Mar 2026 11:11:49 +0100 Subject: [PATCH] exclude internal symfony urls --- src/DependencyInjection/Configuration.php | 9 +- .../PatchlevelEventSourcingExtension.php | 2 + src/RequestListener/AutoSetupListener.php | 10 ++ ...criptionRebuildAfterFileChangeListener.php | 9 ++ tests/Fixtures/FromBeginningSubscriber.php | 13 ++ tests/Fixtures/FromNowSubscriber.php | 13 ++ .../RequestListener/AutoSetupListenerTest.php | 82 +++++++++++ ...tionRebuildAfterFileChangeListenerTest.php | 132 ++++++++++++++++++ 8 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/FromBeginningSubscriber.php create mode 100644 tests/Fixtures/FromNowSubscriber.php create mode 100644 tests/Unit/RequestListener/AutoSetupListenerTest.php create mode 100644 tests/Unit/RequestListener/SubscriptionRebuildAfterFileChangeListenerTest.php diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 001b3b9b..b6fb37a4 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -42,8 +42,13 @@ * enabled: bool, * ids: list, * groups: list, + * exclude_url: string|null * }, - * rebuild_after_file_change: array{enabled: bool, cache_pool: string}, + * rebuild_after_file_change: array{ + * enabled: bool, + * cache_pool: string, + * exclude_url: string|null + * }, * gap_detection: array{ * enabled: bool, * retries_in_ms: list, @@ -277,6 +282,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->arrayNode('ids')->scalarPrototype()->end()->end() ->arrayNode('groups')->scalarPrototype()->end()->end() + ->scalarNode('exclude_url')->defaultValue('^/_(wdt|profiler|error)')->end() ->end() ->end() @@ -285,6 +291,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->addDefaultsIfNotSet() ->children() ->scalarNode('cache_pool')->defaultValue('cache.app')->end() + ->scalarNode('exclude_url')->defaultValue('^/_(wdt|profiler|error)')->end() ->end() ->end() diff --git a/src/DependencyInjection/PatchlevelEventSourcingExtension.php b/src/DependencyInjection/PatchlevelEventSourcingExtension.php index 4f8d6bf3..29e738bd 100644 --- a/src/DependencyInjection/PatchlevelEventSourcingExtension.php +++ b/src/DependencyInjection/PatchlevelEventSourcingExtension.php @@ -572,6 +572,7 @@ static function (ChildDefinition $definition): void { new Reference(SubscriptionEngine::class), $config['subscription']['auto_setup']['ids'] ?: null, $config['subscription']['auto_setup']['groups'] ?: null, + $config['subscription']['auto_setup']['exclude_url'] ?: null, ]) ->addTag('kernel.event_listener', [ 'event' => 'kernel.request', @@ -590,6 +591,7 @@ static function (ChildDefinition $definition): void { new TaggedIteratorArgument('event_sourcing.subscriber'), new Reference($config['subscription']['rebuild_after_file_change']['cache_pool']), new Reference(SubscriberMetadataFactory::class), + $config['subscription']['rebuild_after_file_change']['exclude_url'] ?: null, ]) ->addTag('kernel.event_listener', [ 'event' => 'kernel.request', diff --git a/src/RequestListener/AutoSetupListener.php b/src/RequestListener/AutoSetupListener.php index 19bdcf13..3e59fd2e 100644 --- a/src/RequestListener/AutoSetupListener.php +++ b/src/RequestListener/AutoSetupListener.php @@ -9,6 +9,8 @@ use Patchlevel\EventSourcing\Subscription\Status; use Symfony\Component\HttpKernel\Event\RequestEvent; +use function preg_match; + final class AutoSetupListener { /** @@ -19,6 +21,7 @@ public function __construct( private readonly SubscriptionEngine $subscriptionEngine, private readonly array|null $ids, private readonly array|null $groups, + private readonly string|null $excludeUrl = null, ) { } @@ -28,6 +31,13 @@ public function onKernelRequest(RequestEvent $event): void return; } + if ( + $this->excludeUrl !== null + && preg_match('#' . $this->excludeUrl . '#', $event->getRequest()->getRequestUri()) + ) { + return; + } + $subscriptions = $this->subscriptionEngine->subscriptions( new SubscriptionEngineCriteria( $this->ids, diff --git a/src/RequestListener/SubscriptionRebuildAfterFileChangeListener.php b/src/RequestListener/SubscriptionRebuildAfterFileChangeListener.php index 3fdbcb4d..df17f965 100644 --- a/src/RequestListener/SubscriptionRebuildAfterFileChangeListener.php +++ b/src/RequestListener/SubscriptionRebuildAfterFileChangeListener.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use function filemtime; +use function preg_match; final class SubscriptionRebuildAfterFileChangeListener { @@ -23,6 +24,7 @@ public function __construct( private readonly iterable $subscribers, private readonly CacheItemPoolInterface $cache, private readonly SubscriberMetadataFactory $metadataFactory = new AttributeSubscriberMetadataFactory(), + private readonly string|null $excludeUrl = null, ) { } @@ -32,6 +34,13 @@ public function onKernelRequest(RequestEvent $event): void return; } + if ( + $this->excludeUrl !== null + && preg_match('#' . $this->excludeUrl . '#', $event->getRequest()->getRequestUri()) + ) { + return; + } + $toRemove = []; $itemsToSave = []; diff --git a/tests/Fixtures/FromBeginningSubscriber.php b/tests/Fixtures/FromBeginningSubscriber.php new file mode 100644 index 00000000..4b579a18 --- /dev/null +++ b/tests/Fixtures/FromBeginningSubscriber.php @@ -0,0 +1,13 @@ +createMock(SubscriptionEngine::class); + $subscriptionEngine->expects($this->never())->method('subscriptions'); + $subscriptionEngine->expects($this->never())->method('setup'); + + $listener = new AutoSetupListener($subscriptionEngine, null, null); + $listener->onKernelRequest($this->createRequestEvent('/foo', HttpKernelInterface::SUB_REQUEST)); + } + + public function testSkipExcludedUrl(): void + { + $subscriptionEngine = $this->createMock(SubscriptionEngine::class); + $subscriptionEngine->expects($this->never())->method('subscriptions'); + $subscriptionEngine->expects($this->never())->method('setup'); + + $listener = new AutoSetupListener($subscriptionEngine, null, null, '^/_profiler'); + $listener->onKernelRequest($this->createRequestEvent('/_profiler/test')); + } + + public function testSetupOnlyNewSubscriptions(): void + { + $subscriptionEngine = $this->createMock(SubscriptionEngine::class); + $subscriptionEngine + ->expects($this->once()) + ->method('subscriptions') + ->with($this->callback(static function (SubscriptionEngineCriteria|null $criteria): bool { + return $criteria instanceof SubscriptionEngineCriteria + && $criteria->ids === ['id-1'] + && $criteria->groups === ['group-1']; + })) + ->willReturn([ + new Subscription('new-1'), + new Subscription('active-1', status: Status::Active), + new Subscription('new-2'), + ]); + + $subscriptionEngine + ->expects($this->once()) + ->method('setup') + ->with($this->callback(static function (SubscriptionEngineCriteria|null $criteria): bool { + return $criteria instanceof SubscriptionEngineCriteria + && $criteria->ids === ['new-1', 'new-2'] + && $criteria->groups === null; + }), true) + ->willReturn(new Result()); + + $listener = new AutoSetupListener($subscriptionEngine, ['id-1'], ['group-1'], '^/_profiler'); + $listener->onKernelRequest($this->createRequestEvent('/app')); + } + + private function createRequestEvent( + string $uri, + int $requestType = HttpKernelInterface::MAIN_REQUEST, + ): RequestEvent { + return new RequestEvent( + $this->createMock(HttpKernelInterface::class), + Request::create($uri), + $requestType, + ); + } +} diff --git a/tests/Unit/RequestListener/SubscriptionRebuildAfterFileChangeListenerTest.php b/tests/Unit/RequestListener/SubscriptionRebuildAfterFileChangeListenerTest.php new file mode 100644 index 00000000..ad8ac9ef --- /dev/null +++ b/tests/Unit/RequestListener/SubscriptionRebuildAfterFileChangeListenerTest.php @@ -0,0 +1,132 @@ +createMock(SubscriptionEngine::class); + $subscriptionEngine->expects($this->never())->method('remove'); + $subscriptionEngine->expects($this->never())->method('setup'); + $subscriptionEngine->expects($this->never())->method('boot'); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects($this->never())->method('getItem'); + + $listener = new SubscriptionRebuildAfterFileChangeListener( + $subscriptionEngine, + [new FromBeginningSubscriber()], + $cache, + ); + + $listener->onKernelRequest($this->createRequestEvent('/app', HttpKernelInterface::SUB_REQUEST)); + } + + public function testSkipExcludedUrl(): void + { + $subscriptionEngine = $this->createMock(SubscriptionEngine::class); + $subscriptionEngine->expects($this->never())->method('remove'); + $subscriptionEngine->expects($this->never())->method('setup'); + $subscriptionEngine->expects($this->never())->method('boot'); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects($this->never())->method('getItem'); + + $listener = new SubscriptionRebuildAfterFileChangeListener( + $subscriptionEngine, + [new FromBeginningSubscriber()], + $cache, + excludeUrl: '^/_wdt', + ); + + $listener->onKernelRequest($this->createRequestEvent('/_wdt/abc')); + } + + public function testRebuildChangedFromBeginningSubscriptionsOnly(): void + { + $item = $this->createMock(CacheItemInterface::class); + $item->expects($this->once())->method('get')->willReturn(1); + $item->expects($this->once())->method('set')->with($this->isType('int'))->willReturnSelf(); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects($this->once())->method('getItem')->with('from-beginning')->willReturn($item); + $cache->expects($this->once())->method('save')->with($item)->willReturn(true); + + $subscriptionEngine = $this->createMock(SubscriptionEngine::class); + $criteriaMatcher = $this->callback(static fn (SubscriptionEngineCriteria|null $criteria): bool => $criteria instanceof SubscriptionEngineCriteria && $criteria->ids === ['from-beginning'] && $criteria->groups === null); + + $subscriptionEngine->expects($this->once())->method('remove')->with($criteriaMatcher)->willReturn(new Result()); + $subscriptionEngine->expects($this->once())->method('setup')->with($criteriaMatcher)->willReturn(new Result()); + $subscriptionEngine->expects($this->once())->method('boot')->with($criteriaMatcher)->willReturn(new ProcessedResult(0)); + + $listener = new SubscriptionRebuildAfterFileChangeListener( + $subscriptionEngine, + [new FromBeginningSubscriber(), new FromNowSubscriber()], + $cache, + ); + + $listener->onKernelRequest($this->createRequestEvent('/app')); + } + + public function testNoRebuildWhenFileDidNotChange(): void + { + $subscriberFile = (new \ReflectionClass(FromBeginningSubscriber::class))->getFileName(); + self::assertIsString($subscriberFile); + + $currentModified = filemtime($subscriberFile); + self::assertIsInt($currentModified); + + $item = $this->createMock(CacheItemInterface::class); + $item->expects($this->once())->method('get')->willReturn($currentModified); + $item->expects($this->never())->method('set'); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects($this->once())->method('getItem')->with('from-beginning')->willReturn($item); + $cache->expects($this->never())->method('save'); + + $subscriptionEngine = $this->createMock(SubscriptionEngine::class); + $emptyCriteriaMatcher = $this->callback(static fn (SubscriptionEngineCriteria|null $criteria): bool => $criteria instanceof SubscriptionEngineCriteria && $criteria->ids === []); + + $subscriptionEngine->expects($this->once())->method('remove')->with($emptyCriteriaMatcher)->willReturn(new Result()); + $subscriptionEngine->expects($this->once())->method('setup')->with($emptyCriteriaMatcher)->willReturn(new Result()); + $subscriptionEngine->expects($this->once())->method('boot')->with($emptyCriteriaMatcher)->willReturn(new ProcessedResult(0)); + + $listener = new SubscriptionRebuildAfterFileChangeListener( + $subscriptionEngine, + [new FromBeginningSubscriber()], + $cache, + excludeUrl: '^/_profiler', + ); + + $listener->onKernelRequest($this->createRequestEvent('/app')); + } + + private function createRequestEvent( + string $uri, + int $requestType = HttpKernelInterface::MAIN_REQUEST, + ): RequestEvent { + return new RequestEvent( + $this->createMock(HttpKernelInterface::class), + Request::create($uri), + $requestType, + ); + } +}