Skip to content

Commit 7f6b536

Browse files
authored
V2 (#16)
* Refactor + new features * Update README.md * Upgrade notes + removed old test * Docs + cleanup Co-authored-by: Pascal Baljet <pascal@protone.media>
1 parent 3d308b5 commit 7f6b536

File tree

4 files changed

+119
-78
lines changed

4 files changed

+119
-78
lines changed

README.md

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Total Downloads](https://img.shields.io/packagist/dt/protonemedia/laravel-cross-eloquent-search.svg?style=flat-square)](https://packagist.org/packages/protonemedia/laravel-cross-eloquent-search)
77
[![Buy us a tree](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen)](https://plant.treeware.earth/protonemedia/laravel-cross-eloquent-search)
88

9-
This Laravel package allows you to search through multiple Eloquent models. It supports sorting, pagination, scoped queries, eager load relationships and searching through single or multiple columns.
9+
This Laravel package allows you to search through multiple Eloquent models. It supports sorting, pagination, scoped queries, eager load relationships, and searching through single or multiple columns.
1010

1111
### 📺 Want to watch an implementation of this package? Rewatch the live stream (skip to 13:44 for the good stuff): [https://youtu.be/WigAaQsPgSA](https://youtu.be/WigAaQsPgSA)
1212

@@ -26,9 +26,9 @@ This Laravel package allows you to search through multiple Eloquent models. It s
2626
* In-database [sorting](https://laravel.com/docs/master/queries#ordering-grouping-limit-and-offset) of the combined result.
2727
* Zero third-party dependencies
2828

29-
## Blogpost
29+
## Blog post
3030

31-
If you want to know more about the background of this package, please read [the blogpost](https://protone.media/blog/search-through-multiple-eloquent-models-with-our-latest-laravel-package).
31+
If you want to know more about this package's background, please read [the blog post](https://protone.media/blog/search-through-multiple-eloquent-models-with-our-latest-laravel-package).
3232

3333
## Support
3434

@@ -42,9 +42,17 @@ You can install the package via composer:
4242
composer require protonemedia/laravel-cross-eloquent-search
4343
```
4444

45+
## Upgrading from v1
46+
47+
* The `startWithWildcard` method has been renamed to `beginWithWildcard`.
48+
* The default order column is now evaluated by the `getUpdatedAtColumn` method. Previously it was hard-coded to `updated_at`. You still can use [another column](#sorting) to order by.
49+
* The `allowEmptySearchQuery` method and `EmptySearchQueryException` class have been removed, but you can still [get results without searching](#getting-results-without-searching).
50+
4551
## Usage
4652

47-
Start your search query by adding one or more models to search through. Call the `add` method with the class name of the model and the column you want to search through. Then call the `get` method with the search term, and you'll get a `\Illuminate\Database\Eloquent\Collection` instance with the results. By default, the results are sorted in ascending order by the `updated_at` column.
53+
Start your search query by adding one or more models to search through. Call the `add` method with the model's class name and the column you want to search through. Then call the `get` method with the search term, and you'll get a `\Illuminate\Database\Eloquent\Collection` instance with the results.
54+
55+
The results are sorted in ascending order by the *updated* column by default. In most cases, this column is `updated_at`. If you've [customized](https://laravel.com/docs/master/eloquent#timestamps) your model's `UPDATED_AT` constant, or overwritten the `getUpdatedAtColumn` method, this package will use the customized column. Of course, you can [order by another column](#sorting) as well.
4856

4957
```php
5058
use ProtoneMedia\LaravelCrossEloquentSearch\Search;
@@ -81,25 +89,26 @@ Search::new()
8189
->get('howto');
8290
```
8391

84-
### Sorting
92+
### Wildcards
8593

86-
If you want to sort the results by another column, you can pass that column to the `add` method as a third parameter. Call the `orderByDesc` method to sort the results in descending order.
94+
By default, we split up the search term, and each keyword will get a wildcard symbol to do partial matching. Practically this means the search term `apple ios` will result in `apple%` and `ios%`. If you want a wildcard symbol to begin with as well, you can call the `beginWithWildcard` method. This will result in `%apple%` and `%ios%`.
8795

8896
```php
89-
Search::add(Post::class, 'title', 'publihed_at')
90-
->add(Video::class, 'title', 'released_at')
91-
->orderByDesc()
92-
->get('learn');
97+
Search::add(Post::class, 'title')
98+
->add(Video::class, 'title')
99+
->beginWithWildcard()
100+
->get('os');
93101
```
94102

95-
### Start search term with wildcard
103+
*Note: in previous versions of this package, this method was called `startWithWildcard()`.*
96104

97-
By default, we split up the search term, and each keyword will get a wildcard symbol to do partial matching. Practically this means the search term `apple ios` will result in `apple%` and `ios%`. If you want a wildcard symbol to start with as well, you can call the `startWithWildcard` method. This will result in `%apple%` and `%ios%`.
105+
If you want to disable the behaviour where a wildcard is appended to the terms, you should call the `endWithWildcard` method with `false`:
98106

99107
```php
100108
Search::add(Post::class, 'title')
101109
->add(Video::class, 'title')
102-
->startWithWildcard()
110+
->beginWithWildcard()
111+
->endWithWildcard(false)
103112
->get('os');
104113
```
105114

@@ -122,9 +131,20 @@ Search::add(Post::class, 'title')
122131
->get('macos big sur');
123132
```
124133

134+
### Sorting
135+
136+
If you want to sort the results by another column, you can pass that column to the `add` method as a third parameter. Call the `orderByDesc` method to sort the results in descending order.
137+
138+
```php
139+
Search::add(Post::class, 'title', 'publihed_at')
140+
->add(Video::class, 'title', 'released_at')
141+
->orderByDesc()
142+
->get('learn');
143+
```
144+
125145
### Pagination
126146

127-
We highly recommend to paginate your results. Call the `paginate` method before the `get` method, and you'll get an instance of `\Illuminate\Contracts\Pagination\LengthAwarePaginator` as a result. The `paginate` method takes three (optional) parameters to customize the paginator. These arguments are [the same](https://laravel.com/docs/master/pagination#introduction) as Laravel's database paginator.
147+
We highly recommend paginating your results. Call the `paginate` method before the `get` method, and you'll get an instance of `\Illuminate\Contracts\Pagination\LengthAwarePaginator` as a result. The `paginate` method takes three (optional) parameters to customize the paginator. These arguments are [the same](https://laravel.com/docs/master/pagination#introduction) as Laravel's database paginator.
128148

129149
```php
130150
Search::add(Post::class, 'title')
@@ -170,6 +190,17 @@ Search::add(Post::class, ['title', 'body'])
170190
->get('eloquent');
171191
```
172192

193+
### Sounds like
194+
195+
MySQL has a *soundex* algorithm built-in so you can search for terms that sound almost the same. You can use this feature by calling the `soundsLike` method:
196+
197+
```php
198+
Search::new()
199+
->add(Post::class, 'title')
200+
->add(Video::class, 'title')
201+
->soundsLike()
202+
->get('larafel');
203+
```
173204

174205
### Eager load relationships
175206

@@ -183,23 +214,13 @@ Search::add(Post::with('comments'), 'title')
183214

184215
### Getting results without searching
185216

186-
If you call the `get` method without a term or with an empty term, the package throws an `EmptySearchQueryException`. You can disable this behaviour with the `allowEmptySearchQuery` method.
187-
188-
```php
189-
Search::add(Post::with('comments'), 'title', 'published_at')
190-
->add(Video::with('likes'), 'title', 'released_at')
191-
->allowEmptySearchQuery()
192-
->get();
193-
```
194-
195-
In this case, you can discard the second argument as well. With the `orderBy` method, you can set the column to sort by (previously the third argument):
217+
You call the `get` method without a term or with an empty term. In this case, you can discard the second argument of the `add` method. With the `orderBy` method, you can set the column to sort by (previously the third argument):
196218

197219
```php
198220
Search::add(Post::class)
199221
->orderBy('published_at')
200222
->add(Video::class)
201223
->orderBy('released_at')
202-
->allowEmptySearchQuery()
203224
->get();
204225
```
205226

@@ -258,7 +279,7 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
258279

259280
### Security
260281

261-
If you discover any security related issues, please email pascal@protone.media instead of using the issue tracker.
282+
If you discover any security-related issues, please email pascal@protone.media instead of using the issue tracker.
262283

263284
## Credits
264285

@@ -271,4 +292,4 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio
271292

272293
## Treeware
273294

274-
This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/pascalbaljetmedia/laravel-cross-eloquent-search) to thank us for our work. By contributing to the Treeware forest youll be creating employment for local families and restoring wildlife habitats.
295+
This package is [Treeware](https://treeware.earth). If you use it in production, we ask that you [**buy the world a tree**](https://plant.treeware.earth/pascalbaljetmedia/laravel-cross-eloquent-search) to thank us for our work. By contributing to the Treeware forest, you'll create employment for local families and restoring wildlife habitats.

src/EmptySearchQueryException.php

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/Searcher.php

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,24 @@ class Searcher
2323
protected string $orderByDirection;
2424

2525
/**
26-
* Start the search term with a wildcard.
26+
* Begin the search term with a wildcard.
2727
*/
28-
protected bool $startWithWildcard = false;
28+
protected bool $beginWithWildcard = false;
2929

3030
/**
31-
* Allow an empty search query.
31+
* End the search term with a wildcard.
3232
*/
33-
protected bool $allowEmptySearchQuery = false;
33+
protected bool $endWithWildcard = true;
34+
35+
/**
36+
* Where operator.
37+
*/
38+
protected string $whereOperator = 'like';
39+
40+
/**
41+
* Use soundex to match the terms.
42+
*/
43+
protected bool $soundsLike = false;
3444

3545
/**
3646
* Collection of search terms.
@@ -108,16 +118,6 @@ public function dontParseTerm(): self
108118
return $this;
109119
}
110120

111-
/**
112-
* Allow empty search terms.
113-
*/
114-
public function allowEmptySearchQuery(): self
115-
{
116-
$this->allowEmptySearchQuery = true;
117-
118-
return $this;
119-
}
120-
121121
/**
122122
* Add a model to search through.
123123
*
@@ -126,12 +126,14 @@ public function allowEmptySearchQuery(): self
126126
* @param string $orderByColumn
127127
* @return self
128128
*/
129-
public function add($query, $columns = null, string $orderByColumn = 'updated_at'): self
129+
public function add($query, $columns = null, string $orderByColumn = null): self
130130
{
131+
$builder = is_string($query) ? $query::query() : $query;
132+
131133
$modelToSearchThrough = new ModelToSearchThrough(
132-
is_string($query) ? $query::query() : $query,
134+
$builder,
133135
Collection::wrap($columns),
134-
$orderByColumn,
136+
$orderByColumn ?: $builder->getModel()->getUpdatedAtColumn(),
135137
$this->modelsToSearchThrough->count()
136138
);
137139

@@ -187,13 +189,41 @@ public function orderBy(string $orderByColumn): self
187189
}
188190

189191
/**
190-
* Let's each search term start with a wildcard.
192+
* Let's each search term begin with a wildcard.
191193
*
194+
* @param boolean $state
192195
* @return self
193196
*/
194-
public function startWithWildcard(): self
197+
public function beginWithWildcard($state = true): self
195198
{
196-
$this->startWithWildcard = true;
199+
$this->beginWithWildcard = $state;
200+
201+
return $this;
202+
}
203+
204+
/**
205+
* Let's each search term end with a wildcard.
206+
*
207+
* @param boolean $state
208+
* @return self
209+
*/
210+
public function endWithWildcard($state = true): self
211+
{
212+
$this->endWithWildcard = $state;
213+
214+
return $this;
215+
}
216+
217+
/**
218+
* Use 'sounds like' operator instead of 'like'.
219+
*
220+
* @return self
221+
*/
222+
public function soundsLike($state = true): self
223+
{
224+
$this->soundsLike = $state;
225+
226+
$this->whereOperator = $state ? 'sounds like' : 'like';
197227

198228
return $this;
199229
}
@@ -263,11 +293,15 @@ protected function initializeTerms(string $terms): self
263293

264294
$this->terms = Collection::wrap($terms)
265295
->filter()
266-
->map(fn ($term) => ($this->startWithWildcard ? '%' : '') . "{$term}%");
267-
268-
if (!$this->allowEmptySearchQuery && $this->terms->isEmpty()) {
269-
throw new EmptySearchQueryException;
270-
}
296+
->unless($this->soundsLike, function ($terms) {
297+
return $terms->map(function ($term) {
298+
return implode([
299+
$this->beginWithWildcard ? '%' : '',
300+
$term,
301+
$this->endWithWildcard ? '%' : '',
302+
]);
303+
});
304+
});
271305

272306
return $this;
273307
}
@@ -285,7 +319,7 @@ public function addSearchQueryToBuilder(Builder $builder, ModelToSearchThrough $
285319
{
286320
$builder->where(function ($query) use ($modelToSearchThrough) {
287321
$modelToSearchThrough->getQualifiedColumns()->each(
288-
fn ($field) => $this->terms->each(fn ($term) => $query->orWhere($field, 'like', $term))
322+
fn ($field) => $this->terms->each(fn ($term) => $query->orWhere($field, $this->whereOperator, $term))
289323
);
290324
});
291325
}

tests/SearchTest.php

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
66
use Illuminate\Pagination\Paginator;
77
use Illuminate\Support\Carbon;
8-
use ProtoneMedia\LaravelCrossEloquentSearch\EmptySearchQueryException;
98
use ProtoneMedia\LaravelCrossEloquentSearch\Search;
109

1110
class SearchTest extends TestCase
@@ -123,18 +122,6 @@ public function it_has_a_method_to_parse_the_terms()
123122
$this->assertEquals(['0foo','1bar'], $array);
124123
}
125124

126-
/** @test */
127-
public function it_throws_an_exception_when_the_query_is_empty()
128-
{
129-
try {
130-
Search::get('');
131-
} catch (EmptySearchQueryException $exception) {
132-
return $this->assertTrue(true);
133-
}
134-
135-
$this->fail('Should have thrown EmptySearchQueryException.');
136-
}
137-
138125
/** @test */
139126
public function it_can_search_without_a_term()
140127
{
@@ -146,7 +133,6 @@ public function it_can_search_without_a_term()
146133
$results = Search::new()
147134
->add(Post::class)->orderBy('updated_at')
148135
->add(Video::class)->orderBy('published_at')
149-
->allowEmptySearchQuery()
150136
->get();
151137

152138
$this->assertCount(4, $results);
@@ -191,8 +177,17 @@ public function it_can_search_on_the_left_side_of_the_term()
191177
{
192178
Video::create(['title' => 'foo']);
193179

194-
$this->assertCount(0, Search::add(Video::class, 'title')->get('oo'));
195-
$this->assertCount(1, Search::add(Video::class, 'title')->startWithWildcard()->get('oo'));
180+
$this->assertCount(1, Search::add(Video::class, 'title')->get('fo'));
181+
$this->assertCount(0, Search::add(Video::class, 'title')->endWithWildcard(false)->get('fo'));
182+
}
183+
184+
/** @test */
185+
public function it_can_use_the_sounds_like_operator()
186+
{
187+
Video::create(['title' => 'laravel']);
188+
189+
$this->assertCount(0, Search::add(Video::class, 'title')->get('larafel'));
190+
$this->assertCount(1, Search::add(Video::class, 'title')->soundsLike()->get('larafel'));
196191
}
197192

198193
/** @test */

0 commit comments

Comments
 (0)