TokenTrait::pushCondition()   B
last analyzed

Complexity

Conditions 9
Paths 6

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 6
nop 5
dl 0
loc 35
ccs 14
cts 14
cp 1
crap 9
rs 8.0555
c 0
b 0
f 0
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($this->booleanToToken($boolean), $complex, $tokens, $wrapper);
56
                    return;
57
                }
58
59
                $tokens[] = [$boolean, '('];
60 630
61
                $this->flattenWhere(
62
                    CompilerInterface::TOKEN_AND,
63 64
                    $complex,
64
                    $tokens,
65 64
                    $wrapper,
66 64
                );
67
68
                $tokens[] = ['', ')'];
69
70
                return;
71
            }
72 64
73
            if ($complex instanceof \Closure) {
74 64
                $tokens[] = [$boolean, '('];
75
                $complex($this, $boolean, $wrapper);
76
                $tokens[] = ['', ')'];
77 88
                return;
78 80
            }
79 80
80 80
            if ($complex instanceof FragmentInterface) {
81 80
                $tokens[] = [$boolean, $complex];
82
                return;
83
            }
84 8
85 8
            throw new BuilderException('Expected array where or closure');
86 8
        }
87
88
        switch ($count) {
89
            case 2:
90
                // AND|OR [name] = [valueA]
91
                $tokens[] = [
92
                    $boolean,
93 782
                    [$params[0], '=', $wrapper($params[1])],
94
                ];
95 576
                break;
96 576
            case 3:
97 576
                [$name, $operator, $value] = $params;
98
99 576
                if (\is_string($operator)) {
100 470
                    $operator = \strtoupper($operator);
101 438
                    if ($operator === 'BETWEEN' || $operator === 'NOT BETWEEN') {
102
                        throw new BuilderException('Between statements expects exactly 2 values');
103 438
                    }
104 422
                    if (\is_array($value) && \in_array($operator, ['IN', 'NOT IN'], true)) {
105 422
                        $value = new Parameter($value);
106 422
                    }
107
                } elseif (\is_scalar($operator)) {
108 16
                    $operator = (string) $operator;
109
                }
110
111
                // AND|OR [name] [valueA: OPERATION] [valueA]
112
                $tokens[] = [
113 406
                    $boolean,
114 422
                    [$name, $operator, $wrapper($value)],
115 422
                ];
116
                break;
117 406
            case 4:
118 32
                [$name, $operator] = $params;
119 32
                if (!\is_string($operator)) {
120 32
                    throw new BuilderException('Invalid operator type, string expected');
121
                }
122
123
                $operator = \strtoupper($operator);
124 32
                if ($operator !== 'BETWEEN' && $operator !== 'NOT BETWEEN') {
125 32
                    throw new BuilderException(
126
                        'Only "BETWEEN" or "NOT BETWEEN" can define second comparision value',
127
                    );
128
                }
129
130
                // AND|OR [name] [valueA: BETWEEN|NOT BETWEEN] [value] [valueC]
131
                $tokens[] = [
132 32
                    $boolean,
133 32
                    [
134
                        $name,
135 32
                        \strtoupper($operator),
136 32
                        $wrapper($params[2]),
137 32
                        $wrapper($params[3]),
138 32
                    ],
139
                ];
140
                break;
141 32
            default:
142
                throw new BuilderException('Invalid where method call');
143
        }
144
    }
145 758
146
    /**
147
     * Convert simplified where definition into valid set of where tokens.
148
     *
149
     * @psalm-param non-empty-string $grouper Grouper type (see self::TOKEN_AND, self::TOKEN_OR).
150
     *
151
     * @param array $where Simplified where definition.
152
     * @param array $tokens Array to aggregate compiled tokens. Reference.
153
     * @param callable $wrapper Callback or closure used to wrap/collect every potential parameter.
154
     *
155
     * @throws BuilderException
156
     */
157
    private function flattenWhere(string $grouper, array $where, array &$tokens, callable $wrapper): void
158 726
    {
159
        $boolean = $this->tokenToBoolean($grouper);
160 726
161
        foreach ($where as $key => $value) {
162 726
            // Support for closures
163
            if (\is_int($key) && $value instanceof \Closure) {
164 726
                $tokens[] = [$boolean, '('];
165 8
                $value($this, $boolean, $wrapper);
166 8
                $tokens[] = ['', ')'];
167 8
                continue;
168 8
            }
169
170
            $token = \strtoupper($key);
171 726
172
            // Grouping identifier (@OR, @AND), MongoDB like style
173
            if (
174 726
                $token === CompilerInterface::TOKEN_AND ||
175 88
                $token === CompilerInterface::TOKEN_OR ||
176
                $token === CompilerInterface::TOKEN_AND_NOT ||
177 88
                $token === CompilerInterface::TOKEN_OR_NOT
178 88
            ) {
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[] = [$this->tokenToBoolean($token), '('];
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
    private function tokenToBoolean(string $token): string
272
    {
273
        return match ($token) {
274
            CompilerInterface::TOKEN_AND => 'AND',
275
            CompilerInterface::TOKEN_AND_NOT => 'AND NOT',
276
            CompilerInterface::TOKEN_OR_NOT => 'OR NOT',
277
            default => 'OR',
278
        };
279
    }
280
281
    private function booleanToToken(string $boolean): string
282
    {
283
        return match ($boolean) {
284
            'AND' => CompilerInterface::TOKEN_AND,
285
            'AND NOT' => CompilerInterface::TOKEN_AND_NOT,
286
            'OR NOT' => CompilerInterface::TOKEN_OR_NOT,
287
            default => 'OR',
288
        };
289
    }
290
}
291