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
4 changes: 3 additions & 1 deletion src/Laravel/Eloquent/ApiPlatformEventProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\HttpCache\VarnishXKeyPurger;
use ApiPlatform\Laravel\Eloquent\Listener\PurgeHttpCacheListener;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Http\Events\RequestHandled;
Expand Down Expand Up @@ -90,7 +91,8 @@ public function register(): void
return new PurgeHttpCacheListener(
$app->make(PurgerInterface::class),
$app->make(IriConverterInterface::class),
$app->make(ResourceClassResolverInterface::class)
$app->make(ResourceClassResolverInterface::class),
$app->make(ResourceMetadataCollectionFactoryInterface::class),
);
});
}
Expand Down
105 changes: 85 additions & 20 deletions src/Laravel/Eloquent/Listener/PurgeHttpCacheListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@
use ApiPlatform\HttpCache\PurgerInterface;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use Illuminate\Database\Eloquent\Model;

/**
* Purges responses containing modified models from the proxy cache.
*
* Sub-resource collection operations (those whose URI depends on parent
* `uriVariables` such as `/parents/{parentId}/children`) are not invalidated
* here: the listener has no parent context available.
*/
final class PurgeHttpCacheListener
{
/**
Expand All @@ -32,6 +43,7 @@ public function __construct(
private readonly PurgerInterface $purger,
private readonly IriConverterInterface $iriConverter,
private readonly ResourceClassResolverInterface $resourceClassResolver,
private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
) {
}

Expand All @@ -41,16 +53,7 @@ public function __construct(
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
}
$this->collectTagsFor($model);
}
}

Expand All @@ -60,16 +63,7 @@ public function handleModelSaved(string $eventName, array $data): void
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
}
$this->collectTagsFor($model);
}
}

Expand All @@ -85,4 +79,75 @@ public function postFlush(): void
$this->purger->purge(array_values(array_unique($this->tags)));
$this->tags = [];
}

private function collectTagsFor(Model $model): void
{
if (!$this->resourceClassResolver->isResourceClass($model::class)) {
return;
}

foreach ($this->getItemIris($model) as $iri) {
$this->tags[] = $iri;
}

foreach ($this->getCollectionIris($model) as $iri) {
$this->tags[] = $iri;
}
}

/**
* @return iterable<string>
*/
private function getItemIris(Model $model): iterable
{
if (!$this->resourceMetadataCollectionFactory) {
try {
yield $this->iriConverter->getIriFromResource($model);
} catch (InvalidArgumentException|ItemNotFoundException|OperationNotFoundException) {
}

return;
}

foreach ($this->resourceMetadataCollectionFactory->create($model::class) as $resourceMetadata) {
foreach ($resourceMetadata->getOperations() as $operation) {
if (!$operation instanceof Get) {
continue;
}
try {
yield $this->iriConverter->getIriFromResource($model, UrlGeneratorInterface::ABS_PATH, $operation);
} catch (InvalidArgumentException|ItemNotFoundException|OperationNotFoundException) {
}
}
}
}

/**
* @return iterable<string>
*/
private function getCollectionIris(Model $model): iterable
{
if (!$this->resourceMetadataCollectionFactory) {
try {
yield $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class));
} catch (InvalidArgumentException|ItemNotFoundException|OperationNotFoundException) {
}

return;
}

foreach ($this->resourceMetadataCollectionFactory->create($model::class) as $resourceMetadata) {
foreach ($resourceMetadata->getOperations() as $operation) {
if (!$operation instanceof GetCollection) {
continue;
}
try {
yield $this->iriConverter->getIriFromResource($model::class, UrlGeneratorInterface::ABS_PATH, $operation);
} catch (InvalidArgumentException|ItemNotFoundException|OperationNotFoundException) {
// Sub-resource collections (needing parent uri_variables) cannot
// be resolved here and are intentionally skipped.
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
service('api_platform.property_accessor'),
service('api_platform.object_mapper')->nullOnInvalid(),
service('api_platform.object_mapper.metadata_factory')->nullOnInvalid(),
service('api_platform.metadata.resource.metadata_collection_factory'),
])
->tag('doctrine.event_listener', ['event' => 'preUpdate'])
->tag('doctrine.event_listener', ['event' => 'onFlush'])
Expand Down
79 changes: 71 additions & 8 deletions src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use ApiPlatform\HttpCache\PurgerInterface;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
Expand All @@ -35,6 +37,11 @@
/**
* Purges responses containing modified entities from the proxy cache.
*
* Sub-resource collection operations (those whose URI depends on parent
* `uriVariables` such as `/parents/{parentId}/children`) are not invalidated
* here: the listener has no parent context available. Decorate this service
* or register an additional Doctrine event listener to invalidate such tags.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class PurgeHttpCacheListener
Expand All @@ -50,7 +57,8 @@ public function __construct(private readonly PurgerInterface $purger,
private readonly ResourceClassResolverInterface $resourceClassResolver,
?PropertyAccessorInterface $propertyAccessor = null,
private readonly ?ObjectMapperInterface $objectMapper = null,
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null)
private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null,
private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null)
{
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
}
Expand Down Expand Up @@ -127,15 +135,72 @@ private function gatherResourceAndItemTags(object $entity, bool $purgeItem): voi
$resources = $this->getResourcesForEntity($entity);

foreach ($resources as $resource) {
try {
$iri = $this->iriConverter->getIriFromResource($resource, UrlGeneratorInterface::ABS_PATH, new GetCollection());
foreach ($this->getCollectionIris($resource) as $iri) {
$this->tags[$iri] = $iri;
}

if ($purgeItem) {
$this->addTagForItem($entity);
}
}
}

/**
* @return iterable<string>
*/
private function getCollectionIris(object|string $resource): iterable
{
if (!$this->resourceMetadataCollectionFactory) {
// BC: fall back to the default GetCollection resolution.
try {
yield $this->iriConverter->getIriFromResource($resource, UrlGeneratorInterface::ABS_PATH, new GetCollection());
} catch (OperationNotFoundException|InvalidArgumentException) {
}

if ($purgeItem) {
$this->addTagForItem($entity);
return;
}

$resourceClass = $this->resourceClassResolver->getResourceClass($resource);
foreach ($this->resourceMetadataCollectionFactory->create($resourceClass) as $resourceMetadata) {
foreach ($resourceMetadata->getOperations() as $operation) {
if (!$operation instanceof GetCollection) {
continue;
}
try {
yield $this->iriConverter->getIriFromResource($resource, UrlGeneratorInterface::ABS_PATH, $operation);
} catch (OperationNotFoundException|InvalidArgumentException) {
// Sub-resource collections (needing parent uri_variables) cannot
// be resolved here and are intentionally skipped.
}
}
}
}

/**
* @return iterable<string>
*/
private function getItemIris(object|string $resource): iterable
{
if (!$this->resourceMetadataCollectionFactory) {
try {
yield $this->iriConverter->getIriFromResource($resource);
} catch (OperationNotFoundException|InvalidArgumentException) {
}

return;
}

$resourceClass = $this->resourceClassResolver->getResourceClass($resource);
foreach ($this->resourceMetadataCollectionFactory->create($resourceClass) as $resourceMetadata) {
foreach ($resourceMetadata->getOperations() as $operation) {
if (!$operation instanceof Get) {
continue;
}
try {
yield $this->iriConverter->getIriFromResource($resource, UrlGeneratorInterface::ABS_PATH, $operation);
} catch (OperationNotFoundException|InvalidArgumentException) {
}
}
}
}

Expand Down Expand Up @@ -202,10 +267,8 @@ private function addTagForItem(mixed $value): void
$resources = $this->getResourcesForEntity($value);

foreach ($resources as $resource) {
try {
$iri = $this->iriConverter->getIriFromResource($resource);
foreach ($this->getItemIris($resource) as $iri) {
$this->tags[$iri] = $iri;
} catch (OperationNotFoundException|InvalidArgumentException) {
}
}
}
Expand Down
Loading
Loading