diff --git a/CHANGELOG.md b/CHANGELOG.md
index 976a0c06..eb6f6501 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Add the `ForResponseGroups` target which resolves properties from the serialization groups of the `#[Serialize]`
attribute by @HypeMC in https://github.com/sofascore/purgatory-bundle/pull/154
+- Add the `entity_change_purging` option and a PHPUnit extension with the `#[WithEntityChangePurging]` attribute to
+ enable purging on entity changes only for specific tests by @HypeMC
+ in https://github.com/sofascore/purgatory-bundle/pull/155
## [1.5.0] - 2026-08-25
diff --git a/README.md b/README.md
index c9c9ce91..2573fff8 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,8 @@ If your project doesn't use [Symfony Flex](https://github.com/symfony/flex), con
# Examples:
# - /^_profiler/
# - /^_wdt/
+ # Whether entity changes trigger purge requests, can be disabled in the test environment and enabled per test using the PHPUnit extension.
+ entity_change_purging: true
doctrine_middleware:
enabled: true
diff --git a/config/schema/purgatory.xsd b/config/schema/purgatory.xsd
index 18e21a54..7192822d 100644
--- a/config/schema/purgatory.xsd
+++ b/config/schema/purgatory.xsd
@@ -40,6 +40,7 @@
+
diff --git a/config/services.php b/config/services.php
index 67e32af7..6dd21ceb 100644
--- a/config/services.php
+++ b/config/services.php
@@ -19,6 +19,7 @@
use Sofascore\PurgatoryBundle\Command\DebugCommand;
use Sofascore\PurgatoryBundle\Doctrine\DBAL\Middleware;
use Sofascore\PurgatoryBundle\Listener\EntityChangeListener;
+use Sofascore\PurgatoryBundle\Listener\EntityChangePurgeSwitcher;
use Sofascore\PurgatoryBundle\Purger\AsyncPurger;
use Sofascore\PurgatoryBundle\Purger\InMemoryPurger;
use Sofascore\PurgatoryBundle\Purger\Messenger\PurgeMessageHandler;
@@ -166,11 +167,14 @@
->tag('purgatory.route_provider')
->arg(3, service('sofascore.purgatory.property_accessor'))
+ ->set('sofascore.purgatory.entity_change_purge_switcher', EntityChangePurgeSwitcher::class)
+
->set('sofascore.purgatory.entity_change_listener', EntityChangeListener::class)
->args([
tagged_iterator('purgatory.route_provider'),
service('router'),
service('sofascore.purgatory.purger'),
+ service('sofascore.purgatory.entity_change_purge_switcher'),
])
->set('sofascore.purgatory.purger.void', VoidPurger::class)
diff --git a/docs/README.md b/docs/README.md
index c8acf574..d838a3ff 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -535,6 +535,53 @@ class PurgeTest extends KernelTestCase
}
```
+### Enabling Purging Only for Specific Tests
+
+Generating purge requests on every flush can noticeably slow down a large test suite, even though most tests never
+assert on them. To disable it by default, set the `entity_change_purging` option to `false` in the test environment:
+
+```yaml
+# config/packages/purgatory.yaml
+when@test:
+ purgatory:
+ purger: in-memory
+ entity_change_purging: false
+```
+
+Then register the bundle's PHPUnit extension, which requires PHPUnit 10 or higher:
+
+```xml
+
+
+
+
+```
+
+Purging can now be enabled only where it is needed with the [`#[WithEntityChangePurging]`][6] attribute. When placed on
+a test class, purging is enabled for all of its tests, from `setUpBeforeClass()` until after `tearDownAfterClass()`.
+When placed on a test method, purging is enabled for that test only, from before `setUp()` until after `tearDown()`.
+In both cases the configured default is restored afterwards, or as soon as a test errors or is skipped:
+
+```php
+use Sofascore\PurgatoryBundle\PHPUnit\WithEntityChangePurging;
+use Sofascore\PurgatoryBundle\Test\InteractsWithPurgatory;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+
+class PurgeTest extends KernelTestCase
+{
+ use InteractsWithPurgatory;
+
+ #[WithEntityChangePurging]
+ public function testPurgePost()
+ {
+ // ...
+ }
+}
+```
+
+The switch can also be flipped manually with the static `enable()`, `disable()` and `reset()` methods of the
+[`EntityChangePurgeSwitcher`][7] class, e.g. to skip purging while loading fixtures.
+
## Debugging
The bundle includes integration with the [Symfony Profiler](https://symfony.com/doc/current/profiler.html) to help you
@@ -563,3 +610,5 @@ This command provides insights into which routes and parameters are associated w
[3]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/Listener/Enum/Action.php
[4]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/Test/InteractsWithPurgatory.php
[5]: https://github.com/symfony/symfony/blob/8.1/src/Symfony/Component/HttpKernel/Attribute/Serialize.php
+[6]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/PHPUnit/WithEntityChangePurging.php
+[7]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/Listener/EntityChangePurgeSwitcher.php
diff --git a/phpunit.dist.xml b/phpunit.dist.xml
index adc06783..a2feee79 100644
--- a/phpunit.dist.xml
+++ b/phpunit.dist.xml
@@ -14,6 +14,10 @@
failOnRisky="true"
failOnWarning="true"
>
+
+
+
+
tests
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 57db1d9c..3d70310a 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -33,6 +33,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarPrototype()->end()
->defaultValue([])
->end()
+ ->booleanNode('entity_change_purging')
+ ->info('Whether entity changes trigger purge requests, can be disabled in the test environment and enabled per test using the PHPUnit extension.')
+ ->defaultTrue()
+ ->end()
->arrayNode('doctrine_middleware')
->canBeDisabled()
->children()
diff --git a/src/DependencyInjection/PurgatoryExtension.php b/src/DependencyInjection/PurgatoryExtension.php
index 25fb13df..cefedb08 100644
--- a/src/DependencyInjection/PurgatoryExtension.php
+++ b/src/DependencyInjection/PurgatoryExtension.php
@@ -132,6 +132,9 @@ static function (ChildDefinition $definition, AsExpressionLanguageFunction $attr
$container->getDefinition('sofascore.purgatory.route_metadata_provider.attribute')
->setArgument(2, $mergedConfig['route_ignore_patterns']);
+ $container->getDefinition('sofascore.purgatory.entity_change_purge_switcher')
+ ->setArgument(0, $mergedConfig['entity_change_purging']);
+
/** @var array $doctrineEventListenerPriorities */
$doctrineEventListenerPriorities = $mergedConfig['doctrine_event_listener_priorities'];
diff --git a/src/Listener/EntityChangeListener.php b/src/Listener/EntityChangeListener.php
index d05ca1c8..8568e77f 100644
--- a/src/Listener/EntityChangeListener.php
+++ b/src/Listener/EntityChangeListener.php
@@ -28,6 +28,7 @@ public function __construct(
private readonly iterable $routeProviders,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly PurgerInterface $purger,
+ private readonly EntityChangePurgeSwitcher $entityChangePurgeSwitcher = new EntityChangePurgeSwitcher(),
) {
}
@@ -59,6 +60,12 @@ public function process(): void
return;
}
+ if (!$this->entityChangePurgeSwitcher->isEnabled()) {
+ $this->reset();
+
+ return;
+ }
+
$purgeRequests = array_values($this->queuedPurgeRequests);
$this->reset();
$this->purger->purge($purgeRequests);
@@ -74,6 +81,10 @@ public function reset(): void
*/
private function handleChanges(LifecycleEventArgs $eventArgs, Action $action): void
{
+ if (!$this->entityChangePurgeSwitcher->isEnabled()) {
+ return;
+ }
+
$entity = $eventArgs->getObject();
$entityChangeSet = $eventArgs->getObjectManager()->getUnitOfWork()->getEntityChangeSet($entity);
diff --git a/src/Listener/EntityChangePurgeSwitcher.php b/src/Listener/EntityChangePurgeSwitcher.php
new file mode 100644
index 00000000..ed97a453
--- /dev/null
+++ b/src/Listener/EntityChangePurgeSwitcher.php
@@ -0,0 +1,44 @@
+enabled;
+ }
+
+ public static function enable(): void
+ {
+ self::$override = true;
+ }
+
+ public static function disable(): void
+ {
+ self::$override = false;
+ }
+
+ /**
+ * Restores the configured default.
+ */
+ public static function reset(): void
+ {
+ self::$override = null;
+ }
+}
diff --git a/src/PHPUnit/Metadata/AttributeReader.php b/src/PHPUnit/Metadata/AttributeReader.php
new file mode 100644
index 00000000..29bb717e
--- /dev/null
+++ b/src/PHPUnit/Metadata/AttributeReader.php
@@ -0,0 +1,61 @@
+
+ */
+ private array $cache = [];
+
+ /**
+ * Also looks at the parent classes.
+ *
+ * @param class-string $className
+ */
+ public function forClass(string $className): ?WithEntityChangePurging
+ {
+ if (\array_key_exists($className, $this->cache)) {
+ return $this->cache[$className];
+ }
+
+ $attribute = null;
+ for ($class = new \ReflectionClass($className); false !== $class; $class = $class->getParentClass()) {
+ if (null !== $attribute = $this->readAttribute($class)) {
+ break;
+ }
+ }
+
+ return $this->cache[$className] = $attribute;
+ }
+
+ /**
+ * @param class-string $className
+ */
+ public function forMethod(string $className, string $methodName): ?WithEntityChangePurging
+ {
+ $key = $className.'::'.$methodName;
+
+ if (\array_key_exists($key, $this->cache)) {
+ return $this->cache[$key];
+ }
+
+ return $this->cache[$key] = $this->readAttribute(new \ReflectionMethod($className, $methodName));
+ }
+
+ /**
+ * @param \ReflectionClass