Skip to content

Commit 8f0e9ce

Browse files
authored
Merge pull request #5 from ensi-platform/dev-range
Range filters
2 parents 2ac947a + 2c18102 commit 8f0e9ce

File tree

14 files changed

+278
-37
lines changed

14 files changed

+278
-37
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,12 @@ $this->allowedFilters([AllowedFilter::exact('name', 'name')]);
129129
Types of filters
130130

131131
```php
132-
AllowedFilter::exact('name', 'field'); // The field value is checked for equality to one of the specified
133-
AllowedFilter::exists('name', 'field'); // There is a check that the field is in the document and has a non-zero value.
132+
AllowedFilter::exact('name', 'field'); // The field value is checked for equality to one of the specified
133+
AllowedFilter::exists('name', 'field'); // There is a check that the field is in the document and has a non-zero value.
134+
AllowedFilter::greater('name', 'field'); // The field value must be greater than the specified one.
135+
AllowedFilter::greaterOrEqual('name', 'field'); // The field value must be greater than or equal to the specified one.
136+
AllowedFilter::less('name', 'field'); // The field value must be less than the specified one.
137+
AllowedFilter::lessOrEqual('name', 'field'); // The field value must be less than or equal to the specified one.
134138
```
135139

136140
The sorts available to the client are added by the `allowedSorts` method. The sorting direction is set in its name.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"ensi/laravel-elastic-query": "^0.3.0",
2121
"illuminate/contracts": "^8.37 || ^9.0",
2222
"illuminate/support": "^8.0 || ^9.0",
23-
"spatie/laravel-package-tools": "^1.4.3"
23+
"spatie/laravel-package-tools": "^1.4.3",
24+
"webmozart/assert": "^1.11"
2425
},
2526
"require-dev": {
2627
"friendsofphp/php-cs-fixer": "^3.2",

composer.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Exceptions/InvalidQueryException.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,12 @@ public static function notAllowedFacets(Collection $facets): self
3939

4040
return new self(Response::HTTP_BAD_REQUEST, $message);
4141
}
42+
43+
public static function notSupportMultipleValues(string $filter): self
44+
{
45+
return new self(
46+
Response::HTTP_BAD_REQUEST,
47+
"Filter \"{$filter}\" does not support multiple values"
48+
);
49+
}
4250
}

src/Filtering/AllowedFilter.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,26 @@ public static function exists(string $name, ?string $field = null): self
9797
return new static($name, new ExistsFilterAction(), $field);
9898
}
9999

100+
public static function greater(string $name, ?string $field = null): self
101+
{
102+
return new static($name, new RangeFilterAction('>'), $field);
103+
}
104+
105+
public static function greaterOrEqual(string $name, ?string $field = null): self
106+
{
107+
return new static($name, new RangeFilterAction('>='), $field);
108+
}
109+
110+
public static function less(string $name, ?string $field = null): self
111+
{
112+
return new static($name, new RangeFilterAction('<'), $field);
113+
}
114+
115+
public static function lessOrEqual(string $name, ?string $field = null): self
116+
{
117+
return new static($name, new RangeFilterAction('<='), $field);
118+
}
119+
100120
private function refineValue(mixed $value): mixed
101121
{
102122
if ($value === null) {

src/Filtering/ExactFilterAction.php

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,8 @@ class ExactFilterAction implements FilterAction
99
{
1010
public function __invoke(BoolQuery $query, mixed $value, string $field): void
1111
{
12-
$value = $this->normalizeValue($value);
13-
if ($value === null) {
14-
return;
15-
}
16-
17-
is_array($value)
18-
? $query->whereIn($field, $value)
19-
: $query->where($field, $value);
20-
}
21-
22-
private function normalizeValue(mixed $value): mixed
23-
{
24-
if (!is_array($value)) {
25-
return $value;
26-
}
27-
28-
$normalized = array_filter($value);
29-
if (!$normalized) {
30-
return null;
31-
}
32-
33-
return count($normalized) === 1 ? head($normalized) : array_values($normalized);
12+
FilterValue::make($value)
13+
->whenMultiple(fn (array $value) => $query->whereIn($field, $value))
14+
->whenSingle(fn (mixed $value) => $query->where($field, $value));
3415
}
3516
}

src/Filtering/ExistsFilterAction.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ class ExistsFilterAction implements FilterAction
99
{
1010
public function __invoke(BoolQuery $query, mixed $value, string $field): void
1111
{
12-
$value === true
13-
? $query->whereNotNull($field)
14-
: $query->whereNull($field);
12+
FilterValue::make($value)
13+
->whenSame(true, fn () => $query->whereNotNull($field))
14+
->orElse(fn () => $query->whereNull($field));
1515
}
1616
}

src/Filtering/FilterValue.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Ensi\LaravelElasticQuerySpecification\Filtering;
4+
5+
final class FilterValue
6+
{
7+
protected function __construct(private mixed $value, private bool $disposed)
8+
{
9+
}
10+
11+
public static function make(mixed $value): self
12+
{
13+
return new self(self::normalizeValue($value), false);
14+
}
15+
16+
private static function normalizeValue(mixed $value): mixed
17+
{
18+
if (!is_array($value)) {
19+
return $value;
20+
}
21+
22+
$normalized = array_filter($value);
23+
if (!$normalized) {
24+
return null;
25+
}
26+
27+
return count($normalized) === 1 ? head($normalized) : array_values($normalized);
28+
}
29+
30+
public function when(bool $condition, callable $callback): self
31+
{
32+
if ($this->disposed || !$condition) {
33+
return $this;
34+
}
35+
36+
$callback($this->value);
37+
38+
return new self($this->value, true);
39+
}
40+
41+
public function whenMultiple(callable $callback): self
42+
{
43+
return $this->when(is_array($this->value), $callback);
44+
}
45+
46+
public function whenSingle(callable $callback): self
47+
{
48+
return $this->when(!is_array($this->value), $callback);
49+
}
50+
51+
public function whenSame(mixed $sample, callable $callback): self
52+
{
53+
return $this->when($this->value === $sample, $callback);
54+
}
55+
56+
public function orElse(callable $callback): self
57+
{
58+
return $this->when(true, $callback);
59+
}
60+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Ensi\LaravelElasticQuerySpecification\Filtering;
4+
5+
use Ensi\LaravelElasticQuery\Contracts\BoolQuery;
6+
use Ensi\LaravelElasticQuerySpecification\Contracts\FilterAction;
7+
use Ensi\LaravelElasticQuerySpecification\Exceptions\InvalidQueryException;
8+
9+
class RangeFilterAction implements FilterAction
10+
{
11+
public function __construct(private string $operator)
12+
{
13+
}
14+
15+
public function __invoke(BoolQuery $query, mixed $value, string $field): void
16+
{
17+
FilterValue::make($value)
18+
->whenSingle(fn (mixed $value) => $query->where($field, $this->operator, $value))
19+
->whenMultiple(fn () => throw InvalidQueryException::notSupportMultipleValues($field));
20+
}
21+
}

tests/Integration/FacetingTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,28 @@
9191
->assertBucketKeys('tags', ['water', 'drinks', 'video'])
9292
->assertBucketKeys('seller_id', [10, 15, 20]);
9393
});
94+
95+
test('facet with multiple filters', function () {
96+
$spec = CompositeSpecification::new()
97+
->nested('offers', function (Specification $spec) {
98+
$spec
99+
->allowedFilters([
100+
AllowedFilter::greaterOrEqual('price__gte', 'price'),
101+
AllowedFilter::lessOrEqual('price__lte', 'price'),
102+
AllowedFilter::exact('seller_id'),
103+
])
104+
->allowedFacets([
105+
AllowedFacet::minmax('price', ['price__gte', 'price__lte']),
106+
AllowedFacet::terms('seller_id'),
107+
]);
108+
});
109+
110+
$request = [
111+
'facet' => ['price', 'seller_id'],
112+
'filter' => ['seller_id' => 20, 'price__gte' => 300, 'price__lte' => 20000],
113+
];
114+
115+
facetQuery($spec, $request)
116+
->assertBucketKeys('seller_id', [10, 15, 90])
117+
->assertMinMax('price', 200, 28990);
118+
});

0 commit comments

Comments
 (0)