13
13
14
14
namespace ApiPlatform \Hal \Serializer ;
15
15
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 ;
16
22
use ApiPlatform \Metadata \UrlGeneratorInterface ;
17
23
use ApiPlatform \Metadata \Util \ClassInfoTrait ;
18
24
use ApiPlatform \Serializer \AbstractItemNormalizer ;
19
25
use ApiPlatform \Serializer \CacheKeyTrait ;
20
26
use ApiPlatform \Serializer \ContextTrait ;
27
+ use ApiPlatform \Serializer \TagCollectorInterface ;
28
+ use Symfony \Component \PropertyAccess \PropertyAccessorInterface ;
29
+ use Symfony \Component \Serializer \Exception \CircularReferenceException ;
21
30
use Symfony \Component \Serializer \Exception \LogicException ;
22
31
use Symfony \Component \Serializer \Exception \UnexpectedValueException ;
23
32
use 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 ;
24
36
25
37
/**
26
38
* Converts between objects and array including HAL metadata.
@@ -35,9 +47,25 @@ final class ItemNormalizer extends AbstractItemNormalizer
35
47
36
48
public const FORMAT = 'jsonhal ' ;
37
49
50
+ protected const HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'hal_circular_reference_limit_counters ' ;
51
+
38
52
private array $ componentsCache = [];
39
53
private array $ attributesMetadataCache = [];
40
54
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
+
41
69
/**
42
70
* {@inheritdoc}
43
71
*/
@@ -216,6 +244,10 @@ private function populateRelation(array $data, object $object, ?string $format,
216
244
{
217
245
$ class = $ this ->getObjectClass ($ object );
218
246
247
+ if ($ this ->isHalCircularReference ($ object , $ context )) {
248
+ return $ this ->handleHalCircularReference ($ object , $ format , $ context );
249
+ }
250
+
219
251
$ attributesMetadata = \array_key_exists ($ class , $ this ->attributesMetadataCache ) ?
220
252
$ this ->attributesMetadataCache [$ class ] :
221
253
$ this ->attributesMetadataCache [$ class ] = $ this ->classMetadataFactory ? $ this ->classMetadataFactory ->getMetadataFor ($ class )->getAttributesMetadata () : null ;
@@ -319,4 +351,49 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str
319
351
320
352
return false ;
321
353
}
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
+ }
322
399
}
0 commit comments