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 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
61 changes: 46 additions & 15 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,11 @@ class RelatedCollectionLinkNormalizer implements NormalizerInterface, Serializer
use PropertyHelperTrait;
use ClassInfoTrait;

/**
* @var (Operation|string)[]
*/
private array $exactSearchFilterExistsOperationCache = [];

public function __construct(
private NormalizerInterface $decorated,
private ServiceLocator $filterLocator,
Expand Down Expand Up @@ -111,12 +117,17 @@ public function normalize($data, $format = null, array $context = []): null|arra
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
// If relation is a public property, this property can be checked to be a non-null value
$values = get_object_vars($data);
if (array_key_exists($rel, $values) && null == $values[$rel]) {
// target-value is NULL
continue;
}

if (!$this->getRelatedCollectionHref($data, $rel, $context, $result)) {
continue;
}
$normalized_data['_links'][$rel] = ['href' => $result];
}

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

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

if ($this->nameConverter instanceof NameConverterInterface) {
Expand All @@ -149,7 +160,9 @@ 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);
$href = $this->uriTemplate->expand($uriTemplate, $params);

return true;
}

try {
Expand All @@ -161,7 +174,8 @@ 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.');
// $resourceClass # $rel is not a Doctrine association. Embedding non-Doctrine collections is currently not implemented
return false;
}

$relatedResourceClass = $relationMetadata['targetEntity'];
Expand All @@ -170,21 +184,38 @@ 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');
// The $resourceClass # $rel relation does not have both a targetEntity and a mappedBy or inversedBy property
return false;
}

$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($relatedResourceClass);
$operation = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class);
$lookupKey = $relatedResourceClass.':'.$relatedFilterName;
if (isset($this->exactSearchFilterExistsOperationCache[$lookupKey])) {
$result = $this->exactSearchFilterExistsOperationCache[$lookupKey];
} else {
$result = 'No Operation';
$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) {
// The resource $relatedResourceClass does not implement GetCollection() operation
} else {
$filterExists = $this->exactSearchFilterExists($relatedResourceClass, $relatedFilterName);
if (!$filterExists) {
// The resource $relatedResourceClass does not have a search filter for the relation $relatedFilterName
} else {
$result = $operation;
}
}
$this->exactSearchFilterExistsOperationCache[$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 instanceof Operation) {
$href = $this->router->generate($result->getName(), [$relatedFilterName => urlencode($this->iriConverter->getIriFromResource($object))], UrlGeneratorInterface::ABS_PATH);

return true;
}

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

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