Skip to content

Commit

Permalink
fix(laravel): jsonapi query parameters (page, sort, fields and include)
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Dec 17, 2024
1 parent aaff5c5 commit fc50f78
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 57 deletions.
54 changes: 54 additions & 0 deletions src/JsonApi/Filter/SparseFieldset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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\PropertiesFilterInterface;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\OpenApi\Model\Parameter;

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

public function getOpenApiParameters(MetadataParameter $parameter): Parameter|array|null
{
$example = \sprintf(
'%1$s[]={propertyName}&%1$s[]={anotherPropertyName}',
$parameter->getKey()
);

return new Parameter(
name: $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: '.$example
);
}

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);
}
}
23 changes: 20 additions & 3 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 @@ -84,11 +86,11 @@
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;
use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyMetadataFactory;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyNameCollectionMetadataFactory;
Expand All @@ -108,6 +110,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 +426,15 @@ public function register(): void

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

$this->app->tag([EqualsFilter::class, PartialSearchFilter::class, DateFilter::class, OrderFilter::class, RangeFilter::class, SortFilter::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 +481,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 @@ -482,7 +499,7 @@ public function register(): void
$this->app->singleton(SortFilterParameterProvider::class, function (Application $app) {
return new SortFilterParameterProvider();
});
$this->app->tag([SerializerFilterParameterProvider::class, SortFilterParameterProvider::class], ParameterProviderInterface::class);
$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
31 changes: 14 additions & 17 deletions src/Laravel/Eloquent/Filter/JsonApi/SortFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
namespace ApiPlatform\Laravel\Eloquent\Filter\JsonApi;

use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface;
use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider;
use ApiPlatform\Laravel\Eloquent\Filter\QueryPropertyTrait;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
Expand All @@ -25,29 +23,28 @@

final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, ParameterProviderFilterInterface, PropertiesFilterInterface
{
use QueryPropertyTrait;
public const ASC = 'asc';
public const DESC = 'desc';

/**
* @param Builder<Model> $builder
* @param array<string, mixed> $context
*/
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
{
dd('hi');
// if (!\is_string($values)) {
// $properties = $parameter->getExtraProperties()['_properties'] ?? [];
//
// foreach ($values as $key => $value) {
// if (!isset($properties[$key])) {
// continue;
// }
// $builder = $builder->orderBy($properties[$key], $value);
// }
//
// return $builder;
// }
if (!\is_array($values)) {
return $builder;
}

return $builder->orderBy($this->getQueryProperty($parameter), $values);
foreach ($values as $order => $dir) {
if (self::ASC !== $dir && self::DESC !== $dir) {
continue;
}

$builder->orderBy($order, $dir);
}

return $builder;
}

/**
Expand Down
50 changes: 25 additions & 25 deletions src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,40 @@

namespace ApiPlatform\Laravel\Eloquent\Filter\JsonApi;

use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\State\ParameterProviderInterface;
use Psr\Log\LoggerInterface;

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

$properties = $parameter->getExtraProperties()['_properties'] ?? [];
$value = $parameter->getValue();
$filter = $parameter->getFilter();
//
// if(!$value || !$filter instanceof IriSearchFilter){
// $this->logger->debug('No value or incompatible filter found.');
//
// return $operation;
// }
//
// try {
// $resource = $this->iriConverter->getResourceFromIri($value);
//
// $operation = $operation->withExtraProperties(array_merge(
// $operation->getExtraProperties() ?? [],
// ['resource' => $resource]
// ));
//
// } catch (\Exception $e) {
// $this->logger->error(sprintf('Invalid IRI "%s": %s', $value, $e->getMessage()));
// return null;
// }
//
if (!\is_string($value)) {
return $operation;
}

$values = explode(',', $value);
$orderBy = [];
foreach ($values as $v) {
$dir = SortFilter::ASC;
if (str_starts_with($v, '-')) {
$dir = SortFilter::DESC;
$v = substr($v, 1);
}

if (\array_key_exists($v, $properties)) {
$orderBy[$properties[$v]] = $dir;
}
}

$parameter->setValue($orderBy);

return $operation;
}
}
66 changes: 66 additions & 0 deletions src/Laravel/JsonApi/State/JsonApiProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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\Laravel\JsonApi\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;

/**
* This is a copy of ApiPlatform\JsonApi\State\JsonApiProvider without the support of sort,filter and fields as these should be implemented using QueryParameters and specific Filters.
* At some point we want to merge both classes but for now we don't have the SortFilter inside Symfony.
*
* @internal
*/
final class JsonApiProvider implements ProviderInterface
{
public function __construct(private readonly ProviderInterface $decorated)
{
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$request = $context['request'] ?? null;

if (!$request || 'jsonapi' !== $request->getRequestFormat()) {
return $this->decorated->provide($operation, $uriVariables, $context);
}

$filters = $request->attributes->get('_api_filters', []);
$queryParameters = $request->query->all();

$pageParameter = $queryParameters['page'] ?? null;
if (
\is_array($pageParameter)
) {
$filters = array_merge($pageParameter, $filters);
}

if (isset($pageParameter['offset'])) {
$filters['page'] = $pageParameter['offset'];
unset($filters['offset']);
}

$includeParameter = $queryParameters['include'] ?? null;

if ($includeParameter) {
$request->attributes->set('_api_included', explode(',', $includeParameter));
}

if ($filters) {
$request->attributes->set('_api_filters', $filters);
}

return $this->decorated->provide($operation, $uriVariables, $context);
}
}
Loading

0 comments on commit fc50f78

Please sign in to comment.