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); |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|