Skip to content

Commit ce8419e

Browse files
committed
Use plain IRIs as tags. Allow several Varnish.
1 parent 1f08113 commit ce8419e

File tree

9 files changed

+64
-88
lines changed

9 files changed

+64
-88
lines changed

features/http_cache/tags.feature

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Feature: Cache invalidation trough HTTP Cache tags
2222
Scenario: Tags must be set for items
2323
When I send a "GET" request to "/relation_embedders/1"
2424
Then the response status code should be 200
25-
And the header "Cache-Tags" should be equal to "aa9e2bee5be20590f7dcc520ce2dffca,12a0c94f947a680d68bd6f65e025457d,91774d67418192a057e25dae00345572"
25+
And the header "Cache-Tags" should be equal to "/relation_embedders/1,/related_dummies/1,/third_levels/1"
2626

2727
Scenario: Create some more resources
2828
When I add "Content-Type" header equal to "application/ld+json"
@@ -41,7 +41,7 @@ Feature: Cache invalidation trough HTTP Cache tags
4141
Scenario: Tags must be set for collections
4242
When I send a "GET" request to "/relation_embedders"
4343
Then the response status code should be 200
44-
And the header "Cache-Tags" should be equal to "aa9e2bee5be20590f7dcc520ce2dffca,12a0c94f947a680d68bd6f65e025457d,91774d67418192a057e25dae00345572,0b51526d04eac211b0d5e93ce6b133e3,e3d156c7d2d2b9d2228071aca2aa71ca,4f4c0d1c39015017ca3e97b15c36fc16,245ca118219102ad043d75121434c707"
44+
And the header "Cache-Tags" should be equal to "/relation_embedders/1,/related_dummies/1,/third_levels/1,/relation_embedders/2,/related_dummies/2,/third_levels/2,/relation_embedders"
4545

4646
Scenario: Purge item on update
4747
When I add "Content-Type" header equal to "application/ld+json"
@@ -53,12 +53,12 @@ Feature: Cache invalidation trough HTTP Cache tags
5353
"""
5454
Then the response status code should be 200
5555
And the header "Cache-Tags" should not exist
56-
And "/relation_embedders/1,/related_dummies/1,/third_levels/1" IRIs should be purged
56+
And "/relation_embedders,/relation_embedders/1,/related_dummies/1,/third_levels/1" IRIs should be purged
5757

5858
@dropSchema
5959
Scenario: Purge item and the related collection on update
6060
When I add "Content-Type" header equal to "application/ld+json"
6161
And I send a "DELETE" request to "/relation_embedders/1"
6262
Then the response status code should be 204
6363
And the header "Cache-Tags" should not exist
64-
And "/relation_embedders/1,/relation_embedders" IRIs should be purged
64+
And "/relation_embedders,/relation_embedders/1" IRIs should be purged

src/Bridge/Doctrine/EventListener/PurgeHttpCacheListener.php

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace ApiPlatform\Core\Bridge\Doctrine\EventListener;
1313

14+
use ApiPlatform\Core\Api\IriConverterInterface;
1415
use ApiPlatform\Core\Exception\InvalidArgumentException;
16+
use ApiPlatform\Core\HttpCache\ResourceManagerInterface;
1517
use Doctrine\ORM\Event\OnFlushEventArgs;
1618
use Psr\Container\ContainerInterface;
1719

@@ -27,7 +29,7 @@ final class PurgeHttpCacheListener
2729
private $container;
2830

2931
/**
30-
* @param ContainerInterface|\Symfony\Component\DependencyInjection\ContainerInterface $container
32+
* @param ContainerInterface|\Symfony\Component\DependencyInjection\ContainerInterface $container The container is injected because of a circular reference.
3133
*/
3234
public function __construct($container)
3335
{
@@ -38,38 +40,32 @@ public function onFlush(OnFlushEventArgs $eventArgs)
3840
{
3941
$iriConverter = $this->container->get('api_platform.iri_converter');
4042
$resourceManager = $this->container->get('api_platform.http_cache.resource_manager');
41-
42-
$em = $eventArgs->getEntityManager();
43-
$uow = $em->getUnitOfWork();
43+
$uow = $eventArgs->getEntityManager()->getUnitOfWork();
4444

4545
foreach ($uow->getScheduledEntityInsertions() as $entity) {
46-
if (null !== $resourceClass = $this->getResource($entity)) {
47-
$resourceManager->addResource($iriConverter->getIriFromResourceClass($resourceClass));
48-
}
46+
$this->purge($iriConverter, $resourceManager, $entity, false);
4947
}
5048

5149
foreach ($uow->getScheduledEntityUpdates() as $entity) {
52-
if (null !== $this->getResource($entity)) {
53-
$resourceManager->addResource($iriConverter->getIriFromItem($entity));
54-
}
50+
$this->purge($iriConverter, $resourceManager, $entity, true);
5551
}
5652

5753
foreach ($uow->getScheduledEntityDeletions() as $entity) {
58-
if (null === $resourceClass = $this->getResource($entity)) {
59-
continue;
60-
}
61-
62-
$resourceManager->addResource($iriConverter->getIriFromResourceClass($resourceClass));
63-
$resourceManager->addResource($iriConverter->getIriFromItem($entity));
54+
$this->purge($iriConverter, $resourceManager, $entity, true);
6455
}
6556
}
6657

67-
private function getResource($entity)
58+
private function purge(IriConverterInterface $iriConverter, ResourceManagerInterface $resourceManager, $entity, bool $purgeItem)
6859
{
6960
try {
70-
return $this->container->get('api_platform.resource_class_resolver')->getResourceClass($entity);
61+
$resourceClass = $this->container->get('api_platform.resource_class_resolver')->getResourceClass($entity);
7162
} catch (InvalidArgumentException $e) {
72-
// Return null
63+
return;
64+
}
65+
66+
$resourceManager->addResource($iriConverter->getIriFromResourceClass($resourceClass));
67+
if ($purgeItem) {
68+
$resourceManager->addResource($iriConverter->getIriFromItem($entity));
7369
}
7470
}
7571
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
use Symfony\Component\Cache\Adapter\ArrayAdapter;
1717
use Symfony\Component\Config\FileLocator;
1818
use Symfony\Component\Config\Resource\DirectoryResource;
19+
use Symfony\Component\DependencyInjection\ChildDefinition;
1920
use Symfony\Component\DependencyInjection\ContainerBuilder;
21+
use Symfony\Component\DependencyInjection\DefinitionDecorator;
2022
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
2123
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
24+
use Symfony\Component\DependencyInjection\Reference;
2225
use Symfony\Component\Finder\Finder;
2326
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
2427

@@ -306,15 +309,25 @@ private function registerHttpCache(ContainerBuilder $container, array $config, X
306309
}
307310

308311
$loader->load('http_cache_tags.xml');
309-
if (null === $config['http_cache']['varnish_url']) {
310-
$container->removeDefinition('api_platform.http_cache.purger.varnish_client');
311-
312+
if (!$config['http_cache']['varnish_urls']) {
312313
return;
313314
}
314315

315-
$container->getDefinition('api_platform.http_cache.purger.varnish_client')->addArgument(
316-
['base_uri' => $config['http_cache']['varnish_url']]
317-
);
316+
$references = [];
317+
foreach ($config['http_cache']['varnish_urls'] as $url) {
318+
$id = sprintf('api_platform.http_cache.purger.varnish_client.%s', $url);
319+
$references[] = new Reference($id);
320+
321+
if (class_exists(ChildDefinition::class)) {
322+
$definition = new ChildDefinition('api_platform.http_cache.purger.varnish_client');
323+
} else {
324+
$definition = new DefinitionDecorator('api_platform.http_cache.purger.varnish_client');
325+
}
326+
$definition->addArgument(['base_uri' => $url]);
327+
$container->setDefinition($id, $definition);
328+
}
329+
330+
$container->getDefinition('api_platform.http_cache.purger.varnish')->addArgument($references);
318331
$container->setAlias('api_platform.http_cache.purger', 'api_platform.http_cache.purger.varnish');
319332
}
320333

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ public function getConfigTreeBuilder()
105105
->end()
106106
->booleanNode('public')->defaultNull()->info('To make all responses public by default.')->end()
107107
->booleanNode('enable_tags')->defaultFalse()->info('Add cache tags to the response.')->end()
108-
->scalarNode('varnish_url')->defaultNull()->info('URL of the Varnish server to purge using cache tags when a resource is updated.')->end()
108+
->arrayNode('varnish_urls')
109+
->defaultValue([])
110+
->prototype('scalar')->end()
111+
->info('URL of the Varnish server to purge using cache tags when a resource is updated.')
112+
->end()
109113
->end()
110114
->end()
111115

src/Bridge/Symfony/Bundle/Resources/config/http_cache_tags.xml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@
66

77
<services>
88
<service id="api_platform.http_cache.resource_manager" class="ApiPlatform\Core\HttpCache\ResourceManager" public="false" />
9-
<service id="api_platform.http_cache.purger.varnish_client" class="GuzzleHttp\Client" public="false" />
10-
11-
<service id="api_platform.http_cache.purger.varnish" class="ApiPlatform\Core\HttpCache\VarnishPurger" public="false">
12-
<argument type="service" id="api_platform.http_cache.purger.varnish_client" />
13-
</service>
9+
<service id="api_platform.http_cache.purger.varnish_client" class="GuzzleHttp\Client" abstract="true" public="false" />
10+
<service id="api_platform.http_cache.purger.varnish" class="ApiPlatform\Core\HttpCache\VarnishPurger" public="false" />
1411

1512
<service id="api_platform.http_cache.listener.response.add_tags" class="ApiPlatform\Core\HttpCache\EventListener\AddTagsListener">
1613
<argument type="service" id="api_platform.http_cache.resource_manager" />
@@ -23,9 +20,7 @@
2320
<service id="api_platform.http_cache.listener.response.purge" class="ApiPlatform\Core\HttpCache\EventListener\PurgeListener">
2421
<argument type="service" id="api_platform.http_cache.resource_manager" />
2522
<argument type="service" id="api_platform.http_cache.purger" />
26-
<argument type="service" id="api_platform.iri_converter" />
2723

28-
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="48" />
2924
<tag name="kernel.event_listener" event="kernel.terminate" method="onKernelTerminate" />
3025
</service>
3126
</services>

src/HttpCache/EventListener/AddTagsListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function onKernelResponse(FilterResponseEvent $event)
6161
return;
6262
}
6363

64-
$event->getResponse()->headers->set('Cache-Tags', implode(',', array_map('md5', $resources)));
64+
$event->getResponse()->headers->set('Cache-Tags', implode(',', $resources));
6565
$this->resourceManager->clear();
6666
}
6767
}

src/HttpCache/EventListener/PurgeListener.php

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,8 @@
1111

1212
namespace ApiPlatform\Core\HttpCache\EventListener;
1313

14-
use ApiPlatform\Core\Api\IriConverterInterface;
1514
use ApiPlatform\Core\HttpCache\PurgerInterface;
1615
use ApiPlatform\Core\HttpCache\ResourceManagerInterface;
17-
use ApiPlatform\Core\Util\RequestAttributesExtractor;
18-
use Symfony\Component\HttpFoundation\Request;
19-
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
2016
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
2117

2218
/**
@@ -28,55 +24,20 @@ final class PurgeListener
2824
{
2925
private $resourceManager;
3026
private $purger;
31-
private $iriConverter;
3227

33-
public function __construct(ResourceManagerInterface $resourceManager, PurgerInterface $purger, IriConverterInterface $iriConverter)
28+
public function __construct(ResourceManagerInterface $resourceManager, PurgerInterface $purger)
3429
{
3530
$this->resourceManager = $resourceManager;
3631
$this->purger = $purger;
37-
$this->iriConverter = $iriConverter;
3832
}
3933

40-
/**
41-
* Stores the original IRI of the object to be deleted because it may not be available anymore at purge time (e.g. when using Doctrine).
42-
*
43-
* @param GetResponseForControllerResultEvent $event
44-
*/
45-
public function onKernelView(GetResponseForControllerResultEvent $event)
34+
public function onKernelTerminate()
4635
{
47-
$request = $event->getRequest();
48-
if ($request->isMethod('DELETE') && RequestAttributesExtractor::extractAttributes($request)) {
49-
$this->resourceManager->addResource($this->iriConverter->getIriFromItem($request->attributes->get('data')));
50-
}
51-
}
52-
53-
public function onKernelTerminate(PostResponseEvent $event)
54-
{
55-
$this->addResources($event->getRequest());
56-
5736
if ($iris = $this->resourceManager->getResources()) {
58-
$this->purger->purge($iris);
59-
$this->resourceManager->clear();
60-
}
61-
}
62-
63-
private function addResources(Request $request)
64-
{
65-
if (
66-
$request->isMethodSafe()
67-
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
68-
|| !$data = $request->attributes->get('data')
69-
) {
70-
return;
71-
}
72-
73-
$method = $request->getMethod();
74-
if ('POST' === $method || 'DELETE' === $method) {
75-
$this->resourceManager->addResource($this->iriConverter->getIriFromResourceClass($attributes['resource_class']));
76-
7737
return;
7838
}
7939

80-
$this->resourceManager->addResource($this->iriConverter->getIriFromItem($data));
40+
$this->purger->purge($iris);
41+
$this->resourceManager->clear();
8142
}
8243
}

src/HttpCache/VarnishPurger.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515

1616
final class VarnishPurger implements PurgerInterface
1717
{
18-
private $guzzle;
18+
private $clients;
1919

20-
public function __construct(ClientInterface $guzzle)
20+
/**
21+
* @param ClientInterface[] $clients
22+
*/
23+
public function __construct(array $clients)
2124
{
22-
$this->guzzle = $guzzle;
25+
$this->clients = $clients;
2326
}
2427

2528
/**
@@ -31,9 +34,13 @@ public function purge(array $iris)
3134
return;
3235
}
3336

34-
$hashes = array_map('md5', $iris);
35-
$regex = isset($hashes[1]) ? sprintf('(%s)', implode('|', $hashes)) : $hashes[0];
37+
$parts = array_map(function ($iri) {
38+
return sprintf('(^|\,)%s($|\,)', preg_quote($iri));
39+
}, $iris);
40+
$regex = isset($parts[1]) ? sprintf('(%s)', implode(')|(', $parts)) : $parts[0];
3641

37-
$this->guzzle->sendAsync($this->guzzle->request('BAN', '', ['headers' => ['X-Ban-Regex' => $regex]]));
42+
foreach ($this->clients as $client) {
43+
$client->sendAsync($client->request('BAN', implode('-', $iris), ['headers' => ['X-Ban-Regex' => $regex]]));
44+
}
3845
}
3946
}

tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public function testDefaultConfig()
101101
'shared_max_age' => null,
102102
'vary' => ['Content-Type'],
103103
'public' => null,
104-
'varnish_url' => null,
104+
'varnish_urls' => [],
105105
],
106106
], $config);
107107
}

0 commit comments

Comments
 (0)