Completed
Push — master ( 594ce5...8355c2 )
by Andrii
02:35
created

QueryBuilder::getTableUniqueColumnNames()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 0
cts 24
cp 0
rs 8.7217
c 0
b 0
f 0
cc 6
nc 7
nop 3
crap 42
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use yii\exceptions\InvalidArgumentException;
11
use yii\exceptions\NotSupportedException;
12
use yii\db\conditions\ConditionInterface;
13
use yii\db\conditions\HashCondition;
14
use yii\helpers\StringHelper;
15
16
/**
17
 * QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object.
18
 *
19
 * SQL statements are created from [[Query]] objects using the [[build()]]-method.
20
 *
21
 * QueryBuilder is also used by [[Command]] to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE.
22
 *
23
 * For more details and usage information on QueryBuilder, see the [guide article on query builders](guide:db-query-builder).
24
 *
25
 * @property string[] $conditionClasses Map of condition aliases to condition classes. For example: ```php
26
 * ['LIKE' => yii\db\condition\LikeCondition::class] ``` . This property is write-only.
27
 * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones in
28
 * [[expressionBuilders]] property. This property is write-only.
29
 *
30
 * @author Qiang Xue <[email protected]>
31
 * @since 2.0
32
 */
33
class QueryBuilder extends \yii\base\BaseObject
34
{
35
    /**
36
     * The prefix for automatically generated query binding parameters.
37
     */
38
    const PARAM_PREFIX = ':qp';
39
40
    /**
41
     * @var Connection the database connection.
42
     */
43
    public $db;
44
    /**
45
     * @var string the separator between different fragments of a SQL statement.
46
     * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
47
     */
48
    public $separator = ' ';
49
    /**
50
     * @var array the abstract column types mapped to physical column types.
51
     * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
52
     * Child classes should override this property to declare supported type mappings.
53
     */
54
    public $typeMap = [];
55
56
    /**
57
     * @var array map of condition aliases to condition classes. For example:
58
     *
59
     * ```php
60
     * return [
61
     *     'LIKE' => yii\db\condition\LikeCondition::class,
62
     * ];
63
     * ```
64
     *
65
     * This property is used by [[createConditionFromArray]] method.
66
     * See default condition classes list in [[defaultConditionClasses()]] method.
67
     *
68
     * In case you want to add custom conditions support, use the [[setConditionClasses()]] method.
69
     *
70
     * @see setConditonClasses()
71
     * @see defaultConditionClasses()
72
     * @since 2.0.14
73
     */
74
    protected $conditionClasses = [];
75
    /**
76
     * @var string[]|ExpressionBuilderInterface[] maps expression class to expression builder class.
77
     * For example:
78
     *
79
     * ```php
80
     * [
81
     *    yii\db\Expression::class => yii\db\ExpressionBuilder::class
82
     * ]
83
     * ```
84
     * This property is mainly used by [[buildExpression()]] to build SQL expressions form expression objects.
85
     * See default values in [[defaultExpressionBuilders()]] method.
86
     *
87
     *
88
     * To override existing builders or add custom, use [[setExpressionBuilder()]] method. New items will be added
89
     * to the end of this array.
90
     *
91
     * To find a builder, [[buildExpression()]] will check the expression class for its exact presence in this map.
92
     * In case it is NOT present, the array will be iterated in reverse direction, checking whether the expression
93
     * extends the class, defined in this map.
94
     *
95
     * @see setExpressionBuilders()
96
     * @see defaultExpressionBuilders()
97
     * @since 2.0.14
98
     */
99
    protected $expressionBuilders = [];
100
101
102
    /**
103
     * Constructor.
104
     * @param Connection $connection the database connection.
105
     * @param array $config name-value pairs that will be used to initialize the object properties
0 ignored issues
show
Bug introduced by
There is no parameter named $config. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
106
     */
107 3
    public function __construct($connection)
108
    {
109 3
        $this->db = $connection;
110 3
        $this->expressionBuilders = $this->defaultExpressionBuilders();
111 3
        $this->conditionClasses = $this->defaultConditionClasses();
112
    }
113
114
    /**
115
     * Contains array of default condition classes. Extend this method, if you want to change
116
     * default condition classes for the query builder. See [[conditionClasses]] docs for details.
117
     *
118
     * @return array
119
     * @see conditionClasses
120
     * @since 2.0.14
121
     */
122 3
    protected function defaultConditionClasses()
123
    {
124
        return [
125 3
            'NOT' => conditions\NotCondition::class,
126
            'AND' => conditions\AndCondition::class,
127
            'OR' => conditions\OrCondition::class,
128
            'BETWEEN' => conditions\BetweenCondition::class,
129
            'NOT BETWEEN' => conditions\BetweenCondition::class,
130
            'IN' => conditions\InCondition::class,
131
            'NOT IN' => conditions\InCondition::class,
132
            'LIKE' => conditions\LikeCondition::class,
133
            'NOT LIKE' => conditions\LikeCondition::class,
134
            'OR LIKE' => conditions\LikeCondition::class,
135
            'OR NOT LIKE' => conditions\LikeCondition::class,
136
            'EXISTS' => conditions\ExistsCondition::class,
137
            'NOT EXISTS' => conditions\ExistsCondition::class,
138
        ];
139
    }
140
141
    /**
142
     * Contains array of default expression builders. Extend this method and override it, if you want to change
143
     * default expression builders for this query builder. See [[expressionBuilders]] docs for details.
144
     *
145
     * @return array
146
     * @see $expressionBuilders
147
     * @since 2.0.14
148
     */
149 3
    protected function defaultExpressionBuilders()
150
    {
151
        return [
152 3
            Query::class => QueryExpressionBuilder::class,
153
            PdoValue::class => PdoValueBuilder::class,
154
            Expression::class => ExpressionBuilder::class,
155
            conditions\ConjunctionCondition::class => conditions\ConjunctionConditionBuilder::class,
156
            conditions\NotCondition::class => conditions\NotConditionBuilder::class,
157
            conditions\AndCondition::class => conditions\ConjunctionConditionBuilder::class,
158
            conditions\OrCondition::class => conditions\ConjunctionConditionBuilder::class,
159
            conditions\BetweenCondition::class => conditions\BetweenConditionBuilder::class,
160
            conditions\InCondition::class => conditions\InConditionBuilder::class,
161
            conditions\LikeCondition::class => conditions\LikeConditionBuilder::class,
162
            conditions\ExistsCondition::class => conditions\ExistsConditionBuilder::class,
163
            conditions\SimpleCondition::class => conditions\SimpleConditionBuilder::class,
164
            conditions\HashCondition::class => conditions\HashConditionBuilder::class,
165
            conditions\BetweenColumnsCondition::class => conditions\BetweenColumnsConditionBuilder::class,
166
        ];
167
    }
168
169
    /**
170
     * Setter for [[expressionBuilders]] property.
171
     *
172
     * @param string[] $builders array of builders that should be merged with the pre-defined ones
173
     * in [[expressionBuilders]] property.
174
     * @since 2.0.14
175
     * @see expressionBuilders
176
     */
177
    public function setExpressionBuilders($builders)
178
    {
179
        $this->expressionBuilders = array_merge($this->expressionBuilders, $builders);
180
    }
181
182
    /**
183
     * Setter for [[conditionClasses]] property.
184
     *
185
     * @param string[] $classes map of condition aliases to condition classes. For example:
186
     *
187
     * ```php
188
     * ['LIKE' => yii\db\condition\LikeCondition::class]
189
     * ```
190
     *
191
     * @since 2.0.14.2
192
     * @see conditionClasses
193
     */
194
    public function setConditionClasses($classes)
195
    {
196
        $this->conditionClasses = array_merge($this->conditionClasses, $classes);
197
    }
198
199
    /**
200
     * Generates a SELECT SQL statement from a [[Query]] object.
201
     *
202
     * @param Query $query the [[Query]] object from which the SQL statement will be generated.
203
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
204
     * be included in the result with the additional parameters generated during the query building process.
205
     * @return array the generated SQL statement (the first array element) and the corresponding
206
     * parameters to be bound to the SQL statement (the second array element). The parameters returned
207
     * include those provided in `$params`.
208
     */
209
    public function build($query, $params = [])
210
    {
211
        $query = $query->prepare($this);
212
213
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
214
215
        $clauses = [
216
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
217
            $this->buildFrom($query->from, $params),
218
            $this->buildJoin($query->join, $params),
219
            $this->buildWhere($query->where, $params),
220
            $this->buildGroupBy($query->groupBy, $params),
221
            $this->buildHaving($query->having, $params),
222
        ];
223
224
        $sql = implode($this->separator, array_filter($clauses));
225
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset, $params);
0 ignored issues
show
Bug introduced by
It seems like $query->limit can also be of type object<yii\db\ExpressionInterface>; however, yii\db\QueryBuilder::buildOrderByAndLimit() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $query->offset can also be of type object<yii\db\ExpressionInterface>; however, yii\db\QueryBuilder::buildOrderByAndLimit() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
226
227
        $union = $this->buildUnion($query->union, $params);
228
        if ($union !== '') {
229
            $sql = "($sql){$this->separator}$union";
230
        }
231
232
        return [$sql, $params];
233
    }
234
235
    /**
236
     * Builds given $expression
237
     *
238
     * @param ExpressionInterface $expression the expression to be built
239
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
240
     * be included in the result with the additional parameters generated during the expression building process.
241
     * @return string the SQL statement that will not be neither quoted nor encoded before passing to DBMS
242
     * @see ExpressionInterface
243
     * @see ExpressionBuilderInterface
244
     * @see expressionBuilders
245
     * @since 2.0.14
246
     * @throws InvalidArgumentException when $expression building is not supported by this QueryBuilder.
247
     */
248
    public function buildExpression(ExpressionInterface $expression, &$params = [])
249
    {
250
        $builder = $this->getExpressionBuilder($expression);
251
252
        return $builder->build($expression, $params);
253
    }
254
255
    /**
256
     * Gets object of [[ExpressionBuilderInterface]] that is suitable for $expression.
257
     * Uses [[expressionBuilders]] array to find a suitable builder class.
258
     *
259
     * @param ExpressionInterface $expression
260
     * @return ExpressionBuilderInterface
261
     * @see expressionBuilders
262
     * @since 2.0.14
263
     * @throws InvalidArgumentException when $expression building is not supported by this QueryBuilder.
264
     */
265
    public function getExpressionBuilder(ExpressionInterface $expression)
266
    {
267
        $className = get_class($expression);
268
269
        if (!isset($this->expressionBuilders[$className])) {
270
            foreach (array_reverse($this->expressionBuilders) as $expressionClass => $builderClass) {
271
                if (is_subclass_of($expression, $expressionClass)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $expressionClass can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
272
                    $this->expressionBuilders[$className] = $builderClass;
273
                    break;
274
                }
275
            }
276
277
            if (!isset($this->expressionBuilders[$className])) {
278
                throw new InvalidArgumentException('Expression of class ' . $className . ' can not be built in ' . get_class($this));
279
            }
280
        }
281
282
        if ($this->expressionBuilders[$className] === __CLASS__) {
283
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (yii\db\QueryBuilder) is incompatible with the return type documented by yii\db\QueryBuilder::getExpressionBuilder of type yii\db\ExpressionBuilderInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
284
        }
285
286
        if (!is_object($this->expressionBuilders[$className])) {
287
            $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
288
        }
289
290
        return $this->expressionBuilders[$className];
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->expressionBuilders[$className]; of type string|object adds the type string to the return on line 290 which is incompatible with the return type documented by yii\db\QueryBuilder::getExpressionBuilder of type yii\db\ExpressionBuilderInterface.
Loading history...
291
    }
292
293
    /**
294
     * Creates an INSERT SQL statement.
295
     * For example,
296
     *
297
     * ```php
298
     * $sql = $queryBuilder->insert('user', [
299
     *     'name' => 'Sam',
300
     *     'age' => 30,
301
     * ], $params);
302
     * ```
303
     *
304
     * The method will properly escape the table and column names.
305
     *
306
     * @param string $table the table that new rows will be inserted into.
307
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance
308
     * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement.
309
     * Passing of [[yii\db\Query|Query]] is available since version 2.0.11.
310
     * @param array $params the binding parameters that will be generated by this method.
311
     * They should be bound to the DB command later.
312
     * @return string the INSERT SQL
313
     */
314
    public function insert($table, $columns, &$params)
315
    {
316
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
0 ignored issues
show
Bug introduced by
The variable $names does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $placeholders does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $values does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
317
        return 'INSERT INTO ' . $this->db->quoteTableName($table)
318
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
319
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
320
    }
321
322
    /**
323
     * Prepares a `VALUES` part for an `INSERT` SQL statement.
324
     *
325
     * @param string $table the table that new rows will be inserted into.
326
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance
327
     * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement.
328
     * @param array $params the binding parameters that will be generated by this method.
329
     * They should be bound to the DB command later.
330
     * @return array array of column names, placeholders, values and params.
331
     * @since 2.0.14
332
     */
333
    protected function prepareInsertValues($table, $columns, $params = [])
334
    {
335
        $schema = $this->db->getSchema();
336
        $tableSchema = $schema->getTableSchema($table);
337
        $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
338
        $names = [];
339
        $placeholders = [];
340
        $values = ' DEFAULT VALUES';
341
        if ($columns instanceof Query) {
342
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
343
        } else {
344
            foreach ($columns as $name => $value) {
345
                $names[] = $schema->quoteColumnName($name);
346
                $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
347
348
                if ($value instanceof ExpressionInterface) {
349
                    $placeholders[] = $this->buildExpression($value, $params);
350
                } elseif ($value instanceof \yii\db\Query) {
351
                    [$sql, $params] = $this->build($value, $params);
0 ignored issues
show
Bug introduced by
The variable $sql does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
352
                    $placeholders[] = "($sql)";
353
                } else {
354
                    $placeholders[] = $this->bindParam($value, $params);
355
                }
356
            }
357
        }
358
        return [$names, $placeholders, $values, $params];
359
    }
360
361
    /**
362
     * Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement.
363
     *
364
     * @param Query $columns Object, which represents select query.
365
     * @param \yii\db\Schema $schema Schema object to quote column name.
366
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
367
     * be included in the result with the additional parameters generated during the query building process.
368
     * @return array array of column names, values and params.
369
     * @throws InvalidArgumentException if query's select does not contain named parameters only.
370
     * @since 2.0.11
371
     */
372
    protected function prepareInsertSelectSubQuery($columns, $schema, $params = [])
373
    {
374
        if (!is_array($columns->select) || empty($columns->select) || in_array('*', $columns->select)) {
375
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
376
        }
377
378
        [$values, $params] = $this->build($columns, $params);
0 ignored issues
show
Bug introduced by
The variable $values seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
379
        $names = [];
380
        $values = ' ' . $values;
0 ignored issues
show
Bug introduced by
The variable $values seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
381
        foreach ($columns->select as $title => $field) {
382
            if (is_string($title)) {
383
                $names[] = $schema->quoteColumnName($title);
384
            } elseif (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $field, $matches)) {
385
                $names[] = $schema->quoteColumnName($matches[2]);
386
            } else {
387
                $names[] = $schema->quoteColumnName($field);
388
            }
389
        }
390
391
        return [$names, $values, $params];
392
    }
393
394
    /**
395
     * Generates a batch INSERT SQL statement.
396
     *
397
     * For example,
398
     *
399
     * ```php
400
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
401
     *     ['Tom', 30],
402
     *     ['Jane', 20],
403
     *     ['Linda', 25],
404
     * ]);
405
     * ```
406
     *
407
     * Note that the values in each row must match the corresponding column names.
408
     *
409
     * The method will properly escape the column names, and quote the values to be inserted.
410
     *
411
     * @param string $table the table that new rows will be inserted into.
412
     * @param array $columns the column names
413
     * @param array|\Generator $rows the rows to be batch inserted into the table
414
     * @param array $params the binding parameters. This parameter exists since 2.0.14
415
     * @return string the batch INSERT SQL statement
416
     */
417
    public function batchInsert($table, $columns, $rows, &$params = [])
418
    {
419
        if (empty($rows)) {
420
            return '';
421
        }
422
423
        $schema = $this->db->getSchema();
424
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
425
            $columnSchemas = $tableSchema->columns;
426
        } else {
427
            $columnSchemas = [];
428
        }
429
430
        $values = [];
431
        foreach ($rows as $row) {
432
            $vs = [];
433
            foreach ($row as $i => $value) {
434
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
435
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
436
                }
437
                if (is_string($value)) {
438
                    $value = $schema->quoteValue($value);
439
                } elseif (is_float($value)) {
440
                    // ensure type cast always has . as decimal separator in all locales
441
                    $value = StringHelper::floatToString($value);
442
                } elseif ($value === false) {
443
                    $value = 0;
444
                } elseif ($value === null) {
445
                    $value = 'NULL';
446
                } elseif ($value instanceof ExpressionInterface) {
447
                    $value = $this->buildExpression($value, $params);
448
                }
449
                $vs[] = $value;
450
            }
451
            $values[] = '(' . implode(', ', $vs) . ')';
452
        }
453
        if (empty($values)) {
454
            return '';
455
        }
456
457
        foreach ($columns as $i => $name) {
458
            $columns[$i] = $schema->quoteColumnName($name);
459
        }
460
461
        return 'INSERT INTO ' . $schema->quoteTableName($table)
462
        . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
463
    }
464
465
    /**
466
     * Creates an SQL statement to insert rows into a database table if
467
     * they do not already exist (matching unique constraints),
468
     * or update them if they do.
469
     *
470
     * For example,
471
     *
472
     * ```php
473
     * $sql = $queryBuilder->upsert('pages', [
474
     *     'name' => 'Front page',
475
     *     'url' => 'http://example.com/', // url is unique
476
     *     'visits' => 0,
477
     * ], [
478
     *     'visits' => new \yii\db\Expression('visits + 1'),
479
     * ], $params);
480
     * ```
481
     *
482
     * The method will properly escape the table and column names.
483
     *
484
     * @param string $table the table that new rows will be inserted into/updated in.
485
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
486
     * of [[Query]] to perform `INSERT INTO ... SELECT` SQL statement.
487
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
488
     * If `true` is passed, the column data will be updated to match the insert column data.
489
     * If `false` is passed, no update will be performed if the column data already exists.
490
     * @param array $params the binding parameters that will be generated by this method.
491
     * They should be bound to the DB command later.
492
     * @return string the resulting SQL.
493
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
494
     * @since 2.0.14
495
     */
496
    public function upsert($table, $insertColumns, $updateColumns, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $table is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $insertColumns is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $updateColumns is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
497
    {
498
        throw new NotSupportedException($this->db->getDriverName() . ' does not support upsert statements.');
499
    }
500
501
    /**
502
     * @param string $table
503
     * @param array|Query $insertColumns
504
     * @param array|bool $updateColumns
505
     * @param Constraint[] $constraints this parameter recieves a matched constraint list.
506
     * The constraints will be unique by their column names.
507
     * @return array
508
     * @since 2.0.14
509
     */
510
    protected function prepareUpsertColumns($table, $insertColumns, $updateColumns, &$constraints = [])
511
    {
512
        if ($insertColumns instanceof Query) {
513
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns, $this->db->getSchema());
0 ignored issues
show
Bug introduced by
The variable $insertNames seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
514
        } else {
515
            $insertNames = array_map([$this->db, 'quoteColumnName'], array_keys($insertColumns));
516
        }
517
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
0 ignored issues
show
Bug introduced by
The variable $insertNames does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
518
        $uniqueNames = array_map([$this->db, 'quoteColumnName'], $uniqueNames);
519
        if ($updateColumns !== true) {
520
            return [$uniqueNames, $insertNames, null];
521
        }
522
523
        return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
524
    }
525
526
    /**
527
     * Returns all column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
528
     * for the named table removing constraints which did not cover the specified column list.
529
     * The column list will be unique by column names.
530
     *
531
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
532
     * @param string[] $columns source column list.
533
     * @param Constraint[] $constraints this parameter optionally recieves a matched constraint list.
534
     * The constraints will be unique by their column names.
535
     * @return string[] column list.
536
     */
537
    private function getTableUniqueColumnNames($name, $columns, &$constraints = [])
538
    {
539
        $schema = $this->db->getSchema();
540
        if (!$schema instanceof ConstraintFinderInterface) {
541
            return [];
542
        }
543
544
        $constraints = [];
545
        $primaryKey = $schema->getTablePrimaryKey($name);
546
        if ($primaryKey !== null) {
547
            $constraints[] = $primaryKey;
548
        }
549
        foreach ($schema->getTableIndexes($name) as $constraint) {
550
            if ($constraint->isUnique) {
551
                $constraints[] = $constraint;
552
            }
553
        }
554
        $constraints = array_merge($constraints, $schema->getTableUniques($name));
555
        // Remove duplicates
556
        $constraints = array_combine(array_map(function (Constraint $constraint) {
557
            $columns = $constraint->columnNames;
558
            sort($columns, SORT_STRING);
559
            return json_encode($columns);
560
        }, $constraints), $constraints);
561
        $columnNames = [];
562
        // Remove all constraints which do not cover the specified column list
563
        $constraints = array_values(array_filter($constraints, function (Constraint $constraint) use ($schema, $columns, &$columnNames) {
564
            $constraintColumnNames = array_map([$schema, 'quoteColumnName'], $constraint->columnNames);
565
            $result = !array_diff($constraintColumnNames, $columns);
566
            if ($result) {
567
                $columnNames = array_merge($columnNames, $constraintColumnNames);
568
            }
569
            return $result;
570
        }));
571
        return array_unique($columnNames);
572
    }
573
574
    /**
575
     * Creates an UPDATE SQL statement.
576
     *
577
     * For example,
578
     *
579
     * ```php
580
     * $params = [];
581
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
582
     * ```
583
     *
584
     * The method will properly escape the table and column names.
585
     *
586
     * @param string $table the table to be updated.
587
     * @param array $columns the column data (name => value) to be updated.
588
     * @param array|string $condition the condition that will be put in the WHERE part. Please
589
     * refer to [[Query::where()]] on how to specify condition.
590
     * @param array $params the binding parameters that will be modified by this method
591
     * so that they can be bound to the DB command later.
592
     * @return string the UPDATE SQL
593
     */
594
    public function update($table, $columns, $condition, &$params)
595
    {
596
        [$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
0 ignored issues
show
Bug introduced by
The variable $lines does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
597
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
598
        $where = $this->buildWhere($condition, $params);
599
        return $where === '' ? $sql : $sql . ' ' . $where;
600
    }
601
602
    /**
603
     * Prepares a `SET` parts for an `UPDATE` SQL statement.
604
     * @param string $table the table to be updated.
605
     * @param array $columns the column data (name => value) to be updated.
606
     * @param array $params the binding parameters that will be modified by this method
607
     * so that they can be bound to the DB command later.
608
     * @return array an array `SET` parts for an `UPDATE` SQL statement (the first array element) and params (the second array element).
609
     * @since 2.0.14
610
     */
611
    protected function prepareUpdateSets($table, $columns, $params = [])
612
    {
613
        $tableSchema = $this->db->getTableSchema($table);
614
        $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
615
        $sets = [];
616
        foreach ($columns as $name => $value) {
617
618
            $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
619
            if ($value instanceof ExpressionInterface) {
620
                $placeholder = $this->buildExpression($value, $params);
621
            } else {
622
                $placeholder = $this->bindParam($value, $params);
623
            }
624
625
            $sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
626
        }
627
        return [$sets, $params];
628
    }
629
630
    /**
631
     * Creates a DELETE SQL statement.
632
     *
633
     * For example,
634
     *
635
     * ```php
636
     * $sql = $queryBuilder->delete('user', 'status = 0');
637
     * ```
638
     *
639
     * The method will properly escape the table and column names.
640
     *
641
     * @param string $table the table where the data will be deleted from.
642
     * @param array|string $condition the condition that will be put in the WHERE part. Please
643
     * refer to [[Query::where()]] on how to specify condition.
644
     * @param array $params the binding parameters that will be modified by this method
645
     * so that they can be bound to the DB command later.
646
     * @return string the DELETE SQL
647
     */
648
    public function delete($table, $condition, &$params)
649
    {
650
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
651
        $where = $this->buildWhere($condition, $params);
652
653
        return $where === '' ? $sql : $sql . ' ' . $where;
654
    }
655
656
    /**
657
     * Builds a SQL statement for creating a new DB table.
658
     *
659
     * The columns in the new  table should be specified as name-definition pairs (e.g. 'name' => 'string'),
660
     * where name stands for a column name which will be properly quoted by the method, and definition
661
     * stands for the column type which can contain an abstract DB type.
662
     * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one.
663
     *
664
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
665
     * inserted into the generated SQL.
666
     *
667
     * For example,
668
     *
669
     * ```php
670
     * $sql = $queryBuilder->createTable('user', [
671
     *  'id' => 'pk',
672
     *  'name' => 'string',
673
     *  'age' => 'integer',
674
     * ]);
675
     * ```
676
     *
677
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
678
     * @param array $columns the columns (name => definition) in the new table.
679
     * @param string $options additional SQL fragment that will be appended to the generated SQL.
680
     * @return string the SQL statement for creating a new DB table.
681
     */
682
    public function createTable($table, $columns, $options = null)
683
    {
684
        $cols = [];
685
        foreach ($columns as $name => $type) {
686
            if (is_string($name)) {
687
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
688
            } else {
689
                $cols[] = "\t" . $type;
690
            }
691
        }
692
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
693
694
        return $options === null ? $sql : $sql . ' ' . $options;
695
    }
696
697
    /**
698
     * Builds a SQL statement for renaming a DB table.
699
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
700
     * @param string $newName the new table name. The name will be properly quoted by the method.
701
     * @return string the SQL statement for renaming a DB table.
702
     */
703
    public function renameTable($oldName, $newName)
704
    {
705
        return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
706
    }
707
708
    /**
709
     * Builds a SQL statement for dropping a DB table.
710
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
711
     * @return string the SQL statement for dropping a DB table.
712
     */
713
    public function dropTable($table)
714
    {
715
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
716
    }
717
718
    /**
719
     * Builds a SQL statement for adding a primary key constraint to an existing table.
720
     * @param string $name the name of the primary key constraint.
721
     * @param string $table the table that the primary key constraint will be added to.
722
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
723
     * @return string the SQL statement for adding a primary key constraint to an existing table.
724
     */
725
    public function addPrimaryKey($name, $table, $columns)
726
    {
727
        if (is_string($columns)) {
728
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
729
        }
730
731
        foreach ($columns as $i => $col) {
732
            $columns[$i] = $this->db->quoteColumnName($col);
733
        }
734
735
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
736
            . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
737
            . implode(', ', $columns) . ')';
738
    }
739
740
    /**
741
     * Builds a SQL statement for removing a primary key constraint to an existing table.
742
     * @param string $name the name of the primary key constraint to be removed.
743
     * @param string $table the table that the primary key constraint will be removed from.
744
     * @return string the SQL statement for removing a primary key constraint from an existing table.
745
     */
746
    public function dropPrimaryKey($name, $table)
747
    {
748
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
749
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
750
    }
751
752
    /**
753
     * Builds a SQL statement for truncating a DB table.
754
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
755
     * @return string the SQL statement for truncating a DB table.
756
     */
757
    public function truncateTable($table)
758
    {
759
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
760
    }
761
762
    /**
763
     * Builds a SQL statement for adding a new DB column.
764
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method.
765
     * @param string $column the name of the new column. The name will be properly quoted by the method.
766
     * @param string $type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any)
767
     * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
768
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
769
     * @return string the SQL statement for adding a new column.
770
     */
771
    public function addColumn($table, $column, $type)
772
    {
773
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
774
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
775
            . $this->getColumnType($type);
776
    }
777
778
    /**
779
     * Builds a SQL statement for dropping a DB column.
780
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
781
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
782
     * @return string the SQL statement for dropping a DB column.
783
     */
784
    public function dropColumn($table, $column)
785
    {
786
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
787
            . ' DROP COLUMN ' . $this->db->quoteColumnName($column);
788
    }
789
790
    /**
791
     * Builds a SQL statement for renaming a column.
792
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
793
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
794
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
795
     * @return string the SQL statement for renaming a DB column.
796
     */
797
    public function renameColumn($table, $oldName, $newName)
798
    {
799
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
800
            . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
801
            . ' TO ' . $this->db->quoteColumnName($newName);
802
    }
803
804
    /**
805
     * Builds a SQL statement for changing the definition of a column.
806
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
807
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
808
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
809
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
810
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
811
     * will become 'varchar(255) not null'.
812
     * @return string the SQL statement for changing the definition of a column.
813
     */
814
    public function alterColumn($table, $column, $type)
815
    {
816
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
817
            . $this->db->quoteColumnName($column) . ' '
818
            . $this->db->quoteColumnName($column) . ' '
819
            . $this->getColumnType($type);
820
    }
821
822
    /**
823
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
824
     * The method will properly quote the table and column names.
825
     * @param string $name the name of the foreign key constraint.
826
     * @param string $table the table that the foreign key constraint will be added to.
827
     * @param string|array $columns the name of the column to that the constraint will be added on.
828
     * If there are multiple columns, separate them with commas or use an array to represent them.
829
     * @param string $refTable the table that the foreign key references to.
830
     * @param string|array $refColumns the name of the column that the foreign key references to.
831
     * If there are multiple columns, separate them with commas or use an array to represent them.
832
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
833
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
834
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
835
     */
836
    public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
837
    {
838
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
839
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
840
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
841
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
842
            . ' (' . $this->buildColumns($refColumns) . ')';
843
        if ($delete !== null) {
844
            $sql .= ' ON DELETE ' . $delete;
845
        }
846
        if ($update !== null) {
847
            $sql .= ' ON UPDATE ' . $update;
848
        }
849
850
        return $sql;
851
    }
852
853
    /**
854
     * Builds a SQL statement for dropping a foreign key constraint.
855
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
856
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
857
     * @return string the SQL statement for dropping a foreign key constraint.
858
     */
859
    public function dropForeignKey($name, $table)
860
    {
861
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
862
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
863
    }
864
865
    /**
866
     * Builds a SQL statement for creating a new index.
867
     * @param string $name the name of the index. The name will be properly quoted by the method.
868
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
869
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
870
     * separate them with commas or use an array to represent them. Each column name will be properly quoted
871
     * by the method, unless a parenthesis is found in the name.
872
     * @param bool $unique whether to add UNIQUE constraint on the created index.
873
     * @return string the SQL statement for creating a new index.
874
     */
875
    public function createIndex($name, $table, $columns, $unique = false)
876
    {
877
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
878
            . $this->db->quoteTableName($name) . ' ON '
879
            . $this->db->quoteTableName($table)
880
            . ' (' . $this->buildColumns($columns) . ')';
881
    }
882
883
    /**
884
     * Builds a SQL statement for dropping an index.
885
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
886
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
887
     * @return string the SQL statement for dropping an index.
888
     */
889
    public function dropIndex($name, $table)
890
    {
891
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
892
    }
893
894
    /**
895
     * Creates a SQL command for adding an unique constraint to an existing table.
896
     * @param string $name the name of the unique constraint.
897
     * The name will be properly quoted by the method.
898
     * @param string $table the table that the unique constraint will be added to.
899
     * The name will be properly quoted by the method.
900
     * @param string|array $columns the name of the column to that the constraint will be added on.
901
     * If there are multiple columns, separate them with commas.
902
     * The name will be properly quoted by the method.
903
     * @return string the SQL statement for adding an unique constraint to an existing table.
904
     * @since 2.0.13
905
     */
906
    public function addUnique($name, $table, $columns)
907
    {
908
        if (is_string($columns)) {
909
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
910
        }
911
        foreach ($columns as $i => $col) {
912
            $columns[$i] = $this->db->quoteColumnName($col);
913
        }
914
915
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
916
            . $this->db->quoteColumnName($name) . ' UNIQUE ('
917
            . implode(', ', $columns) . ')';
918
    }
919
920
    /**
921
     * Creates a SQL command for dropping an unique constraint.
922
     * @param string $name the name of the unique constraint to be dropped.
923
     * The name will be properly quoted by the method.
924
     * @param string $table the table whose unique constraint is to be dropped.
925
     * The name will be properly quoted by the method.
926
     * @return string the SQL statement for dropping an unique constraint.
927
     * @since 2.0.13
928
     */
929
    public function dropUnique($name, $table)
930
    {
931
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
932
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
933
    }
934
935
    /**
936
     * Creates a SQL command for adding a check constraint to an existing table.
937
     * @param string $name the name of the check constraint.
938
     * The name will be properly quoted by the method.
939
     * @param string $table the table that the check constraint will be added to.
940
     * The name will be properly quoted by the method.
941
     * @param string $expression the SQL of the `CHECK` constraint.
942
     * @return string the SQL statement for adding a check constraint to an existing table.
943
     * @since 2.0.13
944
     */
945
    public function addCheck($name, $table, $expression)
946
    {
947
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
948
            . $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->quoteSql($expression) . ')';
949
    }
950
951
    /**
952
     * Creates a SQL command for dropping a check constraint.
953
     * @param string $name the name of the check constraint to be dropped.
954
     * The name will be properly quoted by the method.
955
     * @param string $table the table whose check constraint is to be dropped.
956
     * The name will be properly quoted by the method.
957
     * @return string the SQL statement for dropping a check constraint.
958
     * @since 2.0.13
959
     */
960
    public function dropCheck($name, $table)
961
    {
962
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
963
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
964
    }
965
966
    /**
967
     * Creates a SQL command for adding a default value constraint to an existing table.
968
     * @param string $name the name of the default value constraint.
969
     * The name will be properly quoted by the method.
970
     * @param string $table the table that the default value constraint will be added to.
971
     * The name will be properly quoted by the method.
972
     * @param string $column the name of the column to that the constraint will be added on.
973
     * The name will be properly quoted by the method.
974
     * @param mixed $value default value.
975
     * @return string the SQL statement for adding a default value constraint to an existing table.
976
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
977
     * @since 2.0.13
978
     */
979
    public function addDefaultValue($name, $table, $column, $value)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $table is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $column is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
980
    {
981
        throw new NotSupportedException($this->db->getDriverName() . ' does not support adding default value constraints.');
982
    }
983
984
    /**
985
     * Creates a SQL command for dropping a default value constraint.
986
     * @param string $name the name of the default value constraint to be dropped.
987
     * The name will be properly quoted by the method.
988
     * @param string $table the table whose default value constraint is to be dropped.
989
     * The name will be properly quoted by the method.
990
     * @return string the SQL statement for dropping a default value constraint.
991
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
992
     * @since 2.0.13
993
     */
994
    public function dropDefaultValue($name, $table)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $table is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
995
    {
996
        throw new NotSupportedException($this->db->getDriverName() . ' does not support dropping default value constraints.');
997
    }
998
999
    /**
1000
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
1001
     * The sequence will be reset such that the primary key of the next new row inserted
1002
     * will have the specified value or 1.
1003
     * @param string $table the name of the table whose primary key sequence will be reset
1004
     * @param array|string $value the value for the primary key of the next new row inserted. If this is not set,
1005
     * the next new row's primary key will have a value 1.
1006
     * @return string the SQL statement for resetting sequence
1007
     * @throws NotSupportedException if this is not supported by the underlying DBMS
1008
     */
1009
    public function resetSequence($table, $value = null)
0 ignored issues
show
Unused Code introduced by
The parameter $table is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1010
    {
1011
        throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
1012
    }
1013
1014
    /**
1015
     * Builds a SQL statement for enabling or disabling integrity check.
1016
     * @param bool $check whether to turn on or off the integrity check.
1017
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
1018
     * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
1019
     * @return string the SQL statement for checking integrity
1020
     * @throws NotSupportedException if this is not supported by the underlying DBMS
1021
     */
1022
    public function checkIntegrity($check = true, $schema = '', $table = '')
0 ignored issues
show
Unused Code introduced by
The parameter $check is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $schema is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $table is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1023
    {
1024
        throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.');
1025
    }
1026
1027
    /**
1028
     * Builds a SQL command for adding comment to column.
1029
     *
1030
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
1031
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
1032
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1033
     * @return string the SQL statement for adding comment on column
1034
     * @since 2.0.8
1035
     */
1036
    public function addCommentOnColumn($table, $column, $comment)
1037
    {
1038
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS ' . $this->db->quoteValue($comment);
1039
    }
1040
1041
    /**
1042
     * Builds a SQL command for adding comment to table.
1043
     *
1044
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
1045
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1046
     * @return string the SQL statement for adding comment on table
1047
     * @since 2.0.8
1048
     */
1049
    public function addCommentOnTable($table, $comment)
1050
    {
1051
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
1052
    }
1053
1054
    /**
1055
     * Builds a SQL command for adding comment to column.
1056
     *
1057
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
1058
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
1059
     * @return string the SQL statement for adding comment on column
1060
     * @since 2.0.8
1061
     */
1062
    public function dropCommentFromColumn($table, $column)
1063
    {
1064
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS NULL';
1065
    }
1066
1067
    /**
1068
     * Builds a SQL command for adding comment to table.
1069
     *
1070
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
1071
     * @return string the SQL statement for adding comment on column
1072
     * @since 2.0.8
1073
     */
1074
    public function dropCommentFromTable($table)
1075
    {
1076
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
1077
    }
1078
1079
    /**
1080
     * Creates a SQL View.
1081
     *
1082
     * @param string $viewName the name of the view to be created.
1083
     * @param string|Query $subQuery the select statement which defines the view.
1084
     * This can be either a string or a [[Query]] object.
1085
     * @return string the `CREATE VIEW` SQL statement.
1086
     * @since 2.0.14
1087
     */
1088
    public function createView($viewName, $subQuery)
1089
    {
1090
        if ($subQuery instanceof Query) {
1091
            [$rawQuery, $params] = $this->build($subQuery);
0 ignored issues
show
Bug introduced by
The variable $rawQuery does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $params seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
1092
            array_walk(
1093
                $params,
1094
                function(&$param) {
1095
                    $param = $this->db->quoteValue($param);
1096
                }
1097
            );
1098
            $subQuery = strtr($rawQuery, $params);
1099
        }
1100
1101
        return 'CREATE VIEW ' . $this->db->quoteTableName($viewName) . ' AS ' . $subQuery;
1102
    }
1103
1104
    /**
1105
     * Drops a SQL View.
1106
     *
1107
     * @param string $viewName the name of the view to be dropped.
1108
     * @return string the `DROP VIEW` SQL statement.
1109
     * @since 2.0.14
1110
     */
1111
    public function dropView($viewName)
1112
    {
1113
        return 'DROP VIEW ' . $this->db->quoteTableName($viewName);
1114
    }
1115
1116
    /**
1117
     * Converts an abstract column type into a physical column type.
1118
     *
1119
     * The conversion is done using the type map specified in [[typeMap]].
1120
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
1121
     * physical types):
1122
     *
1123
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"
1124
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY"
1125
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY"
1126
     * - `char`: char type, will be converted into "char(1)"
1127
     * - `string`: string type, will be converted into "varchar(255)"
1128
     * - `text`: a long string type, will be converted into "text"
1129
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
1130
     * - `integer`: integer type, will be converted into "int(11)"
1131
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
1132
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
1133
     * - `float``: float number type, will be converted into "float"
1134
     * - `decimal`: decimal number type, will be converted into "decimal"
1135
     * - `datetime`: datetime type, will be converted into "datetime"
1136
     * - `timestamp`: timestamp type, will be converted into "timestamp"
1137
     * - `time`: time type, will be converted into "time"
1138
     * - `date`: date type, will be converted into "date"
1139
     * - `money`: money type, will be converted into "decimal(19,4)"
1140
     * - `binary`: binary data type, will be converted into "blob"
1141
     *
1142
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only
1143
     * the first part will be converted, and the rest of the parts will be appended to the converted result.
1144
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
1145
     *
1146
     * For some of the abstract types you can also specify a length or precision constraint
1147
     * by appending it in round brackets directly to the type.
1148
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database.
1149
     * If the underlying DBMS does not support these kind of constraints for a type it will
1150
     * be ignored.
1151
     *
1152
     * If a type cannot be found in [[typeMap]], it will be returned without any change.
1153
     * @param string|ColumnSchemaBuilder $type abstract column type
1154
     * @return string physical column type.
1155
     */
1156
    public function getColumnType($type)
1157
    {
1158
        if ($type instanceof ColumnSchemaBuilder) {
1159
            $type = $type->__toString();
1160
        }
1161
1162
        if (isset($this->typeMap[$type])) {
1163
            return $this->typeMap[$type];
1164
        } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
1165
            if (isset($this->typeMap[$matches[1]])) {
1166
                return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3];
1167
            }
1168
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
1169
            if (isset($this->typeMap[$matches[1]])) {
1170
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
1171
            }
1172
        }
1173
1174
        return $type;
1175
    }
1176
1177
    /**
1178
     * @param array $columns
1179
     * @param array $params the binding parameters to be populated
1180
     * @param bool $distinct
1181
     * @param string $selectOption
1182
     * @return string the SELECT clause built from [[Query::$select]].
1183
     */
1184 3
    public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
1185
    {
1186 3
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
1187 3
        if ($selectOption !== null) {
1188
            $select .= ' ' . $selectOption;
1189
        }
1190
1191 3
        if (empty($columns)) {
1192
            return $select . ' *';
1193
        }
1194
1195 3
        foreach ($columns as $i => $column) {
1196 3
            if ($column instanceof ExpressionInterface) {
1197
                if (is_int($i)) {
1198
                    $columns[$i] = $this->buildExpression($column, $params);
1199
                } else {
1200
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->db->quoteColumnName($i);
1201
                }
1202 3
            } elseif ($column instanceof Query) {
1203
                [$sql, $params] = $this->build($column, $params);
0 ignored issues
show
Bug introduced by
The variable $sql does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1204
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
1205 3
            } elseif (is_string($i)) {
1206
                if (strpos($column, '(') === false) {
1207
                    $column = $this->db->quoteColumnName($column);
1208
                }
1209
                $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
1210 3
            } elseif (strpos($column, '(') === false) {
1211
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
1212
                    $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
1213
                } else {
1214 3
                    $columns[$i] = $this->db->quoteColumnName($column);
1215
                }
1216
            }
1217
        }
1218
1219 3
        return $select . ' ' . implode(', ', $columns);
1220
    }
1221
1222
    /**
1223
     * @param array $tables
1224
     * @param array $params the binding parameters to be populated
1225
     * @return string the FROM clause built from [[Query::$from]].
1226
     */
1227 3
    public function buildFrom($tables, &$params)
1228
    {
1229 3
        if (empty($tables)) {
1230
            return '';
1231
        }
1232
1233 3
        $tables = $this->quoteTableNames($tables, $params);
1234
1235 3
        return 'FROM ' . implode(', ', $tables);
1236
    }
1237
1238
    /**
1239
     * @param array $joins
1240
     * @param array $params the binding parameters to be populated
1241
     * @return string the JOIN clause built from [[Query::$join]].
1242
     * @throws Exception if the $joins parameter is not in proper format
1243
     */
1244 3
    public function buildJoin($joins, &$params)
1245
    {
1246 3
        if (empty($joins)) {
1247 3
            return '';
1248
        }
1249
1250
        foreach ($joins as $i => $join) {
1251
            if (!is_array($join) || !isset($join[0], $join[1])) {
1252
                throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
1253
            }
1254
            // 0:join type, 1:join table, 2:on-condition (optional)
1255
            [$joinType, $table] = $join;
0 ignored issues
show
Bug introduced by
The variable $joinType does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $table does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1256
            $tables = $this->quoteTableNames((array) $table, $params);
1257
            $table = reset($tables);
1258
            $joins[$i] = "$joinType $table";
1259
            if (isset($join[2])) {
1260
                $condition = $this->buildCondition($join[2], $params);
1261
                if ($condition !== '') {
1262
                    $joins[$i] .= ' ON ' . $condition;
1263
                }
1264
            }
1265
        }
1266
1267
        return implode($this->separator, $joins);
1268
    }
1269
1270
    /**
1271
     * Quotes table names passed.
1272
     *
1273
     * @param array $tables
1274
     * @param array $params
1275
     * @return array
1276
     */
1277 3
    private function quoteTableNames($tables, &$params)
1278
    {
1279 3
        foreach ($tables as $i => $table) {
1280 3
            if ($table instanceof Query) {
1281
                [$sql, $params] = $this->build($table, $params);
0 ignored issues
show
Bug introduced by
The variable $sql does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1282
                $tables[$i] = "($sql) " . $this->db->quoteTableName($i);
1283 3
            } elseif (is_string($i)) {
1284 3
                if (strpos($table, '(') === false) {
1285
                    $table = $this->db->quoteTableName($table);
1286
                }
1287 3
                $tables[$i] = "$table " . $this->db->quoteTableName($i);
1288
            } elseif (strpos($table, '(') === false) {
1289
                if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
1290
                    $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
1291
                } else {
1292 3
                    $tables[$i] = $this->db->quoteTableName($table);
1293
                }
1294
            }
1295
        }
1296
1297 3
        return $tables;
1298
    }
1299
1300
    /**
1301
     * @param string|array $condition
1302
     * @param array $params the binding parameters to be populated
1303
     * @return string the WHERE clause built from [[Query::$where]].
1304
     */
1305 3
    public function buildWhere($condition, &$params)
1306
    {
1307 3
        $where = $this->buildCondition($condition, $params);
1308
1309 3
        return $where === '' ? '' : 'WHERE ' . $where;
1310
    }
1311
1312
    /**
1313
     * @param array $columns
1314
     * @param array $params the binding parameters to be populated
1315
     * @return string the GROUP BY clause
1316
     */
1317 3
    public function buildGroupBy($columns, &$params)
1318
    {
1319 3
        if (empty($columns)) {
1320 3
            return '';
1321
        }
1322
        foreach ($columns as $i => $column) {
1323
            if ($column instanceof ExpressionInterface) {
1324
                $columns[$i] = $this->buildExpression($column);
1325
                $params = array_merge($params, $column->params);
0 ignored issues
show
Bug introduced by
Accessing params on the interface yii\db\ExpressionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1326
            } elseif (strpos($column, '(') === false) {
1327
                $columns[$i] = $this->db->quoteColumnName($column);
1328
            }
1329
        }
1330
1331
        return 'GROUP BY ' . implode(', ', $columns);
1332
    }
1333
1334
    /**
1335
     * @param string|array $condition
1336
     * @param array $params the binding parameters to be populated
1337
     * @return string the HAVING clause built from [[Query::$having]].
1338
     */
1339 3
    public function buildHaving($condition, &$params)
1340
    {
1341 3
        $having = $this->buildCondition($condition, $params);
1342
1343 3
        return $having === '' ? '' : 'HAVING ' . $having;
1344
    }
1345
1346
    /**
1347
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
1348
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
1349
     * @param array $orderBy the order by columns. See [[Query::orderBy]] for more details on how to specify this parameter.
1350
     * @param int $limit the limit number. See [[Query::limit]] for more details.
1351
     * @param int $offset the offset number. See [[Query::offset]] for more details.
1352
     * @param array $params the binding parameters to be populated
1353
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
1354
     */
1355 3
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset, &$params)
1356
    {
1357 3
        $orderBy = $this->buildOrderBy($orderBy, $params);
1358 3
        if ($orderBy !== '') {
1359
            $sql .= $this->separator . $orderBy;
1360
        }
1361 3
        $limit = $this->buildLimit($limit, $offset);
1362 3
        if ($limit !== '') {
1363 1
            $sql .= $this->separator . $limit;
1364
        }
1365
1366 3
        return $sql;
1367
    }
1368
1369
    /**
1370
     * @param array $columns
1371
     * @param array $params the binding parameters to be populated
1372
     * @return string the ORDER BY clause built from [[Query::$orderBy]].
1373
     */
1374 3
    public function buildOrderBy($columns, &$params)
1375
    {
1376 3
        if (empty($columns)) {
1377 3
            return '';
1378
        }
1379
        $orders = [];
1380
        foreach ($columns as $name => $direction) {
1381
            if ($direction instanceof ExpressionInterface) {
1382
                $orders[] = $this->buildExpression($direction);
1383
                $params = array_merge($params, $direction->params);
0 ignored issues
show
Bug introduced by
Accessing params on the interface yii\db\ExpressionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1384
            } else {
1385
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
1386
            }
1387
        }
1388
1389
        return 'ORDER BY ' . implode(', ', $orders);
1390
    }
1391
1392
    /**
1393
     * @param int $limit
1394
     * @param int $offset
1395
     * @return string the LIMIT and OFFSET clauses
1396
     */
1397
    public function buildLimit($limit, $offset)
1398
    {
1399
        $sql = '';
1400
        if ($this->hasLimit($limit)) {
1401
            $sql = 'LIMIT ' . $limit;
1402
        }
1403
        if ($this->hasOffset($offset)) {
1404
            $sql .= ' OFFSET ' . $offset;
1405
        }
1406
1407
        return ltrim($sql);
1408
    }
1409
1410
    /**
1411
     * Checks to see if the given limit is effective.
1412
     * @param mixed $limit the given limit
1413
     * @return bool whether the limit is effective
1414
     */
1415 3
    protected function hasLimit($limit)
1416
    {
1417 3
        return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
1418
    }
1419
1420
    /**
1421
     * Checks to see if the given offset is effective.
1422
     * @param mixed $offset the given offset
1423
     * @return bool whether the offset is effective
1424
     */
1425 3
    protected function hasOffset($offset)
1426
    {
1427 3
        return ($offset instanceof ExpressionInterface) || ctype_digit((string) $offset) && (string) $offset !== '0';
1428
    }
1429
1430
    /**
1431
     * @param array $unions
1432
     * @param array $params the binding parameters to be populated
1433
     * @return string the UNION clause built from [[Query::$union]].
1434
     */
1435
    public function buildUnion($unions, &$params)
1436
    {
1437
        if (empty($unions)) {
1438
            return '';
1439
        }
1440
1441
        $result = '';
1442
1443
        foreach ($unions as $i => $union) {
1444
            $query = $union['query'];
1445
            if ($query instanceof Query) {
1446
                [$unions[$i]['query'], $params] = $this->build($query, $params);
1447
            }
1448
1449
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
1450
        }
1451
1452
        return trim($result);
1453
    }
1454
1455
    /**
1456
     * Processes columns and properly quotes them if necessary.
1457
     * It will join all columns into a string with comma as separators.
1458
     * @param string|array $columns the columns to be processed
1459
     * @return string the processing result
1460
     */
1461
    public function buildColumns($columns)
1462
    {
1463
        if (!is_array($columns)) {
1464
            if (strpos($columns, '(') !== false) {
1465
                return $columns;
1466
            }
1467
1468
            $rawColumns = $columns;
1469
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1470
            if ($columns === false) {
1471
                throw new InvalidArgumentException("$rawColumns is not valid columns.");
1472
            }
1473
        }
1474
        foreach ($columns as $i => $column) {
1475
            if ($column instanceof ExpressionInterface) {
1476
                $columns[$i] = $this->buildExpression($column);
1477
            } elseif (strpos($column, '(') === false) {
1478
                $columns[$i] = $this->db->quoteColumnName($column);
1479
            }
1480
        }
1481
1482
        return implode(', ', $columns);
1483
    }
1484
1485
    /**
1486
     * Parses the condition specification and generates the corresponding SQL expression.
1487
     * @param string|array|ExpressionInterface $condition the condition specification. Please refer to [[Query::where()]]
1488
     * on how to specify a condition.
1489
     * @param array $params the binding parameters to be populated
1490
     * @return string the generated SQL expression
1491
     */
1492 3
    public function buildCondition($condition, &$params)
1493
    {
1494 3
        if (is_array($condition)) {
1495
            if (empty($condition)) {
1496
                return '';
1497
            }
1498
1499
            $condition = $this->createConditionFromArray($condition);
1500
        }
1501
1502 3
        if ($condition instanceof ExpressionInterface) {
1503
            return $this->buildExpression($condition, $params);
1504
        }
1505
1506 3
        return (string) $condition;
1507
    }
1508
1509
    /**
1510
     * Transforms $condition defined in array format (as described in [[Query::where()]]
1511
     * to instance of [[yii\db\condition\ConditionInterface|ConditionInterface]] according to
1512
     * [[conditionClasses]] map.
1513
     *
1514
     * @param string|array $condition
1515
     * @see conditionClasses
1516
     * @return ConditionInterface
1517
     * @since 2.0.14
1518
     */
1519
    public function createConditionFromArray($condition)
1520
    {
1521
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
1522
            $operator = strtoupper(array_shift($condition));
1523
            if (isset($this->conditionClasses[$operator])) {
1524
                $className = $this->conditionClasses[$operator];
1525
            } else {
1526
                $className = 'yii\db\conditions\SimpleCondition';
1527
            }
1528
            /** @var ConditionInterface $className */
1529
            return $className::fromArrayDefinition($operator, $condition);
1530
        }
1531
1532
        // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
1533
        return new HashCondition($condition);
0 ignored issues
show
Bug introduced by
It seems like $condition defined by parameter $condition on line 1519 can also be of type string; however, yii\db\conditions\HashCondition::__construct() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1534
    }
1535
1536
    /**
1537
     * Creates a SELECT EXISTS() SQL statement.
1538
     * @param string $rawSql the subquery in a raw form to select from.
1539
     * @return string the SELECT EXISTS() SQL statement.
1540
     * @since 2.0.8
1541
     */
1542
    public function selectExists($rawSql)
1543
    {
1544
        return 'SELECT EXISTS(' . $rawSql . ')';
1545
    }
1546
1547
    /**
1548
     * Helper method to add $value to $params array using [[PARAM_PREFIX]].
1549
     *
1550
     * @param string|null $value
1551
     * @param array $params passed by reference
1552
     * @return string the placeholder name in $params array
1553
     *
1554
     * @since 2.0.14
1555
     */
1556
    public function bindParam($value, &$params)
1557
    {
1558
        $phName = self::PARAM_PREFIX . count($params);
1559
        $params[$phName] = $value;
1560
1561
        return $phName;
1562
    }
1563
}
1564