88use ApiPlatform \Exception \ResourceClassNotFoundException ;
99use ApiPlatform \Metadata \GetCollection ;
1010use ApiPlatform \Metadata \IriConverterInterface ;
11+ use ApiPlatform \Metadata \Operation ;
1112use ApiPlatform \Metadata \Resource \Factory \ResourceMetadataCollectionFactoryInterface ;
1213use ApiPlatform \Metadata \UrlGeneratorInterface ;
1314use App \Entity \BaseEntity ;
@@ -81,6 +82,11 @@ class RelatedCollectionLinkNormalizer implements NormalizerInterface, Serializer
8182 use PropertyHelperTrait;
8283 use ClassInfoTrait;
8384
85+ /**
86+ * @var (Operation|string)[]
87+ */
88+ private array $ exactSearchFilterExistsOperationCache = [];
89+
8490 public function __construct (
8591 private NormalizerInterface $ decorated ,
8692 private ServiceLocator $ filterLocator ,
@@ -111,12 +117,17 @@ public function normalize($data, $format = null, array $context = []): null|arra
111117 continue ;
112118 }
113119
114- try {
115- $ normalized_data ['_links ' ][$ rel ] = ['href ' => $ this ->getRelatedCollectionHref ($ data , $ rel , $ context )];
116- } catch (UnsupportedRelationException $ e ) {
117- // The relation is not supported, or there is no matching filter defined on the related entity
120+ // If relation is a public property, this property can be checked to be a non-null value
121+ $ values = get_object_vars ($ data );
122+ if (array_key_exists ($ rel , $ values ) && null == $ values [$ rel ]) {
123+ // target-value is NULL
124+ continue ;
125+ }
126+
127+ if (!$ this ->getRelatedCollectionHref ($ data , $ rel , $ context , $ result )) {
118128 continue ;
119129 }
130+ $ normalized_data ['_links ' ][$ rel ] = ['href ' => $ result ];
120131 }
121132
122133 return $ normalized_data ;
@@ -136,7 +147,7 @@ public function setSerializer(SerializerInterface $serializer): void {
136147 }
137148 }
138149
139- public function getRelatedCollectionHref ($ object , $ rel , array $ context = [] ): string {
150+ protected function getRelatedCollectionHref ($ object , $ rel , array $ context, & $ href ): bool {
140151 $ resourceClass = $ this ->getObjectClass ($ object );
141152
142153 if ($ this ->nameConverter instanceof NameConverterInterface) {
@@ -149,7 +160,9 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st
149160 $ params = $ this ->extractUriParams ($ object , $ annotation ->getParams ());
150161 [$ uriTemplate ] = $ this ->uriTemplateFactory ->createFromResourceClass ($ annotation ->getRelatedEntity ());
151162
152- return $ this ->uriTemplate ->expand ($ uriTemplate , $ params );
163+ $ href = $ this ->uriTemplate ->expand ($ uriTemplate , $ params );
164+
165+ return true ;
153166 }
154167
155168 try {
@@ -161,7 +174,8 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st
161174
162175 $ relationMetadata = $ classMetadata ->getAssociationMapping ($ rel );
163176 } catch (MappingException ) {
164- throw new UnsupportedRelationException ($ resourceClass .'# ' .$ rel .' is not a Doctrine association. Embedding non-Doctrine collections is currently not implemented. ' );
177+ // $resourceClass # $rel is not a Doctrine association. Embedding non-Doctrine collections is currently not implemented
178+ return false ;
165179 }
166180
167181 $ relatedResourceClass = $ relationMetadata ['targetEntity ' ];
@@ -170,21 +184,38 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st
170184 $ relatedFilterName ??= $ relationMetadata ['inversedBy ' ];
171185
172186 if (empty ($ relatedResourceClass ) || empty ($ relatedFilterName )) {
173- throw new UnsupportedRelationException ('The ' .$ resourceClass .'# ' .$ rel .' relation does not have both a targetEntity and a mappedBy or inversedBy property ' );
187+ // The $resourceClass # $rel relation does not have both a targetEntity and a mappedBy or inversedBy property
188+ return false ;
174189 }
175190
176- $ resourceMetadataCollection = $ this ->resourceMetadataCollectionFactory ->create ($ relatedResourceClass );
177- $ operation = OperationHelper::findOneByType ($ resourceMetadataCollection , GetCollection::class);
191+ $ lookupKey = $ relatedResourceClass .': ' .$ relatedFilterName ;
192+ if (isset ($ this ->exactSearchFilterExistsOperationCache [$ lookupKey ])) {
193+ $ result = $ this ->exactSearchFilterExistsOperationCache [$ lookupKey ];
194+ } else {
195+ $ result = 'No Operation ' ;
196+ $ resourceMetadataCollection = $ this ->resourceMetadataCollectionFactory ->create ($ relatedResourceClass );
197+ $ operation = OperationHelper::findOneByType ($ resourceMetadataCollection , GetCollection::class);
178198
179- if (!$ operation ) {
180- throw new UnsupportedRelationException ('The resource ' .$ relatedResourceClass .' does not implement GetCollection() operation. ' );
199+ if (!$ operation ) {
200+ // The resource $relatedResourceClass does not implement GetCollection() operation
201+ } else {
202+ $ filterExists = $ this ->exactSearchFilterExists ($ relatedResourceClass , $ relatedFilterName );
203+ if (!$ filterExists ) {
204+ // The resource $relatedResourceClass does not have a search filter for the relation $relatedFilterName
205+ } else {
206+ $ result = $ operation ;
207+ }
208+ }
209+ $ this ->exactSearchFilterExistsOperationCache [$ lookupKey ] = $ result ;
181210 }
182211
183- if (!$ this ->exactSearchFilterExists ($ relatedResourceClass , $ relatedFilterName )) {
184- throw new UnsupportedRelationException ('The resource ' .$ relatedResourceClass .' does not have a search filter for the relation ' .$ relatedFilterName .'. ' );
212+ if ($ result instanceof Operation) {
213+ $ href = $ this ->router ->generate ($ result ->getName (), [$ relatedFilterName => urlencode ($ this ->iriConverter ->getIriFromResource ($ object ))], UrlGeneratorInterface::ABS_PATH );
214+
215+ return true ;
185216 }
186217
187- return $ this -> router -> generate ( $ operation -> getName (), [ $ relatedFilterName => urlencode ( $ this -> iriConverter -> getIriFromResource ( $ object ))], UrlGeneratorInterface:: ABS_PATH ) ;
218+ return false ;
188219 }
189220
190221 protected function getRelatedCollectionLinkAnnotation (string $ className , string $ propertyName ): ?RelatedCollectionLink {
0 commit comments