Skip to content

Commit 5e1233c

Browse files
authored
feat(laravel): search filter (#6534)
1 parent 86365be commit 5e1233c

File tree

20 files changed

+389
-69
lines changed

20 files changed

+389
-69
lines changed

docs/guides/doctrine-search-filter.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,24 @@ public function testGetDocumentation(): void
120120
$this->assertJsonContains([
121121
'hydra:search' => [
122122
'@type' => 'hydra:IriTemplate',
123-
'hydra:template' => '/books.jsonld{?author,title}',
123+
'hydra:template' => '/books.jsonld{?id,title,author}',
124124
'hydra:variableRepresentation' => 'BasicRepresentation',
125125
'hydra:mapping' => [
126126
[
127127
'@type' => 'IriTemplateMapping',
128-
'variable' => 'author',
129-
'property' => 'author',
130-
'required' => false,
128+
'variable' => 'id',
129+
'property' => 'id',
131130
],
132131
[
133132
'@type' => 'IriTemplateMapping',
134133
'variable' => 'title',
135134
'property' => 'title',
136-
'required' => false,
137135
],
136+
[
137+
'@type' => 'IriTemplateMapping',
138+
'variable' => 'author',
139+
'property' => 'author',
140+
]
138141
],
139142
],
140143
]);

src/Laravel/ApiPlatformProvider.php

Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
4141
use ApiPlatform\Laravel\ApiResource\Error;
4242
use ApiPlatform\Laravel\Controller\ApiPlatformController;
43+
use ApiPlatform\Laravel\Eloquent\Extension\FilterQueryExtension;
44+
use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface;
45+
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
46+
use ApiPlatform\Laravel\Eloquent\Filter\SearchFilter;
4347
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyMetadataFactory;
4448
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyNameCollectionFactory;
4549
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory;
@@ -66,6 +70,7 @@
6670
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
6771
use ApiPlatform\Laravel\State\ValidateProvider;
6872
use ApiPlatform\Metadata\Exception\NotExposedHttpException;
73+
use ApiPlatform\Metadata\FilterInterface;
6974
use ApiPlatform\Metadata\IdentifiersExtractor;
7075
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
7176
use ApiPlatform\Metadata\IriConverterInterface;
@@ -90,6 +95,7 @@
9095
use ApiPlatform\Metadata\Resource\Factory\LinkResourceMetadataCollectionFactory;
9196
use ApiPlatform\Metadata\Resource\Factory\NotExposedOperationResourceMetadataCollectionFactory;
9297
use ApiPlatform\Metadata\Resource\Factory\OperationNameResourceMetadataCollectionFactory;
98+
use ApiPlatform\Metadata\Resource\Factory\ParameterResourceMetadataCollectionFactory;
9399
use ApiPlatform\Metadata\Resource\Factory\PhpDocResourceMetadataCollectionFactory;
94100
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
95101
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
@@ -102,21 +108,26 @@
102108
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
103109
use ApiPlatform\OpenApi\Options;
104110
use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer;
111+
use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface;
112+
use ApiPlatform\Serializer\Filter\PropertyFilter;
105113
use ApiPlatform\Serializer\ItemNormalizer;
106114
use ApiPlatform\Serializer\JsonEncoder;
107115
use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory as SerializerClassMetadataFactory;
116+
use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider;
108117
use ApiPlatform\Serializer\SerializerContextBuilder;
109118
use ApiPlatform\State\CallableProcessor;
110119
use ApiPlatform\State\CallableProvider;
111120
use ApiPlatform\State\Pagination\Pagination;
112121
use ApiPlatform\State\Pagination\PaginationOptions;
122+
use ApiPlatform\State\ParameterProviderInterface;
113123
use ApiPlatform\State\Processor\AddLinkHeaderProcessor;
114124
use ApiPlatform\State\Processor\RespondProcessor;
115125
use ApiPlatform\State\Processor\SerializeProcessor;
116126
use ApiPlatform\State\Processor\WriteProcessor;
117127
use ApiPlatform\State\ProcessorInterface;
118128
use ApiPlatform\State\Provider\ContentNegotiationProvider;
119129
use ApiPlatform\State\Provider\DeserializeProvider;
130+
use ApiPlatform\State\Provider\ParameterProvider;
120131
use ApiPlatform\State\Provider\ReadProvider;
121132
use ApiPlatform\State\ProviderInterface;
122133
use ApiPlatform\State\SerializerContextBuilderInterface;
@@ -247,38 +258,42 @@ public function register(): void
247258
// TODO: add cached metadata factories
248259
$this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) use ($config) {
249260
return new EloquentResourceCollectionMetadataFactory(
250-
new AlternateUriResourceMetadataCollectionFactory(
251-
new FiltersResourceMetadataCollectionFactory(
252-
new FormatsResourceMetadataCollectionFactory(
253-
new InputOutputResourceMetadataCollectionFactory(
254-
new PhpDocResourceMetadataCollectionFactory(
255-
new OperationNameResourceMetadataCollectionFactory(
256-
new LinkResourceMetadataCollectionFactory(
257-
$app->make(LinkFactoryInterface::class),
258-
new UriTemplateResourceMetadataCollectionFactory(
261+
new ParameterResourceMetadataCollectionFactory(
262+
$this->app->make(PropertyNameCollectionFactoryInterface::class),
263+
new AlternateUriResourceMetadataCollectionFactory(
264+
new FiltersResourceMetadataCollectionFactory(
265+
new FormatsResourceMetadataCollectionFactory(
266+
new InputOutputResourceMetadataCollectionFactory(
267+
new PhpDocResourceMetadataCollectionFactory(
268+
new OperationNameResourceMetadataCollectionFactory(
269+
new LinkResourceMetadataCollectionFactory(
259270
$app->make(LinkFactoryInterface::class),
260-
$app->make(PathSegmentNameGeneratorInterface::class),
261-
new NotExposedOperationResourceMetadataCollectionFactory(
271+
new UriTemplateResourceMetadataCollectionFactory(
262272
$app->make(LinkFactoryInterface::class),
263-
new AttributesResourceMetadataCollectionFactory(
264-
null,
265-
$app->make(LoggerInterface::class),
266-
[
267-
'routePrefix' => $config->get('api-platform.routes.prefix') ?? '/',
268-
],
269-
false
273+
$app->make(PathSegmentNameGeneratorInterface::class),
274+
new NotExposedOperationResourceMetadataCollectionFactory(
275+
$app->make(LinkFactoryInterface::class),
276+
new AttributesResourceMetadataCollectionFactory(
277+
null,
278+
$app->make(LoggerInterface::class),
279+
[
280+
'routePrefix' => $config->get('api-platform.routes.prefix') ?? '/',
281+
],
282+
false
283+
)
270284
)
271285
)
272286
)
273287
)
274288
)
275-
)
276-
),
277-
$config->get('api-platform.formats'),
278-
$config->get('api-platform.patch_formats'),
289+
),
290+
$config->get('api-platform.formats'),
291+
$config->get('api-platform.patch_formats'),
292+
)
279293
)
280-
)
281-
),
294+
),
295+
$app->make(FilterInterface::class)
296+
)
282297
);
283298
});
284299

@@ -292,6 +307,22 @@ public function register(): void
292307

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

310+
$this->app->tag([SearchFilter::class], EloquentFilterInterface::class);
311+
$this->app->tag([SearchFilter::class, PropertyFilter::class], FilterInterface::class);
312+
$this->app->singleton(FilterInterface::class, function (Application $app) {
313+
$tagged = iterator_to_array($app->tagged(FilterInterface::class));
314+
315+
return new ServiceLocator($tagged);
316+
});
317+
318+
$this->app->bind(FilterQueryExtension::class, function (Application $app) {
319+
$tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class));
320+
321+
return new FilterQueryExtension(new ServiceLocator($tagged));
322+
});
323+
324+
$this->app->tag([FilterQueryExtension::class], QueryExtensionInterface::class);
325+
295326
$this->app->singleton(ItemProvider::class, function (Application $app) {
296327
$tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class));
297328

@@ -300,7 +331,7 @@ public function register(): void
300331
$this->app->singleton(CollectionProvider::class, function (Application $app) {
301332
$tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class));
302333

303-
return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app), new ServiceLocator($tagged));
334+
return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged));
304335
});
305336
$this->app->tag([ItemProvider::class, CollectionProvider::class], ProviderInterface::class);
306337

@@ -326,8 +357,24 @@ public function register(): void
326357
return new DeserializeProvider($app->make(JsonApiProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class));
327358
});
328359

360+
$this->app->tag([PropertyFilter::class], SerializerFilterInterface::class);
361+
362+
$this->app->singleton(SerializerFilterParameterProvider::class, function (Application $app) {
363+
$tagged = iterator_to_array($app->tagged(SerializerFilterInterface::class));
364+
365+
return new SerializerFilterParameterProvider(new ServiceLocator($tagged));
366+
});
367+
368+
$this->app->tag([SerializerFilterParameterProvider::class], ParameterProviderInterface::class);
369+
370+
$this->app->singleton(ParameterProvider::class, function (Application $app) {
371+
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));
372+
373+
return new ParameterProvider($app->make(DeserializeProvider::class), new ServiceLocator($tagged));
374+
});
375+
329376
$this->app->singleton(AccessCheckerProvider::class, function (Application $app) {
330-
return new AccessCheckerProvider($app->make(DeserializeProvider::class), $app->make(ResourceAccessCheckerInterface::class));
377+
return new AccessCheckerProvider($app->make(ParameterProvider::class), $app->make(ResourceAccessCheckerInterface::class));
331378
});
332379

333380
$this->app->singleton(ContentNegotiationProvider::class, function (Application $app) use ($config) {
@@ -339,6 +386,7 @@ public function register(): void
339386
$this->app->tag([RemoveProcessor::class, PersistProcessor::class], ProcessorInterface::class);
340387
$this->app->singleton(CallableProcessor::class, function (Application $app) {
341388
$tagged = iterator_to_array($app->tagged(ProcessorInterface::class));
389+
// TODO: tag SwaggerUiProcessor instead?
342390
$tagged['api_platform.swagger_ui.processor'] = $app->make(SwaggerUiProcessor::class);
343391

344392
return new CallableProcessor(new ServiceLocator($tagged));
@@ -503,8 +551,6 @@ public function register(): void
503551
return new DocumentationAction($app->make(ResourceNameCollectionFactoryInterface::class), $config->get('api-platform.title') ?? '', $config->get('api-platform.description') ?? '', $config->get('api-platform.version') ?? '', $app->make(OpenApiFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), $app->make(Negotiator::class), $config->get('api-platform.docs_formats'));
504552
});
505553

506-
$this->app->singleton(FilterLocator::class, FilterLocator::class);
507-
508554
$this->app->singleton(EntrypointAction::class, function (Application $app) {
509555
return new EntrypointAction($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), ['jsonld' => ['application/ld+json']]);
510556
});
@@ -539,7 +585,7 @@ public function register(): void
539585
$app->make(PropertyNameCollectionFactoryInterface::class),
540586
$app->make(PropertyMetadataFactoryInterface::class),
541587
$app->make(SchemaFactoryInterface::class),
542-
$app->make(FilterLocator::class),
588+
$app->make(FilterInterface::class),
543589
$config->get('api-platform.formats'),
544590
null, // ?Options $openApiOptions = null,
545591
$app->make(PaginationOptions::class), // ?PaginationOptions $paginationOptions = null,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\Extension;
15+
16+
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\State\ParameterNotFound;
19+
use Illuminate\Database\Eloquent\Builder;
20+
use Illuminate\Database\Eloquent\Model;
21+
use Psr\Container\ContainerInterface;
22+
23+
final readonly class FilterQueryExtension implements QueryExtensionInterface
24+
{
25+
public function __construct(
26+
private ContainerInterface $filterLocator
27+
) {
28+
}
29+
30+
/**
31+
* @param Builder<Model> $builder
32+
* @param array<string, string> $uriVariables
33+
* @param array<string, mixed> $context
34+
*
35+
* @return Builder<Model>
36+
*/
37+
public function apply(Builder $builder, array $uriVariables, Operation $operation, $context = []): Builder
38+
{
39+
$context['uri_variables'] = $uriVariables;
40+
$context['operation'] = $operation;
41+
42+
foreach ($operation->getParameters() ?? [] as $parameter) {
43+
if (!($values = $parameter->getValue()) || $values instanceof ParameterNotFound) {
44+
continue;
45+
}
46+
47+
if (null === ($filterId = $parameter->getFilter())) {
48+
continue;
49+
}
50+
51+
$filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null;
52+
if ($filter instanceof FilterInterface) {
53+
$builder = $filter->apply($builder, $values, $parameter, $context);
54+
}
55+
}
56+
57+
return $builder;
58+
}
59+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\Extension;
15+
16+
use ApiPlatform\Metadata\Operation;
17+
use Illuminate\Database\Eloquent\Builder;
18+
use Illuminate\Database\Eloquent\Model;
19+
20+
interface QueryExtensionInterface
21+
{
22+
/**
23+
* @param Builder<Model> $builder
24+
* @param array<string, string> $uriVariables
25+
* @param array<string, mixed> $context
26+
*
27+
* @return Builder<Model>
28+
*/
29+
public function apply(Builder $builder, array $uriVariables, Operation $operation, $context = []): Builder;
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\FilterInterface as MetadataFilterInterface;
17+
use ApiPlatform\Metadata\Parameter;
18+
use Illuminate\Database\Eloquent\Builder;
19+
use Illuminate\Database\Eloquent\Model;
20+
21+
interface FilterInterface extends MetadataFilterInterface
22+
{
23+
/**
24+
* @param Builder<Model> $builder
25+
* @param array<string, mixed> $context
26+
*
27+
* @return Builder<Model>
28+
*/
29+
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder;
30+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 SearchFilter implements FilterInterface
21+
{
22+
/**
23+
* @param Builder<Model> $builder
24+
* @param array<string, mixed> $context
25+
*/
26+
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
27+
{
28+
return $builder->where($parameter->getProperty(), $values);
29+
}
30+
31+
public function getDescription(string $resourceClass): array
32+
{
33+
return [];
34+
}
35+
}

0 commit comments

Comments
 (0)