Skip to content

Commit c9f18d4

Browse files
soyukaNathan
andauthored
feat(laravel): eloquent filters (search, date, equals, or) (#6593)
* feat(laravel): Eloquent filters search date or * feat(laravel): eloquent filters order range equals afterdate * fix(laravel): order afterDate filters * temp * test(laravel): filter with eloquent --------- Co-authored-by: Nathan <[email protected]>
1 parent cfe61b3 commit c9f18d4

25 files changed

+571
-54
lines changed

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function __construct(
4646
private readonly NormalizerInterface $collectionNormalizer,
4747
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
4848
private readonly ResourceClassResolverInterface $resourceClassResolver,
49-
ContainerInterface $filterLocator,
49+
?ContainerInterface $filterLocator = null,
5050
private readonly array $defaultContext = [],
5151
) {
5252
$this->filterLocator = $filterLocator;

src/Laravel/ApiPlatformProvider.php

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
use ApiPlatform\GraphQl\Type\TypesFactory;
4747
use ApiPlatform\GraphQl\Type\TypesFactoryInterface;
4848
use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory;
49+
use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer as HydraFiltersCollectionNormalizer;
4950
use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer;
5051
use ApiPlatform\Hydra\Serializer\DocumentationNormalizer as HydraDocumentationNormalizer;
5152
use ApiPlatform\Hydra\Serializer\EntrypointNormalizer as HydraEntrypointNormalizer;
@@ -73,8 +74,11 @@
7374
use ApiPlatform\Laravel\Controller\ApiPlatformController;
7475
use ApiPlatform\Laravel\Eloquent\Extension\FilterQueryExtension;
7576
use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface;
77+
use ApiPlatform\Laravel\Eloquent\Filter\DateFilter;
78+
use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter;
7679
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
77-
use ApiPlatform\Laravel\Eloquent\Filter\SearchFilter;
80+
use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
81+
use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter;
7882
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyMetadataFactory;
7983
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory;
8084
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyNameCollectionMetadataFactory;
@@ -106,7 +110,6 @@
106110
use ApiPlatform\Laravel\State\SwaggerUiProvider;
107111
use ApiPlatform\Laravel\State\ValidateProvider;
108112
use ApiPlatform\Metadata\Exception\NotExposedHttpException;
109-
use ApiPlatform\Metadata\FilterInterface;
110113
use ApiPlatform\Metadata\IdentifiersExtractor;
111114
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
112115
use ApiPlatform\Metadata\InflectorInterface;
@@ -195,6 +198,7 @@
195198
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
196199
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
197200
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
201+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
198202
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
199203
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
200204
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
@@ -322,6 +326,7 @@ public function register(): void
322326
new EloquentResourceCollectionMetadataFactory(
323327
new ParameterResourceMetadataCollectionFactory(
324328
$this->app->make(PropertyNameCollectionFactoryInterface::class),
329+
$this->app->make(PropertyMetadataFactoryInterface::class),
325330
new AlternateUriResourceMetadataCollectionFactory(
326331
new FiltersResourceMetadataCollectionFactory(
327332
new FormatsResourceMetadataCollectionFactory(
@@ -335,8 +340,8 @@ public function register(): void
335340
$app->make(PathSegmentNameGeneratorInterface::class),
336341
new NotExposedOperationResourceMetadataCollectionFactory(
337342
$app->make(LinkFactoryInterface::class),
338-
new ConcernsResourceMetadataCollectionFactory(
339-
new AttributesResourceMetadataCollectionFactory(
343+
new AttributesResourceMetadataCollectionFactory(
344+
new ConcernsResourceMetadataCollectionFactory(
340345
null,
341346
$app->make(LoggerInterface::class),
342347
[
@@ -349,7 +354,7 @@ public function register(): void
349354
'routePrefix' => $config->get('api-platform.routes.prefix') ?? '/',
350355
],
351356
$config->get('api-platform.graphql.enabled'),
352-
)
357+
),
353358
)
354359
)
355360
)
@@ -361,7 +366,8 @@ public function register(): void
361366
)
362367
)
363368
),
364-
$app->make(FilterInterface::class)
369+
$app->make('filters'),
370+
$app->make(CamelCaseToSnakeCaseNameConverter::class)
365371
)
366372
),
367373
true === $config->get('app.debug') ? 'array' : 'file'
@@ -378,13 +384,7 @@ public function register(): void
378384

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

381-
$this->app->tag([SearchFilter::class], EloquentFilterInterface::class);
382-
$this->app->tag([SearchFilter::class, PropertyFilter::class], FilterInterface::class);
383-
$this->app->singleton(FilterInterface::class, function (Application $app) {
384-
$tagged = iterator_to_array($app->tagged(FilterInterface::class));
385-
386-
return new ServiceLocator($tagged);
387-
});
387+
$this->app->tag([EqualsFilter::class, PartialSearchFilter::class, DateFilter::class, OrderFilter::class], EloquentFilterInterface::class);
388388

389389
$this->app->bind(FilterQueryExtension::class, function (Application $app) {
390390
$tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class));
@@ -445,6 +445,13 @@ public function register(): void
445445

446446
$this->app->tag([SerializerFilterParameterProvider::class], ParameterProviderInterface::class);
447447

448+
$this->app->singleton('filters', function (Application $app) {
449+
return new ServiceLocator(array_merge(
450+
iterator_to_array($app->tagged(SerializerFilterInterface::class)),
451+
iterator_to_array($app->tagged(EloquentFilterInterface::class))
452+
));
453+
});
454+
448455
$this->app->singleton(ParameterProvider::class, function (Application $app) {
449456
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));
450457

@@ -673,7 +680,7 @@ public function register(): void
673680
$app->make(PropertyNameCollectionFactoryInterface::class),
674681
$app->make(PropertyMetadataFactoryInterface::class),
675682
$app->make(SchemaFactoryInterface::class),
676-
$app->make(FilterInterface::class),
683+
null,
677684
$config->get('api-platform.formats'),
678685
null, // ?Options $openApiOptions = null,
679686
$app->make(PaginationOptions::class), // ?PaginationOptions $paginationOptions = null,
@@ -737,12 +744,16 @@ public function register(): void
737744

738745
$this->app->singleton(HydraPartialCollectionViewNormalizer::class, function (Application $app) use ($defaultContext) {
739746
return new HydraPartialCollectionViewNormalizer(
740-
new HydraCollectionNormalizer(
741-
$app->make(ContextBuilderInterface::class),
742-
$app->make(ResourceClassResolverInterface::class),
743-
$app->make(IriConverterInterface::class),
747+
new HydraFiltersCollectionNormalizer(
748+
new HydraCollectionNormalizer(
749+
$app->make(ContextBuilderInterface::class),
750+
$app->make(ResourceClassResolverInterface::class),
751+
$app->make(IriConverterInterface::class),
752+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
753+
$defaultContext
754+
),
744755
$app->make(ResourceMetadataCollectionFactoryInterface::class),
745-
$defaultContext
756+
$app->make(ResourceClassResolverInterface::class),
746757
),
747758
'page',
748759
'pagination',

src/Laravel/Eloquent/Extension/FilterQueryExtension.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ public function apply(Builder $builder, array $uriVariables, Operation $operatio
4848
continue;
4949
}
5050

51-
$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
51+
$filter = $filterId instanceof FilterInterface ? $filterId : ($this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null);
5252
if ($filter instanceof FilterInterface) {
53-
$builder = $filter->apply($builder, $values, $parameter, $context);
53+
$builder = $filter->apply($builder, $values, $parameter, $context + ($parameter->getFilterContext() ?? []));
5454
}
5555
}
5656

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Laravel\Eloquent\Filter;
15+
16+
use ApiPlatform\Metadata\HasSchemaFilterInterface;
17+
use ApiPlatform\Metadata\Parameter;
18+
use Illuminate\Database\Eloquent\Builder;
19+
use Illuminate\Database\Eloquent\Model;
20+
21+
final class DateFilter implements FilterInterface, HasSchemaFilterInterface
22+
{
23+
use QueryPropertyTrait;
24+
25+
/**
26+
* @param Builder<Model> $builder
27+
* @param array<string, mixed> $context
28+
*/
29+
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
30+
{
31+
if (!\is_string($values)) {
32+
return $builder;
33+
}
34+
35+
$datetime = new \DateTimeImmutable($values);
36+
37+
return $builder->{($context['whereClause'] ?? 'where').'Date'}($this->getQueryProperty($parameter), $datetime);
38+
}
39+
40+
/**
41+
* @return array<string, mixed>
42+
*/
43+
public function getSchema(Parameter $parameter): array
44+
{
45+
return ['type' => 'date'];
46+
}
47+
}

src/Laravel/Eloquent/Filter/SearchFilter.php renamed to src/Laravel/Eloquent/Filter/EndSearchFilter.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,16 @@
1717
use Illuminate\Database\Eloquent\Builder;
1818
use Illuminate\Database\Eloquent\Model;
1919

20-
final class SearchFilter implements FilterInterface
20+
final class EndSearchFilter implements FilterInterface
2121
{
22+
use QueryPropertyTrait;
23+
2224
/**
2325
* @param Builder<Model> $builder
2426
* @param array<string, mixed> $context
2527
*/
2628
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
2729
{
28-
return $builder->where($parameter->getProperty(), $values);
29-
}
30-
31-
public function getDescription(string $resourceClass): array
32-
{
33-
return [];
30+
return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), 'like', '%'.$values);
3431
}
3532
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\Laravel\Eloquent\Filter;
15+
16+
use ApiPlatform\Metadata\Parameter;
17+
use Illuminate\Database\Eloquent\Builder;
18+
use Illuminate\Database\Eloquent\Model;
19+
20+
final class EqualsFilter implements FilterInterface
21+
{
22+
use QueryPropertyTrait;
23+
24+
/**
25+
* @param Builder<Model> $builder
26+
* @param array<string, mixed> $context
27+
*/
28+
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
29+
{
30+
return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), $values);
31+
}
32+
}

src/Laravel/Eloquent/Filter/FilterInterface.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313

1414
namespace ApiPlatform\Laravel\Eloquent\Filter;
1515

16-
use ApiPlatform\Metadata\FilterInterface as MetadataFilterInterface;
1716
use ApiPlatform\Metadata\Parameter;
1817
use Illuminate\Database\Eloquent\Builder;
1918
use Illuminate\Database\Eloquent\Model;
2019

21-
interface FilterInterface extends MetadataFilterInterface
20+
interface FilterInterface
2221
{
2322
/**
2423
* @param Builder<Model> $builder
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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\Laravel\Eloquent\Filter;
15+
16+
use ApiPlatform\Metadata\HasOpenApiParameterFilterInterface;
17+
use ApiPlatform\Metadata\HasSchemaFilterInterface;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
20+
use Illuminate\Database\Eloquent\Builder;
21+
use Illuminate\Database\Eloquent\Model;
22+
23+
final readonly class OrFilter implements FilterInterface, HasSchemaFilterInterface, HasOpenApiParameterFilterInterface
24+
{
25+
public function __construct(private FilterInterface $filter)
26+
{
27+
}
28+
29+
/**
30+
* @param Builder<Model> $builder
31+
* @param array<string, mixed> $context
32+
*/
33+
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
34+
{
35+
return $builder->where(function ($builder) use ($values, $parameter, $context): void {
36+
foreach ($values as $value) {
37+
$this->filter->apply($builder, $value, $parameter, ['whereClause' => 'orWhere'] + $context);
38+
}
39+
});
40+
}
41+
42+
/**
43+
* @return array<string, mixed>
44+
*/
45+
public function getSchema(Parameter $parameter): array
46+
{
47+
$schema = $this->filter instanceof HasSchemaFilterInterface ? $this->filter->getSchema($parameter) : ['type' => 'string'];
48+
49+
return ['type' => 'array', 'items' => $schema];
50+
}
51+
52+
public function getOpenApiParameter(Parameter $parameter): ?OpenApiParameter
53+
{
54+
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
55+
}
56+
}

0 commit comments

Comments
 (0)