Skip to content

Commit

Permalink
Merge pull request #6536 from pmattmann/feature/related-collection-li…
Browse files Browse the repository at this point in the history
…nk-normalizer-performance

RelatedCollectionLinkNormalizer performance improved
  • Loading branch information
pmattmann authored Dec 25, 2024
2 parents 14a06d9 + 70a7155 commit 2758830
Showing 1 changed file with 46 additions and 15 deletions.
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

0 comments on commit 2758830

Please sign in to comment.