Skip to content

Commit ef9de10

Browse files
authored
Merge pull request #6798 from soyuka/merge-34
2 parents dc8c09b + e352cfa commit ef9de10

File tree

15 files changed

+349
-17
lines changed

15 files changed

+349
-17
lines changed

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,20 @@ Notes:
187187

188188
* [0d5f35683](https://github.com/api-platform/core/commit/0d5f356839eb6aa9f536044abe4affa736553e76) feat(laravel): laravel component (#5882)
189189

190+
## v3.4.6
191+
192+
### Bug fixes
193+
194+
* [17c916c3a](https://github.com/api-platform/core/commit/17c916c3a1bcc837c9bc842dc48390dbeb043450) fix(symfony): service typo fix BackedEnumProvider for autowiring (#6769)
195+
* [216d9ccaa](https://github.com/api-platform/core/commit/216d9ccaacf7845daaaeab30f3a58bb5567430fe) fix(serializer): fetch type on normalization error when possible (#6761)
196+
* [2f967d934](https://github.com/api-platform/core/commit/2f967d9345004779f409b9ce1b5d0cbba84c7132) fix(doctrine): throw an exception when a filter is not found in a parameter (#6767)
197+
* [736ca045e](https://github.com/api-platform/core/commit/736ca045e6832f04aaa002ddd7b85c55df4696bb) fix(validator): allow to pass both a ConstraintViolationList and a previous exception (#6762)
198+
* [a98332d99](https://github.com/api-platform/core/commit/a98332d99a43338fa3bc0fd6b20f82ac58d1c397) fix(metadata): name convert parameter property (#6766)
199+
* [aa1667de1](https://github.com/api-platform/core/commit/aa1667de116fa9a40842f1480fc90ab49c7c2784) fix(state): empty result when the array paginator is out of bound (#6785)
200+
* [ab88353a3](https://github.com/api-platform/core/commit/ab88353a32f94146b01c34bae377ec5a735846db) fix(hal): detecting and handling circular reference (#6752)
201+
* [bba030614](https://github.com/api-platform/core/commit/bba030614b96887fea4f5c177e3137378ccae8a5) fix: properly support phpstan/phpdoc-parser 2 (#6789)
202+
* [bec147b91](https://github.com/api-platform/core/commit/bec147b916c29e346a698b28ddd4493bf305d9a0) fix(state): do not check content type if no input (#6794)
203+
190204
## v3.4.5
191205

192206
### Bug fixes

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
"orchestra/testbench": "^9.1",
146146
"phpspec/prophecy-phpunit": "^2.2",
147147
"phpstan/extension-installer": "^1.1",
148-
"phpstan/phpdoc-parser": "^1.13",
148+
"phpstan/phpdoc-parser": "^1.13|^2.0",
149149
"phpstan/phpstan": "^1.10",
150150
"phpstan/phpstan-doctrine": "^1.0",
151151
"phpstan/phpstan-phpunit": "^1.0",

src/Hal/Serializer/ItemNormalizer.php

+77
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,26 @@
1313

1414
namespace 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;
1622
use ApiPlatform\Metadata\UrlGeneratorInterface;
1723
use ApiPlatform\Metadata\Util\ClassInfoTrait;
1824
use ApiPlatform\Serializer\AbstractItemNormalizer;
1925
use ApiPlatform\Serializer\CacheKeyTrait;
2026
use ApiPlatform\Serializer\ContextTrait;
27+
use ApiPlatform\Serializer\TagCollectorInterface;
28+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
29+
use Symfony\Component\Serializer\Exception\CircularReferenceException;
2130
use Symfony\Component\Serializer\Exception\LogicException;
2231
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
2332
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;
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
}

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

+4
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
200200
$currentKey = $nameConvertedKey;
201201
}
202202

203+
if ($this->nameConverter && $property = $parameter->getProperty()) {
204+
$parameter = $parameter->withProperty($this->nameConverter->normalize($property));
205+
}
206+
203207
if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) {
204208
$parameter = $parameter->withExtraProperties(['_query_property' => $eloquentRelation['foreign_key']] + $parameter->getExtraProperties());
205209
}

src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use PHPStan\PhpDocParser\Parser\PhpDocParser;
2626
use PHPStan\PhpDocParser\Parser\TokenIterator;
2727
use PHPStan\PhpDocParser\Parser\TypeParser;
28+
use PHPStan\PhpDocParser\ParserConfig;
2829

2930
/**
3031
* Extracts descriptions from PHPDoc.
@@ -58,9 +59,13 @@ public function __construct(private readonly ResourceMetadataCollectionFactoryIn
5859
}
5960
$phpDocParser = null;
6061
$lexer = null;
61-
if (class_exists(PhpDocParser::class)) {
62-
$phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
63-
$lexer = new Lexer();
62+
if (class_exists(PhpDocParser::class) && class_exists(ParserConfig::class)) {
63+
$config = new ParserConfig([]);
64+
$phpDocParser = new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config));
65+
$lexer = new Lexer($config);
66+
} elseif (class_exists(PhpDocParser::class)) {
67+
$phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); // @phpstan-ignore-line
68+
$lexer = new Lexer(); // @phpstan-ignore-line
6469
}
6570
$this->phpDocParser = $phpDocParser;
6671
$this->lexer = $lexer;

src/State/Pagination/ArrayPaginator.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ final class ArrayPaginator implements \IteratorAggregate, PaginatorInterface, Ha
2727

2828
public function __construct(array $results, int $firstResult, int $maxResults)
2929
{
30-
if ($maxResults > 0) {
30+
$this->firstResult = $firstResult;
31+
$this->maxResults = $maxResults;
32+
$this->totalItems = \count($results);
33+
34+
if ($maxResults > 0 && $firstResult < $this->totalItems) {
3135
$this->iterator = new \LimitIterator(new \ArrayIterator($results), $firstResult, $maxResults);
3236
} else {
3337
$this->iterator = new \EmptyIterator();
3438
}
35-
$this->firstResult = $firstResult;
36-
$this->maxResults = $maxResults;
37-
$this->totalItems = \count($results);
3839
}
3940

4041
/**

src/State/Provider/ContentNegotiationProvider.php

+14-6
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ private function flattenMimeTypes(array $formats): array
9292
*/
9393
private function getInputFormat(HttpOperation $operation, Request $request): ?string
9494
{
95+
if (
96+
false === ($input = $operation->getInput())
97+
|| (\is_array($input) && null === $input['class'])
98+
|| false === $operation->canDeserialize()
99+
) {
100+
return null;
101+
}
102+
95103
$contentType = $request->headers->get('CONTENT_TYPE');
96104
if (null === $contentType || '' === $contentType) {
97105
return null;
@@ -103,14 +111,14 @@ private function getInputFormat(HttpOperation $operation, Request $request): ?st
103111
return $format;
104112
}
105113

106-
$supportedMimeTypes = [];
107-
foreach ($formats as $mimeTypes) {
108-
foreach ($mimeTypes as $mimeType) {
109-
$supportedMimeTypes[] = $mimeType;
114+
if (!$request->isMethodSafe() && 'DELETE' !== $request->getMethod()) {
115+
$supportedMimeTypes = [];
116+
foreach ($formats as $mimeTypes) {
117+
foreach ($mimeTypes as $mimeType) {
118+
$supportedMimeTypes[] = $mimeType;
119+
}
110120
}
111-
}
112121

113-
if (!$request->isMethodSafe() && 'DELETE' !== $request->getMethod()) {
114122
throw new UnsupportedMediaTypeHttpException(\sprintf('The content-type "%s" is not supported. Supported MIME types are "%s".', $contentType, implode('", "', $supportedMimeTypes)));
115123
}
116124

src/Symfony/Bundle/Resources/config/metadata/resource.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@
8383
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
8484
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
8585
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.parameter.inner" />
86-
<argument type="service" id="api_platform.filter_locator" />
86+
<argument type="service" id="api_platform.filter_locator" on-invalid="ignore" />
87+
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
8788
</service>
8889

8990
<service id="api_platform.metadata.resource.metadata_collection_factory.cached" class="ApiPlatform\Metadata\Resource\Factory\CachedResourceMetadataCollectionFactory" decorates="api_platform.metadata.resource.metadata_collection_factory" decoration-priority="-10" public="false">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue4358;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\Get;
18+
use Symfony\Component\Serializer\Annotation\Groups;
19+
use Symfony\Component\Serializer\Annotation\MaxDepth;
20+
21+
#[Get(uriTemplate: 'resource_a',
22+
formats: ['jsonhal'],
23+
outputFormats: ['jsonhal'],
24+
normalizationContext: ['groups' => ['ResourceA:read'], 'enable_max_depth' => true],
25+
provider: [self::class, 'provide'])]
26+
final class ResourceA
27+
{
28+
private static ?ResourceA $resourceA = null;
29+
30+
#[ApiProperty(readableLink: true)]
31+
#[Groups(['ResourceA:read', 'ResourceB:read'])]
32+
#[MaxDepth(6)]
33+
public ResourceB $b;
34+
35+
public function __construct(?ResourceB $b = null)
36+
{
37+
if (null !== $b) {
38+
$this->b = $b;
39+
}
40+
}
41+
42+
public static function provide(): self
43+
{
44+
return self::provideWithResource();
45+
}
46+
47+
public static function provideWithResource(?ResourceB $b = null): self
48+
{
49+
if (!isset(self::$resourceA)) {
50+
self::$resourceA = new self($b);
51+
52+
if (null === ResourceB::getInstance()) {
53+
self::$resourceA->b = ResourceB::provideWithResource(self::$resourceA);
54+
}
55+
}
56+
57+
return self::$resourceA;
58+
}
59+
60+
public static function getInstance(): ?self
61+
{
62+
return self::$resourceA;
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue4358;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\Get;
18+
use Symfony\Component\Serializer\Annotation\Groups;
19+
use Symfony\Component\Serializer\Annotation\MaxDepth;
20+
21+
#[Get(uriTemplate: 'resource_b',
22+
formats: ['jsonhal'],
23+
outputFormats: ['jsonhal'],
24+
normalizationContext: ['groups' => ['ResourceB:read'], 'enable_max_depth' => true],
25+
provider: [self::class, 'provide'])]
26+
final class ResourceB
27+
{
28+
private static ?ResourceB $resourceB = null;
29+
30+
#[ApiProperty(readableLink: true)]
31+
#[Groups(['ResourceA:read', 'ResourceB:read'])]
32+
#[MaxDepth(6)]
33+
public ResourceA $a;
34+
35+
public function __construct(?ResourceA $a = null)
36+
{
37+
if (null !== $a) {
38+
$this->a = $a;
39+
}
40+
}
41+
42+
public static function provide(): self
43+
{
44+
return self::provideWithResource();
45+
}
46+
47+
public static function provideWithResource(?ResourceA $a = null): self
48+
{
49+
if (!isset(self::$resourceB)) {
50+
self::$resourceB = new self($a);
51+
52+
if (null === ResourceA::getInstance()) {
53+
self::$resourceB->a = ResourceA::provideWithResource(self::$resourceB);
54+
}
55+
}
56+
57+
return self::$resourceB;
58+
}
59+
60+
public static function getInstance(): ?self
61+
{
62+
return self::$resourceB;
63+
}
64+
}

tests/Fixtures/TestBundle/Document/DummyProduct.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
/**
2424
* Dummy Product.
2525
*
26-
* https://github.com/api-platform/core/issues/1107.
26+
* @see https://github.com/api-platform/core/issues/1107
2727
*
2828
* @author Antoine Bluchet <[email protected]>
2929
*/

tests/Fixtures/TestBundle/Entity/DummyProduct.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
/**
2424
* Dummy Product.
2525
*
26-
* https://github.com/api-platform/core/issues/1107.
26+
* @see https://github.com/api-platform/core/issues/1107
2727
*
2828
* @author Antoine Bluchet <[email protected]>
2929
*/

0 commit comments

Comments
 (0)