Passed
Pull Request — master (#176)
by Wilmer
10:44
created

QueryTrait::limit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Query;
6
7
use Yiisoft\Db\Exception\NotSupportedException;
8
use Yiisoft\Db\Expression\ExpressionInterface;
9
10
/**
11
 * The BaseQuery trait represents the minimum method set of a database Query.
12
 *
13
 * It is supposed to be used in a class that implements the {@see QueryInterface}.
14
 */
15
trait QueryTrait
16
{
17
    private $where;
18
    private $limit;
19
    private $offset;
20
    private array $orderBy = [];
21
    private $indexBy;
22
    private bool $emulateExecution = false;
23
24
    /**
25
     * Sets the {@see indexBy} property.
26
     *
27
     * @param string|callable $column the name of the column by which the query results should be indexed by.
28
     *
29
     * This can also be a callable (e.g. anonymous function) that returns the index value based on the given row data.
30
     *
31
     * The signature of the callable should be:
32
     *
33
     * ```php
34
     * function ($row)
35
     * {
36
     *     // return the index value corresponding to $row
37
     * }
38
     * ```
39
     *
40
     * @return $this the query object itself
41
     */
42 4
    public function indexBy($column): self
43
    {
44 4
        $this->indexBy = $column;
45
46 4
        return $this;
47
    }
48
49
    /**
50
     * Sets the WHERE part of the query.
51
     *
52
     * See {@see QueryInterface::where()} for detailed documentation.
53
     *
54
     * @param array $condition the conditions that should be put in the WHERE part.
55
     *
56
     * @return $this the query object itself
57
     *
58
     * {@see andWhere()}
59
     * {@see orWhere()}
60
     */
61
    public function where(array $condition): self
62
    {
63
        $this->where = $condition;
64
65
        return $this;
66
    }
67
68
    /**
69
     * Adds an additional WHERE condition to the existing one.
70
     *
71
     * The new condition and the existing one will be joined using the 'AND' operator.
72
     *
73
     * @param array $condition the new WHERE condition. Please refer to {@see where()} on how to specify this parameter.
74
     *
75
     * @return $this the query object itself
76
     *
77
     * {@see where()}
78
     * {@see orWhere()}
79
     */
80
    public function andWhere($condition): self
81
    {
82
        if ($this->where === null) {
83
            $this->where = $condition;
84
        } else {
85
            $this->where = ['and', $this->where, $condition];
86
        }
87
88
        return $this;
89
    }
90
91
    /**
92
     * Adds an additional WHERE condition to the existing one.
93
     *
94
     * The new condition and the existing one will be joined using the 'OR' operator.
95
     *
96
     * @param array $condition the new WHERE condition. Please refer to {@see where()} on how to specify this parameter.
97
     *
98
     * @return $this the query object itself
99
     *
100
     * {@see where()}
101
     * {@see andWhere()}
102
     */
103
    public function orWhere($condition): self
104
    {
105
        if ($this->where === null) {
106
            $this->where = $condition;
107
        } else {
108
            $this->where = ['or', $this->where, $condition];
109
        }
110
111
        return $this;
112
    }
113
114
    /**
115
     * Sets the WHERE part of the query but ignores [[isEmpty()|empty operands]].
116
     *
117
     * This method is similar to {@see where()}. The main difference is that this method will remove
118
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
119
     * based on filter values entered by users.
120
     *
121
     * The following code shows the difference between this method and {@see where()}:
122
     *
123
     * ```php
124
     * // WHERE `age`=:age
125
     * $query->filterWhere(['name' => null, 'age' => 20]);
126
     * // WHERE `age`=:age
127
     * $query->where(['age' => 20]);
128
     * // WHERE `name` IS NULL AND `age`=:age
129
     * $query->where(['name' => null, 'age' => 20]);
130
     * ```
131
     *
132
     * Note that unlike {@see where()}, you cannot pass binding parameters to this method.
133
     *
134
     * @param array $condition the conditions that should be put in the WHERE part.
135
     *
136
     * See {@see where()} on how to specify this parameter.
137
     *
138
     * @return $this the query object itself
139
     *
140
     * {@see where()}
141
     * {@see andFilterWhere()}
142
     * {@see orFilterWhere()}
143
     */
144 25
    public function filterWhere(array $condition): self
145
    {
146 25
        $condition = $this->filterCondition($condition);
147
148 25
        if ($condition !== []) {
149 8
            $this->where($condition);
0 ignored issues
show
Bug introduced by
It seems like $condition can also be of type string; however, parameter $condition of Yiisoft\Db\Query\QueryTrait::where() does only seem to accept array, 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

149
            $this->where(/** @scrutinizer ignore-type */ $condition);
Loading history...
150
        }
151
152 25
        return $this;
153
    }
154
155
    /**
156
     * Adds an additional WHERE condition to the existing one but ignores {@see isEmpty()|empty operands}.
157
     *
158
     * The new condition and the existing one will be joined using the 'AND' operator.
159
     *
160
     * This method is similar to {@see andWhere()}. The main difference is that this method will remove
161
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
162
     * based on filter values entered by users.
163
     *
164
     * @param array $condition the new WHERE condition. Please refer to {@see where()} on how to specify this parameter.
165
     *
166
     * @return $this the query object itself
167
     *
168
     * {@see filterWhere()}
169
     * {@see orFilterWhere()}
170
     */
171 3
    public function andFilterWhere(array $condition): self
172
    {
173 3
        $condition = $this->filterCondition($condition);
174
175 3
        if ($condition !== []) {
176 1
            $this->andWhere($condition);
0 ignored issues
show
Bug introduced by
It seems like $condition can also be of type string; however, parameter $condition of Yiisoft\Db\Query\QueryTrait::andWhere() does only seem to accept array, 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

176
            $this->andWhere(/** @scrutinizer ignore-type */ $condition);
Loading history...
177
        }
178
179 3
        return $this;
180
    }
181
182
    /**
183
     * Adds an additional WHERE condition to the existing one but ignores {@see isEmpty()|empty operands}.
184
     *
185
     * The new condition and the existing one will be joined using the 'OR' operator.
186
     *
187
     * This method is similar to {@see orWhere()}. The main difference is that this method will remove
188
     * {@see isEmpty()|empty query operands}. As a result, this method is best suited for building query conditions
189
     * based on filter values entered by users.
190
     *
191
     * @param array $condition the new WHERE condition. Please refer to {@see where()} on how to specify this parameter.
192
     *
193
     * @return $this the query object itself
194
     *
195
     * {@see filterWhere()}
196
     * {@see andFilterWhere()}
197
     */
198 2
    public function orFilterWhere(array $condition): self
199
    {
200 2
        $condition = $this->filterCondition($condition);
201
202 2
        if ($condition !== []) {
203
            $this->orWhere($condition);
0 ignored issues
show
Bug introduced by
It seems like $condition can also be of type string; however, parameter $condition of Yiisoft\Db\Query\QueryTrait::orWhere() does only seem to accept array, 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

203
            $this->orWhere(/** @scrutinizer ignore-type */ $condition);
Loading history...
204
        }
205
206 2
        return $this;
207
    }
208
209
    /**
210
     * Removes {@see isEmpty()|empty operands} from the given query condition.
211
     *
212
     * @param array|string $condition the original condition
213
     *
214
     * @throws NotSupportedException if the condition operator is not supported
215
     *
216
     * @return array|string the condition with {@see isEmpty()|empty operands} removed.
217
     */
218 28
    protected function filterCondition($condition)
219
    {
220 28
        if (!\is_array($condition)) {
221 7
            return $condition;
222
        }
223
224 28
        if (!isset($condition[0])) {
225
            // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
226 3
            foreach ($condition as $name => $value) {
227 3
                if ($this->isEmpty($value)) {
228 2
                    unset($condition[$name]);
229
                }
230
            }
231
232 3
            return $condition;
233
        }
234
235
        // operator format: operator, operand 1, operand 2, ...
236
237 26
        $operator = \array_shift($condition);
238
239 26
        switch (strtoupper($operator)) {
240 26
            case 'NOT':
241 25
            case 'AND':
242 22
            case 'OR':
243 10
                foreach ($condition as $i => $operand) {
244 10
                    $subCondition = $this->filterCondition($operand);
245 10
                    if ($this->isEmpty($subCondition)) {
246 10
                        unset($condition[$i]);
247
                    } else {
248 6
                        $condition[$i] = $subCondition;
249
                    }
250
                }
251
252 10
                if (empty($condition)) {
253 5
                    return [];
254
                }
255
256 6
                break;
257 19
            case 'BETWEEN':
258 18
            case 'NOT BETWEEN':
259 4
                if (\array_key_exists(1, $condition) && \array_key_exists(2, $condition)) {
260 4
                    if ($this->isEmpty($condition[1]) || $this->isEmpty($condition[2])) {
261 4
                        return [];
262
                    }
263
                }
264
265
                break;
266
            default:
267 17
                if (\array_key_exists(1, $condition) && $this->isEmpty($condition[1])) {
268 17
                    return [];
269
                }
270
        }
271
272 9
        \array_unshift($condition, $operator);
273
274 9
        return $condition;
275
    }
276
277
    /**
278
     * Returns a value indicating whether the give value is "empty".
279
     *
280
     * The value is considered "empty", if one of the following conditions is satisfied:
281
     *
282
     * - it is `null`,
283
     * - an empty string (`''`),
284
     * - a string containing only whitespace characters,
285
     * - or an empty array.
286
     *
287
     * @param mixed $value
288
     *
289
     * @return bool if the value is empty
290
     */
291 28
    protected function isEmpty($value): bool
292
    {
293 28
        return $value === '' || $value === [] || $value === null || (\is_string($value) && trim($value) === '');
294
    }
295
296
    /**
297
     * Sets the ORDER BY part of the query.
298
     *
299
     * @param string|array|ExpressionInterface $columns the columns (and the directions) to be ordered by.
300
     *
301
     * Columns can be specified in either a string (e.g. `"id ASC, name DESC"`) or an array
302
     * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
303
     *
304
     * The method will automatically quote the column names unless a column contains some parenthesis
305
     * (which means the column contains a DB expression).
306
     *
307
     * Note that if your order-by is an expression containing commas, you should always use an array
308
     * to represent the order-by information. Otherwise, the method will not be able to correctly determine
309
     * the order-by columns.
310
     *
311
     * Since {@see ExpressionInterface} object can be passed to specify the ORDER BY part explicitly in plain SQL.
312
     *
313
     * @return $this the query object itself
314
     *
315
     * {@see addOrderBy()}
316
     */
317 9
    public function orderBy($columns): self
318
    {
319 9
        $this->orderBy = $this->normalizeOrderBy($columns);
320
321 9
        return $this;
322
    }
323
324
    /**
325
     * Adds additional ORDER BY columns to the query.
326
     *
327
     * @param string|array|ExpressionInterface $columns the columns (and the directions) to be ordered by.
328
     * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
329
     * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
330
     *
331
     * The method will automatically quote the column names unless a column contains some parenthesis
332
     * (which means the column contains a DB expression).
333
     *
334
     * Note that if your order-by is an expression containing commas, you should always use an array
335
     * to represent the order-by information. Otherwise, the method will not be able to correctly determine
336
     * the order-by columns.
337
     *
338
     * Since {@see ExpressionInterface} object can be passed to specify the ORDER BY part explicitly in plain SQL.
339
     *
340
     * @return $this the query object itself
341
     *
342
     * {@see orderBy()}
343
     */
344 1
    public function addOrderBy($columns): self
345
    {
346 1
        $columns = $this->normalizeOrderBy($columns);
347 1
        if ($this->orderBy === null) {
348
            $this->orderBy = $columns;
349
        } else {
350 1
            $this->orderBy = \array_merge($this->orderBy, $columns);
351
        }
352
353 1
        return $this;
354
    }
355
356
    /**
357
     * Normalizes format of ORDER BY data.
358
     *
359
     * @param array|string|ExpressionInterface $columns the columns value to normalize.
360
     *
361
     * See {@see orderBy} and {@see addOrderBy}.
362
     *
363
     * @return array
364
     */
365 9
    protected function normalizeOrderBy($columns): array
366
    {
367 9
        if ($columns instanceof ExpressionInterface) {
368 2
            return [$columns];
369
        }
370
371 9
        if (\is_array($columns)) {
372 4
            return $columns;
373
        }
374
375 7
        $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
376 7
        $result = [];
377 7
        foreach ($columns as $column) {
378 7
            if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) {
379 2
                $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC;
380
            } else {
381 6
                $result[$column] = SORT_ASC;
382
            }
383
        }
384
385 7
        return $result;
386
    }
387
388
    /**
389
     * Sets the LIMIT part of the query.
390
     *
391
     * @param int|ExpressionInterface|null $limit the limit. Use null or negative value to disable limit.
392
     *
393
     * @return $this the query object itself
394
     */
395 6
    public function limit($limit): self
396
    {
397 6
        $this->limit = $limit;
398
399 6
        return $this;
400
    }
401
402
    /**
403
     * Sets the OFFSET part of the query.
404
     *
405
     * @param int|ExpressionInterface|null $offset the offset. Use null or negative value to disable offset.
406
     *
407
     * @return $this the query object itself
408
     */
409 3
    public function offset($offset): self
410
    {
411 3
        $this->offset = $offset;
412
413 3
        return $this;
414
    }
415
416
    /**
417
     * Sets whether to emulate query execution, preventing any interaction with data storage.
418
     *
419
     * After this mode is enabled, methods, returning query results like {@see QueryInterface::one()},
420
     * {@see QueryInterface::all()}, {@see QueryInterface::exists()} and so on, will return empty or false values.
421
     * You should use this method in case your program logic indicates query should not return any results, like
422
     * in case you set false where condition like `0=1`.
423
     *
424
     * @param bool $value whether to prevent query execution.
425
     *
426
     * @return $this the query object itself.
427
     */
428 1
    public function emulateExecution(bool $value = true): self
429
    {
430 1
        $this->emulateExecution = $value;
431
432 1
        return $this;
433
    }
434
435 217
    public function getWhere()
436
    {
437 217
        return $this->where;
438
    }
439
440 213
    public function getLimit()
441
    {
442 213
        return $this->limit;
443
    }
444
445 213
    public function getOffset()
446
    {
447 213
        return $this->offset;
448
    }
449
450 213
    public function getOrderBy(): array
451
    {
452 213
        return $this->orderBy;
453
    }
454
455 2
    public function getIndexBy()
456
    {
457 2
        return $this->indexBy;
458
    }
459
460
    public function isEmulateExecution(): bool
461
    {
462
        return $this->emulateExecution;
463
    }
464
}
465