Passed
Pull Request — 2.x (#73)
by
unknown
19:15
created

TokenTrait   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Test Coverage

Coverage 92.92%

Importance

Changes 0
Metric Value
eloc 125
dl 0
loc 249
ccs 105
cts 113
cp 0.9292
rs 8.96
c 0
b 0
f 0
wmc 43

3 Methods

Rating   Name   Duplication   Size   Complexity  
C flattenWhere() 0 59 12
D registerToken() 0 115 22
B pushCondition() 0 35 9

How to fix   Complexity   

Complex Class

Complex classes like TokenTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TokenTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Query\Traits;
13
14
use Closure;
15
use Cycle\Database\Driver\CompilerInterface;
16
use Cycle\Database\Exception\BuilderException;
17
use Cycle\Database\Injection\FragmentInterface;
18
use Cycle\Database\Injection\Parameter;
19
20
trait TokenTrait
21
{
22
    /**
23
     * Convert various amount of where function arguments into valid where token.
24
     *
25
     * @psalm-param non-empty-string $boolean Boolean joiner (AND | OR).
26
     *
27
     * @param array $params Set of parameters collected from where functions.
28
     * @param array $tokens Array to aggregate compiled tokens. Reference.
29
     * @param callable $wrapper Callback or closure used to wrap/collect every potential parameter.
30
     *
31
     * @throws BuilderException
32 1542
     */
33
    protected function registerToken(string $boolean, array $params, array &$tokens, callable $wrapper): void
34 1542
    {
35 1542
        $count = \count($params);
36
        if ($count === 0) {
37
            // nothing to do
38
            return;
39
        }
40 1542
41 966
        if ($count === 1) {
42
            $complex = $params[0];
43 966
44
            if ($complex === null) {
45
                return;
46
            }
47 966
48 878
            if (\is_array($complex)) {
49
                if (\count($complex) === 0) {
50 192
                    // nothing to do
51
                    return;
52
                }
53 726
54 662
                if (\count($complex) === 1) {
55 662
                    $this->flattenWhere(
56
                        $boolean === 'AND' ? CompilerInterface::TOKEN_AND : CompilerInterface::TOKEN_OR,
57
                        $complex,
58
                        $tokens,
59
                        $wrapper
60 630
                    );
61
                    return;
62
                }
63 64
64
                $tokens[] = [$boolean, '('];
65 64
66 64
                $this->flattenWhere(
67
                    CompilerInterface::TOKEN_AND,
68
                    $complex,
69
                    $tokens,
70
                    $wrapper
71
                );
72 64
73
                $tokens[] = ['', ')'];
74 64
75
                return;
76
            }
77 88
78 80
            if ($complex instanceof Closure) {
79 80
                $tokens[] = [$boolean, '('];
80 80
                $complex($this, $boolean, $wrapper);
81 80
                $tokens[] = ['', ')'];
82
                return;
83
            }
84 8
85 8
            if ($complex instanceof FragmentInterface) {
86 8
                $tokens[] = [$boolean, $complex];
87
                return;
88
            }
89
90
            throw new BuilderException('Expected array where or closure');
91
        }
92
93 782
        switch ($count) {
94
            case 2:
95 576
                // AND|OR [name] = [valueA]
96 576
                $tokens[] = [
97 576
                    $boolean,
98
                    [$params[0], '=', $wrapper($params[1])],
99 576
                ];
100 470
                break;
101 438
            case 3:
102
                [$name, $operator, $value] = $params;
103 438
104 422
                if (\is_string($operator)) {
105 422
                    $operator = \strtoupper($operator);
106 422
                    if ($operator === 'BETWEEN' || $operator === 'NOT BETWEEN') {
107
                        throw new BuilderException('Between statements expects exactly 2 values');
108 16
                    }
109
                    if (\is_array($value) && \in_array($operator, ['IN', 'NOT IN'], true)) {
110
                        $value = new Parameter($value);
111
                    }
112
                } elseif (\is_scalar($operator)) {
113 406
                    $operator = (string)$operator;
114 422
                }
115 422
116
                // AND|OR [name] [valueA: OPERATION] [valueA]
117 406
                $tokens[] = [
118 32
                    $boolean,
119 32
                    [$name, $operator, $wrapper($value)],
120 32
                ];
121
                break;
122
            case 4:
123
                [$name, $operator] = $params;
124 32
                if (!\is_string($operator)) {
125 32
                    throw new BuilderException('Invalid operator type, string expected');
126
                }
127
128
                $operator = \strtoupper($operator);
129
                if ($operator !== 'BETWEEN' && $operator !== 'NOT BETWEEN') {
130
                    throw new BuilderException(
131
                        'Only "BETWEEN" or "NOT BETWEEN" can define second comparision value'
132 32
                    );
133 32
                }
134
135 32
                // AND|OR [name] [valueA: BETWEEN|NOT BETWEEN] [value] [valueC]
136 32
                $tokens[] = [
137 32
                    $boolean,
138 32
                    [
139
                        $name,
140
                        strtoupper($operator),
141 32
                        $wrapper($params[2]),
142
                        $wrapper($params[3]),
143
                    ],
144
                ];
145 758
                break;
146
            default:
147
                throw new BuilderException('Invalid where method call');
148
        }
149
    }
150
151
    /**
152
     * Convert simplified where definition into valid set of where tokens.
153
     *
154
     * @psalm-param non-empty-string $grouper Grouper type (see self::TOKEN_AND, self::TOKEN_OR).
155
     *
156
     * @param array $where Simplified where definition.
157
     * @param array $tokens Array to aggregate compiled tokens. Reference.
158 726
     * @param callable $wrapper Callback or closure used to wrap/collect every potential parameter.
159
     *
160 726
     * @throws BuilderException
161
     */
162 726
    private function flattenWhere(string $grouper, array $where, array &$tokens, callable $wrapper): void
163
    {
164 726
        $boolean = ($grouper === CompilerInterface::TOKEN_AND ? 'AND' : 'OR');
165 8
166 8
        foreach ($where as $key => $value) {
167 8
            // Support for closures
168 8
            if (\is_int($key) && $value instanceof \Closure) {
169
                $tokens[] = [$boolean, '('];
170
                $value($this, $boolean, $wrapper);
171 726
                $tokens[] = ['', ')'];
172
                continue;
173
            }
174 726
175 88
            $token = strtoupper($key);
176
177 88
            // Grouping identifier (@OR, @AND), MongoDB like style
178 88
            if ($token === CompilerInterface::TOKEN_AND || $token === CompilerInterface::TOKEN_OR) {
179 88
                $tokens[] = [$boolean, '('];
180 88
181
                foreach ($value as $nested) {
182
                    if (count($nested) === 1) {
183 16
                        $this->flattenWhere($token, $nested, $tokens, $wrapper);
184 16
                        continue;
185 16
                    }
186
187
                    $tokens[] = [$token === CompilerInterface::TOKEN_AND ? 'AND' : 'OR', '('];
188 88
                    $this->flattenWhere(CompilerInterface::TOKEN_AND, $nested, $tokens, $wrapper);
189
                    $tokens[] = ['', ')'];
190 88
                }
191
192
                $tokens[] = ['', ')'];
193
194 726
                continue;
195 630
            }
196 630
197 630
            // AND|OR [name] = [value]
198
            if (!\is_array($value)) {
199 630
                $tokens[] = [
200
                    $boolean,
201
                    [$key, '=', $wrapper($value)],
202 160
                ];
203 136
                continue;
204 136
            }
205
206
            if (count($value) === 1) {
207
                $this->pushCondition(
208
                    $boolean,
209
                    $key,
210 112
                    $value,
211
                    $tokens,
212
                    $wrapper
213
                );
214 24
                continue;
215 24
            }
216 16
217
            //Multiple values to be joined by AND condition (x = 1, x != 5)
218 694
            $tokens[] = [$boolean, '('];
219
            $this->pushCondition('AND', $key, $value, $tokens, $wrapper);
220
            $tokens[] = ['', ')'];
221
        }
222
    }
223
224
    /**
225
     * Build set of conditions for specified identifier.
226
     *
227
     * @psalm-param non-empty-string $innerJoiner Inner boolean joiner.
228
     * @psalm-param non-empty-string $key Column identifier.
229
     *
230 160
     * @param array $where Operations associated with identifier.
231
     * @param array $tokens Array to aggregate compiled tokens. Reference.
232 160
     * @param callable $wrapper Callback or closure used to wrap/collect every potential parameter.
233 160
     */
234 8
    private function pushCondition(string $innerJoiner, string $key, array $where, &$tokens, callable $wrapper): array
235
    {
236
        foreach ($where as $operation => $value) {
237 152
            if (is_numeric($operation)) {
238 152
                throw new BuilderException('Nested conditions should have defined operator');
239
            }
240 96
241 104
            $operation = strtoupper($operation);
242 104
            if ($operation !== 'BETWEEN' && $operation !== 'NOT BETWEEN') {
243
                // AND|OR [name] [OPERATION] [nestedValue]
244 96
                if (\is_array($value) && \in_array($operation, ['IN', 'NOT IN'], true)) {
245
                    $value = new Parameter($value);
246
                }
247
                $tokens[] = [
248 48
                    $innerJoiner,
249 16
                    [$key, $operation, $wrapper($value)],
250 16
                ];
251
                continue;
252
            }
253
254 32
            // Between and not between condition described using array of [left, right] syntax.
255
            if (!is_array($value) || count($value) !== 2) {
256 32
                throw new BuilderException(
257 32
                    'Exactly 2 array values are required for between statement'
258
                );
259
            }
260
261 128
            $tokens[] = [
262
                //AND|OR [name] [BETWEEN|NOT BETWEEN] [value 1] [value 2]
263
                $innerJoiner,
264
                [$key, $operation, $wrapper($value[0]), $wrapper($value[1])],
265
            ];
266
        }
267
268
        return $tokens;
269
    }
270
}
271