Completed
Push — remove-intl-polyfills ( 129df4...a6e727 )
by Alexander
16:22 queued 12:49
created

Schema   F

Complexity

Total Complexity 107

Size/Duplication

Total Lines 813
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 73.3%

Importance

Changes 0
Metric Value
wmc 107
lcom 1
cbo 13
dl 0
loc 813
rs 2.6666
c 0
b 0
f 0
ccs 162
cts 221
cp 0.733

43 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveTableName() 0 4 1
A findSchemaNames() 0 4 1
A findTableNames() 0 4 1
loadTableSchema() 0 1 ?
A createColumnSchema() 0 4 1
A getTableSchema() 0 4 1
A getTableSchemas() 0 4 1
A getSchemaNames() 0 8 3
A getTableNames() 0 8 3
A getQueryBuilder() 0 8 2
A createQueryBuilder() 0 4 1
A createColumnSchemaBuilder() 0 4 1
A findUniqueIndexes() 0 4 1
A getLastInsertID() 0 8 3
A supportsSavepoint() 0 4 1
A createSavepoint() 0 4 1
A releaseSavepoint() 0 4 1
A rollBackSavepoint() 0 4 1
A setTransactionIsolationLevel() 0 4 1
A quoteValue() 0 13 3
B quoteTableName() 0 15 5
B quoteColumnName() 0 17 5
A quoteSimpleTableName() 0 9 3
A quoteSimpleColumnName() 0 9 4
A unquoteSimpleTableName() 0 9 3
A unquoteSimpleColumnName() 0 9 3
A getRawTableName() 0 10 2
C getColumnPhpType() 0 26 8
B convertException() 0 16 5
A isReadQuery() 0 5 1
A getServerVersion() 0 7 2
A getSchemaMetadata() 0 16 4
A setTableMetadata() 0 4 1
A normalizePdoRowKeyCase() 0 14 3
A refresh() 0 10 4
A refreshTableSchema() 0 11 4
A getCacheKey() 0 9 1
A getCacheTag() 0 8 1
B getTableMetadata() 0 20 8
B loadTableMetadataFromCache() 0 16 5
A saveTableMetadataToCache() 0 15 2
A getPdoType() 0 14 1
A insert() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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;
11
use yii\base\BaseObject;
12
use yii\base\InvalidCallException;
13
use yii\base\InvalidConfigException;
14
use yii\base\NotSupportedException;
15
use yii\caching\Cache;
16
use yii\caching\CacheInterface;
17
use yii\caching\TagDependency;
18
use yii\helpers\StringHelper;
19
20
/**
21
 * Schema is the base class for concrete DBMS-specific schema classes.
22
 *
23
 * Schema represents the database schema information that is DBMS specific.
24
 *
25
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
26
 * sequence object. This property is read-only.
27
 * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
28
 * @property string[] $schemaNames All schema names in the database, except system schemas. This property is
29
 * read-only.
30
 * @property string $serverVersion Server version as a string. This property is read-only.
31
 * @property string[] $tableNames All table names in the database. This property is read-only.
32
 * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an
33
 * instance of [[TableSchema]] or its child class. This property is read-only.
34
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction.
35
 * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]],
36
 * [[Transaction::REPEATABLE_READ]] and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific
37
 * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
38
 *
39
 * @author Qiang Xue <[email protected]>
40
 * @author Sergey Makinen <[email protected]>
41
 * @since 2.0
42
 */
43
abstract class Schema extends BaseObject
44
{
45
    // The following are the supported abstract column data types.
46
    const TYPE_PK = 'pk';
47
    const TYPE_UPK = 'upk';
48
    const TYPE_BIGPK = 'bigpk';
49
    const TYPE_UBIGPK = 'ubigpk';
50
    const TYPE_CHAR = 'char';
51
    const TYPE_STRING = 'string';
52
    const TYPE_TEXT = 'text';
53
    const TYPE_TINYINT = 'tinyint';
54
    const TYPE_SMALLINT = 'smallint';
55
    const TYPE_INTEGER = 'integer';
56
    const TYPE_BIGINT = 'bigint';
57
    const TYPE_FLOAT = 'float';
58
    const TYPE_DOUBLE = 'double';
59
    const TYPE_DECIMAL = 'decimal';
60
    const TYPE_DATETIME = 'datetime';
61
    const TYPE_TIMESTAMP = 'timestamp';
62
    const TYPE_TIME = 'time';
63
    const TYPE_DATE = 'date';
64
    const TYPE_BINARY = 'binary';
65
    const TYPE_BOOLEAN = 'boolean';
66
    const TYPE_MONEY = 'money';
67
    const TYPE_JSON = 'json';
68
    /**
69
     * Schema cache version, to detect incompatibilities in cached values when the
70
     * data format of the cache changes.
71
     */
72
    const SCHEMA_CACHE_VERSION = 1;
73
74
    /**
75
     * @var Connection the database connection
76
     */
77
    public $db;
78
    /**
79
     * @var string the default schema name used for the current session.
80
     */
81
    public $defaultSchema;
82
    /**
83
     * @var array map of DB errors and corresponding exceptions
84
     * If left part is found in DB error message exception class from the right part is used.
85
     */
86
    public $exceptionMap = [
87
        'SQLSTATE[23' => IntegrityException::class,
88
    ];
89
    /**
90
     * @var string|array column schema class or class config
91
     * @since 2.0.11
92
     */
93
    public $columnSchemaClass = ColumnSchema::class;
94
95
    /**
96
     * @var string|string[] character used to quote schema, table, etc. names.
97
     * An array of 2 characters can be used in case starting and ending characters are different.
98
     * @since 2.0.14
99
     */
100
    protected $tableQuoteCharacter = "'";
101
    /**
102
     * @var string|string[] character used to quote column names.
103
     * An array of 2 characters can be used in case starting and ending characters are different.
104
     * @since 2.0.14
105
     */
106
    protected $columnQuoteCharacter = '"';
107
108
    /**
109
     * @var array list of ALL schema names in the database, except system schemas
110
     */
111
    private $_schemaNames;
112
    /**
113
     * @var array list of ALL table names in the database
114
     */
115
    private $_tableNames = [];
116
    /**
117
     * @var array list of loaded table metadata (table name => metadata type => metadata).
118
     */
119
    private $_tableMetadata = [];
120
    /**
121
     * @var QueryBuilder the query builder for this database
122
     */
123
    private $_builder;
124
    /**
125
     * @var string server version as a string.
126
     */
127
    private $_serverVersion;
128
129
130
    /**
131
     * Resolves the table name and schema name (if any).
132
     * @param string $name the table name
133
     * @return TableSchema [[TableSchema]] with resolved table, schema, etc. names.
134
     * @throws NotSupportedException if this method is not supported by the DBMS.
135
     * @since 2.0.13
136
     */
137
    protected function resolveTableName($name)
138
    {
139
        throw new NotSupportedException(get_class($this) . ' does not support resolving table names.');
140
    }
141
142
    /**
143
     * Returns all schema names in the database, including the default one but not system schemas.
144
     * This method should be overridden by child classes in order to support this feature
145
     * because the default implementation simply throws an exception.
146
     * @return array all schema names in the database, except system schemas.
147
     * @throws NotSupportedException if this method is not supported by the DBMS.
148
     * @since 2.0.4
149
     */
150
    protected function findSchemaNames()
151
    {
152
        throw new NotSupportedException(get_class($this) . ' does not support fetching all schema names.');
153
    }
154
155
    /**
156
     * Returns all table names in the database.
157
     * This method should be overridden by child classes in order to support this feature
158
     * because the default implementation simply throws an exception.
159
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
160
     * @return array all table names in the database. The names have NO schema name prefix.
161
     * @throws NotSupportedException if this method is not supported by the DBMS.
162
     */
163
    protected function findTableNames($schema = '')
164
    {
165
        throw new NotSupportedException(get_class($this) . ' does not support fetching all table names.');
166
    }
167
168
    /**
169
     * Loads the metadata for the specified table.
170
     * @param string $name table name
171
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
172
     */
173
    abstract protected function loadTableSchema($name);
174
175
    /**
176
     * Creates a column schema for the database.
177
     * This method may be overridden by child classes to create a DBMS-specific column schema.
178
     * @return ColumnSchema column schema instance.
179
     * @throws InvalidConfigException if a column schema class cannot be created.
180
     */
181 957
    protected function createColumnSchema()
182
    {
183 957
        return Yii::createObject($this->columnSchemaClass);
184
    }
185
186
    /**
187
     * Obtains the metadata for the named table.
188
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
189
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
190
     * @return TableSchema|null table metadata. `null` if the named table does not exist.
191
     */
192 1042
    public function getTableSchema($name, $refresh = false)
193
    {
194 1042
        return $this->getTableMetadata($name, 'schema', $refresh);
195
    }
196
197
    /**
198
     * Returns the metadata for all tables in the database.
199
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
200
     * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`,
201
     * cached data may be returned if available.
202
     * @return TableSchema[] the metadata for all tables in the database.
203
     * Each array element is an instance of [[TableSchema]] or its child class.
204
     */
205 11
    public function getTableSchemas($schema = '', $refresh = false)
206
    {
207 11
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
208
    }
209
210
    /**
211
     * Returns all schema names in the database, except system schemas.
212
     * @param bool $refresh whether to fetch the latest available schema names. If this is false,
213
     * schema names fetched previously (if available) will be returned.
214
     * @return string[] all schema names in the database, except system schemas.
215
     * @since 2.0.4
216
     */
217 2
    public function getSchemaNames($refresh = false)
218
    {
219 2
        if ($this->_schemaNames === null || $refresh) {
220 2
            $this->_schemaNames = $this->findSchemaNames();
221
        }
222
223 2
        return $this->_schemaNames;
224
    }
225
226
    /**
227
     * Returns all table names in the database.
228
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
229
     * If not empty, the returned table names will be prefixed with the schema name.
230
     * @param bool $refresh whether to fetch the latest available table names. If this is false,
231
     * table names fetched previously (if available) will be returned.
232
     * @return string[] all table names in the database.
233
     */
234 17
    public function getTableNames($schema = '', $refresh = false)
235
    {
236 17
        if (!isset($this->_tableNames[$schema]) || $refresh) {
237 17
            $this->_tableNames[$schema] = $this->findTableNames($schema);
238
        }
239
240 17
        return $this->_tableNames[$schema];
241
    }
242
243
    /**
244
     * @return QueryBuilder the query builder for this connection.
245
     */
246 1006
    public function getQueryBuilder()
247
    {
248 1006
        if ($this->_builder === null) {
249 951
            $this->_builder = $this->createQueryBuilder();
250
        }
251
252 1006
        return $this->_builder;
253
    }
254
255
    /**
256
     * Determines the PDO type for the given PHP data value.
257
     * @param mixed $data the data whose PDO type is to be determined
258
     * @return int the PDO type
259
     * @see http://www.php.net/manual/en/pdo.constants.php
260
     */
261 1058
    public function getPdoType($data)
262
    {
263 1058
        static $typeMap = [
264
            // php type => PDO type
265
            'boolean' => \PDO::PARAM_BOOL,
266
            'integer' => \PDO::PARAM_INT,
267
            'string' => \PDO::PARAM_STR,
268
            'resource' => \PDO::PARAM_LOB,
269
            'NULL' => \PDO::PARAM_NULL,
270
        ];
271 1058
        $type = gettype($data);
272
273 1058
        return $typeMap[$type] ?? \PDO::PARAM_STR;
274
    }
275
276
    /**
277
     * Refreshes the schema.
278
     * This method cleans up all cached table schemas so that they can be re-created later
279
     * to reflect the database schema change.
280
     */
281 43
    public function refresh()
282
    {
283
        /* @var $cache CacheInterface */
284 43
        $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
285 43
        if ($this->db->enableSchemaCache && $cache instanceof CacheInterface) {
286
            TagDependency::invalidate($cache, $this->getCacheTag());
287
        }
288 43
        $this->_tableNames = [];
289 43
        $this->_tableMetadata = [];
290 43
    }
291
292
    /**
293
     * Refreshes the particular table schema.
294
     * This method cleans up cached table schema so that it can be re-created later
295
     * to reflect the database schema change.
296
     * @param string $name table name.
297
     * @since 2.0.6
298
     */
299 168
    public function refreshTableSchema($name)
300
    {
301 168
        $rawName = $this->getRawTableName($name);
302 168
        unset($this->_tableMetadata[$rawName]);
303 168
        $this->_tableNames = [];
304
        /* @var $cache CacheInterface */
305 168
        $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
306 168
        if ($this->db->enableSchemaCache && $cache instanceof CacheInterface) {
307
            $cache->delete($this->getCacheKey($rawName));
0 ignored issues
show
Documentation introduced by
$this->getCacheKey($rawName) is of type array<integer,string|arr..."string","3":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
308
        }
309 168
    }
310
311
    /**
312
     * Creates a query builder for the database.
313
     * This method may be overridden by child classes to create a DBMS-specific query builder.
314
     * @return QueryBuilder query builder instance
315
     */
316
    public function createQueryBuilder()
317
    {
318
        return new QueryBuilder($this->db);
319
    }
320
321
    /**
322
     * Create a column schema builder instance giving the type and value precision.
323
     *
324
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
325
     *
326
     * @param string $type type of the column. See [[ColumnSchemaBuilder::$type]].
327
     * @param int|string|array $length length or precision of the column. See [[ColumnSchemaBuilder::$length]].
328
     * @return ColumnSchemaBuilder column schema builder instance
329
     * @since 2.0.6
330
     */
331 10
    public function createColumnSchemaBuilder($type, $length = null)
332
    {
333 10
        return new ColumnSchemaBuilder($type, $length);
334
    }
335
336
    /**
337
     * Returns all unique indexes for the given table.
338
     *
339
     * Each array element is of the following structure:
340
     *
341
     * ```php
342
     * [
343
     *  'IndexName1' => ['col1' [, ...]],
344
     *  'IndexName2' => ['col2' [, ...]],
345
     * ]
346
     * ```
347
     *
348
     * This method should be overridden by child classes in order to support this feature
349
     * because the default implementation simply throws an exception
350
     * @param TableSchema $table the table metadata
351
     * @return array all unique indexes for the given table.
352
     * @throws NotSupportedException if this method is called
353
     */
354
    public function findUniqueIndexes($table)
355
    {
356
        throw new NotSupportedException(get_class($this) . ' does not support getting unique indexes information.');
357
    }
358
359
    /**
360
     * Returns the ID of the last inserted row or sequence value.
361
     * @param string $sequenceName name of the sequence object (required by some DBMS)
362
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
363
     * @throws InvalidCallException if the DB connection is not active
364
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
365
     */
366 82
    public function getLastInsertID($sequenceName = '')
367
    {
368 82
        if ($this->db->isActive) {
369 82
            return $this->db->pdo->lastInsertId($sequenceName === '' ? null : $this->quoteTableName($sequenceName));
370
        }
371
372
        throw new InvalidCallException('DB Connection is not active.');
373
    }
374
375
    /**
376
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
377
     */
378 4
    public function supportsSavepoint()
379
    {
380 4
        return $this->db->enableSavepoint;
381
    }
382
383
    /**
384
     * Creates a new savepoint.
385
     * @param string $name the savepoint name
386
     */
387 4
    public function createSavepoint($name)
388
    {
389 4
        $this->db->createCommand("SAVEPOINT $name")->execute();
390 4
    }
391
392
    /**
393
     * Releases an existing savepoint.
394
     * @param string $name the savepoint name
395
     */
396
    public function releaseSavepoint($name)
397
    {
398
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
399
    }
400
401
    /**
402
     * Rolls back to a previously created savepoint.
403
     * @param string $name the savepoint name
404
     */
405 4
    public function rollBackSavepoint($name)
406
    {
407 4
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
408 4
    }
409
410
    /**
411
     * Sets the isolation level of the current transaction.
412
     * @param string $level The transaction isolation level to use for this transaction.
413
     * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], [[Transaction::REPEATABLE_READ]]
414
     * and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific syntax to be used
415
     * after `SET TRANSACTION ISOLATION LEVEL`.
416
     * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
417
     */
418 6
    public function setTransactionIsolationLevel($level)
419
    {
420 6
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
421 6
    }
422
423
    /**
424
     * Executes the INSERT command, returning primary key values.
425
     * @param string $table the table that new rows will be inserted into.
426
     * @param array $columns the column data (name => value) to be inserted into the table.
427
     * @return array|false primary key values or false if the command fails
428
     * @since 2.0.4
429
     */
430 86
    public function insert($table, $columns)
431
    {
432 86
        $command = $this->db->createCommand()->insert($table, $columns);
433 86
        if (!$command->execute()) {
434
            return false;
435
        }
436 86
        $tableSchema = $this->getTableSchema($table);
437 86
        $result = [];
438 86
        foreach ($tableSchema->primaryKey as $name) {
439 83
            if ($tableSchema->columns[$name]->autoIncrement) {
440 79
                $result[$name] = $this->getLastInsertID($tableSchema->sequenceName);
441 79
                break;
442
            }
443
444 6
            $result[$name] = $columns[$name] ?? $tableSchema->columns[$name]->defaultValue;
445
        }
446
447 86
        return $result;
448
    }
449
450
    /**
451
     * Quotes a string value for use in a query.
452
     * Note that if the parameter is not a string, it will be returned without change.
453
     * @param string $str string to be quoted
454
     * @return string the properly quoted string
455
     * @see http://www.php.net/manual/en/function.PDO-quote.php
456
     */
457 1042
    public function quoteValue($str)
458
    {
459 1042
        if (!is_string($str)) {
460 4
            return $str;
461
        }
462
463 1042
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
464 1042
            return $value;
465
        }
466
467
        // the driver doesn't support quote (e.g. oci)
468
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
469
    }
470
471
    /**
472
     * Quotes a table name for use in a query.
473
     * If the table name contains schema prefix, the prefix will also be properly quoted.
474
     * If the table name is already quoted or contains '(' or '{{',
475
     * then this method will do nothing.
476
     * @param string $name table name
477
     * @return string the properly quoted table name
478
     * @see quoteSimpleTableName()
479
     */
480 1272
    public function quoteTableName($name)
481
    {
482 1272
        if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
483 465
            return $name;
484
        }
485 1256
        if (strpos($name, '.') === false) {
486 1256
            return $this->quoteSimpleTableName($name);
487
        }
488 5
        $parts = explode('.', $name);
489 5
        foreach ($parts as $i => $part) {
490 5
            $parts[$i] = $this->quoteSimpleTableName($part);
491
        }
492
493 5
        return implode('.', $parts);
494
    }
495
496
    /**
497
     * Quotes a column name for use in a query.
498
     * If the column name contains prefix, the prefix will also be properly quoted.
499
     * If the column name is already quoted or contains '(', '[[' or '{{',
500
     * then this method will do nothing.
501
     * @param string $name column name
502
     * @return string the properly quoted column name
503
     * @see quoteSimpleColumnName()
504
     */
505 1341
    public function quoteColumnName($name)
506
    {
507 1341
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
508 124
            return $name;
509
        }
510 1332
        if (($pos = strrpos($name, '.')) !== false) {
511 189
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
512 189
            $name = substr($name, $pos + 1);
513
        } else {
514 1326
            $prefix = '';
515
        }
516 1332
        if (strpos($name, '{{') !== false) {
517 4
            return $name;
518
        }
519
520 1332
        return $prefix . $this->quoteSimpleColumnName($name);
521
    }
522
523
    /**
524
     * Quotes a simple table name for use in a query.
525
     * A simple table name should contain the table name only without any schema prefix.
526
     * If the table name is already quoted, this method will do nothing.
527
     * @param string $name table name
528
     * @return string the properly quoted table name
529
     */
530 1301
    public function quoteSimpleTableName($name)
531
    {
532 1301
        if (is_string($this->tableQuoteCharacter)) {
533 1301
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
534
        } else {
535
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
0 ignored issues
show
Bug introduced by
The variable $startingCharacter 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...
Bug introduced by
The variable $endingCharacter 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...
536
        }
537 1301
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
0 ignored issues
show
Bug introduced by
The variable $startingCharacter 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...
Bug introduced by
The variable $endingCharacter 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...
538
    }
539
540
    /**
541
     * Quotes a simple column name for use in a query.
542
     * A simple column name should contain the column name only without any prefix.
543
     * If the column name is already quoted or is the asterisk character '*', this method will do nothing.
544
     * @param string $name column name
545
     * @return string the properly quoted column name
546
     */
547 1332
    public function quoteSimpleColumnName($name)
548
    {
549 1332
        if (is_string($this->tableQuoteCharacter)) {
550 1332
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
551
        } else {
552
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
0 ignored issues
show
Bug introduced by
The variable $startingCharacter 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...
Bug introduced by
The variable $endingCharacter 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...
553
        }
554 1332
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
0 ignored issues
show
Bug introduced by
The variable $startingCharacter 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...
Bug introduced by
The variable $endingCharacter 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...
555
    }
556
557
    /**
558
     * Unquotes a simple table name.
559
     * A simple table name should contain the table name only without any schema prefix.
560
     * If the table name is not quoted, this method will do nothing.
561
     * @param string $name table name.
562
     * @return string unquoted table name.
563
     * @since 2.0.14
564
     */
565
    public function unquoteSimpleTableName($name)
566
    {
567
        if (is_string($this->tableQuoteCharacter)) {
568
            $startingCharacter = $this->tableQuoteCharacter;
569
        } else {
570
            $startingCharacter = $this->tableQuoteCharacter[0];
571
        }
572
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
573
    }
574
575
    /**
576
     * Unquotes a simple column name.
577
     * A simple column name should contain the column name only without any prefix.
578
     * If the column name is not quoted or is the asterisk character '*', this method will do nothing.
579
     * @param string $name column name.
580
     * @return string unquoted column name.
581
     * @since 2.0.14
582
     */
583
    public function unquoteSimpleColumnName($name)
584
    {
585
        if (is_string($this->columnQuoteCharacter)) {
586
            $startingCharacter = $this->columnQuoteCharacter;
587
        } else {
588
            $startingCharacter = $this->columnQuoteCharacter[0];
589
        }
590
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
591
    }
592
593
    /**
594
     * Returns the actual name of a given table name.
595
     * This method will strip off curly brackets from the given table name
596
     * and replace the percentage character '%' with [[Connection::tablePrefix]].
597
     * @param string $name the table name to be converted
598
     * @return string the real name of the given table name
599
     */
600 1224
    public function getRawTableName($name)
601
    {
602 1224
        if (strpos($name, '{{') !== false) {
603 417
            $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
604
605 417
            return str_replace('%', $this->db->tablePrefix, $name);
606
        }
607
608 899
        return $name;
609
    }
610
611
    /**
612
     * Extracts the PHP type from abstract DB type.
613
     * @param ColumnSchema $column the column schema information
614
     * @return string PHP type name
615
     */
616 957
    protected function getColumnPhpType($column)
617
    {
618 957
        static $typeMap = [
619
            // abstract type => php type
620
            self::TYPE_TINYINT => 'integer',
621
            self::TYPE_SMALLINT => 'integer',
622
            self::TYPE_INTEGER => 'integer',
623
            self::TYPE_BIGINT => 'integer',
624
            self::TYPE_BOOLEAN => 'boolean',
625
            self::TYPE_FLOAT => 'double',
626
            self::TYPE_DOUBLE => 'double',
627
            self::TYPE_BINARY => 'resource',
628
            self::TYPE_JSON => 'array',
629
        ];
630 957
        if (isset($typeMap[$column->type])) {
631 951
            if ($column->type === 'bigint') {
632 44
                return PHP_INT_SIZE === 8 && !$column->unsigned ? 'integer' : 'string';
633 951
            } elseif ($column->type === 'integer') {
634 951
                return PHP_INT_SIZE === 4 && $column->unsigned ? 'string' : 'integer';
635
            }
636
637 469
            return $typeMap[$column->type];
638
        }
639
640 922
        return 'string';
641
    }
642
643
    /**
644
     * Converts a DB exception to a more concrete one if possible.
645
     *
646
     * @param \Exception $e
647
     * @param string $rawSql SQL that produced exception
648
     * @return Exception
649
     */
650 32
    public function convertException(\Exception $e, $rawSql)
651
    {
652 32
        if ($e instanceof Exception) {
653
            return $e;
654
        }
655
656 32
        $exceptionClass = Exception::class;
657 32
        foreach ($this->exceptionMap as $error => $class) {
658 32
            if (strpos($e->getMessage(), $error) !== false) {
659 32
                $exceptionClass = $class;
660
            }
661
        }
662 32
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
663 32
        $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
664 32
        return new $exceptionClass($message, $errorInfo, (int) $e->getCode(), $e);
665
    }
666
667
    /**
668
     * Returns a value indicating whether a SQL statement is for read purpose.
669
     * @param string $sql the SQL statement
670
     * @return bool whether a SQL statement is for read purpose.
671
     */
672 9
    public function isReadQuery($sql)
673
    {
674 9
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
675 9
        return preg_match($pattern, $sql) > 0;
676
    }
677
678
    /**
679
     * Returns a server version as a string comparable by [[\version_compare()]].
680
     * @return string server version as a string.
681
     * @since 2.0.14
682
     */
683 66
    public function getServerVersion()
684
    {
685 66
        if ($this->_serverVersion === null) {
686 66
            $this->_serverVersion = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
687
        }
688 66
        return $this->_serverVersion;
689
    }
690
691
    /**
692
     * Returns the cache key for the specified table name.
693
     * @param string $name the table name.
694
     * @return mixed the cache key.
695
     */
696
    protected function getCacheKey($name)
697
    {
698
        return [
699
            __CLASS__,
700
            $this->db->dsn,
701
            $this->db->username,
702
            $this->getRawTableName($name),
703
        ];
704
    }
705
706
    /**
707
     * Returns the cache tag name.
708
     * This allows [[refresh()]] to invalidate all cached table schemas.
709
     * @return string the cache tag name
710
     */
711
    protected function getCacheTag()
712
    {
713
        return md5(serialize([
714
            __CLASS__,
715
            $this->db->dsn,
716
            $this->db->username,
717
        ]));
718
    }
719
720
    /**
721
     * Returns the metadata of the given type for the given table.
722
     * If there's no metadata in the cache, this method will call
723
     * a `'loadTable' . ucfirst($type)` named method with the table name to obtain the metadata.
724
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
725
     * @param string $type metadata type.
726
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
727
     * @return mixed metadata.
728
     * @since 2.0.13
729
     */
730 1222
    protected function getTableMetadata($name, $type, $refresh)
731
    {
732 1222
        $cache = null;
733 1222
        if ($this->db->enableSchemaCache && !in_array($name, $this->db->schemaCacheExclude, true)) {
734 25
            $schemaCache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
735 25
            if ($schemaCache instanceof CacheInterface) {
736
                $cache = $schemaCache;
737
            }
738
        }
739 1222
        $rawName = $this->getRawTableName($name);
740 1222
        if (!isset($this->_tableMetadata[$rawName])) {
741 1170
            $this->loadTableMetadataFromCache($cache, $rawName);
0 ignored issues
show
Bug introduced by
It seems like $cache defined by $schemaCache on line 736 can also be of type object<yii\caching\CacheInterface>; however, yii\db\Schema::loadTableMetadataFromCache() does only seem to accept object<yii\caching\Cache>|null, 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...
742
        }
743 1222
        if ($refresh || !array_key_exists($type, $this->_tableMetadata[$rawName])) {
744 1170
            $this->_tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
745 1122
            $this->saveTableMetadataToCache($cache, $rawName);
0 ignored issues
show
Bug introduced by
It seems like $cache defined by $schemaCache on line 736 can also be of type object<yii\caching\CacheInterface>; however, yii\db\Schema::saveTableMetadataToCache() does only seem to accept object<yii\caching\Cache>|null, 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...
746
        }
747
748 1174
        return $this->_tableMetadata[$rawName][$type];
749
    }
750
751
    /**
752
     * Returns the metadata of the given type for all tables in the given schema.
753
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name
754
     * and the refresh flag to obtain the metadata.
755
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema name.
756
     * @param string $type metadata type.
757
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`,
758
     * cached data may be returned if available.
759
     * @return array array of metadata.
760
     * @since 2.0.13
761
     */
762 11
    protected function getSchemaMetadata($schema, $type, $refresh)
763
    {
764 11
        $metadata = [];
765 11
        $methodName = 'getTable' . ucfirst($type);
766 11
        foreach ($this->getTableNames($schema, $refresh) as $name) {
767 11
            if ($schema !== '') {
768
                $name = $schema . '.' . $name;
769
            }
770 11
            $tableMetadata = $this->$methodName($name, $refresh);
771 11
            if ($tableMetadata !== null) {
772 11
                $metadata[] = $tableMetadata;
773
            }
774
        }
775
776 11
        return $metadata;
777
    }
778
779
    /**
780
     * Sets the metadata of the given type for the given table.
781
     * @param string $name table name.
782
     * @param string $type metadata type.
783
     * @param mixed $data metadata.
784
     * @since 2.0.13
785
     */
786 207
    protected function setTableMetadata($name, $type, $data)
787
    {
788 207
        $this->_tableMetadata[$this->getRawTableName($name)][$type] = $data;
789 207
    }
790
791
    /**
792
     * Changes row's array key case to lower if PDO's one is set to uppercase.
793
     * @param array $row row's array or an array of row's arrays.
794
     * @param bool $multiple whether multiple rows or a single row passed.
795
     * @return array normalized row or rows.
796
     * @since 2.0.13
797
     */
798 230
    protected function normalizePdoRowKeyCase(array $row, $multiple)
799
    {
800 230
        if ($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
801 190
            return $row;
802
        }
803
804 40
        if ($multiple) {
805 40
            return array_map(function (array $row) {
806 36
                return array_change_key_case($row, CASE_LOWER);
807 40
            }, $row);
808
        }
809
810
        return array_change_key_case($row, CASE_LOWER);
811
    }
812
813
    /**
814
     * Tries to load and populate table metadata from cache.
815
     * @param Cache|null $cache
816
     * @param string $name
817
     */
818 1170
    private function loadTableMetadataFromCache($cache, $name)
819
    {
820 1170
        if ($cache === null) {
821 1170
            $this->_tableMetadata[$name] = [];
822 1170
            return;
823
        }
824
825
        $metadata = $cache->get($this->getCacheKey($name));
0 ignored issues
show
Documentation introduced by
$this->getCacheKey($name) is of type array<integer,string|arr..."string","3":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
826
        if (!is_array($metadata) || !isset($metadata['cacheVersion']) || $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION) {
827
            $this->_tableMetadata[$name] = [];
828
            return;
829
        }
830
831
        unset($metadata['cacheVersion']);
832
        $this->_tableMetadata[$name] = $metadata;
833
    }
834
835
    /**
836
     * Saves table metadata to cache.
837
     * @param Cache|null $cache
838
     * @param string $name
839
     */
840 1122
    private function saveTableMetadataToCache($cache, $name)
841
    {
842 1122
        if ($cache === null) {
843 1122
            return;
844
        }
845
846
        $metadata = $this->_tableMetadata[$name];
847
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
848
        $cache->set(
849
            $this->getCacheKey($name),
850
            $metadata,
851
            $this->db->schemaCacheDuration,
852
            new TagDependency(['tags' => $this->getCacheTag()])
853
        );
854
    }
855
}
856