Skip to content

Commit 716a43b

Browse files
committed
Merge 4.0
2 parents 12c4209 + c0c3f4c commit 716a43b

27 files changed

+607
-30
lines changed

CHANGELOG.md

+46-1
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/GraphQl/State/Processor/NormalizeProcessor.php

+6
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/Type/TypeBuilder.php

+1
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/JsonApi/Filter/SparseFieldset.php

+52
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+
}
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+
}

src/JsonLd/ContextBuilder.php

+12-6
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,24 @@ public function getResourceContextUri(string $resourceClass, ?int $referenceType
115115
public function getAnonymousResourceContext(object $object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array
116116
{
117117
$outputClass = $this->getObjectClass($object);
118-
$operation = $context['operation'] ?? new Get(shortName: (new \ReflectionClass($outputClass))->getShortName());
118+
$operation = $context['operation'] ?? new Get(
119+
shortName: (new \ReflectionClass($outputClass))->getShortName(),
120+
normalizationContext: [
121+
self::HYDRA_CONTEXT_HAS_PREFIX => $context[self::HYDRA_CONTEXT_HAS_PREFIX] ?? $this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true,
122+
'groups' => [],
123+
],
124+
denormalizationContext: [
125+
'groups' => [],
126+
]
127+
);
119128
$shortName = $operation->getShortName();
120129

121130
$jsonLdContext = [
122131
'@context' => $this->getResourceContextWithShortname(
123132
$outputClass,
124133
$referenceType,
125-
$shortName
134+
$shortName,
135+
$operation
126136
),
127137
'@type' => $shortName,
128138
];
@@ -184,10 +194,6 @@ private function getResourceContextWithShortname(string $resourceClass, int $ref
184194
}
185195
}
186196

187-
if (false === ($this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true)) {
188-
return [HYDRA_CONTEXT, $context];
189-
}
190-
191197
return $context;
192198
}
193199
}

src/JsonLd/Serializer/JsonLdContextTrait.php

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\JsonLd\Serializer;
1515

1616
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
17+
use ApiPlatform\JsonLd\ContextBuilder;
1718
use ApiPlatform\JsonLd\ContextBuilderInterface;
1819

1920
/**
@@ -49,13 +50,19 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin
4950

5051
private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, $object, array &$context): array
5152
{
53+
$anonymousContext = ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null];
54+
55+
if (isset($context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX])) {
56+
$anonymousContext[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX] = $context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX];
57+
}
58+
5259
// We're in a collection, don't add the @context part
5360
if (isset($context['jsonld_has_context'])) {
54-
return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null, 'has_context' => true]);
61+
return $contextBuilder->getAnonymousResourceContext($object, ['has_context' => true] + $anonymousContext);
5562
}
5663

5764
$context['jsonld_has_context'] = true;
5865

59-
return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]);
66+
return $contextBuilder->getAnonymousResourceContext($object, $anonymousContext);
6067
}
6168
}

src/Laravel/ApiPlatformProvider.php

+24-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
use ApiPlatform\Hydra\Serializer\HydraPrefixNameConverter;
5858
use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer as HydraPartialCollectionViewNormalizer;
5959
use ApiPlatform\Hydra\State\HydraLinkProcessor;
60+
use ApiPlatform\JsonApi\Filter\SparseFieldset;
61+
use ApiPlatform\JsonApi\Filter\SparseFieldsetParameterProvider;
6062
use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory;
6163
use ApiPlatform\JsonApi\Serializer\CollectionNormalizer as JsonApiCollectionNormalizer;
6264
use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer as JsonApiEntrypointNormalizer;
@@ -85,6 +87,8 @@
8587
use ApiPlatform\Laravel\Eloquent\Filter\DateFilter;
8688
use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter;
8789
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
90+
use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter;
91+
use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider;
8892
use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
8993
use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter;
9094
use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter;
@@ -107,6 +111,7 @@
107111
use ApiPlatform\Laravel\Exception\ErrorHandler;
108112
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
109113
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
114+
use ApiPlatform\Laravel\JsonApi\State\JsonApiProvider;
110115
use ApiPlatform\Laravel\Metadata\CachePropertyMetadataFactory;
111116
use ApiPlatform\Laravel\Metadata\CachePropertyNameCollectionMetadataFactory;
112117
use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory;
@@ -423,7 +428,15 @@ public function register(): void
423428

424429
$this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class);
425430

426-
$this->app->tag([BooleanFilter::class, EqualsFilter::class, PartialSearchFilter::class, DateFilter::class, OrderFilter::class, RangeFilter::class], EloquentFilterInterface::class);
431+
$this->app->tag([
432+
EqualsFilter::class,
433+
PartialSearchFilter::class,
434+
DateFilter::class,
435+
OrderFilter::class,
436+
RangeFilter::class,
437+
SortFilter::class,
438+
SparseFieldset::class,
439+
], EloquentFilterInterface::class);
427440

428441
$this->app->bind(FilterQueryExtension::class, function (Application $app) {
429442
$tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class));
@@ -470,6 +483,12 @@ public function register(): void
470483
return new DeserializeProvider($app->make(ValidateProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class));
471484
});
472485

486+
if (class_exists(JsonApiProvider::class)) {
487+
$this->app->extend(DeserializeProvider::class, function (ProviderInterface $inner, Application $app) {
488+
return new JsonApiProvider($inner);
489+
});
490+
}
491+
473492
$this->app->tag([PropertyFilter::class], SerializerFilterInterface::class);
474493

475494
$this->app->singleton(SerializerFilterParameterProvider::class, function (Application $app) {
@@ -479,7 +498,10 @@ public function register(): void
479498
});
480499
$this->app->alias(SerializerFilterParameterProvider::class, 'api_platform.serializer.filter_parameter_provider');
481500

482-
$this->app->tag([SerializerFilterParameterProvider::class], ParameterProviderInterface::class);
501+
$this->app->singleton(SortFilterParameterProvider::class, function (Application $app) {
502+
return new SortFilterParameterProvider();
503+
});
504+
$this->app->tag([SerializerFilterParameterProvider::class, SortFilterParameterProvider::class, SparseFieldsetParameterProvider::class], ParameterProviderInterface::class);
483505

484506
$this->app->singleton('filters', function (Application $app) {
485507
return new ServiceLocator(array_merge(

0 commit comments

Comments
 (0)