1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Jhormantasayco\LaravelSearchzy; |
4
|
|
|
|
5
|
|
|
use Illuminate\Database\Eloquent\Builder; |
6
|
|
|
use Illuminate\Support\Arr; |
7
|
|
|
use Illuminate\Support\Str; |
8
|
|
|
|
9
|
|
|
trait Searchzy |
10
|
|
|
{ |
11
|
|
|
/** |
12
|
|
|
* Agrupa todas los closures de las relaciones del Modelo, en forma de árbol. |
13
|
|
|
* |
14
|
|
|
* @var array |
15
|
|
|
*/ |
16
|
|
|
private $relationConstraints = []; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Define el array de las relaciones y el query de las relaciones. En el query |
20
|
|
|
* ya se aplicaron las closures de cada relación. |
21
|
|
|
* |
22
|
|
|
* @var array |
23
|
|
|
*/ |
24
|
|
|
private $eagerRelationConstraints = []; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Define el array con todos los inputs searchable del Modelo. |
28
|
|
|
* |
29
|
|
|
* @var array |
30
|
|
|
*/ |
31
|
|
|
private $searchableInputs = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Define el array con todos los inputs filterable del Modelo. |
35
|
|
|
* |
36
|
|
|
* @var array |
37
|
|
|
*/ |
38
|
|
|
private $filterableInputs = []; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Define el array con todos los inputs adicionales del Modelo. |
42
|
|
|
* |
43
|
|
|
* @var array |
44
|
|
|
*/ |
45
|
|
|
private $aditionableInputs = []; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Define el valor de la 'keyword' de searchzy. |
49
|
|
|
* |
50
|
|
|
* @var array |
51
|
|
|
*/ |
52
|
|
|
private $searchableKeyword; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Define el request usado por searchzy. |
56
|
|
|
* |
57
|
|
|
* @var Request |
58
|
|
|
*/ |
59
|
|
|
private $currentRequest; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Scope que realiza una búsqueda searchzy. |
63
|
|
|
* |
64
|
|
|
* @param Illuminate\Database\Eloquent\Builder $query |
65
|
|
|
* @return Illuminate\Database\Eloquent\Builder |
66
|
|
|
*/ |
67
|
|
|
public function scopeSearchzy($query, $keyword = null, $request = null): Builder |
68
|
|
|
{ |
69
|
|
|
$keyword = $keyword ?: config('searchzy.keyword'); |
70
|
|
|
|
71
|
|
|
$this->currentRequest = $request ?: request(); |
72
|
|
|
|
73
|
|
|
$this->searchableKeyword = $this->currentRequest->get($keyword, null); |
74
|
|
|
|
75
|
|
|
$this->searchableInputsKeyword = $this->getInputsKeyword(); |
|
|
|
|
76
|
|
|
|
77
|
|
|
$this->searchableInputs = $this->getInputsFromRequest('searchable', 'searchableInputs'); |
78
|
|
|
|
79
|
|
|
$this->filterableInputs = $this->getInputsFromRequest('filterable', 'filterableInputs'); |
80
|
|
|
|
81
|
|
|
$query = $this->parseInputsKeywordConstraints($query); |
82
|
|
|
|
83
|
|
|
$query = $this->parseRelationConstraints($query); |
84
|
|
|
|
85
|
|
|
$query = $this->loadRelationContraints($query); |
86
|
|
|
|
87
|
|
|
return $query; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Agrupa las relaciones del Modelo. Retorna un array 'arbol' de las relaciones y sus columnas. |
92
|
|
|
* |
93
|
|
|
* @param array $arrInputs |
94
|
|
|
* @return array |
95
|
|
|
*/ |
96
|
|
|
private function parseRelationInputs($arrInputs): array |
97
|
|
|
{ |
98
|
|
|
$relationInputs = []; |
99
|
|
|
|
100
|
|
|
foreach (array_keys($arrInputs) as $attribute) { |
101
|
|
|
|
102
|
|
|
if (Str::contains($attribute, ':')) { |
103
|
|
|
|
104
|
|
|
[$relation, $column] = explode(':', $attribute); |
|
|
|
|
105
|
|
|
|
106
|
|
|
$relationInputs[$relation][] = $column; |
|
|
|
|
107
|
|
|
} |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
return $relationInputs; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Agrupas las columnas propias del Modelo. |
115
|
|
|
* |
116
|
|
|
* @param array $arrInputs |
117
|
|
|
* @return array |
118
|
|
|
*/ |
119
|
|
|
private function parseModelInputs($arrInputs): array |
120
|
|
|
{ |
121
|
|
|
$modelInputs = []; |
122
|
|
|
|
123
|
|
|
foreach (array_keys($arrInputs) as $attribute) { |
124
|
|
|
|
125
|
|
|
if (!Str::contains($attribute, ':')) { |
126
|
|
|
|
127
|
|
|
$modelInputs[] = $attribute; |
128
|
|
|
} |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
return $modelInputs; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Parsea los inputs de búsqueda del Modelo. |
136
|
|
|
* |
137
|
|
|
* @param Builder $query |
138
|
|
|
* @return Builder |
139
|
|
|
*/ |
140
|
|
|
private function parseInputsKeywordConstraints($query): Builder |
141
|
|
|
{ |
142
|
|
|
// Aplicación del los where's de los atributos searchable propios del Modelo. |
143
|
|
|
|
144
|
|
|
$searchableModelInputs = $this->parseModelInputs($this->searchableInputsKeyword); |
|
|
|
|
145
|
|
|
|
146
|
|
|
$query = $query->where(function ($query) use ($searchableModelInputs) { |
147
|
|
|
|
148
|
|
|
// Aplicación de los where's en las columnas propias del Modelo, cuyo valor es el del 'keyword'. |
149
|
|
|
|
150
|
|
|
$query = $query->where(function ($query) use ($searchableModelInputs) { |
151
|
|
|
|
152
|
|
|
foreach ($searchableModelInputs as $attribute) { |
153
|
|
|
|
154
|
|
|
$value = Arr::get($this->searchableInputsKeyword, $attribute, $this->searchableKeyword); |
|
|
|
|
155
|
|
|
|
156
|
|
|
if ($value) { |
157
|
|
|
|
158
|
|
|
$query->orWhere($attribute, 'LIKE', "%{$value}") |
159
|
|
|
->orWhere($attribute, 'LIKE', "{$value}%") |
160
|
|
|
->orWhere($attribute, 'LIKE', "%{$value}%"); |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
}); |
164
|
|
|
|
165
|
|
|
// Aplicación de los where's de las relaciones del Modelo, cuyo valor es el del 'keyword'. |
166
|
|
|
|
167
|
|
|
if ($this->searchableKeyword) { |
168
|
|
|
|
169
|
|
|
$searchableRelationInputs = $this->parseRelationInputs($this->searchableInputsKeyword); |
|
|
|
|
170
|
|
|
|
171
|
|
|
$searchableRelation = $this->parseRelationInputs($this->searchableInputs); |
172
|
|
|
|
173
|
|
|
foreach ($searchableRelationInputs as $attribute => $columns) { |
174
|
|
|
|
175
|
|
|
if (!in_array($attribute, array_keys($searchableRelation))) { |
176
|
|
|
|
177
|
|
|
$query->orWhereHas($attribute, function ($query) use ($attribute, $searchableRelationInputs) { |
178
|
|
|
|
179
|
|
|
$query = $query->where(function ($query) use ($attribute, $searchableRelationInputs) { |
|
|
|
|
180
|
|
|
|
181
|
|
|
$columns = $searchableRelationInputs[$attribute] ?? []; |
182
|
|
|
|
183
|
|
|
foreach ($columns as $column) { |
184
|
|
|
|
185
|
|
|
$value = $this->searchableInputsKeyword["{$attribute}:{$column}"] ?? $this->searchableKeyword; |
|
|
|
|
186
|
|
|
|
187
|
|
|
$query->orWhere($column, 'LIKE', "%{$value}") |
188
|
|
|
->orWhere($column, 'LIKE', "{$value}%") |
189
|
|
|
->orWhere($column, 'LIKE', "%{$value}%"); |
190
|
|
|
} |
191
|
|
|
}); |
192
|
|
|
}); |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
}); |
197
|
|
|
|
198
|
|
|
// Aplicación del los where's de los atributos filterable propios del Modelo. |
199
|
|
|
|
200
|
|
|
$filterableModelInputs = $this->parseModelInputs($this->filterableInputs); |
201
|
|
|
|
202
|
|
|
$filterableModelInputs = Arr::only($this->filterableInputs, $filterableModelInputs); |
203
|
|
|
|
204
|
|
|
foreach ($filterableModelInputs as $column => $value) { |
205
|
|
|
|
206
|
|
|
$operator = is_array($value) ? 'whereIn' : 'where'; |
207
|
|
|
|
208
|
|
|
$value = is_array($value) ? array_filter($value, 'filter_nullables') : str_trimmer($value); |
209
|
|
|
|
210
|
|
|
if (is_array($value) and !count($value)) { |
211
|
|
|
break; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
$query->{$operator}($column, $value); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
// Se añade los constraints para las relaciones definidads en el searchable del Modelo. |
218
|
|
|
|
219
|
|
|
$searchableRelationInputs = $this->parseRelationInputs($this->searchableInputs); |
220
|
|
|
|
221
|
|
|
foreach ($searchableRelationInputs as $attribute => $columns) { |
222
|
|
|
|
223
|
|
|
$this->addRelationConstraints([$attribute => function ($query) use ($attribute, $searchableRelationInputs) { |
224
|
|
|
|
225
|
|
|
$columns = $searchableRelationInputs[$attribute] ?? []; |
226
|
|
|
|
227
|
|
|
foreach ($columns as $column) { |
228
|
|
|
|
229
|
|
|
$value = Arr::get($this->searchableInputs, "{$attribute}:{$column}"); |
230
|
|
|
|
231
|
|
|
$query->where(function ($query) use ($column, $value) { |
232
|
|
|
|
233
|
|
|
$query->orWhere($column, 'LIKE', "%{$value}") |
234
|
|
|
->orWhere($column, 'LIKE', "{$value}%") |
235
|
|
|
->orWhere($column, 'LIKE', "%{$value}%"); |
236
|
|
|
}); |
237
|
|
|
} |
238
|
|
|
}]); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
// Se añade los constraints de las relaciones definidads en el filterable del Modelo. |
242
|
|
|
|
243
|
|
|
$filterableRelationInputs = $this->parseRelationInputs($this->filterableInputs); |
244
|
|
|
|
245
|
|
|
foreach ($filterableRelationInputs as $attribute => $columns) { |
246
|
|
|
|
247
|
|
|
$this->addRelationConstraints([$attribute => function ($query) use ($attribute, $filterableRelationInputs) { |
248
|
|
|
|
249
|
|
|
$columns = $filterableRelationInputs[$attribute] ?? []; |
250
|
|
|
|
251
|
|
|
foreach ($columns as $column) { |
252
|
|
|
|
253
|
|
|
$value = Arr::get($this->filterableInputs, "{$attribute}:{$column}"); |
254
|
|
|
|
255
|
|
|
$operator = is_array($value) ? 'whereIn' : 'where'; |
256
|
|
|
|
257
|
|
|
$value = is_array($value) ? array_filter($value, 'filter_nullables') : str_trimmer($value); |
258
|
|
|
|
259
|
|
|
if (is_array($value) and !count($value)) { |
260
|
|
|
break; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
$query->{$operator}($column, $value); |
264
|
|
|
} |
265
|
|
|
}]); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
return $query; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Agrupa las closures por cada relación en {relationConstraints}. |
273
|
|
|
* |
274
|
|
|
* @param array $relations |
275
|
|
|
* @return void |
276
|
|
|
*/ |
277
|
|
|
private function addRelationConstraints(array $relations): void |
278
|
|
|
{ |
279
|
|
|
foreach ($relations as $name => $closure) { |
280
|
|
|
|
281
|
|
|
$this->relationConstraints[$name][] = $closure; |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Sí hay closures en las relaciones, aplica al query y agrupalas |
287
|
|
|
* por cada la relación en {eagerRelationConstraints}. |
288
|
|
|
* |
289
|
|
|
* @param Builder $query |
290
|
|
|
* @return Builder |
291
|
|
|
*/ |
292
|
|
|
private function parseRelationConstraints($query): Builder |
293
|
|
|
{ |
294
|
|
|
if ($this->relationConstraints) { |
295
|
|
|
|
296
|
|
|
foreach ($this->relationConstraints as $relation => $constraints) { |
297
|
|
|
|
298
|
|
|
$this->eagerRelationConstraints[$relation] = function ($query) use ($constraints) { |
299
|
|
|
|
300
|
|
|
foreach ($constraints as $constraint) { |
301
|
|
|
|
302
|
|
|
$constraint($query); |
303
|
|
|
} |
304
|
|
|
}; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $query; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Aplica los 'closures' que estan en {eagerRelationConstraints} por cada relación vía whereHas. |
313
|
|
|
* |
314
|
|
|
* @param Builder $query |
315
|
|
|
* @return Builder |
316
|
|
|
*/ |
317
|
|
|
private function loadRelationContraints($query): Builder |
318
|
|
|
{ |
319
|
|
|
if ($this->eagerRelationConstraints) { |
320
|
|
|
|
321
|
|
|
foreach ($this->eagerRelationConstraints as $relation => $closure) { |
322
|
|
|
|
323
|
|
|
$query->whereHas($relation, $closure); |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
return $query; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Retorna un array con los inputs 'searchables' cuyo valor será el ingresado en la 'keyword'. |
332
|
|
|
* |
333
|
|
|
* @return array |
334
|
|
|
*/ |
335
|
|
|
private function getInputsKeyword(): array |
336
|
|
|
{ |
337
|
|
|
$arrInputs = []; |
338
|
|
|
|
339
|
|
|
$searchableInputsFromModel = $this->getInputsFromModel('searchable', 'searchableInputs'); |
340
|
|
|
|
341
|
|
|
if (count($searchableInputsFromModel)) { |
342
|
|
|
|
343
|
|
|
foreach (array_keys($searchableInputsFromModel) as $column) { |
344
|
|
|
|
345
|
|
|
$arrInputs[$column] = $this->searchableKeyword ?: $this->currentRequest->get($column, null); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
$arrInputs = array_keys_replace($arrInputs, $searchableInputsFromModel); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
return $arrInputs; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Obtiene los inputs definidos en el Modelo y que se encuentran en el Request. |
356
|
|
|
* |
357
|
|
|
* @param string $property |
358
|
|
|
* @param string $method |
359
|
|
|
* @return array |
360
|
|
|
*/ |
361
|
|
|
private function getInputsFromRequest($property, $method): array |
|
|
|
|
362
|
|
|
{ |
363
|
|
|
$inputsFromModel = $this->getInputsFromModel('filterable', 'filterableInputs'); |
364
|
|
|
|
365
|
|
|
$filledInputs = array_filter_empty($this->currentRequest->only(array_keys($inputsFromModel))); |
366
|
|
|
|
367
|
|
|
$filledInputs = array_keys_replace($filledInputs, $inputsFromModel); |
368
|
|
|
|
369
|
|
|
$filledInputs = array_filter_recursive($filledInputs, 'filter_nullables'); |
370
|
|
|
|
371
|
|
|
return $filledInputs; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
/** |
375
|
|
|
* Obtiene los inputs definidos en el Model, tanto en la propiedad como método. |
376
|
|
|
* |
377
|
|
|
* @param string $property |
378
|
|
|
* @param string $method |
379
|
|
|
* @param bool $keys |
380
|
|
|
* @return array |
381
|
|
|
*/ |
382
|
|
|
private function getInputsFromModel($property, $method, $keys = false): array |
383
|
|
|
{ |
384
|
|
|
$inputs = []; |
385
|
|
|
|
386
|
|
|
$inputs = property_exists($this, $property) ? Arr::wrap($this->{$property}) : $inputs; |
387
|
|
|
|
388
|
|
|
$inputs = method_exists($this, $method) ? Arr::wrap($this->{$method}()) : $inputs; |
389
|
|
|
|
390
|
|
|
$inputs = $keys ? (Arr::isAssoc($inputs) ? array_keys($inputs) : $inputs) : $inputs; |
391
|
|
|
|
392
|
|
|
return $inputs; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* Obtiene los inputs de searchzy (keyword, searchzy, extra) cuyo valor será el de Request o el definido por defecto. |
397
|
|
|
* |
398
|
|
|
* @link (https://timacdonald.me/query-scopes-meet-action-scopes/) |
399
|
|
|
* @param Builder $query |
400
|
|
|
* @param array $extra |
401
|
|
|
* @param string $default |
402
|
|
|
* @return array |
403
|
|
|
*/ |
404
|
|
|
public function scopeSearchzyInputs($query, $extra = [], $default = '', $request = null): array |
|
|
|
|
405
|
|
|
{ |
406
|
|
|
$this->currentRequest = $request ?: request(); |
407
|
|
|
|
408
|
|
|
$searchable = $this->getInputsFromModel('searchable', 'searchableInputs', true); |
409
|
|
|
|
410
|
|
|
$filterable = $this->getInputsFromModel('filterable', 'filterableInputs', true); |
411
|
|
|
|
412
|
|
|
$aditionable = $this->getInputsFromModel('aditionable', 'aditionableInputs', true); |
413
|
|
|
|
414
|
|
|
$extra = Arr::wrap($extra); |
415
|
|
|
|
416
|
|
|
$keyword = Arr::wrap(config('searchzy.keyword')); |
417
|
|
|
|
418
|
|
|
$inputs = array_merge($searchable, $filterable, $aditionable, $keyword, $extra); |
419
|
|
|
|
420
|
|
|
$inputs = array_filler($this->currentRequest->all(), $inputs, $default); |
421
|
|
|
|
422
|
|
|
return $inputs; |
423
|
|
|
} |
424
|
|
|
} |
425
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.