Passed
Push — master ( c65027...a39d50 )
by Wilmer
18:53 queued 16:02
created

InConditionBuilder::buildValues()   B

Complexity

Conditions 8
Paths 28

Size

Total Lines 35
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 35
rs 8.4444
c 0
b 0
f 0
cc 8
nc 28
nop 3
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
        $nullConditionOperator = '';
49
        $operator = strtoupper($expression->getOperator());
50
        $values = $expression->getValues();
51
52
        if ($column === []) {
53
            /** no columns to test against */
54
            return $operator === 'IN' ? '0=1' : '';
55
        }
56
57
        if ($values instanceof QueryInterface) {
58
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
59
        }
60
61
        if (!is_array($values) && !is_iterable($values)) {
62
            /** ensure values is an array */
63
            $values = (array) $values;
64
        }
65
66
        if (is_array($column)) {
67
            if (count($column) > 1) {
68
                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

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

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

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