InConditionBuilder   C
last analyzed

Complexity

Total Complexity 54

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 54
eloc 106
c 1
b 1
f 0
dl 0
loc 271
rs 6.4799

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
F build() 0 77 23
B buildValues() 0 38 8
A getRawValuesFromTraversableObject() 0 16 3
B buildCompositeInCondition() 0 44 10
A getNullCondition() 0 9 2
B buildSubqueryInCondition() 0 31 7

How to fix   Complexity   

Complex Class

Complex classes like InConditionBuilder 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 InConditionBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\QueryBuilder\Condition\Builder;
6
7
use ArrayAccess;
8
use Iterator;
9
use Traversable;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidArgumentException;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
15
use Yiisoft\Db\Expression\ExpressionInterface;
16
use Yiisoft\Db\QueryBuilder\Condition\InCondition;
17
use Yiisoft\Db\QueryBuilder\Condition\Interface\InConditionInterface;
18
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
19
use Yiisoft\Db\Query\QueryInterface;
20
21
use function array_merge;
22
use function array_values;
23
use function count;
24
use function implode;
25
use function in_array;
26
use function is_array;
27
use function iterator_count;
28
use function reset;
29
use function sprintf;
30
use function str_contains;
31
use function strtoupper;
32
33
/**
34
 * Build an object of {@see InCondition} into SQL expressions.
35
 */
36
class InConditionBuilder implements ExpressionBuilderInterface
37
{
38
    public function __construct(protected QueryBuilderInterface $queryBuilder)
39
    {
40
    }
41
42
    /**
43
     * Build SQL for {@see InCondition}.
44
     *
45
     * @throws Exception
46
     * @throws InvalidArgumentException
47
     * @throws InvalidConfigException
48
     * @throws NotSupportedException
49
     */
50
    public function build(InConditionInterface $expression, array &$params = []): string
51
    {
52
        $column = $expression->getColumn();
53
        $operator = strtoupper($expression->getOperator());
54
        $values = $expression->getValues();
55
56
        if ($column === []) {
57
            /** no columns to test against */
58
            return $operator === 'IN' ? '0=1' : '';
59
        }
60
61
        if ($column instanceof ExpressionInterface) {
62
            $column = $this->queryBuilder->buildExpression($column);
63
        }
64
65
        if ($values instanceof QueryInterface) {
66
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
67
        }
68
69
        if (!is_array($values) && !is_iterable($values)) {
70
            /** ensure values is an array */
71
            $values = (array) $values;
72
        }
73
74
        if (is_array($column)) {
75
            if (count($column) > 1) {
76
                return $this->buildCompositeInCondition($operator, $column, $values, $params);
0 ignored issues
show
Bug introduced by
It seems like $values can also be of type integer; however, parameter $values of Yiisoft\Db\QueryBuilder\...dCompositeInCondition() does only seem to accept Iterator|iterable, 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

76
                return $this->buildCompositeInCondition($operator, $column, /** @scrutinizer ignore-type */ $values, $params);
Loading history...
77
            }
78
79
            /** @psalm-var mixed $column */
80
            $column = reset($column);
81
        }
82
83
        if ($column instanceof Iterator) {
84
            if (iterator_count($column) > 1) {
85
                return $this->buildCompositeInCondition($operator, $column, $values, $params);
86
            }
87
88
            $column->rewind();
89
            /** @psalm-var mixed $column */
90
            $column = $column->current();
91
        }
92
93
        if (is_array($values)) {
94
            $rawValues = $values;
95
        } else {
96
            $rawValues = $this->getRawValuesFromTraversableObject($values);
0 ignored issues
show
Bug introduced by
It seems like $values can also be of type integer; however, parameter $traversableObject of Yiisoft\Db\QueryBuilder\...FromTraversableObject() does only seem to accept Traversable, 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

96
            $rawValues = $this->getRawValuesFromTraversableObject(/** @scrutinizer ignore-type */ $values);
Loading history...
97
        }
98
99
        $nullCondition = null;
100
        $nullConditionOperator = null;
101
        if (is_string($column) && in_array(null, $rawValues, true)) {
102
            $nullCondition = $this->getNullCondition($operator, $column);
103
            $nullConditionOperator = $operator === 'IN' ? 'OR' : 'AND';
104
        }
105
106
        $sqlValues = $this->buildValues($expression, $values, $params);
0 ignored issues
show
Bug introduced by
It seems like $values can also be of type integer; however, parameter $values of Yiisoft\Db\QueryBuilder\...nBuilder::buildValues() does only seem to accept iterable, 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

106
        $sqlValues = $this->buildValues($expression, /** @scrutinizer ignore-type */ $values, $params);
Loading history...
107
108
        if (empty($sqlValues)) {
109
            return $nullCondition ?? ($operator === 'IN' ? '0=1' : '');
110
        }
111
112
        if (is_string($column) && !str_contains($column, '(')) {
113
            $column = $this->queryBuilder->quoter()->quoteColumnName($column);
114
        }
115
116
        if (count($sqlValues) > 1) {
117
            $sql = "$column $operator (" . implode(', ', $sqlValues) . ')';
118
        } else {
119
            $operator = $operator === 'IN' ? '=' : '<>';
120
            $sql = (string) $column . $operator . reset($sqlValues);
121
        }
122
123
        /** @var int|string|null $nullCondition */
124
        return $nullCondition !== null && $nullConditionOperator !== null
125
            ? sprintf('%s %s %s', $sql, $nullConditionOperator, $nullCondition)
126
            : $sql;
127
    }
128
129
    /**
130
     * Builds `$values` to use in {@see InCondition}.
131
     *
132
     * @throws Exception
133
     * @throws InvalidArgumentException
134
     * @throws InvalidConfigException
135
     * @throws NotSupportedException
136
     *
137
     * @psalm-return string[]
138
     *
139
     * @psalm-suppress MixedArrayTypeCoercion
140
     * @psalm-suppress MixedArrayOffset
141
     */
142
    protected function buildValues(InConditionInterface $condition, iterable $values, array &$params = []): array
143
    {
144
        $sqlValues = [];
145
        $column = $condition->getColumn();
146
147
        if (is_array($column)) {
148
            /** @psalm-var mixed $column */
149
            $column = reset($column);
150
        }
151
152
        if ($column instanceof Iterator) {
153
            $column->rewind();
154
            /** @psalm-var mixed $column */
155
            $column = $column->current();
156
        }
157
158
        /**
159
         * @psalm-var string|int $i
160
         * @psalm-var mixed $value
161
         */
162
        foreach ($values as $i => $value) {
163
            if (is_array($value) || $value instanceof ArrayAccess) {
164
                /** @psalm-var mixed $value */
165
                $value = $value[$column] ?? null;
166
            }
167
168
            if ($value === null) {
169
                continue;
170
            }
171
172
            if ($value instanceof ExpressionInterface) {
173
                $sqlValues[$i] = $this->queryBuilder->buildExpression($value, $params);
174
            } else {
175
                $sqlValues[$i] = $this->queryBuilder->bindParam($value, $params);
176
            }
177
        }
178
179
        return $sqlValues;
180
    }
181
182
    /**
183
     * Build SQL for composite `IN` condition.
184
     *
185
     * @throws Exception
186
     * @throws InvalidArgumentException
187
     * @throws InvalidConfigException
188
     * @throws NotSupportedException
189
     */
190
    protected function buildSubqueryInCondition(
191
        string $operator,
192
        iterable|string|Iterator $columns,
193
        ExpressionInterface $values,
194
        array &$params = []
195
    ): string {
196
        $query = '';
197
        $sql = $this->queryBuilder->buildExpression($values, $params);
198
199
        if (is_array($columns)) {
200
            /** @psalm-var string[] $columns */
201
            foreach ($columns as $i => $col) {
202
                if ($col instanceof ExpressionInterface) {
203
                    $columns[$i] = $this->queryBuilder->buildExpression($col);
204
                    continue;
205
                }
206
207
                if (!str_contains($col, '(')) {
208
                    $columns[$i] = $this->queryBuilder->quoter()->quoteColumnName($col);
209
                }
210
            }
211
212
            $query = '(' . implode(', ', $columns) . ") $operator $sql";
213
        }
214
215
        if (is_string($columns) && !str_contains($columns, '(')) {
216
            $columns = $this->queryBuilder->quoter()->quoteColumnName($columns);
217
            $query = "$columns $operator $sql";
218
        }
219
220
        return $query;
221
    }
222
223
    /**
224
     * Builds an SQL statement for checking the existence of rows with the specified composite column values.
225
     *
226
     * @throws Exception
227
     * @throws InvalidConfigException
228
     * @throws InvalidArgumentException
229
     * @throws NotSupportedException
230
     */
231
    protected function buildCompositeInCondition(
232
        string|null $operator,
233
        iterable $columns,
234
        iterable|Iterator $values,
235
        array &$params = []
236
    ): string {
237
        $vss = [];
238
239
        /** @psalm-var string[][] $values */
240
        foreach ($values as $value) {
241
            $vs = [];
242
            /** @psalm-var string[] $columns */
243
            foreach ($columns as $column) {
244
                if ($column instanceof ExpressionInterface) {
245
                    $column = $this->queryBuilder->buildExpression($column);
246
                }
247
248
                if (isset($value[$column])) {
249
                    $vs[] = $this->queryBuilder->bindParam($value[$column], $params);
250
                } else {
251
                    $vs[] = 'NULL';
252
                }
253
            }
254
            $vss[] = '(' . implode(', ', $vs) . ')';
255
        }
256
257
        if (empty($vss)) {
258
            return $operator === 'IN' ? '0=1' : '';
259
        }
260
261
        $sqlColumns = [];
262
263
        /** @psalm-var string[] $columns */
264
        foreach ($columns as $column) {
265
            if ($column instanceof ExpressionInterface) {
266
                $sqlColumns[] = $this->queryBuilder->buildExpression($column);
267
                continue;
268
            }
269
270
            $sqlColumns[] = !str_contains($column, '(')
271
                ? $this->queryBuilder->quoter()->quoteColumnName($column) : $column;
272
        }
273
274
        return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
275
    }
276
277
    /**
278
     * The Builds are `null/is` not `null` condition for column based on the operator.
279
     */
280
    protected function getNullCondition(string $operator, string $column): string
281
    {
282
        $column = $this->queryBuilder->quoter()->quoteColumnName($column);
283
284
        if ($operator === 'IN') {
285
            return sprintf('%s IS NULL', $column);
286
        }
287
288
        return sprintf('%s IS NOT NULL', $column);
289
    }
290
291
    protected function getRawValuesFromTraversableObject(Traversable $traversableObject): array
292
    {
293
        $rawValues = [];
294
295
        /** @psalm-var mixed $value */
296
        foreach ($traversableObject as $value) {
297
            if (is_array($value)) {
298
                $values = array_values($value);
299
                $rawValues = array_merge($rawValues, $values);
300
            } else {
301
                /** @psalm-var mixed */
302
                $rawValues[] = $value;
303
            }
304
        }
305
306
        return $rawValues;
307
    }
308
}
309