FiltersQueries   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 162
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 33
eloc 67
c 1
b 0
f 0
dl 0
loc 162
rs 9.76

6 Methods

Rating   Name   Duplication   Size   Complexity  
A applyRelationFilter() 0 12 4
C filter() 0 42 16
A getCaseInsensitiveLikeOperator() 0 6 2
A getKeyForFilter() 0 9 3
A applyFilters() 0 6 1
B applyFiltersRecursively() 0 27 7
1
<?php
2
3
namespace Bakery\Traits;
4
5
use Illuminate\Support\Str;
6
use Bakery\Support\Arguments;
7
use Bakery\Eloquent\ModelSchema;
8
use Bakery\Support\TypeRegistry;
9
use Illuminate\Support\Facades\DB;
10
use Illuminate\Database\Connection;
11
use Bakery\Types\CollectionFilterType;
12
use Illuminate\Database\Query\Grammars;
13
use Illuminate\Database\Eloquent\Builder;
14
15
/**
16
 * @property ModelSchema $modelSchema
17
 * @property TypeRegistry $registry
18
 */
19
trait FiltersQueries
20
{
21
    /**
22
     * Filter the query based on the filter argument.
23
     *
24
     * @param \Illuminate\Database\Eloquent\Builder $query
25
     * @param Arguments $args
26
     * @return \Illuminate\Database\Eloquent\Builder
27
     */
28
    protected function applyFilters(Builder $query, Arguments $args): Builder
29
    {
30
        // We wrap the query in a closure to make sure it
31
        // does not clash with other (scoped) queries that are on the builder.
32
        return $query->where(function ($query) use ($args) {
33
            return $this->applyFiltersRecursively($query, $args);
34
        });
35
    }
36
37
    /**
38
     * Apply filters recursively.
39
     *
40
     * @param \Illuminate\Database\Eloquent\Builder $query
41
     * @param Arguments $args
42
     * @param mixed $type
43
     * @return \Illuminate\Database\Eloquent\Builder
44
     */
45
    protected function applyFiltersRecursively(Builder $query, Arguments $args = null, $type = null): Builder
46
    {
47
        foreach ($args as $filter => $value) {
48
            if ($filter === 'AND' || $filter === 'OR') {
49
                $query->where(function ($query) use ($value, $filter) {
50
                    foreach ($value as $set) {
51
                        if (! empty($set)) {
52
                            $this->applyFiltersRecursively($query, $set, $filter);
53
                        }
54
                    }
55
                });
56
            } else {
57
                $key = $this->getKeyForFilter($filter);
58
                $schema = $this->registry->resolveSchemaForModel(get_class($query->getModel()));
59
                $field = $schema->getFieldByKey($key);
60
61
                if ($field->isRelationship()) {
62
                    $relation = $field->getAccessor();
63
                    $this->applyRelationFilter($query, $relation, $value, $type);
64
                } else {
65
                    $column = $field->getAccessor();
66
                    $this->filter($query, $filter, $column, $value, $type);
67
                }
68
            }
69
        }
70
71
        return $query;
72
    }
73
74
    /**
75
     * Filter the query based on the filter argument that contain relations.
76
     *
77
     * @param \Illuminate\Database\Eloquent\Builder $query
78
     * @param string $relation
79
     * @param Arguments $args
80
     * @param string $type
81
     * @return \Illuminate\Database\Eloquent\Builder
82
     */
83
    protected function applyRelationFilter(Builder $query, string $relation, Arguments $args = null, $type = null): Builder
84
    {
85
        $count = 1;
86
        $operator = '>=';
87
        $type = $type ?: 'and';
88
89
        if (! $args || $args->isEmpty()) {
90
            return $query->doesntHave($relation, $type);
91
        }
92
93
        return $query->has($relation, $operator, $count, $type, function ($subQuery) use ($args) {
94
            return $this->applyFiltersRecursively($subQuery, $args);
95
        });
96
    }
97
98
    /**
99
     * Filter the query by a key and value.
100
     *
101
     * @param \Illuminate\Database\Eloquent\Builder $query
102
     * @param string $key
103
     * @param string $column
104
     * @param mixed $value
105
     * @param string $type (AND or OR)
106
     * @return \Illuminate\Database\Eloquent\Builder
107
     */
108
    protected function filter(Builder $query, string $key, string $column, $value, $type): Builder
109
    {
110
        $type = $type ?: 'AND';
111
112
        $likeOperator = $this->getCaseInsensitiveLikeOperator();
113
114
        $table = $query->getModel()->getTable();
115
        $qualifiedColumn = $table.'.'.$column;
116
117
        $value = $value instanceof Arguments ? $value->toArray() : $value;
118
119
        if (Str::endsWith($key, 'NotContains')) {
120
            $query->where($qualifiedColumn, 'NOT '.$likeOperator, '%'.$value.'%', $type);
0 ignored issues
show
Bug introduced by
Are you sure $value of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

120
            $query->where($qualifiedColumn, 'NOT '.$likeOperator, '%'./** @scrutinizer ignore-type */ $value.'%', $type);
Loading history...
121
        } elseif (Str::endsWith($key, 'Contains')) {
122
            $query->where($qualifiedColumn, $likeOperator, '%'.$value.'%', $type);
123
        } elseif (Str::endsWith($key, 'NotStartsWith')) {
124
            $query->where($qualifiedColumn, 'NOT '.$likeOperator, $value.'%', $type);
125
        } elseif (Str::endsWith($key, 'StartsWith')) {
126
            $query->where($qualifiedColumn, $likeOperator, $value.'%', $type);
127
        } elseif (Str::endsWith($key, 'NotEndsWith')) {
128
            $query->where($qualifiedColumn, 'NOT '.$likeOperator, '%'.$value, $type);
129
        } elseif (Str::endsWith($key, 'EndsWith')) {
130
            $query->where($qualifiedColumn, $likeOperator, '%'.$value, $type);
131
        } elseif (Str::endsWith($key, 'Not')) {
132
            $query->where($qualifiedColumn, '!=', $value, $type);
133
        } elseif (Str::endsWith($key, 'NotIn')) {
134
            $query->whereNotIn($qualifiedColumn, $value, $type);
135
        } elseif (Str::endsWith($key, 'In')) {
136
            $query->whereIn($qualifiedColumn, $value, $type);
137
        } elseif (Str::endsWith($key, 'LessThan')) {
138
            $query->where($qualifiedColumn, '<', $value, $type);
139
        } elseif (Str::endsWith($key, 'LessThanOrEquals')) {
140
            $query->where($qualifiedColumn, '<=', $value, $type);
141
        } elseif (Str::endsWith($key, 'GreaterThan')) {
142
            $query->where($qualifiedColumn, '>', $value, $type);
143
        } elseif (Str::endsWith($key, 'GreaterThanOrEquals')) {
144
            $query->where($qualifiedColumn, '>=', $value, $type);
145
        } else {
146
            $query->where($qualifiedColumn, '=', $value, $type);
147
        }
148
149
        return $query;
150
    }
151
152
    /**
153
     * Check if the current database grammar supports the insensitive like operator.
154
     *
155
     * @return string
156
     */
157
    protected function getCaseInsensitiveLikeOperator()
158
    {
159
        /** @var Connection $connection */
160
        $connection = DB::connection();
161
162
        return $connection->getQueryGrammar() instanceof Grammars\PostgresGrammar ? 'ILIKE' : 'LIKE';
163
    }
164
165
    /**
166
     * Get the key for a certain filter.
167
     * E.g. TitleStartsWith => Title.
168
     *
169
     * @param string $subject
170
     * @return string
171
     */
172
    protected function getKeyForFilter(string $subject): string
173
    {
174
        foreach (CollectionFilterType::$filters as $filter) {
175
            if (Str::endsWith($subject, $filter)) {
176
                return Str::before($subject, $filter);
177
            }
178
        }
179
180
        return $subject;
181
    }
182
}
183