Passed
Push — master ( 4281e6...ce1733 )
by Alexander
03:46
created

InConditionBuilder::buildCompositeInCondition()   B

Complexity

Conditions 8
Paths 20

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8.0155

Importance

Changes 0
Metric Value
cc 8
eloc 16
nc 20
nop 4
dl 0
loc 27
ccs 15
cts 16
cp 0.9375
crap 8.0155
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 ArrayAccess;
8
use Traversable;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
14
use Yiisoft\Db\Expression\ExpressionBuilderTrait;
15
use Yiisoft\Db\Expression\ExpressionInterface;
16
use Yiisoft\Db\Query\Query;
17
18
use function array_merge;
19
use function array_values;
20
use function count;
21
use function implode;
22
use function is_array;
23
use function iterator_count;
24
use function reset;
25
use function sprintf;
26
use function strpos;
27
use function strtoupper;
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
     *
41
     * @param array $params the binding parameters.
42
     *
43
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
44
     *
45
     * @return string the raw SQL that will not be additionally escaped or quoted.
46
     */
47 391
    public function build(ExpressionInterface $expression, array &$params = []): string
48
    {
49 391
        $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

49
        $operator = strtoupper($expression->/** @scrutinizer ignore-call */ getOperator());
Loading history...
50 391
        $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

50
        /** @scrutinizer ignore-call */ 
51
        $column = $expression->getColumn();
Loading history...
51 391
        $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

51
        /** @scrutinizer ignore-call */ 
52
        $values = $expression->getValues();
Loading history...
52
53 391
        if ($column === []) {
54
            /** no columns to test against */
55
            return $operator === 'IN' ? '0=1' : '';
56
        }
57
58 391
        if ($values instanceof Query) {
59 12
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
60
        }
61
62 379
        if (!is_array($values) && !$values instanceof Traversable) {
63
            /** ensure values is an array */
64 12
            $values = (array) $values;
65
        }
66
67 379
        if (is_array($column)) {
68 211
            if (count($column) > 1) {
69 20
                return $this->buildCompositeInCondition($operator, $column, $values, $params);
70
            }
71
72 195
            $column = reset($column);
73
        }
74
75 363
        if ($column instanceof Traversable) {
76 8
            if (iterator_count($column) > 1) {
77 4
                return $this->buildCompositeInCondition($operator, $column, $values, $params);
78
            }
79
80 4
            $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

80
            $column->/** @scrutinizer ignore-call */ 
81
                     rewind();
Loading history...
81 4
            $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

81
            /** @scrutinizer ignore-call */ 
82
            $column = $column->current();
Loading history...
82
        }
83
84 359
        if (is_array($values)) {
85 319
            $rawValues = $values;
86 40
        } elseif ($values instanceof Traversable) {
0 ignored issues
show
introduced by
$values is always a sub-type of Traversable.
Loading history...
87 40
            $rawValues = $this->getRawValuesFromTraversableObject($values);
88
        }
89
90 359
        if (isset($rawValues) && in_array(null, $rawValues, true)) {
91 24
            $nullCondition = $this->getNullCondition($operator, $column);
92 24
            $nullConditionOperator = $operator === 'IN' ? 'OR' : 'AND';
93
        }
94
95 359
        $sqlValues = $this->buildValues($expression, $values, $params);
96
97 359
        if (empty($sqlValues)) {
98 32
            return $nullCondition ?? ($operator === 'IN' ? '0=1' : '');
99
        }
100
101 347
        if (strpos($column, '(') === false) {
102 347
            $column = $this->queryBuilder->getDb()->quoteColumnName($column);
0 ignored issues
show
Bug introduced by
The method quoteColumnName() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

102
            $column = $this->queryBuilder->getDb()->/** @scrutinizer ignore-call */ quoteColumnName($column);
Loading history...
103
        }
104
105 347
        if (count($sqlValues) > 1) {
106 206
            $sql = "$column $operator (" . implode(', ', $sqlValues) . ')';
107
        } else {
108 249
            $operator = $operator === 'IN' ? '=' : '<>';
109 249
            $sql = $column . $operator . reset($sqlValues);
110
        }
111
112 347
        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...
113
    }
114
115
    /**
116
     * Builds $values to be used in {@see InCondition}.
117
     *
118
     * @param ConditionInterface|InCondition $condition
119
     * @param object|array $values
120
     * @param array $params the binding parameters.
121
     *
122
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
123
     *
124
     * @return array of prepared for SQL placeholders.
125
     */
126 359
    protected function buildValues(ConditionInterface $condition, $values, array &$params = []): array
127
    {
128 359
        $sqlValues = [];
129 359
        $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

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