Completed
Push — master ( e1a362...4feb09 )
by Jhorman Alexander
04:07
created

Searchzy::addRelationFilterableFromConstraint()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 3
nop 1
dl 0
loc 29
rs 8.8337
c 0
b 0
f 0
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 los valores searchable con la 'keyword' de searchzy.
28
     *
29
     * @var array
30
     */
31
    private $searchableInputsKeyword;
32
33
    /**
34
     * Define el array con todos los inputs searchable del Modelo.
35
     *
36
     * @var array
37
     */
38
    private $searchableInputs = [];
39
40
    /**
41
     * Define el array con todos los inputs filterable del Modelo.
42
     *
43
     * @var array
44
     */
45
    private $filterableInputs = [];
46
47
    /**
48
     * Define el array con todos los inputs adicionales del Modelo.
49
     *
50
     * @var array
51
     */
52
    private $aditionableInputs = [];
53
54
    /**
55
     * Define el valor de la 'keyword' de searchzy.
56
     *
57
     * @var array
58
     */
59
    private $searchableKeyword;
60
61
    /**
62
     * Define el request usado por searchzy.
63
     *
64
     * @var Request
65
     */
66
    private $currentRequest;
67
68
    /**
69
     * Scope que realiza una búsqueda searchzy.
70
     *
71
     * @param  Illuminate\Database\Eloquent\Builder $query
72
     * @return Illuminate\Database\Eloquent\Builder
73
     */
74
    public function scopeSearchzy($query, $keyword = null, $request = null): Builder
75
    {
76
        $keyword = $keyword ?: config('searchzy.keyword');
77
78
        $this->currentRequest = $request ?: request();
79
80
        $this->searchableKeyword = $this->currentRequest->get($keyword, null);
81
82
        $this->searchableInputsKeyword = $this->getInputsKeyword();
83
84
        $this->searchableInputs = $this->getInputsFromRequest('searchable', 'searchableInputs');
85
86
        $this->filterableInputs = $this->getInputsFromRequest('filterable', 'filterableInputs');
87
88
        $query = $this->parseInputsKeywordConstraints($query);
89
90
        $query = $this->parseRelationConstraints($query);
91
92
        $query = $this->loadRelationContraints($query);
93
94
        return $query;
95
    }
96
97
    /**
98
     * Agrupa las relaciones del Modelo. Retorna un array 'arbol' de las relaciones y sus columnas.
99
     *
100
     * @param  array $arrInputs
101
     * @return array
102
     */
103
    private function parseRelationInputs($arrInputs): array
104
    {
105
        $relationInputs = [];
106
107
        foreach (array_keys($arrInputs) as $attribute) {
108
109
            if (Str::contains($attribute, ':')) {
110
111
                [$relation, $column] = explode(':', $attribute);
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relationInputs?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
Bug introduced by
The variable $column does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
112
113
                $relationInputs[$relation][] = $column;
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relationInputs?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
114
            }
115
        }
116
117
        return $relationInputs;
118
    }
119
120
    /**
121
     * Agrupas las columnas propias del Modelo.
122
     *
123
     * @param  array $arrInputs
124
     * @return array
125
     */
126
    private function parseModelInputs($arrInputs): array
127
    {
128
        $modelInputs = [];
129
130
        foreach (array_keys($arrInputs) as $attribute) {
131
132
            if (!Str::contains($attribute, ':')) {
133
134
                $modelInputs[] = $attribute;
135
            }
136
        }
137
138
        return $modelInputs;
139
    }
140
141
    /**
142
     * Aplica los 'wheres' que serán aplicado en la tabla del Modelo
143
     *
144
     * @param  Builder $query
145
     * @return Builder
146
     */
147
    private function applySearchableConstraints($query): Builder
148
    {
149
150
        $searchableModelInputs = $this->parseModelInputs($this->searchableInputsKeyword);
151
152
        if (count($searchableModelInputs)) {
153
154
            $query = $query->where(function ($query) use ($searchableModelInputs) {
155
156
                foreach ($searchableModelInputs as $attribute) {
157
158
                    $value = Arr::get($this->searchableInputsKeyword, $attribute, $this->searchableKeyword);
159
160
                    if (filter_nullables($value)) {
161
162
                        $query->orWhere($attribute, 'LIKE', "%{$value}")
163
                            ->orWhere($attribute, 'LIKE', "{$value}%")
164
                            ->orWhere($attribute, 'LIKE', "%{$value}%");
165
                    }
166
                }
167
            });
168
        }
169
170
        return $query;
171
    }
172
173
    /**
174
     * Aplica los 'wheres' que serán aplicado en la relaciones del Modelo.
175
     *
176
     * @param  Builder $query
177
     * @return Builder
178
     */
179
    private function applySearchableRelationConstraints($query): Builder
180
    {
181
182
        if (filter_nullables($this->searchableKeyword)) {
183
184
            $searchableRelationInputs = $this->parseRelationInputs($this->searchableInputsKeyword);
185
186
            $searchableRelation = $this->parseRelationInputs($this->searchableInputs);
187
188
            foreach ($searchableRelationInputs as $attribute => $columns) {
189
190
                if (!in_array($attribute, array_keys($searchableRelation))) {
191
192
                    $query->orWhereHas($attribute, function ($query) use ($attribute, $searchableRelationInputs) {
193
194
                        $query = $query->where(function ($query) use ($attribute, $searchableRelationInputs) {
0 ignored issues
show
Unused Code introduced by
$query is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
195
196
                            $columns = $searchableRelationInputs[$attribute] ?? [];
197
198
                            foreach ($columns as $column) {
199
200
                                $value = $this->searchableInputsKeyword["{$attribute}:{$column}"] ?? $this->searchableKeyword;
201
202
                                if (filter_nullables($value)) {
203
204
                                    $query->orWhere($column, 'LIKE', "%{$value}")
205
                                        ->orWhere($column, 'LIKE', "{$value}%")
206
                                        ->orWhere($column, 'LIKE', "%{$value}%");
207
                                }
208
                            }
209
                        });
210
                    });
211
                }
212
            }
213
        }
214
215
        return $query;
216
    }
217
218
    /**
219
     * Obtiene el operador del where usado en el armado de la consulta de filterable.
220
     *
221
     * @param  mixed $value
222
     * @return array
223
     */
224
    private function getOperatorFilterable($value): array
225
    {
226
227
        $operator = is_array($value) ? 'whereIn' : 'where';
228
229
        $value = is_array($value) ? array_filter($value, 'filter_nullables') : str_trimmer($value);
230
231
        return [$operator, $value];
232
    }
233
234
    /**
235
     * Aplicación del los where's de los atributos filterable propios del Modelo.
236
     *
237
     * @param  Builder $query
238
     * @return Builder
239
     */
240
    private function applyFilterableConstraints($query): Builder
241
    {
242
243
        $filterableModelInputs = $this->parseModelInputs($this->filterableInputs);
244
245
        $filterableModelInputs = Arr::only($this->filterableInputs, $filterableModelInputs);
246
247
        foreach ($filterableModelInputs as $column => $value) {
248
249
            list($operator, $value) = $this->getOperatorFilterable($value);
250
251
            if (is_array($value) and !count($value)) {
252
                break;
253
            }
254
255
            $query->{$operator}($column, $value);
256
        }
257
258
        return $query;
259
    }
260
261
    /**
262
     * Añade la relación y su callback en un array para despues ser llamada via whereHas.
263
     *
264
     * @param  Builder $query
265
     * @return Builder
266
     */
267
    private function addRelationSearchableFromConstraint($query): void
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
268
    {
269
270
        $searchableRelationInputs = $this->parseRelationInputs($this->searchableInputs);
271
272
        if (count($searchableRelationInputs)) {
273
274
            foreach ($searchableRelationInputs as $attribute => $columns) {
275
276
                $this->addRelationConstraints([$attribute => function ($query) use ($attribute, $searchableRelationInputs) {
277
278
                    $columns = $searchableRelationInputs[$attribute] ?? [];
279
280
                    foreach ($columns as $column) {
281
282
                        $value = Arr::get($this->searchableInputs, "{$attribute}:{$column}");
283
284
                        if (filter_nullables($value)) {
285
286
                            $query->where(function ($query) use ($column, $value) {
287
288
                                $query->orWhere($column, 'LIKE', "%{$value}")
289
                                    ->orWhere($column, 'LIKE', "{$value}%")
290
                                    ->orWhere($column, 'LIKE', "%{$value}%");
291
                            });
292
                        }
293
                    }
294
                }]);
295
            }
296
        }
297
    }
298
299
    /**
300
     * Añade la relación y su callback en un array para despues ser llamada via whereHas.
301
     *
302
     * @param  Builder $query
303
     * @return Builder
304
     */
305
    private function addRelationFilterableFromConstraint($query): void
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
306
    {
307
308
        $filterableRelationInputs = $this->parseRelationInputs($this->filterableInputs);
309
310
        if (count($filterableRelationInputs)) {
311
312
            foreach ($filterableRelationInputs as $attribute => $columns) {
313
314
                $this->addRelationConstraints([$attribute => function ($query) use ($attribute, $filterableRelationInputs) {
315
316
                    $columns = $filterableRelationInputs[$attribute] ?? [];
317
318
                    foreach ($columns as $column) {
319
320
                        $value = Arr::get($this->filterableInputs, "{$attribute}:{$column}");
321
322
                        list($operator, $value) = $this->getOperatorFilterable($value);
323
324
                        if (is_array($value) and !count($value)) {
325
                            break;
326
                        }
327
328
                        $query->{$operator}($column, $value);
329
                    }
330
                }]);
331
            }
332
        }
333
    }
334
335
    /**
336
     * Parsea los inputs de búsqueda del Modelo.
337
     *
338
     * @param  Builder $query
339
     * @return Builder
340
     */
341
    private function parseInputsKeywordConstraints($query): Builder
342
    {
343
        $query = $query->where(function ($query) {
344
            $this->applySearchableConstraints($query);
345
            $this->applySearchableRelationConstraints($query);
346
        });
347
348
        $query = $this->applyFilterableConstraints($query);
349
350
        /** Añade los closures a un array para ser luego usado cuando se cargan su relaciones */
351
352
        $this->addRelationSearchableFromConstraint($query);
353
354
        $this->addRelationFilterableFromConstraint($query);
355
356
        return $query;
357
    }
358
359
    /**
360
     * Agrupa las closures por cada relación en {relationConstraints}.
361
     *
362
     * @param  array  $relations
363
     * @return void
364
     */
365
    private function addRelationConstraints(array $relations): void
366
    {
367
        foreach ($relations as $name => $closure) {
368
369
            $this->relationConstraints[$name][] = $closure;
370
        }
371
    }
372
373
    /**
374
     * Sí hay closures en las relaciones, aplica al query y agrupalas
375
     * por cada la relación en {eagerRelationConstraints}.
376
     *
377
     * @param  Builder $query
378
     * @return Builder
379
     */
380
    private function parseRelationConstraints($query): Builder
381
    {
382
        if ($this->relationConstraints) {
383
384
            foreach ($this->relationConstraints as $relation => $constraints) {
385
386
                $this->eagerRelationConstraints[$relation] = function ($query) use ($constraints) {
387
388
                    foreach ($constraints as $constraint) {
389
390
                        $constraint($query);
391
                    }
392
                };
393
            }
394
        }
395
396
        return $query;
397
    }
398
399
    /**
400
     * Aplica los 'closures' que estan en {eagerRelationConstraints} por cada relación vía whereHas.
401
     *
402
     * @param  Builder $query
403
     * @return Builder
404
     */
405
    private function loadRelationContraints($query): Builder
406
    {
407
        if ($this->eagerRelationConstraints) {
408
409
            foreach ($this->eagerRelationConstraints as $relation => $closure) {
410
411
                $query->whereHas($relation, $closure);
412
            }
413
        }
414
415
        return $query;
416
    }
417
418
    /**
419
     * Retorna un array con los inputs 'searchables' cuyo valor será el ingresado en la 'keyword'.
420
     *
421
     * @return array
422
     */
423
    private function getInputsKeyword(): array
424
    {
425
        $arrInputs  = [];
426
427
        $searchableInputsFromModel = $this->getInputsFromModel('searchable', 'searchableInputs');
428
429
        if (count($searchableInputsFromModel)) {
430
431
            foreach (array_keys($searchableInputsFromModel) as $column) {
432
433
                $arrInputs[$column] = $this->searchableKeyword ?: $this->currentRequest->get($column, null);
434
            }
435
436
            $arrInputs = array_keys_replace($arrInputs, $searchableInputsFromModel);
437
        }
438
439
        return $arrInputs;
440
    }
441
442
    /**
443
     * Obtiene los inputs definidos en el Modelo y que se encuentran en el Request.
444
     *
445
     * @param  string $property
446
     * @param  string $method
447
     * @return array
448
     */
449
    private function getInputsFromRequest($property, $method): array
0 ignored issues
show
Unused Code introduced by
The parameter $property is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $method is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
450
    {
451
        $inputsFromModel = $this->getInputsFromModel('filterable', 'filterableInputs');
452
453
        $filledInputs = array_filter_empty($this->currentRequest->only(array_keys($inputsFromModel)));
454
455
        $filledInputs = array_keys_replace($filledInputs, $inputsFromModel);
456
457
        $filledInputs = array_filter_recursive($filledInputs, 'filter_nullables');
458
459
        return $filledInputs;
460
    }
461
462
    /**
463
     * Obtiene los inputs definidos en el Model, tanto en la propiedad como método.
464
     *
465
     * @param  string $property
466
     * @param  string $method
467
     * @param  bool $keys
468
     * @return array
469
     */
470
    private function getInputsFromModel($property, $method, $keys = false): array
471
    {
472
        $inputs = [];
473
474
        $inputs = property_exists($this, $property) ? Arr::wrap($this->{$property}) : $inputs;
475
476
        $inputs = method_exists($this, $method) ? Arr::wrap($this->{$method}()) : $inputs;
477
478
        $inputs = $keys ? (Arr::isAssoc($inputs) ? array_keys($inputs) : $inputs) : $inputs;
479
480
        return $inputs;
481
    }
482
483
    /**
484
     * Obtiene los inputs de searchzy (keyword, searchzy, extra) cuyo valor será el de Request o el definido por defecto.
485
     *
486
     * @link    (https://timacdonald.me/query-scopes-meet-action-scopes/)
487
     * @param   Builder $query
488
     * @param   array   $extra
489
     * @param   string  $default
490
     * @return  array
491
     */
492
    public function scopeSearchzyInputs($query, $extra = [], $default = '', $request = null): array
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
493
    {
494
        $this->currentRequest = $request ?: request();
495
496
        $searchable  = $this->getInputsFromModel('searchable', 'searchableInputs', true);
497
498
        $filterable  = $this->getInputsFromModel('filterable', 'filterableInputs', true);
499
500
        $aditionable = $this->getInputsFromModel('aditionable', 'aditionableInputs', true);
501
502
        $extra   = Arr::wrap($extra);
503
504
        $keyword = Arr::wrap(config('searchzy.keyword'));
505
506
        $inputs = array_merge($searchable, $filterable, $aditionable, $keyword, $extra);
507
508
        $inputs = array_filler($this->currentRequest->all(), $inputs, $default);
509
510
        return $inputs;
511
    }
512
}
513