diff --git a/src/Builder.php b/src/Builder.php index c25ed9b9..3588cdbd 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -71,6 +71,13 @@ class Builder */ public $whereNotIns = []; + /** + * The "where" comparisons added to the query. + * + * @var array + */ + public $whereComparisons = []; + /** * The "limit" that should be applied to the search. * @@ -167,6 +174,21 @@ public function whereNotIn($field, array $values) return $this; } + /** + * Add a "where comparison" constraint to the search query. + * + * @param string $field + * @param string $operator + * @param mixed $value + * @return $this + */ + public function whereComparison($field, $operator, $value) + { + $this->whereComparisons[] = compact('field', 'operator', 'value'); + + return $this; + } + /** * Include soft deleted records in the results. * diff --git a/src/Engines/AlgoliaEngine.php b/src/Engines/AlgoliaEngine.php index cc2a3b8a..71d05423 100644 --- a/src/Engines/AlgoliaEngine.php +++ b/src/Engines/AlgoliaEngine.php @@ -174,6 +174,9 @@ protected function filters(Builder $builder) return collect($values)->map(function ($value) use ($key) { return $key.'='.$value; })->all(); + })->values()) + ->merge(collect($builder->whereComparisons)->map(function ($comparison) { + return $comparison['field'] . $comparison['operator'] . $comparison['value']; })->values())->values()->all(); } diff --git a/src/Engines/CollectionEngine.php b/src/Engines/CollectionEngine.php index a759e6c9..58ee408f 100644 --- a/src/Engines/CollectionEngine.php +++ b/src/Engines/CollectionEngine.php @@ -105,6 +105,11 @@ protected function searchModels(Builder $builder) $query->whereNotIn($key, $values); } }) + ->when(! $builder->callback && count($builder->whereComparisons) > 0, function ($query) use ($builder) { + foreach ($builder->whereComparisons as $comparison) { + $query->where($comparison['field'], $comparison['operator'], $comparison['value']); + } + }) ->when($builder->orders, function ($query) use ($builder) { foreach ($builder->orders as $order) { $query->orderBy($order['column'], $order['direction']); diff --git a/src/Engines/DatabaseEngine.php b/src/Engines/DatabaseEngine.php index 99813728..3fcdfc41 100644 --- a/src/Engines/DatabaseEngine.php +++ b/src/Engines/DatabaseEngine.php @@ -253,6 +253,10 @@ protected function addAdditionalConstraints(Builder $builder, $query) foreach ($builder->whereNotIns as $key => $values) { $query->whereNotIn($key, $values); } + })->when(! $builder->callback && count($builder->whereComparisons) > 0, function ($query) use ($builder) { + foreach ($builder->whereComparisons as $comparison) { + $query->where($comparison['field'], $comparison['operator'], $comparison['value']); + } })->when(! is_null($builder->queryCallback), function ($query) use ($builder) { call_user_func($builder->queryCallback, $query); }); diff --git a/src/Engines/MeilisearchEngine.php b/src/Engines/MeilisearchEngine.php index feacb2d0..191ecbec 100644 --- a/src/Engines/MeilisearchEngine.php +++ b/src/Engines/MeilisearchEngine.php @@ -177,13 +177,7 @@ protected function performSearch(Builder $builder, array $searchParams = []) protected function filters(Builder $builder) { $filters = collect($builder->wheres)->map(function ($value, $key) { - if (is_bool($value)) { - return sprintf('%s=%s', $key, $value ? 'true' : 'false'); - } - - return is_numeric($value) - ? sprintf('%s=%s', $key, $value) - : sprintf('%s="%s"', $key, $value); + return sprintf('%s=%s', $key, $this->formatValue($value)); }); $whereInOperators = [ @@ -207,9 +201,33 @@ protected function filters(Builder $builder) } } + collect($builder->whereComparisons)->each(function ($comparison) use ($filters) { + $filters->push(sprintf( + '%s%s%s', + $comparison['field'], + $comparison['operator'], + $this->formatValue($comparison['value'])) + ); + }); + return $filters->values()->implode(' AND '); } + /** + * Format the value for the filter depending on its type. + * + * @param mixed $value + * @return string + */ + protected function formatValue($value) + { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + return is_numeric($value) ? $value : sprintf('"%s"', $value); + } + /** * Get the sort array for the query. * diff --git a/src/Engines/TypesenseEngine.php b/src/Engines/TypesenseEngine.php index b55622a5..5bf3a98a 100644 --- a/src/Engines/TypesenseEngine.php +++ b/src/Engines/TypesenseEngine.php @@ -300,9 +300,15 @@ protected function filters(Builder $builder): string ->values() ->implode(' && '); - return $whereFilter.( - ($whereFilter !== '' && $whereInFilter !== '') ? ' && ' : '' - ).$whereInFilter; + $whereComparisonFilter = collect($builder->whereComparisons) + ->map(fn ($comparison) => sprintf('%s:%s%s', $comparison['field'], $comparison['operator'], $comparison['value'])) + ->values() + ->implode(' && '); + + return collect([$whereFilter, $whereInFilter, $whereComparisonFilter]) + ->filter() + ->values() + ->implode(' && '); } /** diff --git a/tests/Unit/AlgoliaEngineTest.php b/tests/Unit/AlgoliaEngineTest.php index 98c991b1..42bbb4cd 100644 --- a/tests/Unit/AlgoliaEngineTest.php +++ b/tests/Unit/AlgoliaEngineTest.php @@ -150,6 +150,25 @@ public function test_search_sends_correct_parameters_to_algolia_for_empty_where_ $engine->search($builder); } + public function test_search_sends_correct_parameters_to_algolia_for_where_comparison_search() + { + $client = m::mock(SearchClient::class); + $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('search')->with('zonda', [ + 'numericFilters' => ['foo=1', ['bar=1', 'bar=2'], 'baz>2', 'qux<=3'], + ]); + + $engine = new AlgoliaEngine($client); + $builder = new Builder(new SearchableModel, 'zonda'); + $builder + ->where('foo', 1) + ->whereIn('bar', [1, 2]) + ->whereComparison('baz', '>', 2) + ->whereComparison('qux', '<=', 3); + ; + $engine->search($builder); + } + public function test_map_correctly_maps_results_to_models() { $client = m::mock(SearchClient::class); diff --git a/tests/Unit/MeilisearchEngineTest.php b/tests/Unit/MeilisearchEngineTest.php index ff57c7f9..85dd9c51 100644 --- a/tests/Unit/MeilisearchEngineTest.php +++ b/tests/Unit/MeilisearchEngineTest.php @@ -569,6 +569,30 @@ public function test_where_not_in_conditions_are_applied() $engine->search($builder); } + public function test_where_comparison_conditions_are_applied() + { + $builder = new Builder(new SearchableModel(), ''); + $builder->where('foo', 'bar'); + $builder->where('bar', 'baz'); + $builder->whereIn('qux', [1, 2]); + $builder->whereIn('quux', [1, 2]); + $builder->whereNotIn('eaea', [3]); + $builder->whereComparison('gt', '>', 10); + $builder->whereComparison('lt', '<', 20); + $builder->whereComparison('gte', '>=', 30); + $builder->whereComparison('lte', '<=', 40); + + $client = m::mock(Client::class); + $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'foo="bar" AND bar="baz" AND qux IN [1, 2] AND quux IN [1, 2] AND eaea NOT IN [3] AND gt>10 AND lt<20 AND gte>=30 AND lte<=40', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine = new MeilisearchEngine($client); + $engine->search($builder); + } + public function test_where_in_conditions_are_applied_without_other_conditions() { $builder = new Builder(new SearchableModel(), ''); @@ -602,6 +626,25 @@ public function test_where_not_in_conditions_are_applied_without_other_condition $engine->search($builder); } + public function test_where_comparison_conditions_are_applied_without_other_conditions() + { + $builder = new Builder(new SearchableModel(), ''); + $builder->whereComparison('gt', '>', 10); + $builder->whereComparison('lt', '<', 20); + $builder->whereComparison('gte', '>=', 30); + $builder->whereComparison('lte', '<=', 40); + + $client = m::mock(Client::class); + $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'gt>10 AND lt<20 AND gte>=30 AND lte<=40', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine = new MeilisearchEngine($client); + $engine->search($builder); + } + public function test_empty_where_in_conditions_are_applied_correctly() { $builder = new Builder(new SearchableModel(), '');