Completed
Branch develop (85a9c8)
by Anton
05:44
created

AbstractWhere::whereWrapper()   C

Complexity

Conditions 7
Paths 1

Size

Total Lines 25
Code Lines 11

Duplication

Lines 25
Ratio 100 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 25
loc 25
rs 6.7272
cc 7
eloc 11
nc 1
nop 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Database\Builders\Prototypes;
9
10
use Spiral\Database\Entities\QueryBuilder;
11
use Spiral\Database\Exceptions\BuilderException;
12
use Spiral\Database\Injections\ExpressionInterface;
13
use Spiral\Database\Injections\FragmentInterface;
14
use Spiral\Database\Injections\Parameter;
15
use Spiral\Database\Injections\ParameterInterface;
16
17
/**
18
 * Abstract query with WHERE conditions generation support. Provides simplified way to generate
19
 * WHERE tokens using set of where methods. Class support different where conditions, simplified
20
 * definitions
21
 * (using arrays) and closures to describe nested conditions:
22
 *
23
 * 1) Simple token/nested query or expression
24
 * $select->where(new SQLFragment('(SELECT count(*) from `table`)'));
25
 *
26
 * 2) Simple assessment
27
 * $select->where('column', $value);
28
 * $select->where('column', new SQLFragment('CONCAT(columnA, columnB)'));
29
 *
30
 * 3) Assessment with specified operator (operator will be converted to uppercase automatically)
31
 * $select->where('column', '=', $value);
32
 * $select->where('column', 'IN', [1, 2, 3]);
33
 * $select->where('column', 'LIKE', $string);
34
 * $select->where('column', 'IN', new SQLFragment('(SELECT id from `table` limit 1)'));
35
 *
36
 * 4) Between and not between statements
37
 * $select->where('column', 'between', 1, 10);
38
 * $select->where('column', 'not between', 1, 10);
39
 * $select->where('column', 'not between', new SQLFragment('MIN(price)'), $maximum);
40
 *
41
 * 5) Closure with nested conditions
42
 * $this->where(function(AbstractWhere $select){
43
 *      $select->where("name", "Wolfy-J")->orWhere("balance", ">", 100)
44
 * });
45
 *
46
 * 6) Simplified array based condition definition
47
 * $select->where(["column" => 1]);
48
 * $select->where(["column" => [
49
 *      ">" => 1,
50
 *      "<" => 10
51
 * ]]);
52
 *
53
 * Tokens "@or" and "@and" used to aggregate nested conditions.
54
 * $select->where([
55
 *      "@or" => [
56
 *          ["id" => 1],
57
 *          ["column" => ["like" => "name"]]
58
 *      ]
59
 * ]);
60
 *
61
 * $select->where([
62
 *      "@or" => [
63
 *          ["id" => 1], ["id" => 2], ["id" => 3], ["id" => 4], ["id" => 5]
64
 *      ],
65
 *      "column" => [
66
 *          "like" => "name"
67
 *      ],
68
 *      "x" => [
69
 *          ">" => 1,
70
 *          "<" => 10
71
 *      ]
72
 * ]);
73
 *
74
 * To describe between or not between condition use array with two arguments.
75
 * $select->where([
76
 *      "column" => [
77
 *          "between" => [1, 100]
78
 *      ]
79
 * ]);
80
 */
81
abstract class AbstractWhere extends QueryBuilder
82
{
83
    /**
84
     * Tokens for nested OR and AND conditions.
85
     */
86
    const TOKEN_AND = "@AND";
87
    const TOKEN_OR  = "@OR";
88
89
    /**
90
     * Set of generated where tokens, format must be supported by QueryCompilers.
91
     *
92
     * @var array
93
     */
94
    protected $whereTokens = [];
95
96
    /**
97
     * Parameters collected while generating WHERE tokens, must be in a same order as parameters
98
     * in resulted query.
99
     *
100
     * @var array
101
     */
102
    protected $whereParameters = [];
103
104
    /**
105
     * Simple WHERE condition with various set of arguments.
106
     *
107
     * @see AbstractWhere
108
     * @param string|mixed $identifier Column or expression.
109
     * @param mixed        $variousA   Operator or value.
110
     * @param mixed        $variousB   Value, if operator specified.
111
     * @param mixed        $variousC   Required only in between statements.
112
     * @return $this
113
     * @throws BuilderException
114
     */
115
    public function where($identifier, $variousA = null, $variousB = null, $variousC = null)
0 ignored issues
show
Unused Code introduced by
The parameter $identifier 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 $variousA 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 $variousB 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 $variousC 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...
116
    {
117
        $this->whereToken('AND', func_get_args(), $this->whereTokens, $this->whereWrapper());
118
119
        return $this;
120
    }
121
122
    /**
123
     * Simple AND WHERE condition with various set of arguments.
124
     *
125
     * @see AbstractWhere
126
     * @param string|mixed $identifier Column or expression.
127
     * @param mixed        $variousA   Operator or value.
128
     * @param mixed        $variousB   Value, if operator specified.
129
     * @param mixed        $variousC   Required only in between statements.
130
     * @return $this
131
     * @throws BuilderException
132
     */
133
    public function andWhere($identifier, $variousA = null, $variousB = null, $variousC = null)
0 ignored issues
show
Unused Code introduced by
The parameter $identifier 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 $variousA 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 $variousB 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 $variousC 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...
134
    {
135
        $this->whereToken('AND', func_get_args(), $this->whereTokens, $this->whereWrapper());
136
137
        return $this;
138
    }
139
140
    /**
141
     * Simple OR WHERE condition with various set of arguments.
142
     *
143
     * @see AbstractWhere
144
     * @param string|mixed $identifier Column or expression.
145
     * @param mixed        $variousA   Operator or value.
146
     * @param mixed        $variousB   Value, if operator specified.
147
     * @param mixed        $variousC   Required only in between statements.
148
     * @return $this
149
     * @throws BuilderException
150
     */
151
    public function orWhere($identifier, $variousA = [], $variousB = null, $variousC = null)
0 ignored issues
show
Unused Code introduced by
The parameter $identifier 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 $variousA 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 $variousB 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 $variousC 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...
152
    {
153
        $this->whereToken('OR', func_get_args(), $this->whereTokens, $this->whereWrapper());
154
155
        return $this;
156
    }
157
158
    /**
159
     * Convert various amount of where function arguments into valid where token.
160
     *
161
     * @see AbstractWhere
162
     * @param string   $joiner     Boolean joiner (AND | OR).
163
     * @param array    $parameters Set of parameters collected from where functions.
164
     * @param array    $tokens     Array to aggregate compiled tokens. Reference.
165
     * @param callable $wrapper    Callback or closure used to wrap/collect every potential
166
     *                             parameter.
167
     * @throws BuilderException
168
     */
169
    protected function whereToken($joiner, array $parameters, &$tokens = [], callable $wrapper)
170
    {
171
        list($identifier, $valueA, $valueB, $valueC) = $parameters + array_fill(0, 5, null);
172
173
        if (empty($identifier)) {
174
            //Nothing to do
175
            return;
176
        }
177
178
        //Where conditions specified in array form
179
        if (is_array($identifier)) {
180
            if (count($identifier) == 1) {
181
                $this->arrayWhere(
182
                    $joiner == 'AND' ? self::TOKEN_AND : self::TOKEN_OR,
183
                    $identifier,
184
                    $tokens,
185
                    $wrapper
186
                );
187
188
                return;
189
            }
190
191
            $tokens[] = [$joiner, '('];
192
            $this->arrayWhere(self::TOKEN_AND, $identifier, $tokens, $wrapper);
193
            $tokens[] = ['', ')'];
194
195
            return;
196
        }
197
198
        if ($identifier instanceof \Closure) {
199
            $tokens[] = [$joiner, '('];
200
            call_user_func($identifier, $this, $joiner, $wrapper);
201
            $tokens[] = ['', ')'];
202
203
            return;
204
        }
205
206
        if ($identifier instanceof QueryBuilder) {
207
            //Will copy every parameter from QueryBuilder
208
            $wrapper($identifier);
209
        }
210
211
        switch (count($parameters)) {
212
            case 1:
213
                //AND|OR [identifier: sub-query]
214
                $tokens[] = [$joiner, $identifier];
215
                break;
216 View Code Duplication
            case 2:
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...
217
                //AND|OR [identifier] = [valueA]
1 ignored issue
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
218
                $tokens[] = [$joiner, [$identifier, '=', $wrapper($valueA)]];
219
                break;
220 View Code Duplication
            case 3:
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...
221
                //AND|OR [identifier] [valueA: OPERATION] [valueA]
222
                $tokens[] = [$joiner, [$identifier, strtoupper($valueA), $wrapper($valueB)]];
223
                break;
224
            case 4:
225
                //BETWEEN or NOT BETWEEN
226
                $valueA = strtoupper($valueA);
227
                if (!in_array($valueA, ['BETWEEN', 'NOT BETWEEN'])) {
228
                    throw new BuilderException(
229
                        'Only "BETWEEN" or "NOT BETWEEN" can define second comparasions value.'
230
                    );
231
                }
232
233
                //AND|OR [identifier] [valueA: BETWEEN|NOT BETWEEN] [valueB] [valueC]
1 ignored issue
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
234
                $tokens[] = [$joiner, [$identifier, $valueA, $wrapper($valueB), $wrapper($valueC)]];
235
        }
236
    }
237
238
    /**
239
     * Convert simplified where definition into valid set of where tokens.
240
     *
241
     * @see AbstractWhere
242
     * @param string   $grouper         Grouper type (see self::TOKEN_AND, self::TOKEN_OR).
243
     * @param array    $where           Simplified where definition.
244
     * @param array    $tokens          Array to aggregate compiled tokens. Reference.
245
     * @param callable $wrapper         Callback or closure used to wrap/collect every potential
246
     *                                  parameter.
247
     * @throws BuilderException
248
     */
249
    private function arrayWhere($grouper, array $where, &$tokens, callable $wrapper)
250
    {
251
        $joiner = ($grouper == self::TOKEN_AND ? 'AND' : 'OR');
252
253
        foreach ($where as $key => $value) {
254
            $token = strtoupper($key);
255
256
            //Grouping identifier (@OR, @AND), MongoDB like style
1 ignored issue
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
257
            if ($token == self::TOKEN_AND || $token == self::TOKEN_OR) {
258
                $tokens[] = [$joiner, '('];
259
260
                foreach ($value as $nested) {
261
                    if (count($nested) == 1) {
262
                        $this->arrayWhere($token, $nested, $tokens, $wrapper);
263
                        continue;
264
                    }
265
266
                    $tokens[] = [$token == self::TOKEN_AND ? 'AND' : 'OR', '('];
267
                    $this->arrayWhere(self::TOKEN_AND, $nested, $tokens, $wrapper);
268
                    $tokens[] = ['', ')'];
269
                }
270
271
                $tokens[] = ['', ')'];
272
273
                continue;
274
            }
275
276
            //AND|OR [name] = [value]
1 ignored issue
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
277
            if (!is_array($value)) {
278
                $tokens[] = [$joiner, [$key, '=', $wrapper($value)]];
279
                continue;
280
            }
281
282
            if (count($value) > 1) {
283
                //Multiple values to be joined by AND condition (x = 1, x != 5)
284
                $tokens[] = [$joiner, '('];
285
                $this->builtConditions('AND', $key, $value, $tokens, $wrapper);
286
                $tokens[] = ['', ')'];
287
            } else {
288
                $this->builtConditions($joiner, $key, $value, $tokens, $wrapper);
289
            }
290
        }
291
292
        return;
293
    }
294
295
    /**
296
     * Build set of conditions for specified identifier.
297
     *
298
     * @param string   $innerJoiner     Inner boolean joiner.
299
     * @param string   $key             Column identifier.
300
     * @param array    $where           Operations associated with identifier.
301
     * @param array    $tokens          Array to aggregate compiled tokens. Reference.
302
     * @param callable $wrapper         Callback or closure used to wrap/collect every potential
303
     *                                  parameter.
304
     * @return array
305
     */
306
    private function builtConditions($innerJoiner, $key, $where, &$tokens, callable $wrapper)
307
    {
308
        foreach ($where as $operation => $value) {
309
            if (is_numeric($operation)) {
310
                throw new BuilderException("Nested conditions should have defined operator.");
311
            }
312
313
            $operation = strtoupper($operation);
314
            if (!in_array($operation, ['BETWEEN', 'NOT BETWEEN'])) {
315
                //AND|OR [name] [OPERATION] [nestedValue]
316
                $tokens[] = [$innerJoiner, [$key, $operation, $wrapper($value)]];
317
                continue;
318
            }
319
320
            /**
321
             * Between and not between condition described using array of [left, right] syntax.
322
             */
323
324
            if (!is_array($value) || count($value) != 2) {
325
                throw new BuilderException(
326
                    "Exactly 2 array values are required for between statement."
327
                );
328
            }
329
330
            $tokens[] = [
331
                //AND|OR [name] [BETWEEN|NOT BETWEEN] [value 1] [value 2]
1 ignored issue
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
332
                $innerJoiner,
333
                [$key, $operation, $wrapper($value[0]), $wrapper($value[1])]
334
            ];
335
        }
336
337
        return $tokens;
338
    }
339
340
    /**
341
     * Applied to every potential parameter while where tokens generation. Used to prepare and
342
     * collect where parameters.
343
     *
344
     * @return \Closure
345
     */
346 View Code Duplication
    private function whereWrapper()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
347
    {
348
        return function ($parameter) {
349
            if ($parameter instanceof FragmentInterface) {
350
                //We are only not creating bindings for plan fragments
351
                if (!$parameter instanceof ParameterInterface && !$parameter instanceof QueryBuilder) {
352
                    return $parameter;
353
                }
354
            }
355
356
            if (is_array($parameter)) {
357
                throw new BuilderException("Arrays must be wrapped with Parameter instance.");
358
            }
359
360
            //Wrapping all values with ParameterInterface
361
            if (!$parameter instanceof ParameterInterface && !$parameter instanceof ExpressionInterface) {
362
                $parameter = new Parameter($parameter, Parameter::DETECT_TYPE);
363
            };
364
365
            //Let's store to sent to driver when needed
366
            $this->whereParameters[] = $parameter;
367
368
            return $parameter;
369
        };
370
    }
371
}