Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions src/HttpCache/SouinPurger.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class SouinPurger extends SurrogateKeysPurger
/**
* @param HttpClientInterface[] $clients
*/
public function __construct(iterable $clients)
public function __construct(iterable $clients, int $maxHeaderLength = self::MAX_HEADER_SIZE_PER_BATCH)
{
parent::__construct($clients, self::MAX_HEADER_SIZE_PER_BATCH, self::HEADER, self::SEPARATOR);
parent::__construct($clients, $maxHeaderLength, self::HEADER, self::SEPARATOR);
}
}
12 changes: 10 additions & 2 deletions src/HttpCache/State/AddHeadersProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ final class AddHeadersProcessor implements ProcessorInterface
/**
* @param ProcessorInterface<T1, T2> $decorated
*/
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)
{
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,
) {
}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
Expand Down
31 changes: 25 additions & 6 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
use ApiPlatform\Hal\Serializer\EntrypointNormalizer as HalEntrypointNormalizer;
use ApiPlatform\Hal\Serializer\ItemNormalizer as HalItemNormalizer;
use ApiPlatform\Hal\Serializer\ObjectNormalizer as HalObjectNormalizer;
use ApiPlatform\HttpCache\State\AddHeadersProcessor;
use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory;
use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer as HydraCollectionFiltersNormalizer;
use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer;
Expand Down Expand Up @@ -88,7 +89,6 @@
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
use ApiPlatform\Laravel\Eloquent\PropertyInfo\EloquentExtractor;
use ApiPlatform\Laravel\Eloquent\Serializer\EloquentNameConverter;
use ApiPlatform\Laravel\Eloquent\Serializer\Mapping\Loader\RelationMetadataLoader;
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
Expand Down Expand Up @@ -403,8 +403,25 @@ public function register(): void

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

$this->app->singleton(RespondProcessor::class, function () {
return new AddLinkHeaderProcessor(new RespondProcessor(), new HttpHeaderSerializer());
$this->app->singleton(RespondProcessor::class, function (Application $app) {
$decorated = new RespondProcessor();
if (class_exists(AddHeadersProcessor::class)) {
/** @var ConfigRepository */
$config = $app['config']->get('api-platform.http_cache') ?? [];

$decorated = new AddHeadersProcessor(
$decorated,
etag: $config['etag'] ?? false,
maxAge: $config['max_age'] ?? null,
sharedMaxAge: $config['shared_max_age'] ?? null,
vary: $config['vary'] ?? null,
public: $config['public'] ?? null,
staleWhileRevalidate: $config['stale_while_revalidate'] ?? null,
staleIfError: $config['stale_if_error'] ?? null
);
}

return new AddLinkHeaderProcessor($decorated, new HttpHeaderSerializer());
});

$this->app->singleton(SerializeProcessor::class, function (Application $app) {
Expand Down Expand Up @@ -555,7 +572,8 @@ public function register(): void
$app->make(LoggerInterface::class),
$app->make(ResourceMetadataCollectionFactoryInterface::class),
$app->make(ResourceAccessCheckerInterface::class),
$defaultContext
$defaultContext,
// $app->make(TagCollectorInterface::class)
);
});

Expand Down Expand Up @@ -603,6 +621,7 @@ public function register(): void
$defaultContext,
$app->make(ResourceMetadataCollectionFactoryInterface::class),
$app->make(ResourceAccessCheckerInterface::class),
// $app->make(TagCollectorInterface::class),
);
});

Expand Down Expand Up @@ -848,7 +867,6 @@ public function register(): void
$defaultContext,
$app->make(ResourceMetadataCollectionFactoryInterface::class),
$app->make(ResourceAccessCheckerInterface::class),
null
// $app->make(TagCollectorInterface::class),
);
});
Expand Down Expand Up @@ -943,7 +961,8 @@ public function register(): void
$app->make(NameConverterInterface::class),
$app->make(ClassMetadataFactoryInterface::class),
$defaultContext,
$app->make(ResourceAccessCheckerInterface::class)
$app->make(ResourceAccessCheckerInterface::class),
// $app->make(TagCollectorInterface::class)
);
});

Expand Down
8 changes: 8 additions & 0 deletions src/Laravel/Controller/ApiPlatformController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@

namespace ApiPlatform\Laravel\Controller;

use ApiPlatform\HttpCache\PurgerInterface;
use ApiPlatform\Laravel\Eloquent\Listener\PurgeHttpCacheListener;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Event;
use Symfony\Component\HttpFoundation\Response;

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

if (interface_exists(PurgerInterface::class)) {
Event::listen('eloquent.saved: *', [PurgeHttpCacheListener::class, 'handleModelSaved']);
Event::listen('eloquent.deleted: *', [PurgeHttpCacheListener::class, 'handleModelDeleted']);
}

return $this->processor->process($body, $operation, $uriVariables, $context);
}

Expand Down
115 changes: 115 additions & 0 deletions src/Laravel/Eloquent/ApiPlatformEventProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\Eloquent;

use ApiPlatform\HttpCache\PurgerInterface;
use ApiPlatform\HttpCache\SouinPurger;
use ApiPlatform\HttpCache\VarnishPurger;
use ApiPlatform\HttpCache\VarnishXKeyPurger;
use ApiPlatform\Laravel\Eloquent\Listener\PurgeHttpCacheListener;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Http\Events\RequestHandled;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Symfony\Component\HttpClient\HttpClient;

class ApiPlatformEventProvider extends ServiceProvider
{
/**
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [];

public function register(): void
{
if (!interface_exists(PurgerInterface::class)) {
return;
}

$this->app->singleton('api_platform.http_cache.clients_array', function (Application $app) {
$purgerUrls = Config::get('api-platform.http_cache.invalidation.urls', []);
$requestOptions = Config::get('api-platform.http_cache.invalidation.request_options', []);

$clients = [];
foreach ($purgerUrls as $url) {
$clients[] = HttpClient::create(array_merge($requestOptions, ['base_uri' => $url]));
}

return $clients;
});

$httpClients = fn (Application $app) => $app->make('api_platform.http_cache.clients_array');

$this->app->singleton(VarnishPurger::class, function (Application $app) use ($httpClients) {
return new VarnishPurger($httpClients($app));
});

$this->app->singleton(VarnishXKeyPurger::class, function (Application $app) use ($httpClients) {
return new VarnishXKeyPurger(
$httpClients($app),
Config::get('api-platform.http_cache.invalidation.max_header_length', 7500),
Config::get('api-platform.http_cache.invalidation.xkey.glue', ' ')
);
});

$this->app->singleton(SouinPurger::class, function (Application $app) use ($httpClients) {
return new SouinPurger(
$httpClients($app),
Config::get('api-platform.http_cache.invalidation.max_header_length', 7500)
);
});

$this->app->singleton(PurgerInterface::class, function (Application $app) {
$purgerClass = Config::get(
'api-platform.http_cache.invalidation.purger',
SouinPurger::class
);

if (!class_exists($purgerClass)) {
throw new \InvalidArgumentException("Purger class '{$purgerClass}' configured in api-platform.php was not found.");
}

return $app->make($purgerClass);
});

$this->app->singleton(PurgeHttpCacheListener::class, function (Application $app) {
return new PurgeHttpCacheListener(
$app->make(PurgerInterface::class),
$app->make(IriConverterInterface::class),
$app->make(ResourceClassResolverInterface::class)
);
});
}

public function boot(): void
{
if (!interface_exists(PurgerInterface::class)) {
return;
}

Event::listen(RequestHandled::class, function (): void {
Event::forget('eloquent.saved: *');
Event::forget('eloquent.deleted: *');
$this->app->make(PurgeHttpCacheListener::class)->postFlush();
});
}

public function shouldDiscoverEvents(): bool
{
return false;
}
}
88 changes: 88 additions & 0 deletions src/Laravel/Eloquent/Listener/PurgeHttpCacheListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\Eloquent\Listener;

use ApiPlatform\HttpCache\PurgerInterface;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use Illuminate\Database\Eloquent\Model;

final class PurgeHttpCacheListener
{
/**
* @var string[]
*/
private array $tags = [];

public function __construct(
private readonly PurgerInterface $purger,
private readonly IriConverterInterface $iriConverter,
private readonly ResourceClassResolverInterface $resourceClassResolver,
) {
}

/**
* @param Model[] $data
*/
public function handleModelSaved(string $eventName, array $data): void
{
foreach ($data as $model) {
if (!$this->resourceClassResolver->isResourceClass($model::class)) {
return;
}

try {
$this->tags[] = $this->iriConverter->getIriFromResource($model);
$this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class));
} catch (InvalidArgumentException|ItemNotFoundException $e) {
// do nothing
}
}
}

/**
* @param Model[] $data
*/
public function handleModelDeleted(string $eventName, array $data): void
{
foreach ($data as $model) {
if (!$this->resourceClassResolver->isResourceClass($model::class)) {
return;
}

try {
$this->tags[] = $this->iriConverter->getIriFromResource($model);
$this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class));
} catch (InvalidArgumentException|ItemNotFoundException $e) {
// do nothing
}
}
}

/**
* Purges all collected tags at the end of the request.
*/
public function postFlush(): void
{
if (empty($this->tags)) {
return;
}

$this->purger->purge(array_values(array_unique($this->tags)));
$this->tags = [];
}
}
Loading
Loading