Skip to content

Commit fa2bc95

Browse files
authored
fix(laravel): http cache compatibility (#7380)
1 parent 4abec44 commit fa2bc95

File tree

12 files changed

+425
-12
lines changed

12 files changed

+425
-12
lines changed

src/HttpCache/SouinPurger.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class SouinPurger extends SurrogateKeysPurger
2929
/**
3030
* @param HttpClientInterface[] $clients
3131
*/
32-
public function __construct(iterable $clients)
32+
public function __construct(iterable $clients, int $maxHeaderLength = self::MAX_HEADER_SIZE_PER_BATCH)
3333
{
34-
parent::__construct($clients, self::MAX_HEADER_SIZE_PER_BATCH, self::HEADER, self::SEPARATOR);
34+
parent::__construct($clients, $maxHeaderLength, self::HEADER, self::SEPARATOR);
3535
}
3636
}

src/HttpCache/State/AddHeadersProcessor.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ final class AddHeadersProcessor implements ProcessorInterface
2929
/**
3030
* @param ProcessorInterface<T1, T2> $decorated
3131
*/
32-
public function __construct(private readonly ProcessorInterface $decorated, private readonly bool $etag = false, private readonly ?int $maxAge = null, private readonly ?int $sharedMaxAge = null, private readonly ?array $vary = null, private readonly ?bool $public = null, private readonly ?int $staleWhileRevalidate = null, private readonly ?int $staleIfError = null)
33-
{
32+
public function __construct(
33+
private readonly ProcessorInterface $decorated,
34+
private readonly bool $etag = false,
35+
private readonly ?int $maxAge = null,
36+
private readonly ?int $sharedMaxAge = null,
37+
private readonly ?array $vary = null,
38+
private readonly ?bool $public = null,
39+
private readonly ?int $staleWhileRevalidate = null,
40+
private readonly ?int $staleIfError = null,
41+
) {
3442
}
3543

3644
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed

src/Laravel/ApiPlatformProvider.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
use ApiPlatform\Hal\Serializer\EntrypointNormalizer as HalEntrypointNormalizer;
5050
use ApiPlatform\Hal\Serializer\ItemNormalizer as HalItemNormalizer;
5151
use ApiPlatform\Hal\Serializer\ObjectNormalizer as HalObjectNormalizer;
52+
use ApiPlatform\HttpCache\State\AddHeadersProcessor;
5253
use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory;
5354
use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer as HydraCollectionFiltersNormalizer;
5455
use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer;
@@ -88,7 +89,6 @@
8889
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
8990
use ApiPlatform\Laravel\Eloquent\PropertyInfo\EloquentExtractor;
9091
use ApiPlatform\Laravel\Eloquent\Serializer\EloquentNameConverter;
91-
use ApiPlatform\Laravel\Eloquent\Serializer\Mapping\Loader\RelationMetadataLoader;
9292
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
9393
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
9494
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
@@ -403,8 +403,25 @@ public function register(): void
403403

404404
$this->app->bind(ProviderInterface::class, ContentNegotiationProvider::class);
405405

406-
$this->app->singleton(RespondProcessor::class, function () {
407-
return new AddLinkHeaderProcessor(new RespondProcessor(), new HttpHeaderSerializer());
406+
$this->app->singleton(RespondProcessor::class, function (Application $app) {
407+
$decorated = new RespondProcessor();
408+
if (class_exists(AddHeadersProcessor::class)) {
409+
/** @var ConfigRepository */
410+
$config = $app['config']->get('api-platform.http_cache') ?? [];
411+
412+
$decorated = new AddHeadersProcessor(
413+
$decorated,
414+
etag: $config['etag'] ?? false,
415+
maxAge: $config['max_age'] ?? null,
416+
sharedMaxAge: $config['shared_max_age'] ?? null,
417+
vary: $config['vary'] ?? null,
418+
public: $config['public'] ?? null,
419+
staleWhileRevalidate: $config['stale_while_revalidate'] ?? null,
420+
staleIfError: $config['stale_if_error'] ?? null
421+
);
422+
}
423+
424+
return new AddLinkHeaderProcessor($decorated, new HttpHeaderSerializer());
408425
});
409426

410427
$this->app->singleton(SerializeProcessor::class, function (Application $app) {
@@ -555,7 +572,8 @@ public function register(): void
555572
$app->make(LoggerInterface::class),
556573
$app->make(ResourceMetadataCollectionFactoryInterface::class),
557574
$app->make(ResourceAccessCheckerInterface::class),
558-
$defaultContext
575+
$defaultContext,
576+
// $app->make(TagCollectorInterface::class)
559577
);
560578
});
561579

@@ -603,6 +621,7 @@ public function register(): void
603621
$defaultContext,
604622
$app->make(ResourceMetadataCollectionFactoryInterface::class),
605623
$app->make(ResourceAccessCheckerInterface::class),
624+
// $app->make(TagCollectorInterface::class),
606625
);
607626
});
608627

@@ -848,7 +867,6 @@ public function register(): void
848867
$defaultContext,
849868
$app->make(ResourceMetadataCollectionFactoryInterface::class),
850869
$app->make(ResourceAccessCheckerInterface::class),
851-
null
852870
// $app->make(TagCollectorInterface::class),
853871
);
854872
});
@@ -943,7 +961,8 @@ public function register(): void
943961
$app->make(NameConverterInterface::class),
944962
$app->make(ClassMetadataFactoryInterface::class),
945963
$defaultContext,
946-
$app->make(ResourceAccessCheckerInterface::class)
964+
$app->make(ResourceAccessCheckerInterface::class),
965+
// $app->make(TagCollectorInterface::class)
947966
);
948967
});
949968

src/Laravel/Controller/ApiPlatformController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313

1414
namespace ApiPlatform\Laravel\Controller;
1515

16+
use ApiPlatform\HttpCache\PurgerInterface;
17+
use ApiPlatform\Laravel\Eloquent\Listener\PurgeHttpCacheListener;
1618
use ApiPlatform\Metadata\HttpOperation;
1719
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
1820
use ApiPlatform\State\ProcessorInterface;
1921
use ApiPlatform\State\ProviderInterface;
2022
use Illuminate\Http\Request;
2123
use Illuminate\Routing\Controller;
24+
use Illuminate\Support\Facades\Event;
2225
use Symfony\Component\HttpFoundation\Response;
2326

2427
class ApiPlatformController extends Controller
@@ -93,6 +96,11 @@ public function __invoke(Request $request): Response
9396
$operation = $operation->withSerialize(true);
9497
}
9598

99+
if (interface_exists(PurgerInterface::class)) {
100+
Event::listen('eloquent.saved: *', [PurgeHttpCacheListener::class, 'handleModelSaved']);
101+
Event::listen('eloquent.deleted: *', [PurgeHttpCacheListener::class, 'handleModelDeleted']);
102+
}
103+
96104
return $this->processor->process($body, $operation, $uriVariables, $context);
97105
}
98106

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Eloquent;
15+
16+
use ApiPlatform\HttpCache\PurgerInterface;
17+
use ApiPlatform\HttpCache\SouinPurger;
18+
use ApiPlatform\HttpCache\VarnishPurger;
19+
use ApiPlatform\HttpCache\VarnishXKeyPurger;
20+
use ApiPlatform\Laravel\Eloquent\Listener\PurgeHttpCacheListener;
21+
use ApiPlatform\Metadata\IriConverterInterface;
22+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
23+
use Illuminate\Contracts\Foundation\Application;
24+
use Illuminate\Foundation\Http\Events\RequestHandled;
25+
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
26+
use Illuminate\Support\Facades\Config;
27+
use Illuminate\Support\Facades\Event;
28+
use Symfony\Component\HttpClient\HttpClient;
29+
30+
class ApiPlatformEventProvider extends ServiceProvider
31+
{
32+
/**
33+
* @var array<class-string, array<int, class-string>>
34+
*/
35+
protected $listen = [];
36+
37+
public function register(): void
38+
{
39+
if (!interface_exists(PurgerInterface::class)) {
40+
return;
41+
}
42+
43+
$this->app->singleton('api_platform.http_cache.clients_array', function (Application $app) {
44+
$purgerUrls = Config::get('api-platform.http_cache.invalidation.urls', []);
45+
$requestOptions = Config::get('api-platform.http_cache.invalidation.request_options', []);
46+
47+
$clients = [];
48+
foreach ($purgerUrls as $url) {
49+
$clients[] = HttpClient::create(array_merge($requestOptions, ['base_uri' => $url]));
50+
}
51+
52+
return $clients;
53+
});
54+
55+
$httpClients = fn (Application $app) => $app->make('api_platform.http_cache.clients_array');
56+
57+
$this->app->singleton(VarnishPurger::class, function (Application $app) use ($httpClients) {
58+
return new VarnishPurger($httpClients($app));
59+
});
60+
61+
$this->app->singleton(VarnishXKeyPurger::class, function (Application $app) use ($httpClients) {
62+
return new VarnishXKeyPurger(
63+
$httpClients($app),
64+
Config::get('api-platform.http_cache.invalidation.max_header_length', 7500),
65+
Config::get('api-platform.http_cache.invalidation.xkey.glue', ' ')
66+
);
67+
});
68+
69+
$this->app->singleton(SouinPurger::class, function (Application $app) use ($httpClients) {
70+
return new SouinPurger(
71+
$httpClients($app),
72+
Config::get('api-platform.http_cache.invalidation.max_header_length', 7500)
73+
);
74+
});
75+
76+
$this->app->singleton(PurgerInterface::class, function (Application $app) {
77+
$purgerClass = Config::get(
78+
'api-platform.http_cache.invalidation.purger',
79+
SouinPurger::class
80+
);
81+
82+
if (!class_exists($purgerClass)) {
83+
throw new \InvalidArgumentException("Purger class '{$purgerClass}' configured in api-platform.php was not found.");
84+
}
85+
86+
return $app->make($purgerClass);
87+
});
88+
89+
$this->app->singleton(PurgeHttpCacheListener::class, function (Application $app) {
90+
return new PurgeHttpCacheListener(
91+
$app->make(PurgerInterface::class),
92+
$app->make(IriConverterInterface::class),
93+
$app->make(ResourceClassResolverInterface::class)
94+
);
95+
});
96+
}
97+
98+
public function boot(): void
99+
{
100+
if (!interface_exists(PurgerInterface::class)) {
101+
return;
102+
}
103+
104+
Event::listen(RequestHandled::class, function (): void {
105+
Event::forget('eloquent.saved: *');
106+
Event::forget('eloquent.deleted: *');
107+
$this->app->make(PurgeHttpCacheListener::class)->postFlush();
108+
});
109+
}
110+
111+
public function shouldDiscoverEvents(): bool
112+
{
113+
return false;
114+
}
115+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Eloquent\Listener;
15+
16+
use ApiPlatform\HttpCache\PurgerInterface;
17+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
18+
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
19+
use ApiPlatform\Metadata\GetCollection;
20+
use ApiPlatform\Metadata\IriConverterInterface;
21+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
22+
use Illuminate\Database\Eloquent\Model;
23+
24+
final class PurgeHttpCacheListener
25+
{
26+
/**
27+
* @var string[]
28+
*/
29+
private array $tags = [];
30+
31+
public function __construct(
32+
private readonly PurgerInterface $purger,
33+
private readonly IriConverterInterface $iriConverter,
34+
private readonly ResourceClassResolverInterface $resourceClassResolver,
35+
) {
36+
}
37+
38+
/**
39+
* @param Model[] $data
40+
*/
41+
public function handleModelSaved(string $eventName, array $data): void
42+
{
43+
foreach ($data as $model) {
44+
if (!$this->resourceClassResolver->isResourceClass($model::class)) {
45+
return;
46+
}
47+
48+
try {
49+
$this->tags[] = $this->iriConverter->getIriFromResource($model);
50+
$this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class));
51+
} catch (InvalidArgumentException|ItemNotFoundException $e) {
52+
// do nothing
53+
}
54+
}
55+
}
56+
57+
/**
58+
* @param Model[] $data
59+
*/
60+
public function handleModelDeleted(string $eventName, array $data): void
61+
{
62+
foreach ($data as $model) {
63+
if (!$this->resourceClassResolver->isResourceClass($model::class)) {
64+
return;
65+
}
66+
67+
try {
68+
$this->tags[] = $this->iriConverter->getIriFromResource($model);
69+
$this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class));
70+
} catch (InvalidArgumentException|ItemNotFoundException $e) {
71+
// do nothing
72+
}
73+
}
74+
}
75+
76+
/**
77+
* Purges all collected tags at the end of the request.
78+
*/
79+
public function postFlush(): void
80+
{
81+
if (empty($this->tags)) {
82+
return;
83+
}
84+
85+
$this->purger->purge(array_values(array_unique($this->tags)));
86+
$this->tags = [];
87+
}
88+
}

0 commit comments

Comments
 (0)