Skip to content

Commit fd50f8b

Browse files
NathanPesneauNathansoyuka
authored
feat(laravel): eloquent filters date range (#6606)
* feat(laravel): eloquent filters date range * cs * parameter * fix(laravel): eloquent test * fix test * fix(laravel): corrections * fix(larevel): eloquent test filters * fix(laravel): date filter const * fix(laravel): range filter const --------- Co-authored-by: Nathan <[email protected]> Co-authored-by: soyuka <[email protected]>
1 parent 0f89f3f commit fd50f8b

File tree

14 files changed

+495
-63
lines changed

14 files changed

+495
-63
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
8080
use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
8181
use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter;
82+
use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter;
8283
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyMetadataFactory;
8384
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory;
8485
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyNameCollectionMetadataFactory;
@@ -389,7 +390,7 @@ public function register(): void
389390

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

392-
$this->app->tag([EqualsFilter::class, PartialSearchFilter::class, DateFilter::class, OrderFilter::class], EloquentFilterInterface::class);
393+
$this->app->tag([EqualsFilter::class, PartialSearchFilter::class, DateFilter::class, OrderFilter::class, RangeFilter::class], EloquentFilterInterface::class);
393394

394395
$this->app->bind(FilterQueryExtension::class, function (Application $app) {
395396
$tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class));

src/Laravel/Eloquent/Filter/DateFilter.php

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,67 @@
1313

1414
namespace ApiPlatform\Laravel\Eloquent\Filter;
1515

16-
use ApiPlatform\Metadata\HasSchemaFilterInterface;
16+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
1718
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\Metadata\QueryParameter;
20+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
1821
use Illuminate\Database\Eloquent\Builder;
1922
use Illuminate\Database\Eloquent\Model;
2023

21-
final class DateFilter implements FilterInterface, HasSchemaFilterInterface
24+
final class DateFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
2225
{
2326
use QueryPropertyTrait;
2427

28+
private const OPERATOR_VALUE = [
29+
'eq' => '=',
30+
'gt' => '>',
31+
'lt' => '<',
32+
'gte' => '>=',
33+
'lte' => '<=',
34+
];
35+
2536
/**
2637
* @param Builder<Model> $builder
2738
* @param array<string, mixed> $context
2839
*/
2940
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
3041
{
31-
if (!\is_string($values)) {
42+
if (!\is_array($values)) {
43+
return $builder;
44+
}
45+
46+
$values = array_intersect_key($values, self::OPERATOR_VALUE);
47+
48+
if (!$values) {
49+
return $builder;
50+
}
51+
52+
if (true === ($parameter->getFilterContext()['include_nulls'] ?? false)) {
53+
foreach ($values as $key => $value) {
54+
$datetime = $this->getDateTime($value);
55+
if (null === $datetime) {
56+
continue;
57+
}
58+
$builder->{$context['whereClause'] ?? 'where'}(function (Builder $query) use ($parameter, $datetime, $key): void {
59+
$queryProperty = $this->getQueryProperty($parameter);
60+
$query->whereDate($queryProperty, self::OPERATOR_VALUE[$key], $datetime)
61+
->orWhereNull($queryProperty);
62+
});
63+
}
64+
3265
return $builder;
3366
}
3467

35-
$datetime = new \DateTimeImmutable($values);
68+
foreach ($values as $key => $value) {
69+
$datetime = $this->getDateTime($value);
70+
if (null === $datetime) {
71+
continue;
72+
}
73+
$builder = $builder->{($context['whereClause'] ?? 'where').'Date'}($this->getQueryProperty($parameter), self::OPERATOR_VALUE[$key], $datetime);
74+
}
3675

37-
return $builder->{($context['whereClause'] ?? 'where').'Date'}($this->getQueryProperty($parameter), $datetime);
76+
return $builder;
3877
}
3978

4079
/**
@@ -44,4 +83,27 @@ public function getSchema(Parameter $parameter): array
4483
{
4584
return ['type' => 'date'];
4685
}
86+
87+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
88+
{
89+
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
90+
$key = $parameter->getKey();
91+
92+
return [
93+
new OpenApiParameter(name: $key.'[eq]', in: $in),
94+
new OpenApiParameter(name: $key.'[gt]', in: $in),
95+
new OpenApiParameter(name: $key.'[lt]', in: $in),
96+
new OpenApiParameter(name: $key.'[gte]', in: $in),
97+
new OpenApiParameter(name: $key.'[lte]', in: $in),
98+
];
99+
}
100+
101+
private function getDateTime(string $value): ?\DateTimeImmutable
102+
{
103+
try {
104+
return new \DateTimeImmutable($value);
105+
} catch (\DateMalformedStringException|\Exception) {
106+
return null;
107+
}
108+
}
47109
}

src/Laravel/Eloquent/Filter/OrFilter.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313

1414
namespace ApiPlatform\Laravel\Eloquent\Filter;
1515

16-
use ApiPlatform\Metadata\HasOpenApiParameterFilterInterface;
17-
use ApiPlatform\Metadata\HasSchemaFilterInterface;
16+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
1818
use ApiPlatform\Metadata\Parameter;
1919
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
2020
use Illuminate\Database\Eloquent\Builder;
2121
use Illuminate\Database\Eloquent\Model;
2222

23-
final readonly class OrFilter implements FilterInterface, HasSchemaFilterInterface, HasOpenApiParameterFilterInterface
23+
final readonly class OrFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
2424
{
2525
public function __construct(private FilterInterface $filter)
2626
{
@@ -44,12 +44,12 @@ public function apply(Builder $builder, mixed $values, Parameter $parameter, arr
4444
*/
4545
public function getSchema(Parameter $parameter): array
4646
{
47-
$schema = $this->filter instanceof HasSchemaFilterInterface ? $this->filter->getSchema($parameter) : ['type' => 'string'];
47+
$schema = $this->filter instanceof JsonSchemaFilterInterface ? $this->filter->getSchema($parameter) : ['type' => 'string'];
4848

4949
return ['type' => 'array', 'items' => $schema];
5050
}
5151

52-
public function getOpenApiParameter(Parameter $parameter): ?OpenApiParameter
52+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
5353
{
5454
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
5555
}

src/Laravel/Eloquent/Filter/OrderFilter.php

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313

1414
namespace ApiPlatform\Laravel\Eloquent\Filter;
1515

16-
use ApiPlatform\Metadata\HasOpenApiParameterFilterInterface;
17-
use ApiPlatform\Metadata\HasSchemaFilterInterface;
16+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
1818
use ApiPlatform\Metadata\Parameter;
1919
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
2020
use Illuminate\Database\Eloquent\Builder;
2121
use Illuminate\Database\Eloquent\Model;
2222

23-
final class OrderFilter implements FilterInterface, HasSchemaFilterInterface, HasOpenApiParameterFilterInterface
23+
final class OrderFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
2424
{
2525
use QueryPropertyTrait;
2626

@@ -37,7 +37,6 @@ public function apply(Builder $builder, mixed $values, Parameter $parameter, arr
3737
if (!isset($properties[$key])) {
3838
continue;
3939
}
40-
4140
$builder = $builder->orderBy($properties[$key], $value);
4241
}
4342

@@ -52,22 +51,19 @@ public function apply(Builder $builder, mixed $values, Parameter $parameter, arr
5251
*/
5352
public function getSchema(Parameter $parameter): array
5453
{
55-
if (str_contains($parameter->getKey(), ':property')) {
56-
$properties = [];
57-
foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) {
58-
$properties[$property] = ['type' => 'string', 'enum' => ['asc', 'desc']];
59-
}
60-
61-
return ['type' => 'object', 'properties' => $properties, 'required' => []];
62-
}
63-
6454
return ['type' => 'string', 'enum' => ['asc', 'desc']];
6555
}
6656

67-
public function getOpenApiParameter(Parameter $parameter): ?OpenApiParameter
57+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
6858
{
6959
if (str_contains($parameter->getKey(), ':property')) {
70-
return new OpenApiParameter(name: str_replace('[:property]', '', $parameter->getKey()), in: 'query', style: 'deepObject', explode: true);
60+
$parameters = [];
61+
$key = str_replace('[:property]', '', $parameter->getKey());
62+
foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) {
63+
$parameters[] = new OpenApiParameter(name: \sprintf('%s[%s]', $key, $property), in: 'query');
64+
}
65+
66+
return $parameters;
7167
}
7268

7369
return null;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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\JsonSchemaFilterInterface;
17+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\Metadata\QueryParameter;
20+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
21+
use Illuminate\Database\Eloquent\Builder;
22+
use Illuminate\Database\Eloquent\Model;
23+
24+
final class RangeFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
25+
{
26+
use QueryPropertyTrait;
27+
28+
private const OPERATOR_VALUE = [
29+
'lt' => '<',
30+
'gt' => '>',
31+
'lte' => '<=',
32+
'gte' => '>=',
33+
];
34+
35+
/**
36+
* @param Builder<Model> $builder
37+
* @param array<string, mixed> $context
38+
*/
39+
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
40+
{
41+
$queryProperty = $this->getQueryProperty($parameter);
42+
43+
foreach ($values as $key => $value) {
44+
$builder = $builder->{$context['whereClause'] ?? 'where'}($queryProperty, self::OPERATOR_VALUE[$key], $value);
45+
}
46+
47+
return $builder;
48+
}
49+
50+
public function getSchema(Parameter $parameter): array
51+
{
52+
return ['type' => 'number'];
53+
}
54+
55+
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
56+
{
57+
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
58+
$key = $parameter->getKey();
59+
60+
return [
61+
new OpenApiParameter(name: $key.'[gt]', in: $in),
62+
new OpenApiParameter(name: $key.'[lt]', in: $in),
63+
new OpenApiParameter(name: $key.'[gte]', in: $in),
64+
new OpenApiParameter(name: $key.'[lte]', in: $in),
65+
];
66+
}
67+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\Tests\Eloquent\Filter;
15+
16+
use ApiPlatform\Laravel\Eloquent\Filter\DateFilter;
17+
use ApiPlatform\Metadata\QueryParameter;
18+
use Illuminate\Database\Eloquent\Builder;
19+
use PHPUnit\Framework\TestCase;
20+
21+
class DateFilterTest extends TestCase
22+
{
23+
public function testOperator(): void
24+
{
25+
$f = new DateFilter();
26+
$builder = $this->createStub(Builder::class);
27+
$this->assertEquals($builder, $f->apply($builder, ['neq' => '2020-02-02'], new QueryParameter(key: 'date', property: 'date')));
28+
}
29+
}

0 commit comments

Comments
 (0)