Skip to content

Commit

Permalink
Merge pull request #6798 from soyuka/merge-34
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka authored Nov 15, 2024
2 parents dc8c09b + e352cfa commit ef9de10
Show file tree
Hide file tree
Showing 15 changed files with 349 additions and 17 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,20 @@ Notes:

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

## v3.4.6

### Bug fixes

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

## v3.4.5

### Bug fixes
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
"orchestra/testbench": "^9.1",
"phpspec/prophecy-phpunit": "^2.2",
"phpstan/extension-installer": "^1.1",
"phpstan/phpdoc-parser": "^1.13",
"phpstan/phpdoc-parser": "^1.13|^2.0",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-doctrine": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
Expand Down
77 changes: 77 additions & 0 deletions src/Hal/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,26 @@

namespace ApiPlatform\Hal\Serializer;

use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Serializer\AbstractItemNormalizer;
use ApiPlatform\Serializer\CacheKeyTrait;
use ApiPlatform\Serializer\ContextTrait;
use ApiPlatform\Serializer\TagCollectorInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

/**
* Converts between objects and array including HAL metadata.
Expand All @@ -35,9 +47,25 @@ final class ItemNormalizer extends AbstractItemNormalizer

public const FORMAT = 'jsonhal';

protected const HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'hal_circular_reference_limit_counters';

private array $componentsCache = [];
private array $attributesMetadataCache = [];

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)
{
$defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] = function ($object): ?array {
$iri = $this->iriConverter->getIriFromResource($object);
if (null === $iri) {
return null;
}

return ['_links' => ['self' => ['href' => $iri]]];
};

parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector);
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -216,6 +244,10 @@ private function populateRelation(array $data, object $object, ?string $format,
{
$class = $this->getObjectClass($object);

if ($this->isHalCircularReference($object, $context)) {
return $this->handleHalCircularReference($object, $format, $context);
}

$attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ?
$this->attributesMetadataCache[$class] :
$this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
Expand Down Expand Up @@ -319,4 +351,49 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str

return false;
}

/**
* Detects if the configured circular reference limit is reached.
*
* @throws CircularReferenceException
*/
protected function isHalCircularReference(object $object, array &$context): bool
{
$objectHash = spl_object_hash($object);

$circularReferenceLimit = $context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT];
if (isset($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) {
if ($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) {
unset($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]);

return true;
}

++$context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash];
} else {
$context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1;
}

return false;
}

/**
* Handles a circular reference.
*
* If a circular reference handler is set, it will be called. Otherwise, a
* {@class CircularReferenceException} will be thrown.
*
* @final
*
* @throws CircularReferenceException
*/
protected function handleHalCircularReference(object $object, ?string $format = null, array $context = []): mixed
{
$circularReferenceHandler = $context[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER];
if ($circularReferenceHandler) {
return $circularReferenceHandler($object, $format, $context);
}

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]));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
$currentKey = $nameConvertedKey;
}

if ($this->nameConverter && $property = $parameter->getProperty()) {
$parameter = $parameter->withProperty($this->nameConverter->normalize($property));
}

if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) {
$parameter = $parameter->withExtraProperties(['_query_property' => $eloquentRelation['foreign_key']] + $parameter->getExtraProperties());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\ParserConfig;

/**
* Extracts descriptions from PHPDoc.
Expand Down Expand Up @@ -58,9 +59,13 @@ public function __construct(private readonly ResourceMetadataCollectionFactoryIn
}
$phpDocParser = null;
$lexer = null;
if (class_exists(PhpDocParser::class)) {
$phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
$lexer = new Lexer();
if (class_exists(PhpDocParser::class) && class_exists(ParserConfig::class)) {
$config = new ParserConfig([]);
$phpDocParser = new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config));
$lexer = new Lexer($config);
} elseif (class_exists(PhpDocParser::class)) {
$phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); // @phpstan-ignore-line
$lexer = new Lexer(); // @phpstan-ignore-line
}
$this->phpDocParser = $phpDocParser;
$this->lexer = $lexer;
Expand Down
9 changes: 5 additions & 4 deletions src/State/Pagination/ArrayPaginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ final class ArrayPaginator implements \IteratorAggregate, PaginatorInterface, Ha

public function __construct(array $results, int $firstResult, int $maxResults)
{
if ($maxResults > 0) {
$this->firstResult = $firstResult;
$this->maxResults = $maxResults;
$this->totalItems = \count($results);

if ($maxResults > 0 && $firstResult < $this->totalItems) {
$this->iterator = new \LimitIterator(new \ArrayIterator($results), $firstResult, $maxResults);
} else {
$this->iterator = new \EmptyIterator();
}
$this->firstResult = $firstResult;
$this->maxResults = $maxResults;
$this->totalItems = \count($results);
}

/**
Expand Down
20 changes: 14 additions & 6 deletions src/State/Provider/ContentNegotiationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ private function flattenMimeTypes(array $formats): array
*/
private function getInputFormat(HttpOperation $operation, Request $request): ?string
{
if (
false === ($input = $operation->getInput())
|| (\is_array($input) && null === $input['class'])
|| false === $operation->canDeserialize()
) {
return null;
}

$contentType = $request->headers->get('CONTENT_TYPE');
if (null === $contentType || '' === $contentType) {
return null;
Expand All @@ -103,14 +111,14 @@ private function getInputFormat(HttpOperation $operation, Request $request): ?st
return $format;
}

$supportedMimeTypes = [];
foreach ($formats as $mimeTypes) {
foreach ($mimeTypes as $mimeType) {
$supportedMimeTypes[] = $mimeType;
if (!$request->isMethodSafe() && 'DELETE' !== $request->getMethod()) {
$supportedMimeTypes = [];
foreach ($formats as $mimeTypes) {
foreach ($mimeTypes as $mimeType) {
$supportedMimeTypes[] = $mimeType;
}
}
}

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

Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Bundle/Resources/config/metadata/resource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory.parameter.inner" />
<argument type="service" id="api_platform.filter_locator" />
<argument type="service" id="api_platform.filter_locator" on-invalid="ignore" />
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
</service>

<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">
Expand Down
64 changes: 64 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceA.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue4358;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Get;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;

#[Get(uriTemplate: 'resource_a',
formats: ['jsonhal'],
outputFormats: ['jsonhal'],
normalizationContext: ['groups' => ['ResourceA:read'], 'enable_max_depth' => true],
provider: [self::class, 'provide'])]
final class ResourceA
{
private static ?ResourceA $resourceA = null;

#[ApiProperty(readableLink: true)]
#[Groups(['ResourceA:read', 'ResourceB:read'])]
#[MaxDepth(6)]
public ResourceB $b;

public function __construct(?ResourceB $b = null)
{
if (null !== $b) {
$this->b = $b;
}
}

public static function provide(): self
{
return self::provideWithResource();
}

public static function provideWithResource(?ResourceB $b = null): self
{
if (!isset(self::$resourceA)) {
self::$resourceA = new self($b);

if (null === ResourceB::getInstance()) {
self::$resourceA->b = ResourceB::provideWithResource(self::$resourceA);
}
}

return self::$resourceA;
}

public static function getInstance(): ?self
{
return self::$resourceA;
}
}
64 changes: 64 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceB.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue4358;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Get;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;

#[Get(uriTemplate: 'resource_b',
formats: ['jsonhal'],
outputFormats: ['jsonhal'],
normalizationContext: ['groups' => ['ResourceB:read'], 'enable_max_depth' => true],
provider: [self::class, 'provide'])]
final class ResourceB
{
private static ?ResourceB $resourceB = null;

#[ApiProperty(readableLink: true)]
#[Groups(['ResourceA:read', 'ResourceB:read'])]
#[MaxDepth(6)]
public ResourceA $a;

public function __construct(?ResourceA $a = null)
{
if (null !== $a) {
$this->a = $a;
}
}

public static function provide(): self
{
return self::provideWithResource();
}

public static function provideWithResource(?ResourceA $a = null): self
{
if (!isset(self::$resourceB)) {
self::$resourceB = new self($a);

if (null === ResourceA::getInstance()) {
self::$resourceB->a = ResourceA::provideWithResource(self::$resourceB);
}
}

return self::$resourceB;
}

public static function getInstance(): ?self
{
return self::$resourceB;
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/TestBundle/Document/DummyProduct.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
/**
* Dummy Product.
*
* https://github.com/api-platform/core/issues/1107.
* @see https://github.com/api-platform/core/issues/1107
*
* @author Antoine Bluchet <[email protected]>
*/
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/TestBundle/Entity/DummyProduct.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
/**
* Dummy Product.
*
* https://github.com/api-platform/core/issues/1107.
* @see https://github.com/api-platform/core/issues/1107
*
* @author Antoine Bluchet <[email protected]>
*/
Expand Down
Loading

0 comments on commit ef9de10

Please sign in to comment.