Passed
Pull Request — master (#491)
by Def
02:20
created

InConditionBuilder   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 101
dl 0
loc 243
rs 7.44
c 1
b 1
f 0
wmc 52

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
F build() 0 77 23
B buildValues() 0 35 8
A getRawValuesFromTraversableObject() 0 16 3
B buildCompositeInCondition() 0 35 8
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
 * Class InConditionBuilder builds objects of {@see InCondition}.
35
 */
36
class InConditionBuilder implements ExpressionBuilderInterface
37
{
38
    public function __construct(private QueryBuilderInterface $queryBuilder)
39
    {
40
    }
41
42
    /**
43
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
44
     */
45
    public function build(InConditionInterface $expression, array &$params = []): string
46
    {
47
        $column = $expression->getColumn();
48
        $operator = strtoupper($expression->getOperator());
49
        $values = $expression->getValues();
50
51
        if ($column === []) {
52
            /** no columns to test against */
53
            return $operator === 'IN' ? '0=1' : '';
54
        }
55
56
        if ($column instanceof ExpressionInterface) {
57
            $column = (string) $column;
58
        }
59
60
        if ($values instanceof QueryInterface) {
61
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
62
        }
63
64
        if (!is_array($values) && !is_iterable($values)) {
65
            /** ensure values is an array */
66
            $values = (array) $values;
67
        }
68
69
        if (is_array($column)) {
70
            if (count($column) > 1) {
71
                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

71
                return $this->buildCompositeInCondition($operator, $column, /** @scrutinizer ignore-type */ $values, $params);
Loading history...
72
            }
73
74
            /** @var mixed */
75
            $column = reset($column);
76
        }
77
78
        if ($column instanceof Iterator) {
79
            if (iterator_count($column) > 1) {
80
                return $this->buildCompositeInCondition($operator, $column, $values, $params);
81
            }
82
83
            $column->rewind();
84
            /** @var mixed */
85
            $column = $column->current();
86
        }
87
88
        if (is_array($values)) {
89
            $rawValues = $values;
90
        } else {
91
            $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

91
            $rawValues = $this->getRawValuesFromTraversableObject(/** @scrutinizer ignore-type */ $values);
Loading history...
92
        }
93
94
        $nullCondition = null;
95
        $nullConditionOperator = null;
96
        if (is_string($column) && in_array(null, $rawValues, true)) {
97
            $nullCondition = $this->getNullCondition($operator, $column);
98
            $nullConditionOperator = $operator === 'IN' ? 'OR' : 'AND';
99
        }
100
101
        $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 Traversable|array, 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

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