Issues (25)

src/QueryBuilder.php (1 issue)

1
<?php
2
3
namespace Laraplus\Data;
4
5
use Closure;
6
use Illuminate\Support\Str;
7
use InvalidArgumentException;
8
use Illuminate\Database\Query\Builder;
9
use Illuminate\Database\Query\Expression;
10
use Illuminate\Database\Eloquent\Model as Eloquent;
11
use Illuminate\Database\Query\Grammars\MySqlGrammar;
12
use Illuminate\Database\Query\Grammars\SQLiteGrammar;
13
use Illuminate\Database\Query\Grammars\PostgresGrammar;
14
use Illuminate\Database\Query\Grammars\SqlServerGrammar;
15
16
class QueryBuilder extends Builder
17
{
18
    protected $model;
19
20
    /**
21
     * Set a model instance.
22
     *
23
     * @param Eloquent $model
24
     *
25
     * @return $this
26
     */
27
    public function setModel(Eloquent $model)
28
    {
29
        $this->model = $model;
30
31
        return $this;
32
    }
33
34
    /**
35
     * Set the columns to be selected.
36
     *
37
     * @param array|mixed $columns
38
     *
39
     * @return $this
40
     *
41
     * @throws \Exception
42
     */
43
    public function select($columns = ['*'])
44
    {
45
        parent::select($columns);
46
47
        $this->columns = $this->qualifyColumns($this->columns);
48
49
        return $this;
50
    }
51
52
    /**
53
     * Add a new select column to the query.
54
     *
55
     * @param array|mixed $column
56
     *
57
     * @return $this
58
     *
59
     * @throws \Exception
60
     */
61
    public function addSelect($column)
62
    {
63
        $column = $this->qualifyColumns(is_array($column) ? $column : func_get_args());
64
65
        $this->columns = array_merge((array) $this->columns, $column);
66
67
        return $this;
68
    }
69
70
    /**
71
     * Qualify translated columns.
72
     *
73
     * @param $columns
74
     *
75
     * @return mixed
76
     *
77
     * @throws \Exception
78
     */
79
    protected function qualifyColumns($columns)
80
    {
81
        foreach ($columns as &$column) {
82
            if (!in_array($column, $this->model->translatableAttributes())) {
83
                continue;
84
            }
85
86
            $primary = $this->qualifyTranslationColumn($column);
87
            $fallback = $this->qualifyTranslationColumn($column, true);
88
89
            if ($this->model->shouldFallback()) {
90
                $column = new Expression($this->compileIfNull($primary, $fallback, $column));
91
            } else {
92
                $column = $primary;
93
            }
94
        }
95
96
        return $columns;
97
    }
98
99
    /**
100
     * Add a where clause to the query.
101
     *
102
     * @param string|Closure $column
103
     * @param string $operator
104
     * @param mixed $value
105
     * @param string $boolean
106
     *
107
     * @return Builder|QueryBuilder
108
     *
109
     * @throws \InvalidArgumentException
110
     * @throws \Exception
111
     */
112
    public function where($column, $operator = null, $value = null, $boolean = 'and')
113
    {
114
        // If the column is an array, we will assume it is an array of key-value pairs
115
        // and can add them each as a where clause. We will maintain the boolean we
116
        // received when the method was called and pass it into the nested where.
117
        if (is_array($column)) {
118
            return $this->addArrayOfWheres($column, $boolean);
119
        }
120
121
        // Then we need to check if we are dealing with a translated column and defer
122
        // to the "whereTranslated" clause in that case. That way the user doesn't
123
        // need to worry about translated columns and let us handle the details.
124
        if (in_array($column, $this->model->translatableAttributes())) {
125
            return $this->whereTranslated($column, $operator, $value, $boolean);
126
        }
127
128
        return parent::where($column, $operator, $value, $boolean);
129
    }
130
131
    /**
132
     * Add a where clause to the query and don't modify it for i18n.
133
     *
134
     * @param string|Closure $column
135
     * @param string $operator
136
     * @param mixed $value
137
     * @param string $boolean
138
     *
139
     * @return Builder|QueryBuilder
140
     *
141
     * @throws \InvalidArgumentException
142
     */
143
    public function whereOriginal($column, $operator = null, $value = null, $boolean = 'and')
144
    {
145
        return parent::where($column, $operator, $value, $boolean);
146
    }
147
148
    /**
149
     * Add a translation where clause to the query.
150
     *
151
     * @param string|Closure $column
152
     * @param string $operator
153
     * @param mixed $value
154
     * @param string $boolean
155
     *
156
     * @return $this
157
     *
158
     * @throws \InvalidArgumentException
159
     * @throws \Exception
160
     */
161
    public function whereTranslated($column, $operator = null, $value = null, $boolean = 'and')
162
    {
163
        // Here we will make some assumptions about the operator. If only 2 values are
164
        // passed to the method, we will assume that the operator is an equals sign
165
        // and keep going. Otherwise, we'll require the operator to be passed in.
166
        if (func_num_args() == 2) {
167
            [$value, $operator] = [$operator, '='];
168
        } elseif ($this->invalidOperatorAndValue($operator, $value)) {
169
            throw new InvalidArgumentException('Illegal operator and value combination.');
170
        }
171
172
        // If the given operator is not found in the list of valid operators we will
173
        // assume that the developer is just short-cutting the '=' operators, and
174
        // we will set the operators to '=' and set the values appropriately.
175
        if (!in_array(strtolower($operator), $this->operators, true)) {
0 ignored issues
show
It seems like $operator can also be of type null; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

175
        if (!in_array(strtolower(/** @scrutinizer ignore-type */ $operator), $this->operators, true)) {
Loading history...
176
            [$value, $operator] = [$operator, '='];
177
        }
178
179
        $fallbackColumn = $this->qualifyTranslationColumn($column, true);
180
        $column = $this->qualifyTranslationColumn($column);
181
182
        // Finally, we'll check whether we need to consider fallback translations. In
183
        // that case we need to create a complex "ifnull" clause, otherwise we can
184
        // just prepend the translation alias and add the where clause normally.
185
        if (!$this->model->shouldFallback() || $column instanceof Closure) {
186
            return $this->where($column, $operator, $value, $boolean);
187
        }
188
189
        $condition = $this->compileIfNull($column, $fallbackColumn);
190
191
        return $this->whereRaw("$condition $operator ?", [$value], $boolean);
192
    }
193
194
    /**
195
     * Add a translation or where clause to the query.
196
     *
197
     * @param string|array|Closure $column
198
     * @param string $operator
199
     * @param mixed $value
200
     *
201
     * @return $this
202
     *
203
     * @throws \InvalidArgumentException
204
     * @throws \Exception
205
     */
206
    public function orWhereTranslated($column, $operator = null, $value = null)
207
    {
208
        return $this->whereTranslated($column, $operator, $value, 'or');
209
    }
210
211
    /**
212
     * Add a full sub-select to the query.
213
     *
214
     * @param string $column
215
     * @param Builder $query
216
     * @param string $boolean
217
     *
218
     * @return $this
219
     */
220
    public function whereSubQuery($column, $query, $boolean = 'and')
221
    {
222
        [$type, $operator] = ['Sub', 'in'];
223
224
        $this->wheres[] = compact('type', 'column', 'operator', 'query', 'boolean');
225
        $this->addBinding($query->getBindings(), 'where');
226
227
        return $this;
228
    }
229
230
    /**
231
     * Add an "order by" clause by translated column to the query.
232
     *
233
     * @param string $column
234
     * @param string $direction
235
     *
236
     * @return Builder|QueryBuilder
237
     *
238
     * @throws \Exception
239
     */
240
    public function orderBy($column, $direction = 'asc')
241
    {
242
        if (in_array($column, $this->model->translatableAttributes())) {
243
            return $this->orderByTranslated($column, $direction);
244
        }
245
246
        return parent::orderBy($column, $direction);
247
    }
248
249
    /**
250
     * Add an "order by" clause by translated column to the query.
251
     *
252
     * @param string $column
253
     * @param string $direction
254
     *
255
     * @return $this
256
     *
257
     * @throws \Exception
258
     */
259
    public function orderByTranslated($column, $direction = 'asc')
260
    {
261
        $fallbackColumn = $this->qualifyTranslationColumn($column, true);
262
        $column = $this->qualifyTranslationColumn($column);
263
264
        if (!$this->model->shouldFallback()) {
265
            return $this->orderBy($column, $direction);
266
        }
267
268
        $condition = $this->compileIfNull($column, $fallbackColumn);
269
270
        return $this->orderByRaw("{$condition} {$direction}");
271
    }
272
273
    /**
274
     * Qualify translation column.
275
     *
276
     * @param $column
277
     * @param $fallback
278
     *
279
     * @return string
280
     */
281
    protected function qualifyTranslationColumn($column, $fallback = false)
282
    {
283
        $alias = $this->model->getI18nTable();
284
        $fallback = $fallback ? '_fallback' : '';
285
286
        if (Str::contains($column, '.')) {
287
            [$table, $field] = explode('.', $column);
288
            $suffix = $this->model->getTranslationTableSuffix();
289
290
            return Str::endsWith($alias, $suffix) ?
291
                "{$table}{$fallback}.{$field}" : "{$table}{$suffix}{$fallback}.{$field}";
292
        }
293
294
        return "{$alias}{$fallback}.{$column}";
295
    }
296
297
    /**
298
     * Compile if null.
299
     *
300
     * @param string $primary
301
     * @param string $fallback
302
     * @param string|null $alias
303
     *
304
     * @return string
305
     *
306
     * @throws \Exception
307
     */
308
    public function compileIfNull($primary, $fallback, $alias = null)
309
    {
310
        if ($this->grammar instanceof SqlServerGrammar) {
311
            $ifNull = 'isnull';
312
        } elseif ($this->grammar instanceof MySqlGrammar || $this->grammar instanceof SQLiteGrammar) {
313
            $ifNull = 'ifnull';
314
        } elseif ($this->grammar instanceof PostgresGrammar) {
315
            $ifNull = 'coalesce';
316
        } else {
317
            throw new \Exception('Cannot compile IFNULL statement for grammar '.get_class($this->grammar));
318
        }
319
320
        $primary = $this->grammar->wrap($primary);
321
        $fallback = $this->grammar->wrap($fallback);
322
        $alias = $alias ? ' as '.$this->grammar->wrap($alias) : '';
323
324
        return "{$ifNull}($primary, $fallback)".$alias;
325
    }
326
327
    /**
328
     * Get a new instance of the query builder.
329
     *
330
     * @return Builder
331
     */
332
    public function newQuery()
333
    {
334
        $query = parent::newQuery();
335
336
        return $query->setModel($this->model);
337
    }
338
}
339