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

InConditionBuilder::buildSubqueryInCondition()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 32
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
c 0
b 0
f 0
nc 4
nop 4
dl 0
loc 32
rs 8.8333
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
            /** @psalm-suppress InvalidCast */
58
            $column = (string) $column;
59
        }
60
61
        if ($values instanceof QueryInterface) {
62
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
63
        }
64
65
        if (!is_array($values) && !is_iterable($values)) {
66
            /** ensure values is an array */
67
            $values = (array) $values;
68
        }
69
70
        if (is_array($column)) {
71
            if (count($column) > 1) {
72
                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

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

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

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