Skip to content

Commit fb47197

Browse files
committed
Merge 4.0
2 parents 8a4218b + ffadacc commit fb47197

File tree

20 files changed

+503
-36
lines changed

20 files changed

+503
-36
lines changed

CHANGELOG.md

+34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## v4.0.9
4+
5+
### Bug fixes
6+
7+
* [417fef5da](https://github.com/api-platform/core/commit/417fef5da9b1f3f16e323a193dec141f13b1ebc5) fix(laravel): overlaping route format (#6782)
8+
* [81099065e](https://github.com/api-platform/core/commit/81099065e30eb0356881907bd52095b85b9cae3d) fix(laravel): declare normalizer list as a service (#6786)
9+
* [dc8c09b1e](https://github.com/api-platform/core/commit/dc8c09b1e1ac15a7bcd4961fc3e80b06bec82e77) fix(laravel) graphQl Relationship loading (#6792)
10+
11+
Also contains [v3.4.6 changes](#v346).
12+
13+
### Features
14+
15+
## v4.0.8
16+
17+
### Bug fixes
18+
19+
* [dddb97075](https://github.com/api-platform/core/commit/dddb97075af9c6e2517e1881b803c9d31a1913cf) fix(symfony): default formats order (#6780)
20+
21+
### Features
22+
323
## v4.0.7
424

525
### Bug fixes
@@ -179,6 +199,20 @@ Notes:
179199

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

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

184218
### 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/Laravel/ApiPlatformProvider.php

+11-8
Original file line numberDiff line numberDiff line change
@@ -432,12 +432,12 @@ public function register(): void
432432
$this->app->singleton(ItemProvider::class, function (Application $app) {
433433
$tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class));
434434

435-
return new ItemProvider(new LinksHandler($app), new ServiceLocator($tagged));
435+
return new ItemProvider(new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), new ServiceLocator($tagged));
436436
});
437437
$this->app->singleton(CollectionProvider::class, function (Application $app) {
438438
$tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class));
439439

440-
return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged));
440+
return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged));
441441
});
442442
$this->app->tag([ItemProvider::class, CollectionProvider::class], ProviderInterface::class);
443443

@@ -973,11 +973,7 @@ public function register(): void
973973
);
974974
});
975975

976-
$this->app->bind(SerializerInterface::class, Serializer::class);
977-
$this->app->bind(NormalizerInterface::class, Serializer::class);
978-
$this->app->singleton(Serializer::class, function (Application $app) {
979-
/** @var ConfigRepository */
980-
$config = $app['config'];
976+
$this->app->singleton('api_platform_normalizer_list', function (Application $app) {
981977
$list = new \SplPriorityQueue();
982978
$list->insert($app->make(HydraEntrypointNormalizer::class), -800);
983979
$list->insert($app->make(HydraPartialCollectionViewNormalizer::class), -800);
@@ -1011,14 +1007,20 @@ public function register(): void
10111007
$list->insert($app->make(GraphQlRuntimeExceptionNormalizer::class), -780);
10121008
}
10131009

1010+
return $list;
1011+
});
1012+
1013+
$this->app->bind(SerializerInterface::class, Serializer::class);
1014+
$this->app->bind(NormalizerInterface::class, Serializer::class);
1015+
$this->app->singleton(Serializer::class, function (Application $app) {
10141016
// TODO: unused + implement hal/jsonapi ?
10151017
// $list->insert($dataUriNormalizer, -920);
10161018
// $list->insert($unwrappingDenormalizer, 1000);
10171019
// $list->insert($jsonserializableNormalizer, -900);
10181020
// $list->insert($uuidDenormalizer, -895); //Todo ramsey uuid support ?
10191021
10201022
return new Serializer(
1021-
iterator_to_array($list),
1023+
iterator_to_array($app->make('api_platform_normalizer_list')),
10221024
[
10231025
new JsonEncoder('json'),
10241026
$app->make(JsonEncoder::class),
@@ -1342,6 +1344,7 @@ public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollect
13421344
// _format is read by the middleware
13431345
$uriTemplate = $operation->getRoutePrefix().str_replace('{._format}', '{_format?}', $uriTemplate);
13441346
$route = (new Route([$operation->getMethod()], $uriTemplate, [ApiPlatformController::class, '__invoke']))
1347+
->where('_format', '^\.[a-zA-Z]+')
13451348
->name($operation->getName())
13461349
->setDefaults(['_api_operation_name' => $operation->getName(), '_api_resource_class' => $operation->getClass()]);
13471350

src/Laravel/Eloquent/State/LinksHandler.php

+62-11
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313

1414
namespace ApiPlatform\Laravel\Eloquent\State;
1515

16+
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
17+
use ApiPlatform\Metadata\GraphQl\Operation;
18+
use ApiPlatform\Metadata\GraphQl\Query;
1619
use ApiPlatform\Metadata\HttpOperation;
20+
use ApiPlatform\Metadata\Link;
21+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
1722
use Illuminate\Contracts\Foundation\Application;
1823
use Illuminate\Database\Eloquent\Builder;
1924
use Illuminate\Database\Eloquent\Model;
@@ -25,6 +30,7 @@ final class LinksHandler implements LinksHandlerInterface
2530
{
2631
public function __construct(
2732
private readonly Application $application,
33+
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
2834
) {
2935
}
3036

@@ -34,27 +40,72 @@ public function handleLinks(Builder $builder, array $uriVariables, array $contex
3440

3541
if ($operation instanceof HttpOperation) {
3642
foreach (array_reverse($operation->getUriVariables() ?? []) as $uriVariable => $link) {
37-
$identifier = $uriVariables[$uriVariable];
43+
$builder = $this->buildQuery($builder, $link, $uriVariables[$uriVariable]);
44+
}
3845

39-
if ($to = $link->getToProperty()) {
40-
$builder = $builder->where($builder->getModel()->{$to}()->getQualifiedForeignKeyName(), $identifier);
46+
return $builder;
47+
}
4148

42-
continue;
43-
}
49+
if (!($linkClass = $context['linkClass'] ?? false)) {
50+
return $builder;
51+
}
4452

45-
if ($from = $link->getFromProperty()) {
46-
$relation = $this->application->make($link->getFromClass());
47-
$builder = $builder->getModel()->where($relation->{$from}()->getQualifiedForeignKeyName(), $identifier);
53+
$newLink = null;
54+
$linkedOperation = null;
55+
$linkProperty = $context['linkProperty'] ?? null;
4856

49-
continue;
57+
try {
58+
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($linkClass);
59+
$linkedOperation = $resourceMetadataCollection->getOperation($operation->getName());
60+
} catch (OperationNotFoundException) {
61+
// Instead, we'll look for the first Query available.
62+
foreach ($resourceMetadataCollection as $resourceMetadata) {
63+
foreach ($resourceMetadata->getGraphQlOperations() as $op) {
64+
if ($op instanceof Query) {
65+
$linkedOperation = $op;
66+
}
5067
}
68+
}
69+
}
70+
71+
if (!$linkedOperation instanceof Operation) {
72+
return $builder;
73+
}
5174

52-
$builder->where($builder->getModel()->qualifyColumn($link->getIdentifiers()[0]), $identifier);
75+
$resourceClass = $builder->getModel()::class;
76+
foreach ($linkedOperation->getLinks() ?? [] as $link) {
77+
if ($resourceClass === $link->getToClass() && $linkProperty === $link->getFromProperty()) {
78+
$newLink = $link;
79+
break;
5380
}
81+
}
5482

83+
if (!$newLink) {
5584
return $builder;
5685
}
5786

58-
return $builder;
87+
return $this->buildQuery($builder, $newLink, $uriVariables[$newLink->getIdentifiers()[0]]);
88+
}
89+
90+
/**
91+
* @param Builder<Model> $builder
92+
*
93+
* @throws \Illuminate\Contracts\Container\BindingResolutionException
94+
*
95+
* @return Builder<Model> $builder
96+
*/
97+
private function buildQuery(Builder $builder, Link $link, mixed $identifier): Builder
98+
{
99+
if ($to = $link->getToProperty()) {
100+
return $builder->where($builder->getModel()->{$to}()->getQualifiedForeignKeyName(), $identifier);
101+
}
102+
103+
if ($from = $link->getFromProperty()) {
104+
$relation = $this->application->make($link->getFromClass());
105+
106+
return $builder->getModel()->where($relation->{$from}()->getQualifiedForeignKeyName(), $identifier);
107+
}
108+
109+
return $builder->where($builder->getModel()->qualifyColumn($link->getIdentifiers()[0]), $identifier);
59110
}
60111
}

src/Laravel/Tests/JsonLdTest.php

+11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ protected function defineEnvironment($app): void
4141
{
4242
tap($app['config'], function (Repository $config): void {
4343
$config->set('app.debug', true);
44+
$config->set('api-platform.resources', [app_path('Models'), app_path('ApiResource')]);
4445
});
4546
}
4647

@@ -337,4 +338,14 @@ public function testRelationWithGroups(): void
337338
$this->assertArrayHasKey('relation', $content);
338339
$this->assertArrayHasKey('name', $content['relation']);
339340
}
341+
342+
/**
343+
* @see https://github.com/api-platform/core/issues/6779
344+
*/
345+
public function testSimilarRoutesWithFormat(): void
346+
{
347+
$response = $this->get('/api/staff_position_histories?page=1', ['accept' => 'application/ld+json']);
348+
$response->assertStatus(200);
349+
$this->assertSame('/api/staff_position_histories', $response->json()['@id']);
350+
}
340351
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 Workbench\App\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
18+
#[ApiResource(provider: [self::class, 'provide'])]
19+
class Staff
20+
{
21+
public static function provide()
22+
{
23+
return [];
24+
}
25+
}

0 commit comments

Comments
 (0)