SearchScope::apply()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
3
namespace Goopil\RestFilter\Scopes;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Database\Eloquent\Builder;
7
use Goopil\RestFilter\Contracts\Searchable;
8
9
/**
10
 * Class SearchScope.
11
 */
12
class SearchScope extends BaseScope
13
{
14
    /**
15
     * valid db conditions.
16
     *
17
     * @var array
18
     */
19
    protected $acceptedConditions;
20
21
    /**
22
     * default db condition when not set.
23
     *
24
     * @var string
25
     */
26
    protected $defaultCondition;
27
28
    /**
29
     * force where query symbol.
30
     *
31
     * @var string
32
     */
33
    protected $forceWhereSymbol;
34
35
    /**
36
     * searchable fields.
37
     *
38
     * @var
39
     */
40
    protected $validFields;
41
42
    /**
43
     * model table.
44
     *
45
     * @var string
46
     */
47
    protected $modelTableName;
48
49
    /**
50
     * @var Model
51
     */
52
    protected $model;
53
54
    /**
55
     * @param Builder $builder
56
     * @param Model   $model
57
     *
58
     * @return Builder
59
     */
60
    public function apply(Builder $builder, Model $model)
61
    {
62
        if (! $model instanceof Searchable) {
63
            return $builder;
64
        }
65
66
        $this->forceWhereSymbol = config('queryScope.search.forceWhere', '!');
67
        $this->acceptedConditions = config('queryScope.search.acceptedConditions', ['=', '>=', '<=', '<', '>', 'like', 'ilike']);
0 ignored issues
show
Documentation Bug introduced by
It seems like config('queryScope.searc... '>', 'like', 'ilike')) of type * is incompatible with the declared type array of property $acceptedConditions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
68
        $this->defaultCondition = config('queryScope.search.default', '=');
69
70
        return $this->handle($builder, $model);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->handle($builder, $model); of type Goopil\RestFilter\Scopes...tabase\Eloquent\Builder adds the type Goopil\RestFilter\Scopes\SearchScope to the return on line 70 which is incompatible with the return type declared by the abstract method Goopil\RestFilter\Scopes\BaseScope::apply of type Illuminate\Database\Eloquent\Builder.
Loading history...
71
    }
72
73
    /**
74
     * @param Builder    $builder
75
     * @param Searchable $model
76
     *
77
     * @return $this|Builder
78
     */
79
    protected function handle(Builder $builder, Searchable $model)
80
    {
81
        $search = $this->request->get(config('queryScope.search.searchParam', 'search'), null);
82
        $this->validFields = $model::searchable();
83
84
        if ($search === null || ! is_array($this->validFields) || count($this->validFields) < 1) {
85
            return $builder;
86
        }
87
88
        $this->model = new $model();
89
        $this->modelTableName = $this->model->getTable();
90
        $fieldsByType = $this->parseQuery($search);
91
        $isFirstField = true;
92
93
        return $builder->where(function (Builder $query) use ($fieldsByType, $isFirstField) {
94 View Code Duplication
            foreach ($fieldsByType['and'] as $fieldName => $fieldQuery) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
95
                if (str_contains($fieldName, '.')) {
96
                    $this->formatWhereHasClause($query, $fieldName, $fieldQuery, true);
97
                } else {
98
                    $this->formatWhereClause($query, $fieldName, $fieldQuery, true);
99
                }
100
            }
101
102
            $isOrAlone = count($fieldsByType['or']) > 1 ? 'where' : 'orWhere';
103
            $query->{$isOrAlone}(function ($query) use ($fieldsByType) {
104 View Code Duplication
                foreach ($fieldsByType['or'] as $fieldName => $fieldQuery) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
105
                    if (str_contains($fieldName, '.')) {
106
                        $this->formatWhereHasClause($query, $fieldName, $fieldQuery);
107
                    } else {
108
                        $this->formatWhereClause($query, $fieldName, $fieldQuery);
109
                    }
110
                }
111
            });
112
113
            return $query;
114
        });
115
    }
116
117
    /**
118
     * format fields from query.
119
     *
120
     * @param mixed $query
121
     *
122
     * @return array []
123
     */
124
    protected function parseQuery($query)
125
    {
126
        $query = $this->hasArray($query);
127
        $result = ['or' => [], 'and' => []];
128
129
        foreach ($query as $fieldName => $value) {
130
            list($type, $tempValue) = $this->parseCondition($value);
131
132
            if (in_array($fieldName, $this->validFields, true)) {
133
                $result[$type][$fieldName][] = $tempValue;
134
            } elseif (is_int($fieldName)) {
135
                foreach ($this->validFields as $validField) {
136
                    $result[$type][$validField][] = $tempValue;
137
                }
138
            }
139
        }
140
141
        return $result;
142
    }
143
144
    /**
145
     * parse condition symbol and determine if force where is needed.
146
     *
147
     * @param $value
148
     *
149
     * @return array
150
     */
151
    protected function parseCondition($value)
152
    {
153
        $segments = explode($this->secondary, $value);
154
        $condition = $this->defaultCondition;
155
        $forceWhere = false;
156
157
        if (count($segments) > 1) {
158
            $tempCondition = $segments[1];
159
            if (str_contains($tempCondition, $this->forceWhereSymbol)) {
160
                $forceWhere = true;
161
                $tempCondition = str_replace($this->forceWhereSymbol, '', $tempCondition);
162
            }
163
164
            $condition = in_array($tempCondition, $this->acceptedConditions) ? $tempCondition : $this->defaultCondition;
165
        }
166
167
        return [
168
            $forceWhere ? 'and' : 'or',
169
            [
170
                'value'     => $segments[0],
171
                'condition' => $condition,
172
            ],
173
        ];
174
    }
175
176
    protected function mendSpecificFields($parameters)
177
    {
178
        if (in_array($parameters['condition'], ['like', 'ilike'])) {
179
            $parameters['value'] = "%{$parameters['value']}%";
180
        }
181
182
        // todo: implements check with casts typing from model
183
184
        return $parameters;
185
    }
186
187
    protected function formatWhereHasClause(Builder $query, $fieldName, $fieldQuery, $force = false)
188
    {
189
        foreach ($fieldQuery as $parameters) {
190
            $parameters = $this->mendSpecificFields($parameters);
191
            $method = $force ? 'whereHas' : 'orWhereHas';
192
            $temp = explode('.', $fieldName);
193
            $relation = $temp[0];
194
            $fieldName = $temp[1];
195
196
            if (method_exists($this->model, $relation)) {
197
                $query->{$method}($relation, function (Builder $query) use ($relation, $fieldName, $parameters) {
198
                    $query->where(
199
                        "{$relation}.{$fieldName}",
200
                        $parameters['condition'],
201
                        $parameters['value']
202
                    );
203
                });
204
            } else {
205
                throw new \InvalidArgumentException("Relation {$relation} not implemented in ".get_class($this->model));
206
            }
207
        }
208
209
        return $query;
210
    }
211
212
    /**
213
     * parse force where symbol.
214
     *
215
     * @param Builder $query
216
     * @param string  $fieldName
217
     * @param arry    $fieldQuery
218
     * @param bool    $force
219
     *
220
     * @return Builder
221
     */
222
    protected function formatWhereClause(Builder $query, $fieldName, $fieldQuery, $force = false)
223
    {
224
        foreach ($fieldQuery as $parameters) {
225
            $parameters = $this->mendSpecificFields($parameters);
226
            $method = $force ? 'where' : 'orWhere';
227
228
            $query->{$method}(
229
                "{$this->modelTableName}.{$fieldName}",
230
                $parameters['condition'],
231
                $parameters['value']
232
            );
233
        }
234
235
        return $query;
236
    }
237
}
238