Passed
Push — master ( 9607e5...058f8b )
by Wilmer
09:15
created

InConditionBuilder::buildValues()   B

Complexity

Conditions 8
Paths 28

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 28
nop 3
dl 0
loc 31
ccs 17
cts 17
cp 1
crap 8
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Query\Conditions;
6
7
use function array_merge;
8
use function array_values;
9
use ArrayAccess;
10
use function count;
11
use function implode;
12
use function is_array;
13
use function iterator_count;
14
use function reset;
15
use function sprintf;
16
use function strpos;
17
18
use function strtoupper;
19
use Traversable;
20
use Yiisoft\Db\Exception\Exception;
21
use Yiisoft\Db\Exception\InvalidArgumentException;
22
use Yiisoft\Db\Exception\InvalidConfigException;
23
use Yiisoft\Db\Exception\NotSupportedException;
24
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
25
use Yiisoft\Db\Expression\ExpressionBuilderTrait;
26
use Yiisoft\Db\Expression\ExpressionInterface;
27
use Yiisoft\Db\Query\Query;
28
29
/**
30
 * Class InConditionBuilder builds objects of {@see InCondition}.
31
 */
32
class InConditionBuilder implements ExpressionBuilderInterface
33
{
34
    use ExpressionBuilderTrait;
35
36
    /**
37
     * Method builds the raw SQL from the $expression that will not be additionally escaped or quoted.
38
     *
39
     * @param ExpressionInterface|InCondition $expression the expression to be built.
40
     * @param array $params the binding parameters.
41
     *
42
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
43
     *
44
     * @return string the raw SQL that will not be additionally escaped or quoted.
45
     */
46 568
    public function build(ExpressionInterface $expression, array &$params = []): string
47
    {
48 568
        $operator = strtoupper($expression->getOperator());
0 ignored issues
show
Bug introduced by
The method getOperator() does not exist on Yiisoft\Db\Expression\ExpressionInterface. It seems like you code against a sub-type of Yiisoft\Db\Expression\ExpressionInterface such as Yiisoft\Db\Query\Conditions\ConjunctionCondition or Yiisoft\Db\Query\Conditions\BetweenCondition or Yiisoft\Db\Query\Conditions\SimpleCondition or Yiisoft\Db\Query\Conditions\ExistsCondition or Yiisoft\Db\Query\Conditions\InCondition or Yiisoft\Db\Query\Conditi...BetweenColumnsCondition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

48
        $operator = strtoupper($expression->/** @scrutinizer ignore-call */ getOperator());
Loading history...
49 568
        $column = $expression->getColumn();
0 ignored issues
show
Bug introduced by
The method getColumn() does not exist on Yiisoft\Db\Expression\ExpressionInterface. It seems like you code against a sub-type of Yiisoft\Db\Expression\ExpressionInterface such as Yiisoft\Db\Query\Conditions\BetweenCondition or Yiisoft\Db\Query\Conditions\SimpleCondition or Yiisoft\Db\Query\Conditions\InCondition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

49
        /** @scrutinizer ignore-call */ 
50
        $column = $expression->getColumn();
Loading history...
50 568
        $values = $expression->getValues();
0 ignored issues
show
Bug introduced by
The method getValues() does not exist on Yiisoft\Db\Expression\ExpressionInterface. It seems like you code against a sub-type of Yiisoft\Db\Expression\ExpressionInterface such as Yiisoft\Db\Query\Conditions\InCondition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

50
        /** @scrutinizer ignore-call */ 
51
        $values = $expression->getValues();
Loading history...
51
52 568
        if ($column === []) {
53
            /** no columns to test against */
54
            return $operator === 'IN' ? '0=1' : '';
55
        }
56
57 568
        if ($values instanceof Query) {
58 16
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
59
        }
60
61 552
        if (!is_array($values) && !$values instanceof Traversable) {
62
            /** ensure values is an array */
63 15
            $values = (array) $values;
64
        }
65
66 552
        if (is_array($column)) {
67 289
            if (count($column) > 1) {
68 25
                return $this->buildCompositeInCondition($operator, $column, $values, $params);
69
            }
70
71 269
            $column = reset($column);
72
        }
73
74 532
        if ($column instanceof Traversable) {
75 10
            if (iterator_count($column) > 1) {
76 5
                return $this->buildCompositeInCondition($operator, $column, $values, $params);
77
            }
78
79 5
            $column->rewind();
0 ignored issues
show
Bug introduced by
The method rewind() does not exist on Traversable. It seems like you code against a sub-type of Traversable such as Yaf_Config_Simple or Yaf\Session or Yaf_Session or Yaf\Config\Simple or Yaf\Config\Ini or Iterator or Yaf_Config_Ini or MongoGridFSCursor or SimpleXMLIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

79
            $column->/** @scrutinizer ignore-call */ 
80
                     rewind();
Loading history...
80 5
            $column = $column->current();
0 ignored issues
show
Bug introduced by
The method current() does not exist on Traversable. It seems like you code against a sub-type of Traversable such as IntlCodePointBreakIterator or Yaf_Config_Simple or Yaf\Session or IntlRuleBasedBreakIterator or Yaf_Session or Yaf\Config\Simple or Yaf\Config\Ini or Iterator or Yaf_Config_Ini or IntlBreakIterator or MongoGridFSCursor or SimpleXMLIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

80
            /** @scrutinizer ignore-call */ 
81
            $column = $column->current();
Loading history...
81
        }
82
83 527
        if (is_array($values)) {
84 477
            $rawValues = $values;
85 50
        } elseif ($values instanceof Traversable) {
0 ignored issues
show
introduced by
$values is always a sub-type of Traversable.
Loading history...
86 50
            $rawValues = $this->getRawValuesFromTraversableObject($values);
87
        }
88
89 527
        if (isset($rawValues) && in_array(null, $rawValues, true)) {
90 30
            $nullCondition = $this->getNullCondition($operator, $column);
91 30
            $nullConditionOperator = $operator === 'IN' ? 'OR' : 'AND';
92
        }
93
94 527
        $sqlValues = $this->buildValues($expression, $values, $params);
95
96 527
        if (empty($sqlValues)) {
97 40
            return $nullCondition ?? ($operator === 'IN' ? '0=1' : '');
98
        }
99
100 512
        if (strpos($column, '(') === false) {
101 512
            $column = $this->queryBuilder->getDb()->quoteColumnName($column);
102
        }
103
104 512
        if (count($sqlValues) > 1) {
105 275
            $sql = "$column $operator (" . implode(', ', $sqlValues) . ')';
106
        } else {
107 377
            $operator = $operator === 'IN' ? '=' : '<>';
108 377
            $sql = $column . $operator . reset($sqlValues);
109
        }
110
111 512
        return isset($nullCondition) ? sprintf('%s %s %s', $sql, $nullConditionOperator, $nullCondition) : $sql;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $nullConditionOperator does not seem to be defined for all execution paths leading up to this point.
Loading history...
112
    }
113
114
    /**
115
     * Builds $values to be used in {@see InCondition}.
116
     *
117
     * @param ConditionInterface|InCondition $condition
118
     * @param mixed $values
119
     * @param array $params the binding parameters.
120
     *
121
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
122
     *
123
     * @return array of prepared for SQL placeholders.
124
     */
125 527
    protected function buildValues(ConditionInterface $condition, $values, array &$params = []): array
126
    {
127 527
        $sqlValues = [];
128 527
        $column = $condition->getColumn();
0 ignored issues
show
Bug introduced by
The method getColumn() does not exist on Yiisoft\Db\Query\Conditions\ConditionInterface. It seems like you code against a sub-type of Yiisoft\Db\Query\Conditions\ConditionInterface such as Yiisoft\Db\Query\Conditions\BetweenCondition or Yiisoft\Db\Query\Conditions\SimpleCondition or Yiisoft\Db\Query\Conditions\InCondition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

128
        /** @scrutinizer ignore-call */ 
129
        $column = $condition->getColumn();
Loading history...
129
130 527
        if (is_array($column)) {
131 269
            $column = reset($column);
132
        }
133
134 527
        if ($column instanceof Traversable) {
135 5
            $column->rewind();
136 5
            $column = $column->current();
137
        }
138
139 527
        foreach ($values as $i => $value) {
140 522
            if (is_array($value) || $value instanceof ArrayAccess) {
141 10
                $value = $value[$column] ?? null;
142
            }
143
144 522
            if ($value === null) {
145 30
                continue;
146
            }
147
148 512
            if ($value instanceof ExpressionInterface) {
149 5
                $sqlValues[$i] = $this->queryBuilder->buildExpression($value, $params);
150
            } else {
151 512
                $sqlValues[$i] = $this->queryBuilder->bindParam($value, $params);
152
            }
153
        }
154
155 527
        return $sqlValues;
156
    }
157
158
    /**
159
     * Builds SQL for IN condition.
160
     *
161
     * @param string $operator
162
     * @param array|string $columns
163
     * @param Query $values
164
     * @param array $params
165
     *
166
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
167
     *
168
     * @return string SQL
169
     */
170 16
    protected function buildSubqueryInCondition(string $operator, $columns, Query $values, array &$params = []): string
171
    {
172 16
        $sql = $this->queryBuilder->buildExpression($values, $params);
173
174 16
        if (is_array($columns)) {
175 6
            foreach ($columns as $i => $col) {
176 6
                if (strpos($col, '(') === false) {
177 6
                    $columns[$i] = $this->queryBuilder->getDb()->quoteColumnName($col);
178
                }
179
            }
180
181 6
            return '(' . implode(', ', $columns) . ") $operator $sql";
182
        }
183
184 10
        if (strpos($columns, '(') === false) {
185 10
            $columns = $this->queryBuilder->getDb()->quoteColumnName($columns);
186
        }
187
188 10
        return "$columns $operator $sql";
189
    }
190
191
    /**
192
     * Builds SQL for IN condition.
193
     *
194
     * @param string $operator
195
     * @param array|Traversable $columns
196
     * @param array|iterable $values
197
     * @param array $params
198
     *
199
     * @return string SQL
200
     */
201 18
    protected function buildCompositeInCondition(string $operator, $columns, $values, array &$params = []): string
202
    {
203 18
        $vss = [];
204 18
        foreach ($values as $value) {
205 18
            $vs = [];
206
207 18
            foreach ($columns as $column) {
208 18
                if (isset($value[$column])) {
209 18
                    $vs[] = $this->queryBuilder->bindParam($value[$column], $params);
210
                } else {
211 3
                    $vs[] = 'NULL';
212
                }
213
            }
214 18
            $vss[] = '(' . implode(', ', $vs) . ')';
215
        }
216
217 18
        if (empty($vss)) {
218
            return $operator === 'IN' ? '0=1' : '';
219
        }
220
221 18
        $sqlColumns = [];
222 18
        foreach ($columns as $i => $column) {
223 18
            $sqlColumns[] = strpos($column, '(') === false
224 18
                ? $this->queryBuilder->getDb()->quoteColumnName($column) : $column;
225
        }
226
227 18
        return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
228
    }
229
230
    /**
231
     * Builds is null/is not null condition for column based on operator.
232
     *
233
     * @param string $operator
234
     * @param string $column
235
     *
236
     * @return string is null or is not null condition
237
     */
238 30
    protected function getNullCondition(string $operator, string $column): string
239
    {
240 30
        $column = $this->queryBuilder->getDb()->quoteColumnName($column);
241
242 30
        if ($operator === 'IN') {
243 15
            return sprintf('%s IS NULL', $column);
244
        }
245
246 15
        return sprintf('%s IS NOT NULL', $column);
247
    }
248
249
    /**
250
     * @param Traversable $traversableObject
251
     *
252
     * @return array raw values
253
     */
254 50
    protected function getRawValuesFromTraversableObject(Traversable $traversableObject): array
255
    {
256 50
        $rawValues = [];
257 50
        foreach ($traversableObject as $value) {
258 50
            if (is_array($value)) {
259 5
                $values = array_values($value);
260 5
                $rawValues = array_merge($rawValues, $values);
261
            } else {
262 45
                $rawValues[] = $value;
263
            }
264
        }
265
266 50
        return $rawValues;
267
    }
268
}
269