Skip to content

Commit ff10f30

Browse files
committed
Merge branch 'feature-searchable' of https://github.com/DeltaSystems/laravel-livewire-tables into DeltaSystems-feature-searchable
2 parents c028602 + 4dd6be3 commit ff10f30

File tree

10 files changed

+487
-22
lines changed

10 files changed

+487
-22
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,11 @@ class Table extends DataTableComponent
6666
{
6767
return [
6868
Column::make('Type')
69-
->sortable(),
69+
->sortable()
70+
->searchable(),
7071
Column::make('Name')
71-
->sortable(),
72+
->sortable()
73+
->searchable(),
7274
Column::make('Permissions'),
7375
Column::blank(),
7476
];
@@ -345,7 +347,14 @@ public array $filterNames = [
345347

346348
### Adding Search
347349

348-
The search is a special built-in filter that is managed by the component, but you need to define the search query, you can do so the same as any other filter:
350+
The search is a special built-in filter that is managed by the component, but you need to define the behavior. For a simple default search behavior, add searchable() to columns:
351+
352+
```php
353+
Column::make('Type')
354+
->searchable()
355+
```
356+
357+
Sometimes the default search behavior may not meet your requirements. If this is the case, skip using the searchable() method on columns and define your own behavior directly on the query.
349358

350359
```php
351360
public function query(): Builder

src/DataTableComponent.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,19 @@ public function rowsQuery(): Builder
123123
{
124124
$this->cleanFilters();
125125

126-
return $this->applySorting($this->query());
127-
}
126+
$query = $this->query();
128127

129-
/**
130-
* @return Builder
131-
*/
132-
public function getRowsQueryProperty(): Builder
133-
{
134-
return $this->rowsQuery();
128+
// sorting?
129+
if (method_exists($this, 'applySorting')) {
130+
$query = $this->applySorting($query);
131+
}
132+
133+
// searching?
134+
if (method_exists($this, 'applySearchFilter')) {
135+
$query = $this->applySearchFilter($query);
136+
}
137+
138+
return $query;
135139
}
136140

137141
/**

src/Traits/WithFilters.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
namespace Rappasoft\LaravelLivewireTables\Traits;
44

5+
use Illuminate\Database\Eloquent\Builder;
6+
use Rappasoft\LaravelLivewireTables\Utilities\ColumnUtilities;
7+
use Rappasoft\LaravelLivewireTables\Views\Column;
8+
59
/**
610
* Trait WithFilters.
711
*/
@@ -183,4 +187,77 @@ public function getFilterOptions(string $filter): array
183187
{
184188
return array_filter(array_keys($this->filters()[$filter]->options() ?? []));
185189
}
190+
191+
/**
192+
* Collects columns with $searchable = true
193+
*
194+
* @return Column[]
195+
*/
196+
public function getSearchableColumns() : array
197+
{
198+
return array_filter($this->columns(), function (Column $column) {
199+
return $column->isSearchable();
200+
});
201+
}
202+
203+
/**
204+
* Apply Search Filter
205+
*
206+
* @param Builder $query
207+
* @return Builder
208+
*/
209+
public function applySearchFilter(Builder $query): Builder
210+
{
211+
if ($this->hasFilter('search')) {
212+
213+
// get search value
214+
$search = $this->getFilter('search');
215+
216+
// trim
217+
$search = trim($search);
218+
219+
// group search conditions together
220+
$query->where(function (Builder $subQuery) use ($search, $query) {
221+
foreach ($this->getSearchableColumns() as $column) {
222+
223+
// does this column have an alias or relation?
224+
$hasRelation = ColumnUtilities::hasRelation($column->column());
225+
226+
// let's try to map this column to a selected column
227+
$selectedColumn = ColumnUtilities::mapToSelected($column->column(), $query);
228+
229+
// if the column has a search callback, just use that
230+
if ($column->searchCallback) {
231+
232+
// call the callback
233+
($column->searchCallback)($query, $search);
234+
235+
// if the column isn't a relation or if it was previously selected
236+
} elseif (! $hasRelation || $selectedColumn) {
237+
$whereColumn = $selectedColumn ?? $column->column();
238+
239+
// @todo: skip aggregates
240+
if (! $hasRelation && $query instanceof Builder) {
241+
$whereColumn = $query->getModel()->getTable() . '.' . $whereColumn;
242+
}
243+
244+
// we can use a simple where clause
245+
$subQuery->orWhere($whereColumn, 'like', '%' . $search . '%');
246+
} else {
247+
248+
// parse the column
249+
$relationName = ColumnUtilities::parseRelation($column->column());
250+
$fieldName = ColumnUtilities::parseField($column->column());
251+
252+
// we use whereHas which can work with unselected relations
253+
$subQuery->orWhereHas($relationName, function (Builder $hasQuery) use ($fieldName, $column, $search) {
254+
$hasQuery->where($fieldName, 'like', '%' . $search . '%');
255+
});
256+
}
257+
}
258+
});
259+
}
260+
261+
return $query;
262+
}
186263
}

src/Utilities/ColumnUtilities.php

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
3+
namespace Rappasoft\LaravelLivewireTables\Utilities;
4+
5+
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
6+
use Illuminate\Database\Query\Builder as Builder;
7+
use Illuminate\Support\Str;
8+
9+
class ColumnUtilities
10+
{
11+
/**
12+
* Grab the relation part of a column
13+
*
14+
* @param $column
15+
* @return bool
16+
*/
17+
public static function hasRelation($column)
18+
{
19+
return Str::contains($column, '.');
20+
}
21+
22+
/**
23+
* Grab the relation part of a column
24+
*
25+
* @param $column
26+
* @return string
27+
*/
28+
public static function parseRelation($column)
29+
{
30+
return Str::beforeLast($column, '.');
31+
}
32+
33+
/**
34+
* Grab the field part of a column
35+
*
36+
* @param $column
37+
* @return string
38+
*/
39+
public static function parseField($column)
40+
{
41+
return Str::afterLast($column, '.');
42+
}
43+
44+
/**
45+
* Is the column selected?
46+
*
47+
* @param $column
48+
* @param $searchColumns
49+
* @return bool
50+
*/
51+
public static function hasMatch($column, $searchColumns)
52+
{
53+
return array_search($column, $searchColumns ?? []) !== false;
54+
}
55+
56+
/**
57+
* Is the column selected by a wildcard match?
58+
*
59+
* @param $column
60+
* @param $searchColumns
61+
* @return bool
62+
*/
63+
public static function hasWildcardMatch($column, $searchColumns)
64+
{
65+
return count(array_filter($searchColumns ?? [], function ($searchColumn) use ($column) {
66+
67+
// match wildcards such as * or table.*
68+
$hasWildcard = Str::endsWith($searchColumn, '*');
69+
70+
// if no wildcard, skip
71+
if (! $hasWildcard) {
72+
return false;
73+
}
74+
75+
if (! self::hasRelation($column)) {
76+
return true;
77+
} else {
78+
$selectColumnPrefix = self::parseRelation($searchColumn);
79+
$columnPrefix = self::parseRelation($column);
80+
81+
return $selectColumnPrefix === $columnPrefix;
82+
}
83+
})) > 0;
84+
}
85+
86+
/**
87+
* @param EloquentBuilder|Builder $queryBuilder
88+
* @return null
89+
*/
90+
public static function columnsFromBuilder($queryBuilder = null)
91+
{
92+
if ($queryBuilder instanceof EloquentBuilder) {
93+
return $queryBuilder->getQuery()->columns;
94+
} elseif ($queryBuilder instanceof Builder) {
95+
return $queryBuilder->columns;
96+
} else {
97+
return null;
98+
}
99+
}
100+
101+
/**
102+
* Try to map a given column to an already selected column
103+
*
104+
* @param $column
105+
* @param $queryBuilder
106+
* @return string
107+
*/
108+
public static function mapToSelected($column, $queryBuilder)
109+
{
110+
// grab select
111+
$select = self::columnsFromBuilder($queryBuilder);
112+
113+
// can't match anything if no select
114+
if (is_null($select)) {
115+
return null;
116+
}
117+
118+
// search builder select for a match
119+
$hasMatch = self::hasMatch($column, $select);
120+
121+
// example 2 - match
122+
// column: service_statuses.name
123+
// select: service_statuses.name
124+
// maps to: service_statuses.name
125+
126+
// if we found a match, lets use that instead of searching relations
127+
if ($hasMatch) {
128+
return $column;
129+
}
130+
131+
// search builder select for a wildcard match
132+
$hasWildcardMatch = self::hasWildcardMatch($column, $select);
133+
134+
// example 3 - wildcard match
135+
// column: service_statuses.name
136+
// select: service_statuses.*
137+
// maps to: service_statuses.name
138+
139+
// if we found a wildcard match, lets use that instead of matching relations
140+
if ($hasWildcardMatch) {
141+
return $column;
142+
}
143+
144+
// split the relation and field
145+
$hasRelation = self::hasRelation($column);
146+
$relationName = self::parseRelation($column);
147+
$fieldName = self::parseField($column);
148+
149+
// we know there is a relation and we know it doesn't match any of the
150+
// select columns. Let's try to grab the table name for the relation
151+
// and see if that matches something in the select
152+
//
153+
// example 4 - relation to already selected table
154+
// column: serviceStatus.name
155+
// select: service_statuses.name
156+
// maps to: service_statuses.name
157+
158+
// if we didn't previously match the column and there isn't a relation
159+
if (! $hasRelation) {
160+
161+
// there's nothing else to do
162+
return null;
163+
164+
// this is easiest when using the eloquent query builder
165+
} elseif ($queryBuilder instanceof EloquentBuilder) {
166+
$relation = $queryBuilder->getRelation($relationName);
167+
$possibleTable = $relation->getModel()->getTable();
168+
} elseif ($queryBuilder instanceof Builder) {
169+
170+
// @todo: possible ways to do this?
171+
$possibleTable = null;
172+
} else {
173+
174+
// we would have already returned before this is possible
175+
$possibleTable = null;
176+
}
177+
178+
// if we found a possible table
179+
if (! is_null($possibleTable)) {
180+
181+
// build possible selected column
182+
$possibleSelectColumn = $possibleTable . '.' . $fieldName;
183+
184+
$possibleMatch = self::hasMatch($possibleSelectColumn, $select);
185+
186+
// if we found a possible match for a relation to an already selected
187+
// column, let's use that
188+
if ($possibleMatch) {
189+
return $possibleSelectColumn;
190+
}
191+
192+
$possibleWildcardMatch = self::hasWildcardMatch($possibleSelectColumn, $select);
193+
194+
// ditto with a possible wildcard match
195+
if ($possibleWildcardMatch) {
196+
return $possibleSelectColumn;
197+
}
198+
}
199+
200+
// we couldn't match to a selected column
201+
return null;
202+
}
203+
}

0 commit comments

Comments
 (0)