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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions config/schema/purgatory.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<xsd:element name="purger" type="purger" minOccurs="0" />
<xsd:element name="messenger" type="messenger" minOccurs="0" />
</xsd:choice>
<xsd:attribute name="entity-change-purging" type="xsd:boolean" />
<xsd:attribute name="doctrine-middleware" type="xsd:boolean" />
<xsd:attribute name="doctrine-event-listener-priorities" type="xsd:int" />
<xsd:attribute name="profiler-integration" type="xsd:boolean" />
Expand Down
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
49 changes: 49 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<!-- phpunit.xml -->
<extensions>
<bootstrap class="Sofascore\PurgatoryBundle\PHPUnit\PurgatoryExtension" />
</extensions>
```

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
Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions phpunit.dist.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
failOnRisky="true"
failOnWarning="true"
>
<extensions>
<bootstrap class="Sofascore\PurgatoryBundle\PHPUnit\PurgatoryExtension" />
</extensions>

<testsuites>
<testsuite name="default">
<directory>tests</directory>
Expand Down
4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions src/DependencyInjection/PurgatoryExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<DoctrineEvents::*, ?int> $doctrineEventListenerPriorities */
$doctrineEventListenerPriorities = $mergedConfig['doctrine_event_listener_priorities'];

Expand Down
11 changes: 11 additions & 0 deletions src/Listener/EntityChangeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
) {
}

Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down
44 changes: 44 additions & 0 deletions src/Listener/EntityChangePurgeSwitcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Sofascore\PurgatoryBundle\Listener;

/**
* Controls whether entity changes trigger purge requests.
*
* The configured value is the default. It can be overridden globally at
* runtime using the static methods, e.g. by the PHPUnit extension.
*/
final class EntityChangePurgeSwitcher
{
private static ?bool $override = null;

public function __construct(
private readonly bool $enabled = true,
) {
}

public function isEnabled(): bool
{
return self::$override ?? $this->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;
}
}
61 changes: 61 additions & 0 deletions src/PHPUnit/Metadata/AttributeReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Sofascore\PurgatoryBundle\PHPUnit\Metadata;

use Sofascore\PurgatoryBundle\PHPUnit\WithEntityChangePurging;

/**
* @internal
*/
final class AttributeReader
{
/**
* @var array<string, ?WithEntityChangePurging>
*/
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<object>|\ReflectionMethod $reflection
*/
private function readAttribute(\ReflectionClass|\ReflectionMethod $reflection): ?WithEntityChangePurging
{
return ($reflection->getAttributes(WithEntityChangePurging::class)[0] ?? null)?->newInstance();
}
}
Loading
Loading