Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RelatedCollectionLinkNormalizer performance improved #6536

Merged
Changes from 3 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
54 changes: 37 additions & 17 deletions api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use ApiPlatform\Exception\ResourceClassNotFoundException;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use App\Entity\BaseEntity;
Expand Down Expand Up @@ -81,6 +82,8 @@ class RelatedCollectionLinkNormalizer implements NormalizerInterface, Serializer
use PropertyHelperTrait;
use ClassInfoTrait;

private $exactSearchFilterExistsCache = [];
pmattmann marked this conversation as resolved.
Show resolved Hide resolved

public function __construct(
private NormalizerInterface $decorated,
private ServiceLocator $filterLocator,
Expand Down Expand Up @@ -110,12 +113,15 @@ public function normalize($data, $format = null, array $context = []): null|arra
if (isset($link['href'])) {
continue;
}

try {
$normalized_data['_links'][$rel] = ['href' => $this->getRelatedCollectionHref($data, $rel, $context)];
} catch (UnsupportedRelationException $e) {
// The relation is not supported, or there is no matching filter defined on the related entity
continue;
// Only consider relations with non-null value (object or collection)
// (does not yet work, some properties are private and cannot be read out)
pmattmann marked this conversation as resolved.
Show resolved Hide resolved
// if (property_exists($data, $rel) && !isset($data->$rel)) {
// continue;
// }

list($ok, $href) = $this->getRelatedCollectionHref($data, $rel, $context);
pmattmann marked this conversation as resolved.
Show resolved Hide resolved
if ($ok) {
pmattmann marked this conversation as resolved.
Show resolved Hide resolved
$normalized_data['_links'][$rel] = ['href' => $href];
}
}

Expand All @@ -136,7 +142,7 @@ public function setSerializer(SerializerInterface $serializer): void {
}
}

public function getRelatedCollectionHref($object, $rel, array $context = []): string {
public function getRelatedCollectionHref($object, $rel, array $context = []): array {
$resourceClass = $this->getObjectClass($object);

if ($this->nameConverter instanceof NameConverterInterface) {
Expand All @@ -149,7 +155,7 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st
$params = $this->extractUriParams($object, $annotation->getParams());
[$uriTemplate] = $this->uriTemplateFactory->createFromResourceClass($annotation->getRelatedEntity());

return $this->uriTemplate->expand($uriTemplate, $params);
return [true, $this->uriTemplate->expand($uriTemplate, $params)];
}

try {
Expand All @@ -161,7 +167,7 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st

$relationMetadata = $classMetadata->getAssociationMapping($rel);
} catch (MappingException) {
throw new UnsupportedRelationException($resourceClass.'#'.$rel.' is not a Doctrine association. Embedding non-Doctrine collections is currently not implemented.');
return [false, $resourceClass.'#'.$rel.' is not a Doctrine association. Embedding non-Doctrine collections is currently not implemented.'];
}

$relatedResourceClass = $relationMetadata['targetEntity'];
Expand All @@ -170,21 +176,35 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st
$relatedFilterName ??= $relationMetadata['inversedBy'];

if (empty($relatedResourceClass) || empty($relatedFilterName)) {
throw new UnsupportedRelationException('The '.$resourceClass.'#'.$rel.' relation does not have both a targetEntity and a mappedBy or inversedBy property');
return [false, 'The '.$resourceClass.'#'.$rel.' relation does not have both a targetEntity and a mappedBy or inversedBy property'];
}

$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($relatedResourceClass);
$operation = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class);
$lookupKey = $relatedResourceClass.':'.$relatedFilterName;
if (isset($this->exactSearchFilterExistsCache[$lookupKey])) {
$result = $this->exactSearchFilterExistsCache[$lookupKey];
} else {
$result = [null, ''];
pmattmann marked this conversation as resolved.
Show resolved Hide resolved
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($relatedResourceClass);
$operation = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class);

if (!$operation) {
throw new UnsupportedRelationException('The resource '.$relatedResourceClass.' does not implement GetCollection() operation.');
if (!$operation) {
$result = [null, 'The resource '.$relatedResourceClass.' does not implement GetCollection() operation.'];
} else {
$filterExists = $this->exactSearchFilterExists($relatedResourceClass, $relatedFilterName);
if (!$filterExists) {
$result = [null, 'The resource '.$relatedResourceClass.' does not have a search filter for the relation '.$relatedFilterName.'.'];
} else {
$result = [$operation, ''];
}
}
$this->exactSearchFilterExistsCache[$lookupKey] = $result;
}

if (!$this->exactSearchFilterExists($relatedResourceClass, $relatedFilterName)) {
throw new UnsupportedRelationException('The resource '.$relatedResourceClass.' does not have a search filter for the relation '.$relatedFilterName.'.');
if ($result[0] instanceof Operation) {
return [true, $this->router->generate($result[0]->getName(), [$relatedFilterName => urlencode($this->iriConverter->getIriFromResource($object))], UrlGeneratorInterface::ABS_PATH)];
}

return $this->router->generate($operation->getName(), [$relatedFilterName => urlencode($this->iriConverter->getIriFromResource($object))], UrlGeneratorInterface::ABS_PATH);
return [false, $result[1]];
}

protected function getRelatedCollectionLinkAnnotation(string $className, string $propertyName): ?RelatedCollectionLink {
Expand Down
Loading