Skip to content

Commit cc99bb5

Browse files
authored
Merge branch 'api-platform:main' into main
2 parents 77dd6a4 + 9389b4f commit cc99bb5

File tree

58 files changed

+849
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+849
-37
lines changed

CHANGELOG.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
11
# Changelog
22

3+
## v4.0.13
4+
5+
### Bug fixes
6+
7+
* [3a694624b](https://github.com/api-platform/core/commit/3a694624bdd6c82df756ff04682b5f90fd625f59) fix(laravel): eloquent BelongsToMany relation (#6862)
8+
* [c8db7aef0](https://github.com/api-platform/core/commit/c8db7aef05557675940c3e610c94c6a2184d90ba) fix(laravel): jsonapi query parameters (page, sort, fields and include) (#6876)
9+
* [f2c998158](https://github.com/api-platform/core/commit/f2c998158a70632a26efcdd29a17d7f3a2cb859c) fix(jsonld): anonymous context hydra_prefix value (#6873)
10+
11+
Also contains [v3.4.10 changes](#v3410).
12+
13+
### Features
14+
15+
## v4.0.12
16+
17+
### Bug fixes
18+
19+
* [4db72f55f](https://github.com/api-platform/core/commit/4db72f55fa9dcd48518dc62b5bf472895b6a966b) fix: filter may not use FilterInterface (#6858)
20+
* [c899a3da1](https://github.com/api-platform/core/commit/c899a3da14eb2dff49095d28855ef8f2a1c4072a) fix(laravel): use tagged resolvers as graphql resolvers
21+
(#6855)
22+
* [e0f8c38b9](https://github.com/api-platform/core/commit/e0f8c38b98a05f29ad36b37725e54a036209a859) fix(laravel): graphql currentPage (#6857)
23+
24+
Also contains [v3.4.9 changes](#v349).
25+
26+
### Features
27+
328
## v4.0.11
429

530
### Bug fixes
631

732
* [af66075fd](https://github.com/api-platform/core/commit/af66075fdd6b83bdebc1c4ca33cc0ab7e1a7f8af) fix(laravel): fix foregin keys (relations) beeing in attributes (#6843)
833

9-
1034
### Features
1135

1236
* [2d59c6369](https://github.com/api-platform/core/commit/2d59c63699b4602cfe4d62504896c6d4121c1be4) feat(laravel): belongs to many relations (#6818)
@@ -224,6 +248,27 @@ Notes:
224248

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

251+
## v3.4.10
252+
253+
### Bug fixes
254+
255+
* [2ee5eb496](https://github.com/api-platform/core/commit/2ee5eb4967f507d04ae07280914bea3c712d8cad) fix(symfony): mercure exception formatting by calling array_keys() (#6879)
256+
257+
## v3.4.9
258+
259+
### Bug fixes
260+
261+
* [22cbd0147](https://github.com/api-platform/core/commit/22cbd0147ef6f817093533d62dc8279add67a647) fix(metadata): various parameter improvements (#6867)
262+
* [978975ef0](https://github.com/api-platform/core/commit/978975ef01d7b9d230291676527aa1140a7e552f) fix(jsonschema): hashmaps produces invalid openapi schema (#6830)
263+
* [a209dd440](https://github.com/api-platform/core/commit/a209dd440957176099247acf35b82611073352b1) fix: add missing error normalizer trait and remove deprecated interface (#6853)
264+
* [abbc031ee](https://github.com/api-platform/core/commit/abbc031eece83b54781502cd6373b47a09e109f4) fix: test empty parameter against null (#6852)
265+
266+
### Notes
267+
268+
- `Parameter::getValue()` now takes a default value as argument `getValue(mixed $default = new ParameterNotFound()): mixed`
269+
- `Parametes::get(string $key, string $parameterClass = QueryParameter::class)` (but also `has` and `remove`) now has a default value as second argument to `QueryParameter::class`
270+
- Constraint violation had the wrong message when using `property`, fixed by using the `key` instead
271+
227272
## v3.4.8
228273

229274
### Bug fixes

src/Doctrine/Common/State/LinksHandlerLocatorTrait.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,33 @@ private function getLinksHandler(Operation $operation): ?callable
3535
return $handleLinks;
3636
}
3737

38+
if (\is_array($handleLinks) && 2 === \count($handleLinks) && class_exists($handleLinks[0])) {
39+
[$className, $methodName] = $handleLinks;
40+
41+
if (method_exists($className, $methodName)) {
42+
return $handleLinks;
43+
}
44+
45+
$suggestedMethod = $this->findSimilarMethod($className, $methodName);
46+
47+
throw new RuntimeException(\sprintf('Method "%s" does not exist in class "%s".%s', $methodName, $className, $suggestedMethod ? \sprintf(' Did you mean "%s"?', $suggestedMethod) : ''));
48+
}
49+
3850
if ($this->handleLinksLocator && \is_string($handleLinks) && $this->handleLinksLocator->has($handleLinks)) {
3951
return [$this->handleLinksLocator->get($handleLinks), 'handleLinks'];
4052
}
4153

4254
throw new RuntimeException(\sprintf('Could not find handleLinks service "%s"', $handleLinks));
4355
}
56+
57+
private function findSimilarMethod(string $className, string $methodName): ?string
58+
{
59+
$methods = get_class_methods($className);
60+
61+
$similarMethods = array_filter($methods, function ($method) use ($methodName) {
62+
return levenshtein($methodName, $method) <= 3;
63+
});
64+
65+
return $similarMethods ? reset($similarMethods) : null;
66+
}
4467
}

src/GraphQl/Executor.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use GraphQL\Type\Schema;
1919
use GraphQL\Validator\DocumentValidator;
2020
use GraphQL\Validator\Rules\DisableIntrospection;
21+
use GraphQL\Validator\Rules\QueryComplexity;
22+
use GraphQL\Validator\Rules\QueryDepth;
2123

2224
/**
2325
* Wrapper for the GraphQL facade.
@@ -26,13 +28,19 @@
2628
*/
2729
final class Executor implements ExecutorInterface
2830
{
29-
public function __construct(private readonly bool $graphQlIntrospectionEnabled = true)
31+
public function __construct(private readonly bool $graphQlIntrospectionEnabled = true, private readonly int $maxQueryComplexity = 500, private readonly int $maxQueryDepth = 20)
3032
{
3133
DocumentValidator::addRule(
3234
new DisableIntrospection(
3335
$this->graphQlIntrospectionEnabled ? DisableIntrospection::DISABLED : DisableIntrospection::ENABLED
3436
)
3537
);
38+
39+
$queryComplexity = new QueryComplexity($this->maxQueryComplexity);
40+
DocumentValidator::addRule($queryComplexity);
41+
42+
$queryDepth = new QueryDepth($this->maxQueryDepth);
43+
DocumentValidator::addRule($queryDepth);
3644
}
3745

3846
/**

src/GraphQl/State/Processor/NormalizeProcessor.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ private function serializePageBasedPaginatedCollection(iterable $collection, arr
213213
}
214214
$data['paginationInfo']['totalCount'] = $collection->getTotalItems();
215215
}
216+
if (isset($selection['paginationInfo']['currentPage'])) {
217+
if (!($collection instanceof PartialPaginatorInterface)) {
218+
throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return currentPage field.', PartialPaginatorInterface::class));
219+
}
220+
$data['paginationInfo']['currentPage'] = $collection->getCurrentPage();
221+
}
216222
if (isset($selection['paginationInfo']['lastPage'])) {
217223
if (!($collection instanceof PaginatorInterface)) {
218224
throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class));

src/GraphQl/Tests/ExecutorTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use ApiPlatform\GraphQl\Executor;
1717
use GraphQL\Validator\DocumentValidator;
1818
use GraphQL\Validator\Rules\DisableIntrospection;
19+
use GraphQL\Validator\Rules\QueryComplexity;
20+
use GraphQL\Validator\Rules\QueryDepth;
1921
use PHPUnit\Framework\TestCase;
2022

2123
/**
@@ -38,4 +40,20 @@ public function testDisableIntrospectionQuery(): void
3840
$expected = new DisableIntrospection(DisableIntrospection::ENABLED);
3941
$this->assertEquals($expected, DocumentValidator::getRule(DisableIntrospection::class));
4042
}
43+
44+
public function testChangeValueOfMaxQueryDepth(): void
45+
{
46+
$executor = new Executor(true, 20);
47+
48+
$expected = new QueryComplexity(20);
49+
$this->assertEquals($expected, DocumentValidator::getRule(QueryComplexity::class));
50+
}
51+
52+
public function testChangeValueOfMaxQueryComplexity(): void
53+
{
54+
$executor = new Executor(true, maxQueryDepth: 20);
55+
56+
$expected = new QueryDepth(20);
57+
$this->assertEquals($expected, DocumentValidator::getRule(QueryDepth::class));
58+
}
4159
}

src/GraphQl/Type/TypeBuilder.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ private function getPageBasedPaginationFields(GraphQLType $resourceType): array
278278
'itemsPerPage' => GraphQLType::nonNull(GraphQLType::int()),
279279
'lastPage' => GraphQLType::nonNull(GraphQLType::int()),
280280
'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
281+
'currentPage' => GraphQLType::nonNull(GraphQLType::int()),
281282
'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
282283
],
283284
];

src/Hydra/Serializer/DocumentationNormalizer.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public function normalize(mixed $object, ?string $format = null, array $context
7575
continue;
7676
}
7777

78+
if (true === $resourceMetadata->getHideHydraOperation()) {
79+
continue;
80+
}
81+
7882
$shortName = $resourceMetadata->getShortName();
7983

8084
$prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";
@@ -243,6 +247,10 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio
243247
$hydraOperations = [];
244248
foreach ($resourceMetadataCollection as $resourceMetadata) {
245249
foreach ($resourceMetadata->getOperations() as $operation) {
250+
if (true === $operation->getHideHydraOperation()) {
251+
continue;
252+
}
253+
246254
if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
247255
continue;
248256
}

src/Hydra/Serializer/EntrypointNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
5656

5757
foreach ($resource->getOperations() as $operation) {
5858
$key = lcfirst($resource->getShortName());
59-
if (!$operation instanceof CollectionOperationInterface || isset($entrypoint[$key])) {
59+
if (true === $operation->getHideHydraOperation() || !$operation instanceof CollectionOperationInterface || isset($entrypoint[$key])) {
6060
continue;
6161
}
6262

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\JsonApi\Filter;
15+
16+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
18+
use ApiPlatform\Metadata\Parameter as MetadataParameter;
19+
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
20+
use ApiPlatform\Metadata\PropertiesAwareInterface;
21+
use ApiPlatform\Metadata\QueryParameter;
22+
use ApiPlatform\OpenApi\Model\Parameter;
23+
24+
final class SparseFieldset implements OpenApiParameterFilterInterface, JsonSchemaFilterInterface, ParameterProviderFilterInterface, PropertiesAwareInterface
25+
{
26+
public function getSchema(MetadataParameter $parameter): array
27+
{
28+
return [
29+
'type' => 'array',
30+
'items' => [
31+
'type' => 'string',
32+
],
33+
];
34+
}
35+
36+
public function getOpenApiParameters(MetadataParameter $parameter): Parameter|array|null
37+
{
38+
return new Parameter(
39+
name: ($k = $parameter->getKey()).'[]',
40+
in: $parameter instanceof QueryParameter ? 'query' : 'header',
41+
description: 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.\sprintf(
42+
'%1$s[]={propertyName}&%1$s[]={anotherPropertyName}',
43+
$k
44+
)
45+
);
46+
}
47+
48+
public static function getParameterProvider(): string
49+
{
50+
return SparseFieldsetParameterProvider::class;
51+
}
52+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\JsonApi\Filter;
15+
16+
use ApiPlatform\Metadata\Operation;
17+
use ApiPlatform\Metadata\Parameter;
18+
use ApiPlatform\State\ParameterProviderInterface;
19+
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
20+
21+
final readonly class SparseFieldsetParameterProvider implements ParameterProviderInterface
22+
{
23+
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
24+
{
25+
if (!($operation = $context['operation'] ?? null)) {
26+
return null;
27+
}
28+
29+
$allowedProperties = $parameter->getExtraProperties()['_properties'] ?? [];
30+
$value = $parameter->getValue();
31+
$normalizationContext = $operation->getNormalizationContext();
32+
33+
if (!\is_array($value)) {
34+
return null;
35+
}
36+
37+
$properties = [];
38+
$shortName = strtolower($operation->getShortName());
39+
foreach ($value as $resource => $fields) {
40+
if (strtolower($resource) === $shortName) {
41+
$p = &$properties;
42+
} else {
43+
$properties[$resource] = [];
44+
$p = &$properties[$resource];
45+
}
46+
47+
foreach (explode(',', $fields) as $f) {
48+
if (\array_key_exists($f, $allowedProperties)) {
49+
$p[] = $f;
50+
}
51+
}
52+
}
53+
54+
if (isset($normalizationContext[AbstractNormalizer::ATTRIBUTES])) {
55+
$properties = array_merge_recursive((array) $normalizationContext[AbstractNormalizer::ATTRIBUTES], $properties);
56+
}
57+
58+
$normalizationContext[AbstractNormalizer::ATTRIBUTES] = $properties;
59+
60+
return $operation->withNormalizationContext($normalizationContext);
61+
}
62+
}

0 commit comments

Comments
 (0)