Skip to content

Commit

Permalink
Merge 4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Dec 20, 2024
2 parents 12c4209 + c0c3f4c commit 716a43b
Show file tree
Hide file tree
Showing 27 changed files with 607 additions and 30 deletions.
47 changes: 46 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
# Changelog

## v4.0.13

### Bug fixes

* [3a694624b](https://github.com/api-platform/core/commit/3a694624bdd6c82df756ff04682b5f90fd625f59) fix(laravel): eloquent BelongsToMany relation (#6862)
* [c8db7aef0](https://github.com/api-platform/core/commit/c8db7aef05557675940c3e610c94c6a2184d90ba) fix(laravel): jsonapi query parameters (page, sort, fields and include) (#6876)
* [f2c998158](https://github.com/api-platform/core/commit/f2c998158a70632a26efcdd29a17d7f3a2cb859c) fix(jsonld): anonymous context hydra_prefix value (#6873)

Also contains [v3.4.10 changes](#v3410).

### Features

## v4.0.12

### Bug fixes

* [4db72f55f](https://github.com/api-platform/core/commit/4db72f55fa9dcd48518dc62b5bf472895b6a966b) fix: filter may not use FilterInterface (#6858)
* [c899a3da1](https://github.com/api-platform/core/commit/c899a3da14eb2dff49095d28855ef8f2a1c4072a) fix(laravel): use tagged resolvers as graphql resolvers
(#6855)
* [e0f8c38b9](https://github.com/api-platform/core/commit/e0f8c38b98a05f29ad36b37725e54a036209a859) fix(laravel): graphql currentPage (#6857)

Also contains [v3.4.9 changes](#v349).

### Features

## v4.0.11

### Bug fixes

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


### Features

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

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

## v3.4.10

### Bug fixes

* [2ee5eb496](https://github.com/api-platform/core/commit/2ee5eb4967f507d04ae07280914bea3c712d8cad) fix(symfony): mercure exception formatting by calling array_keys() (#6879)

## v3.4.9

### Bug fixes

* [22cbd0147](https://github.com/api-platform/core/commit/22cbd0147ef6f817093533d62dc8279add67a647) fix(metadata): various parameter improvements (#6867)
* [978975ef0](https://github.com/api-platform/core/commit/978975ef01d7b9d230291676527aa1140a7e552f) fix(jsonschema): hashmaps produces invalid openapi schema (#6830)
* [a209dd440](https://github.com/api-platform/core/commit/a209dd440957176099247acf35b82611073352b1) fix: add missing error normalizer trait and remove deprecated interface (#6853)
* [abbc031ee](https://github.com/api-platform/core/commit/abbc031eece83b54781502cd6373b47a09e109f4) fix: test empty parameter against null (#6852)

### Notes

- `Parameter::getValue()` now takes a default value as argument `getValue(mixed $default = new ParameterNotFound()): mixed`
- `Parametes::get(string $key, string $parameterClass = QueryParameter::class)` (but also `has` and `remove`) now has a default value as second argument to `QueryParameter::class`
- Constraint violation had the wrong message when using `property`, fixed by using the `key` instead

## v3.4.8

### Bug fixes
Expand Down
6 changes: 6 additions & 0 deletions src/GraphQl/State/Processor/NormalizeProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ private function serializePageBasedPaginatedCollection(iterable $collection, arr
}
$data['paginationInfo']['totalCount'] = $collection->getTotalItems();
}
if (isset($selection['paginationInfo']['currentPage'])) {
if (!($collection instanceof PartialPaginatorInterface)) {
throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return currentPage field.', PartialPaginatorInterface::class));
}
$data['paginationInfo']['currentPage'] = $collection->getCurrentPage();
}
if (isset($selection['paginationInfo']['lastPage'])) {
if (!($collection instanceof PaginatorInterface)) {
throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class));
Expand Down
1 change: 1 addition & 0 deletions src/GraphQl/Type/TypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ private function getPageBasedPaginationFields(GraphQLType $resourceType): array
'itemsPerPage' => GraphQLType::nonNull(GraphQLType::int()),
'lastPage' => GraphQLType::nonNull(GraphQLType::int()),
'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
'currentPage' => GraphQLType::nonNull(GraphQLType::int()),
'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
],
];
Expand Down
52 changes: 52 additions & 0 deletions src/JsonApi/Filter/SparseFieldset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?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\JsonApi\Filter;

use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Parameter as MetadataParameter;
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
use ApiPlatform\Metadata\PropertiesAwareInterface;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\OpenApi\Model\Parameter;

final class SparseFieldset implements OpenApiParameterFilterInterface, JsonSchemaFilterInterface, ParameterProviderFilterInterface, PropertiesAwareInterface
{
public function getSchema(MetadataParameter $parameter): array
{
return [
'type' => 'array',
'items' => [
'type' => 'string',
],
];
}

public function getOpenApiParameters(MetadataParameter $parameter): Parameter|array|null
{
return new Parameter(
name: ($k = $parameter->getKey()).'[]',
in: $parameter instanceof QueryParameter ? 'query' : 'header',
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(
'%1$s[]={propertyName}&%1$s[]={anotherPropertyName}',
$k
)
);
}

public static function getParameterProvider(): string
{
return SparseFieldsetParameterProvider::class;
}
}
62 changes: 62 additions & 0 deletions src/JsonApi/Filter/SparseFieldsetParameterProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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\JsonApi\Filter;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\State\ParameterProviderInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

final readonly class SparseFieldsetParameterProvider implements ParameterProviderInterface
{
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
{
if (!($operation = $context['operation'] ?? null)) {
return null;
}

$allowedProperties = $parameter->getExtraProperties()['_properties'] ?? [];
$value = $parameter->getValue();
$normalizationContext = $operation->getNormalizationContext();

if (!\is_array($value)) {
return null;
}

$properties = [];
$shortName = strtolower($operation->getShortName());
foreach ($value as $resource => $fields) {
if (strtolower($resource) === $shortName) {
$p = &$properties;
} else {
$properties[$resource] = [];
$p = &$properties[$resource];
}

foreach (explode(',', $fields) as $f) {
if (\array_key_exists($f, $allowedProperties)) {
$p[] = $f;
}
}
}

if (isset($normalizationContext[AbstractNormalizer::ATTRIBUTES])) {
$properties = array_merge_recursive((array) $normalizationContext[AbstractNormalizer::ATTRIBUTES], $properties);
}

$normalizationContext[AbstractNormalizer::ATTRIBUTES] = $properties;

return $operation->withNormalizationContext($normalizationContext);
}
}
18 changes: 12 additions & 6 deletions src/JsonLd/ContextBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,24 @@ public function getResourceContextUri(string $resourceClass, ?int $referenceType
public function getAnonymousResourceContext(object $object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array
{
$outputClass = $this->getObjectClass($object);
$operation = $context['operation'] ?? new Get(shortName: (new \ReflectionClass($outputClass))->getShortName());
$operation = $context['operation'] ?? new Get(
shortName: (new \ReflectionClass($outputClass))->getShortName(),
normalizationContext: [
self::HYDRA_CONTEXT_HAS_PREFIX => $context[self::HYDRA_CONTEXT_HAS_PREFIX] ?? $this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true,
'groups' => [],
],
denormalizationContext: [
'groups' => [],
]
);
$shortName = $operation->getShortName();

$jsonLdContext = [
'@context' => $this->getResourceContextWithShortname(
$outputClass,
$referenceType,
$shortName
$shortName,
$operation
),
'@type' => $shortName,
];
Expand Down Expand Up @@ -184,10 +194,6 @@ private function getResourceContextWithShortname(string $resourceClass, int $ref
}
}

if (false === ($this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true)) {
return [HYDRA_CONTEXT, $context];
}

return $context;
}
}
11 changes: 9 additions & 2 deletions src/JsonLd/Serializer/JsonLdContextTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\JsonLd\Serializer;

use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
use ApiPlatform\JsonLd\ContextBuilder;
use ApiPlatform\JsonLd\ContextBuilderInterface;

/**
Expand Down Expand Up @@ -49,13 +50,19 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin

private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, $object, array &$context): array
{
$anonymousContext = ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null];

if (isset($context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX])) {
$anonymousContext[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX] = $context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX];
}

// We're in a collection, don't add the @context part
if (isset($context['jsonld_has_context'])) {
return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null, 'has_context' => true]);
return $contextBuilder->getAnonymousResourceContext($object, ['has_context' => true] + $anonymousContext);
}

$context['jsonld_has_context'] = true;

return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]);
return $contextBuilder->getAnonymousResourceContext($object, $anonymousContext);
}
}
26 changes: 24 additions & 2 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
use ApiPlatform\Hydra\Serializer\HydraPrefixNameConverter;
use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer as HydraPartialCollectionViewNormalizer;
use ApiPlatform\Hydra\State\HydraLinkProcessor;
use ApiPlatform\JsonApi\Filter\SparseFieldset;
use ApiPlatform\JsonApi\Filter\SparseFieldsetParameterProvider;
use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory;
use ApiPlatform\JsonApi\Serializer\CollectionNormalizer as JsonApiCollectionNormalizer;
use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer as JsonApiEntrypointNormalizer;
Expand Down Expand Up @@ -85,6 +87,8 @@
use ApiPlatform\Laravel\Eloquent\Filter\DateFilter;
use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter;
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter;
use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider;
use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter;
use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter;
Expand All @@ -107,6 +111,7 @@
use ApiPlatform\Laravel\Exception\ErrorHandler;
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
use ApiPlatform\Laravel\JsonApi\State\JsonApiProvider;
use ApiPlatform\Laravel\Metadata\CachePropertyMetadataFactory;
use ApiPlatform\Laravel\Metadata\CachePropertyNameCollectionMetadataFactory;
use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory;
Expand Down Expand Up @@ -423,7 +428,15 @@ public function register(): void

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

$this->app->tag([BooleanFilter::class, EqualsFilter::class, PartialSearchFilter::class, DateFilter::class, OrderFilter::class, RangeFilter::class], EloquentFilterInterface::class);
$this->app->tag([
EqualsFilter::class,
PartialSearchFilter::class,
DateFilter::class,
OrderFilter::class,
RangeFilter::class,
SortFilter::class,
SparseFieldset::class,
], EloquentFilterInterface::class);

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

if (class_exists(JsonApiProvider::class)) {
$this->app->extend(DeserializeProvider::class, function (ProviderInterface $inner, Application $app) {
return new JsonApiProvider($inner);
});
}

$this->app->tag([PropertyFilter::class], SerializerFilterInterface::class);

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

$this->app->tag([SerializerFilterParameterProvider::class], ParameterProviderInterface::class);
$this->app->singleton(SortFilterParameterProvider::class, function (Application $app) {
return new SortFilterParameterProvider();
});
$this->app->tag([SerializerFilterParameterProvider::class, SortFilterParameterProvider::class, SparseFieldsetParameterProvider::class], ParameterProviderInterface::class);

$this->app->singleton('filters', function (Application $app) {
return new ServiceLocator(array_merge(
Expand Down
Loading

0 comments on commit 716a43b

Please sign in to comment.