Skip to content
Merged
Changes from 4 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
52 changes: 37 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,8 @@ class RelatedCollectionLinkNormalizer implements NormalizerInterface, Serializer
use PropertyHelperTrait;
use ClassInfoTrait;

private $exactSearchFilterExistsCache = [];

public function __construct(
private NormalizerInterface $decorated,
private ServiceLocator $filterLocator,
Expand Down Expand Up @@ -111,12 +114,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;
}

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

return $normalized_data;
Expand All @@ -136,7 +144,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 +157,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 +169,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 +178,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, ''];
$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