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 */
@@ -56,6 +84,10 @@ public function getSupportedTypes($format): array
5684 */
5785 public function normalize (mixed $ object , ?string $ format = null , array $ context = []): array |string |int |float |bool |\ArrayObject |null
5886 {
87+ if ($ this ->isHalCircularReference ($ object , $ context )) {
88+ return $ this ->handleHalCircularReference ($ object , $ format , $ context );
89+ }
90+
5991 $ resourceClass = $ this ->getObjectClass ($ object );
6092 if ($ this ->getOutputClass ($ context )) {
6193 return parent ::normalize ($ object , $ format , $ context );
@@ -319,4 +351,53 @@ 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+ * @return bool
359+ *
360+ * @throws CircularReferenceException
361+ */
362+ protected function isHalCircularReference (object $ object , array &$ context ): bool
363+ {
364+ $ objectHash = spl_object_hash ($ object );
365+
366+ $ circularReferenceLimit = $ context [AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT ] ?? $ this ->defaultContext [AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT ];
367+ if (isset ($ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ])) {
368+ if ($ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ] >= $ circularReferenceLimit ) {
369+ unset($ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ]);
370+
371+ return true ;
372+ }
373+
374+ ++$ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ];
375+ } else {
376+ $ context [self ::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS ][$ objectHash ] = 1 ;
377+ }
378+
379+ return false ;
380+ }
381+
382+ /**
383+ * Handles a circular reference.
384+ *
385+ * If a circular reference handler is set, it will be called. Otherwise, a
386+ * {@class CircularReferenceException} will be thrown.
387+ *
388+ * @final
389+ *
390+ * @return mixed
391+ *
392+ * @throws CircularReferenceException
393+ */
394+ protected function handleHalCircularReference (object $ object , string $ format = null , array $ context = []): mixed
395+ {
396+ $ circularReferenceHandler = $ context [AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER ] ?? $ this ->defaultContext [AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER ];
397+ if ($ circularReferenceHandler ) {
398+ return $ circularReferenceHandler ($ object , $ format , $ context );
399+ }
400+
401+ 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 ]));
402+ }
322403}
0 commit comments