1313
1414namespace ApiPlatform \Hal \Serializer ;
1515
16+ use ApiPlatform \Metadata \IriConverterInterface ;
17+ use ApiPlatform \Metadata \Property \Factory \PropertyMetadataFactoryInterface ;
18+ use ApiPlatform \Metadata \Property \Factory \PropertyNameCollectionFactoryInterface ;
19+ use ApiPlatform \Metadata \Resource \Factory \ResourceMetadataCollectionFactoryInterface ;
20+ use ApiPlatform \Metadata \ResourceAccessCheckerInterface ;
21+ use ApiPlatform \Metadata \ResourceClassResolverInterface ;
1622use ApiPlatform \Metadata \UrlGeneratorInterface ;
1723use ApiPlatform \Metadata \Util \ClassInfoTrait ;
1824use ApiPlatform \Serializer \AbstractItemNormalizer ;
1925use ApiPlatform \Serializer \CacheKeyTrait ;
2026use ApiPlatform \Serializer \ContextTrait ;
27+ use ApiPlatform \Serializer \TagCollectorInterface ;
28+ use Symfony \Component \PropertyAccess \PropertyAccessorInterface ;
29+ use Symfony \Component \Serializer \Exception \CircularReferenceException ;
2130use Symfony \Component \Serializer \Exception \LogicException ;
2231use Symfony \Component \Serializer \Exception \UnexpectedValueException ;
2332use Symfony \Component \Serializer \Mapping \AttributeMetadataInterface ;
33+ use Symfony \Component \Serializer \Mapping \Factory \ClassMetadataFactoryInterface ;
34+ use Symfony \Component \Serializer \NameConverter \NameConverterInterface ;
35+ use Symfony \Component \Serializer \Normalizer \AbstractNormalizer ;
2436
2537/**
2638 * Converts between objects and array including HAL metadata.
@@ -35,9 +47,25 @@ final class ItemNormalizer extends AbstractItemNormalizer
3547
3648 public const FORMAT = 'jsonhal ' ;
3749
50+ protected const HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'hal_circular_reference_limit_counters ' ;
51+
3852 private array $ componentsCache = [];
3953 private array $ attributesMetadataCache = [];
4054
55+ public function __construct (PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , PropertyMetadataFactoryInterface $ propertyMetadataFactory , IriConverterInterface $ iriConverter , ResourceClassResolverInterface $ resourceClassResolver , ?PropertyAccessorInterface $ propertyAccessor = null , ?NameConverterInterface $ nameConverter = null , ?ClassMetadataFactoryInterface $ classMetadataFactory = null , array $ defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $ resourceMetadataCollectionFactory = null , ?ResourceAccessCheckerInterface $ resourceAccessChecker = null , ?TagCollectorInterface $ tagCollector = null )
56+ {
57+ $ defaultContext [AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER ] = function ($ object ): ?array {
58+ $ iri = $ this ->iriConverter ->getIriFromResource ($ object );
59+ if (null === $ iri ) {
60+ return null ;
61+ }
62+
63+ return ['_links ' => ['self ' => ['href ' => $ iri ]]];
64+ };
65+
66+ parent ::__construct ($ propertyNameCollectionFactory , $ propertyMetadataFactory , $ iriConverter , $ resourceClassResolver , $ propertyAccessor , $ nameConverter , $ classMetadataFactory , $ defaultContext , $ resourceMetadataCollectionFactory , $ resourceAccessChecker , $ tagCollector );
67+ }
68+
4169 /**
4270 * {@inheritdoc}
4371 */
@@ -216,6 +244,10 @@ private function populateRelation(array $data, object $object, ?string $format,
216244 {
217245 $ class = $ this ->getObjectClass ($ object );
218246
247+ if ($ this ->isHalCircularReference ($ object , $ context )) {
248+ return $ this ->handleHalCircularReference ($ object , $ format , $ context );
249+ }
250+
219251 $ attributesMetadata = \array_key_exists ($ class , $ this ->attributesMetadataCache ) ?
220252 $ this ->attributesMetadataCache [$ class ] :
221253 $ this ->attributesMetadataCache [$ class ] = $ this ->classMetadataFactory ? $ this ->classMetadataFactory ->getMetadataFor ($ class )->getAttributesMetadata () : null ;
@@ -319,4 +351,49 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str
319351
320352 return false ;
321353 }
354+
355+ /**
356+ * Detects if the configured circular reference limit is reached.
357+ *
358+ * @throws CircularReferenceException
359+ */
360+ protected function isHalCircularReference (object $ object , array &$ context ): bool
361+ {
362+ $ objectHash = spl_object_hash ($ object );
363+
364+ $ circularReferenceLimit = $ context [AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT ] ?? $ this ->defaultContext [AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT ];
365+ if (isset ($ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ])) {
366+ if ($ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ] >= $ circularReferenceLimit ) {
367+ unset($ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ]);
368+
369+ return true ;
370+ }
371+
372+ ++$ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ];
373+ } else {
374+ $ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ] = 1 ;
375+ }
376+
377+ return false ;
378+ }
379+
380+ /**
381+ * Handles a circular reference.
382+ *
383+ * If a circular reference handler is set, it will be called. Otherwise, a
384+ * {@class CircularReferenceException} will be thrown.
385+ *
386+ * @final
387+ *
388+ * @throws CircularReferenceException
389+ */
390+ protected function handleHalCircularReference (object $ object , ?string $ format = null , array $ context = []): mixed
391+ {
392+ $ circularReferenceHandler = $ context [AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER ] ?? $ this ->defaultContext [AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER ];
393+ if ($ circularReferenceHandler ) {
394+ return $ circularReferenceHandler ($ object , $ format , $ context );
395+ }
396+
397+ throw new CircularReferenceException (\sprintf ('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d). ' , get_debug_type ($ object ), $ context [AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT ] ?? $ this ->defaultContext [AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT ]));
398+ }
322399}
0 commit comments