Completed
Branch develop (f7dc53)
by Anton
05:49
created

AbstractWhere   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 291
Duplicated Lines 11.34 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 37
c 4
b 0
f 0
lcom 1
cbo 3
dl 33
loc 291
rs 8.6

7 Methods

Rating   Name   Duplication   Size   Complexity  
A where() 0 6 1
A andWhere() 0 6 1
A orWhere() 0 6 1
C whereToken() 8 68 12
D arrayWhere() 0 45 10
B builtConditions() 0 33 6
B whereWrapper() 25 25 6

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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\FragmentInterface;
13
use Spiral\Database\Injections\Parameter;
14
use Spiral\Database\Injections\ParameterInterface;
15
16
/**
17
 * Abstract query with WHERE conditions generation support. Provides simplified way to generate
18
 * WHERE tokens using set of where methods. Class support different where conditions, simplified
19
 * definitions
20
 * (using arrays) and closures to describe nested conditions:
21
 *
22
 * 1) Simple token/nested query or expression
23
 * $select->where(new SQLFragment('(SELECT count(*) from `table`)'));
24
 *
25
 * 2) Simple assessment
26
 * $select->where('column', $value);
27
 * $select->where('column', new SQLFragment('CONCAT(columnA, columnB)'));
28
 *
29
 * 3) Assessment with specified operator (operator will be converted to uppercase automatically)
30
 * $select->where('column', '=', $value);
31
 * $select->where('column', 'IN', [1, 2, 3]);
32
 * $select->where('column', 'LIKE', $string);
33
 * $select->where('column', 'IN', new SQLFragment('(SELECT id from `table` limit 1)'));
34
 *
35
 * 4) Between and not between statements
36
 * $select->where('column', 'between', 1, 10);
37
 * $select->where('column', 'not between', 1, 10);
38
 * $select->where('column', 'not between', new SQLFragment('MIN(price)'), $maximum);
39
 *
40
 * 5) Closure with nested conditions
41
 * $this->where(function(AbstractWhere $select){
42
 *      $select->where("name", "Wolfy-J")->orWhere("balance", ">", 100)
43
 * });
44
 *
45
 * 6) Simplified array based condition definition
46
 * $select->where(["column" => 1]);
47
 * $select->where(["column" => [
48
 *      ">" => 1,
49
 *      "<" => 10
50
 * ]]);
51
 *
52
 * Tokens "@or" and "@and" used to aggregate nested conditions.
53
 * $select->where([
54
 *      "@or" => [
55
 *          ["id" => 1],
56
 *          ["column" => ["like" => "name"]]
57
 *      ]
58
 * ]);
59
 *
60
 * $select->where([
61
 *      "@or" => [
62
 *          ["id" => 1], ["id" => 2], ["id" => 3], ["id" => 4], ["id" => 5]
63
 *      ],
64
 *      "column" => [
65
 *          "like" => "name"
66
 *      ],
67
 *      "x" => [
68
 *          ">" => 1,
69
 *          "<" => 10
70
 *      ]
71
 * ]);
72
 *
73
 * To describe between or not between condition use array with two arguments.
74
 * $select->where([
75
 *      "column" => [
76
 *          "between" => [1, 100]
77
 *      ]
78
 * ]);
79
 */
80
abstract class AbstractWhere extends QueryBuilder
81
{
82
    /**
83
     * Tokens for nested OR and AND conditions.
84
     */
85
    const TOKEN_AND = "@AND";
86
    const TOKEN_OR  = "@OR";
87
88
    /**
89
     * Set of generated where tokens, format must be supported by QueryCompilers.
90
     *
91
     * @var array
92
     */
93
    protected $whereTokens = [];
94
95
    /**
96
     * Parameters collected while generating WHERE tokens, must be in a same order as parameters
97
     * in resulted query.
98
     *
99
     * @var array
100
     */
101
    protected $whereParameters = [];
102
103
    /**
104
     * Simple WHERE condition with various set of arguments.
105
     *
106
     * @see AbstractWhere
107
     * @param string|mixed $identifier Column or expression.
108
     * @param mixed        $variousA   Operator or value.
109
     * @param mixed        $variousB   Value, if operator specified.
110
     * @param mixed        $variousC   Required only in between statements.
111
     * @return $this
112
     * @throws BuilderException
113
     */
114
    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...
115
    {
116
        $this->whereToken('AND', func_get_args(), $this->whereTokens, $this->whereWrapper());
117
118
        return $this;
119
    }
120
121
    /**
122
     * Simple AND WHERE condition with various set of arguments.
123
     *
124
     * @see AbstractWhere
125
     * @param string|mixed $identifier Column or expression.
126
     * @param mixed        $variousA   Operator or value.
127
     * @param mixed        $variousB   Value, if operator specified.
128
     * @param mixed        $variousC   Required only in between statements.
129
     * @return $this
130
     * @throws BuilderException
131
     */
132
    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...
133
    {
134
        $this->whereToken('AND', func_get_args(), $this->whereTokens, $this->whereWrapper());
135
136
        return $this;
137
    }
138
139
    /**
140
     * Simple OR WHERE condition with various set of arguments.
141
     *
142
     * @see AbstractWhere
143
     * @param string|mixed $identifier Column or expression.
144
     * @param mixed        $variousA   Operator or value.
145
     * @param mixed        $variousB   Value, if operator specified.
146
     * @param mixed        $variousC   Required only in between statements.
147
     * @return $this
148
     * @throws BuilderException
149
     */
150
    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...
151
    {
152
        $this->whereToken('OR', func_get_args(), $this->whereTokens, $this->whereWrapper());
153
154
        return $this;
155
    }
156
157
    /**
158
     * Convert various amount of where function arguments into valid where token.
159
     *
160
     * @see AbstractWhere
161
     * @param string   $joiner     Boolean joiner (AND | OR).
162
     * @param array    $parameters Set of parameters collected from where functions.
163
     * @param array    $tokens     Array to aggregate compiled tokens. Reference.
164
     * @param callable $wrapper    Callback or closure used to wrap/collect every potential
165
     *                             parameter.
166
     * @throws BuilderException
167
     */
168
    protected function whereToken($joiner, array $parameters, &$tokens = [], callable $wrapper)
169
    {
170
        list($identifier, $valueA, $valueB, $valueC) = $parameters + array_fill(0, 5, null);
171
172
        if (empty($identifier)) {
173
            //Nothing to do
174
            return;
175
        }
176
177
        //Where conditions specified in array form
178
        if (is_array($identifier)) {
179
            if (count($identifier) == 1) {
180
                $this->arrayWhere(
181
                    $joiner == 'AND' ? self::TOKEN_AND : self::TOKEN_OR,
182
                    $identifier,
183
                    $tokens,
184
                    $wrapper
185
                );
186
187
                return;
188
            }
189
190
            $tokens[] = [$joiner, '('];
191
            $this->arrayWhere(self::TOKEN_AND, $identifier, $tokens, $wrapper);
192
            $tokens[] = ['', ')'];
193
194
            return;
195
        }
196
197
        if ($identifier instanceof \Closure) {
198
            $tokens[] = [$joiner, '('];
199
            call_user_func($identifier, $this, $joiner, $wrapper);
200
            $tokens[] = ['', ')'];
201
202
            return;
203
        }
204
205
        if ($identifier instanceof QueryBuilder) {
206
            //Will copy every parameter from QueryBuilder
207
            $wrapper($identifier);
208
        }
209
210
        switch (count($parameters)) {
211
            case 1:
212
                //AND|OR [identifier: sub-query]
213
                $tokens[] = [$joiner, $identifier];
214
                break;
215 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...
216
                //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...
217
                $tokens[] = [$joiner, [$identifier, '=', $wrapper($valueA)]];
218
                break;
219 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...
220
                //AND|OR [identifier] [valueA: OPERATION] [valueA]
221
                $tokens[] = [$joiner, [$identifier, strtoupper($valueA), $wrapper($valueB)]];
222
                break;
223
            case 4:
224
                //BETWEEN or NOT BETWEEN
225
                $valueA = strtoupper($valueA);
226
                if (!in_array($valueA, ['BETWEEN', 'NOT BETWEEN'])) {
227
                    throw new BuilderException(
228
                        'Only "BETWEEN" or "NOT BETWEEN" can define second comparasions value.'
229
                    );
230
                }
231
232
                //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...
233
                $tokens[] = [$joiner, [$identifier, $valueA, $wrapper($valueB), $wrapper($valueC)]];
234
        }
235
    }
236
237
    /**
238
     * Convert simplified where definition into valid set of where tokens.
239
     *
240
     * @see AbstractWhere
241
     * @param string   $grouper         Grouper type (see self::TOKEN_AND, self::TOKEN_OR).
242
     * @param array    $where           Simplified where definition.
243
     * @param array    $tokens          Array to aggregate compiled tokens. Reference.
244
     * @param callable $wrapper         Callback or closure used to wrap/collect every potential
245
     *                                  parameter.
246
     * @throws BuilderException
247
     */
248
    private function arrayWhere($grouper, array $where, &$tokens, callable $wrapper)
249
    {
250
        $joiner = ($grouper == self::TOKEN_AND ? 'AND' : 'OR');
251
252
        foreach ($where as $key => $value) {
253
            $token = strtoupper($key);
254
255
            //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...
256
            if ($token == self::TOKEN_AND || $token == self::TOKEN_OR) {
257
                $tokens[] = [$joiner, '('];
258
259
                foreach ($value as $nested) {
260
                    if (count($nested) == 1) {
261
                        $this->arrayWhere($token, $nested, $tokens, $wrapper);
262
                        continue;
263
                    }
264
265
                    $tokens[] = [$token == self::TOKEN_AND ? 'AND' : 'OR', '('];
266
                    $this->arrayWhere(self::TOKEN_AND, $nested, $tokens, $wrapper);
267
                    $tokens[] = ['', ')'];
268
                }
269
270
                $tokens[] = ['', ')'];
271
272
                continue;
273
            }
274
275
            //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...
276
            if (!is_array($value)) {
277
                $tokens[] = [$joiner, [$key, '=', $wrapper($value)]];
278
                continue;
279
            }
280
281
            if (count($value) > 1) {
282
                //Multiple values to be joined by AND condition (x = 1, x != 5)
283
                $tokens[] = [$joiner, '('];
284
                $this->builtConditions('AND', $key, $value, $tokens, $wrapper);
285
                $tokens[] = ['', ')'];
286
            } else {
287
                $this->builtConditions($joiner, $key, $value, $tokens, $wrapper);
288
            }
289
        }
290
291
        return;
292
    }
293
294
    /**
295
     * Build set of conditions for specified identifier.
296
     *
297
     * @param string   $innerJoiner     Inner boolean joiner.
298
     * @param string   $key             Column identifier.
299
     * @param array    $where           Operations associated with identifier.
300
     * @param array    $tokens          Array to aggregate compiled tokens. Reference.
301
     * @param callable $wrapper         Callback or closure used to wrap/collect every potential
302
     *                                  parameter.
303
     * @return array
304
     */
305
    private function builtConditions($innerJoiner, $key, $where, &$tokens, callable $wrapper)
306
    {
307
        foreach ($where as $operation => $value) {
308
            if (is_numeric($operation)) {
309
                throw new BuilderException("Nested conditions should have defined operator.");
310
            }
311
312
            $operation = strtoupper($operation);
313
            if (!in_array($operation, ['BETWEEN', 'NOT BETWEEN'])) {
314
                //AND|OR [name] [OPERATION] [nestedValue]
315
                $tokens[] = [$innerJoiner, [$key, $operation, $wrapper($value)]];
316
                continue;
317
            }
318
319
            /**
320
             * Between and not between condition described using array of [left, right] syntax.
321
             */
322
323
            if (!is_array($value) || count($value) != 2) {
324
                throw new BuilderException(
325
                    "Exactly 2 array values are required for between statement."
326
                );
327
            }
328
329
            $tokens[] = [
330
                //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...
331
                $innerJoiner,
332
                [$key, $operation, $wrapper($value[0]), $wrapper($value[1])]
333
            ];
334
        }
335
336
        return $tokens;
337
    }
338
339
    /**
340
     * Applied to every potential parameter while where tokens generation. Used to prepare and
341
     * collect where parameters.
342
     *
343
     * @return \Closure
344
     */
345 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...
346
    {
347
        return function ($parameter) {
348
            if ($parameter instanceof FragmentInterface) {
349
                //We are only not creating bindings for plan fragments
350
                if (!$parameter instanceof ParameterInterface && !$parameter instanceof QueryBuilder) {
351
                    return $parameter;
352
                }
353
            }
354
355
            if (is_array($parameter)) {
356
                throw new BuilderException("Arrays must be wrapped with Parameter instance.");
357
            }
358
359
            //Wrapping all values with ParameterInterface
360
            if (!$parameter instanceof ParameterInterface) {
361
                $parameter = new Parameter($parameter, Parameter::DETECT_TYPE);
362
            };
363
364
            //Let's store to sent to driver when needed
365
            $this->whereParameters[] = $parameter;
366
367
            return $parameter;
368
        };
369
    }
370
}