Skip to content

Commit e3d95e9

Browse files
committed
fix(laravel): http cache compatibility
1 parent 949c3c9 commit e3d95e9

File tree

12 files changed

+424
-12
lines changed

12 files changed

+424
-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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
$app->make(ResourceClassResolverInterface::class)
95+
);
96+
});
97+
}
98+
99+
public function boot(): void
100+
{
101+
if (!interface_exists(PurgerInterface::class)) {
102+
return;
103+
}
104+
105+
Event::listen(RequestHandled::class, function (): void {
106+
Event::forget('eloquent.saved: *');
107+
Event::forget('eloquent.deleted: *');
108+
$this->app->make(PurgeHttpCacheListener::class)->postFlush();
109+
});
110+
}
111+
112+
public function shouldDiscoverEvents(): bool
113+
{
114+
return false;
115+
}
116+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
23+
final class PurgeHttpCacheListener
24+
{
25+
/**
26+
* @var string[]
27+
*/
28+
private array $tags = [];
29+
30+
public function __construct(
31+
private readonly PurgerInterface $purger,
32+
private readonly IriConverterInterface $iriConverter,
33+
private readonly ResourceClassResolverInterface $resourceClassResolver,
34+
) {
35+
}
36+
37+
/**
38+
* Handle the "saved" event for any model.
39+
*/
40+
public function handleModelSaved(string $eventName, array $data): void
41+
{
42+
foreach ($data as $model) {
43+
if (!$this->resourceClassResolver->isResourceClass($model::class)) {
44+
return;
45+
}
46+
47+
try {
48+
$this->tags[] = $this->iriConverter->getIriFromResource($model);
49+
$this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class));
50+
} catch (InvalidArgumentException|ItemNotFoundException $e) {
51+
// do nothing
52+
}
53+
}
54+
}
55+
56+
/**
57+
* Handle the "deleted" event for any model.
58+
*/
59+
public function handleModelDeleted(string $eventName, array $data): void
60+
{
61+
foreach ($data as $model) {
62+
if (!$this->resourceClassResolver->isResourceClass($model::class)) {
63+
return;
64+
}
65+
66+
try {
67+
$this->tags[] = $this->iriConverter->getIriFromResource($model);
68+
$this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class));
69+
} catch (InvalidArgumentException|ItemNotFoundException $e) {
70+
// do nothing
71+
}
72+
}
73+
}
74+
75+
/**
76+
* Purges all collected tags at the end of the request.
77+
*/
78+
public function postFlush(): void
79+
{
80+
if (empty($this->tags)) {
81+
return;
82+
}
83+
84+
$this->purger->purge(array_values(array_unique($this->tags)));
85+
$this->tags = [];
86+
}
87+
}

0 commit comments

Comments
 (0)