Skip to content

Commit

Permalink
feat(laravel): eloquent filters date range (#6606)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Sep 11, 2024
1 parent 0f89f3f commit fd50f8b
Show file tree
Hide file tree
Showing 14 changed files with 495 additions and 63 deletions.
3 changes: 2 additions & 1 deletion src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter;
use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter;
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 Down Expand Up @@ -389,7 +390,7 @@ public function register(): void

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

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

$this->app->bind(FilterQueryExtension::class, function (Application $app) {
$tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class));
Expand Down
72 changes: 67 additions & 5 deletions src/Laravel/Eloquent/Filter/DateFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,67 @@

namespace ApiPlatform\Laravel\Eloquent\Filter;

use ApiPlatform\Metadata\HasSchemaFilterInterface;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

final class DateFilter implements FilterInterface, HasSchemaFilterInterface
final class DateFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use QueryPropertyTrait;

private const OPERATOR_VALUE = [
'eq' => '=',
'gt' => '>',
'lt' => '<',
'gte' => '>=',
'lte' => '<=',
];

/**
* @param Builder<Model> $builder
* @param array<string, mixed> $context
*/
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
{
if (!\is_string($values)) {
if (!\is_array($values)) {
return $builder;
}

$values = array_intersect_key($values, self::OPERATOR_VALUE);

if (!$values) {
return $builder;
}

if (true === ($parameter->getFilterContext()['include_nulls'] ?? false)) {
foreach ($values as $key => $value) {
$datetime = $this->getDateTime($value);
if (null === $datetime) {
continue;
}
$builder->{$context['whereClause'] ?? 'where'}(function (Builder $query) use ($parameter, $datetime, $key): void {
$queryProperty = $this->getQueryProperty($parameter);
$query->whereDate($queryProperty, self::OPERATOR_VALUE[$key], $datetime)
->orWhereNull($queryProperty);
});
}

return $builder;
}

$datetime = new \DateTimeImmutable($values);
foreach ($values as $key => $value) {
$datetime = $this->getDateTime($value);
if (null === $datetime) {
continue;
}
$builder = $builder->{($context['whereClause'] ?? 'where').'Date'}($this->getQueryProperty($parameter), self::OPERATOR_VALUE[$key], $datetime);
}

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

/**
Expand All @@ -44,4 +83,27 @@ public function getSchema(Parameter $parameter): array
{
return ['type' => 'date'];
}

public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
$key = $parameter->getKey();

return [
new OpenApiParameter(name: $key.'[eq]', in: $in),
new OpenApiParameter(name: $key.'[gt]', in: $in),
new OpenApiParameter(name: $key.'[lt]', in: $in),
new OpenApiParameter(name: $key.'[gte]', in: $in),
new OpenApiParameter(name: $key.'[lte]', in: $in),
];
}

private function getDateTime(string $value): ?\DateTimeImmutable
{
try {
return new \DateTimeImmutable($value);
} catch (\DateMalformedStringException|\Exception) {
return null;
}
}
}
10 changes: 5 additions & 5 deletions src/Laravel/Eloquent/Filter/OrFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@

namespace ApiPlatform\Laravel\Eloquent\Filter;

use ApiPlatform\Metadata\HasOpenApiParameterFilterInterface;
use ApiPlatform\Metadata\HasSchemaFilterInterface;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

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

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

public function getOpenApiParameter(Parameter $parameter): ?OpenApiParameter
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true);
}
Expand Down
26 changes: 11 additions & 15 deletions src/Laravel/Eloquent/Filter/OrderFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@

namespace ApiPlatform\Laravel\Eloquent\Filter;

use ApiPlatform\Metadata\HasOpenApiParameterFilterInterface;
use ApiPlatform\Metadata\HasSchemaFilterInterface;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

final class OrderFilter implements FilterInterface, HasSchemaFilterInterface, HasOpenApiParameterFilterInterface
final class OrderFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use QueryPropertyTrait;

Expand All @@ -37,7 +37,6 @@ public function apply(Builder $builder, mixed $values, Parameter $parameter, arr
if (!isset($properties[$key])) {
continue;
}

$builder = $builder->orderBy($properties[$key], $value);
}

Expand All @@ -52,22 +51,19 @@ public function apply(Builder $builder, mixed $values, Parameter $parameter, arr
*/
public function getSchema(Parameter $parameter): array
{
if (str_contains($parameter->getKey(), ':property')) {
$properties = [];
foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) {
$properties[$property] = ['type' => 'string', 'enum' => ['asc', 'desc']];
}

return ['type' => 'object', 'properties' => $properties, 'required' => []];
}

return ['type' => 'string', 'enum' => ['asc', 'desc']];
}

public function getOpenApiParameter(Parameter $parameter): ?OpenApiParameter
public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
if (str_contains($parameter->getKey(), ':property')) {
return new OpenApiParameter(name: str_replace('[:property]', '', $parameter->getKey()), in: 'query', style: 'deepObject', explode: true);
$parameters = [];
$key = str_replace('[:property]', '', $parameter->getKey());
foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) {
$parameters[] = new OpenApiParameter(name: \sprintf('%s[%s]', $key, $property), in: 'query');
}

return $parameters;
}

return null;
Expand Down
67 changes: 67 additions & 0 deletions src/Laravel/Eloquent/Filter/RangeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?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\Eloquent\Filter;

use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

final class RangeFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use QueryPropertyTrait;

private const OPERATOR_VALUE = [
'lt' => '<',
'gt' => '>',
'lte' => '<=',
'gte' => '>=',
];

/**
* @param Builder<Model> $builder
* @param array<string, mixed> $context
*/
public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder
{
$queryProperty = $this->getQueryProperty($parameter);

foreach ($values as $key => $value) {
$builder = $builder->{$context['whereClause'] ?? 'where'}($queryProperty, self::OPERATOR_VALUE[$key], $value);
}

return $builder;
}

public function getSchema(Parameter $parameter): array
{
return ['type' => 'number'];
}

public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null
{
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
$key = $parameter->getKey();

return [
new OpenApiParameter(name: $key.'[gt]', in: $in),
new OpenApiParameter(name: $key.'[lt]', in: $in),
new OpenApiParameter(name: $key.'[gte]', in: $in),
new OpenApiParameter(name: $key.'[lte]', in: $in),
];
}
}
29 changes: 29 additions & 0 deletions src/Laravel/Tests/Eloquent/Filter/DateFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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\Tests\Eloquent\Filter;

use ApiPlatform\Laravel\Eloquent\Filter\DateFilter;
use ApiPlatform\Metadata\QueryParameter;
use Illuminate\Database\Eloquent\Builder;
use PHPUnit\Framework\TestCase;

class DateFilterTest extends TestCase
{
public function testOperator(): void
{
$f = new DateFilter();
$builder = $this->createStub(Builder::class);
$this->assertEquals($builder, $f->apply($builder, ['neq' => '2020-02-02'], new QueryParameter(key: 'date', property: 'date')));
}
}
Loading

0 comments on commit fd50f8b

Please sign in to comment.