Passed
Pull Request — master (#163)
by Wilmer
17:48 queued 02:54
created

Schema   F

Complexity

Total Complexity 108

Size/Duplication

Total Lines 909
Duplicated Lines 0 %

Test Coverage

Coverage 76.5%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 230
c 5
b 0
f 0
dl 0
loc 909
ccs 166
cts 217
cp 0.765
rs 2
wmc 108

45 Methods

Rating   Name   Duplication   Size   Complexity  
A findTableNames() 0 3 1
A getQueryBuilder() 0 7 2
A getPdoType() 0 13 1
A getSchemaNames() 0 7 3
A __construct() 0 4 1
A resolveTableName() 0 3 1
A getTableNames() 0 7 3
A createColumnSchema() 0 3 1
A findSchemaNames() 0 3 1
A createColumnSchemaBuilder() 0 3 1
A createQueryBuilder() 0 3 1
A refreshTableSchema() 0 10 3
A getTableSchema() 0 3 1
A getTableSchemas() 0 3 1
A refresh() 0 11 4
A insert() 0 21 4
A createSavepoint() 0 3 1
A quoteValue() 0 12 3
A getLastInsertID() 0 9 3
A setTransactionIsolationLevel() 0 3 1
A rollBackSavepoint() 0 3 1
A supportsSavepoint() 0 3 1
A releaseSavepoint() 0 3 1
A quoteSimpleColumnName() 0 10 4
A quoteTableName() 0 21 6
A quoteColumnName() 0 18 5
A getCacheKey() 0 7 1
A saveTableMetadataToCache() 0 15 2
A getTableNameParts() 0 3 1
A normalizePdoRowKeyCase() 0 13 3
A quoteSimpleTableName() 0 9 3
A loadTableMetadataFromCache() 0 18 5
A getSchemaMetadata() 0 18 4
A getServerVersion() 0 7 2
A isReadQuery() 0 5 1
A getTableMetadata() 0 18 6
B getColumnPhpType() 0 28 8
A convertException() 0 18 5
A getDefaultSchema() 0 3 1
A unquoteSimpleColumnName() 0 9 3
A unquoteSimpleTableName() 0 9 3
A setTableMetadata() 0 3 1
A getCacheTag() 0 6 1
A getDb() 0 3 1
A getRawTableName() 0 9 2

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.

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

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

283
        $cache = \is_string($this->db->/** @scrutinizer ignore-call */ getSchemaCache()) ? $this->cache : $this->db->getSchemaCache();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
284
285
        if ($this->db->isSchemaCacheEnabled() && $cache instanceof CacheInterface) {
286
            TagDependency::invalidate($cache, $this->getCacheTag());
287
        }
288
289
        $this->tableNames = [];
290
        $this->tableMetadata = [];
291
    }
292
293
    /**
294
     * Refreshes the particular table schema.
295
     *
296
     * This method cleans up cached table schema so that it can be re-created later to reflect the database schema
297
     * change.
298
     *
299
     * @param string $name table name.
300
     */
301 65
    public function refreshTableSchema(string $name): void
302
    {
303 65
        $rawName = $this->getRawTableName($name);
304
305 65
        unset($this->tableMetadata[$rawName]);
306
307 65
        $this->tableNames = [];
308
309 65
        if ($this->db->isSchemaCacheEnabled() && $this->cache instanceof CacheInterface) {
310 65
            $this->cache->delete($this->getCacheKey($rawName));
0 ignored issues
show
Bug introduced by
$this->getCacheKey($rawName) of type array<integer,null|string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::delete(). ( Ignorable by Annotation )

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

310
            $this->cache->delete(/** @scrutinizer ignore-type */ $this->getCacheKey($rawName));
Loading history...
311
        }
312 65
    }
313
314
    /**
315
     * Creates a query builder for the database.
316
     *
317
     * This method may be overridden by child classes to create a DBMS-specific query builder.
318
     *
319
     * @return QueryBuilder query builder instance.
320
     */
321
    public function createQueryBuilder()
322
    {
323
        return new QueryBuilder($this->db);
0 ignored issues
show
Bug introduced by
It seems like $this->db can also be of type null; however, parameter $db of Yiisoft\Db\Query\QueryBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, maybe add an additional type check? ( Ignorable by Annotation )

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

323
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->db);
Loading history...
324
    }
325
326
    /**
327
     * Create a column schema builder instance giving the type and value precision.
328
     *
329
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
330
     *
331
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
332
     * @param int|string|array $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
333
     *
334
     * @return ColumnSchemaBuilder column schema builder instance
335
     */
336 4
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
337
    {
338 4
        return new ColumnSchemaBuilder($type, $length);
339
    }
340
341
    /**
342
     * Returns the ID of the last inserted row or sequence value.
343
     *
344
     * @param string $sequenceName name of the sequence object (required by some DBMS)
345
     *
346
     * @throws InvalidCallException if the DB connection is not active
347
     *
348
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
349
     *
350
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
351
     */
352
    public function getLastInsertID(string $sequenceName = ''): string
353
    {
354
        if ($this->db->isActive()) {
355
            return $this->db->getPDO()->lastInsertId(
356
                $sequenceName === '' ? null : $this->quoteTableName($sequenceName)
357
            );
358
        }
359
360
        throw new InvalidCallException('DB Connection is not active.');
361
    }
362
363
    /**
364
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
365
     */
366
    public function supportsSavepoint(): bool
367
    {
368
        return $this->db->isSavepointEnabled();
369
    }
370
371
    /**
372
     * Creates a new savepoint.
373
     *
374
     * @param string $name the savepoint name
375
     */
376
    public function createSavepoint(string $name): void
377
    {
378 3
        $this->db->createCommand("SAVEPOINT $name")->execute();
379
    }
380 3
381 3
    /**
382 3
     * Releases an existing savepoint.
383
     *
384
     * @param string $name the savepoint name
385
     */
386
    public function releaseSavepoint(string $name): void
387
    {
388
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
389
    }
390
391
    /**
392 6
     * Rolls back to a previously created savepoint.
393
     *
394 6
     * @param string $name the savepoint name
395
     */
396
    public function rollBackSavepoint(string $name): void
397
    {
398
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
399
    }
400
401
    /**
402 3
     * Sets the isolation level of the current transaction.
403
     *
404 3
     * @param string $level The transaction isolation level to use for this transaction.
405 3
     *
406
     * This can be one of {@see Transaction::READ_UNCOMMITTED}, {@see Transaction::READ_COMMITTED},
407
     * {@see Transaction::REPEATABLE_READ} and {@see Transaction::SERIALIZABLE} but also a string containing DBMS
408
     * specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
409
     *
410
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
411
     */
412
    public function setTransactionIsolationLevel(string $level): void
413
    {
414
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
415
    }
416
417
    /**
418
     * Executes the INSERT command, returning primary key values.
419
     *
420
     * @param string $table the table that new rows will be inserted into.
421
     * @param array $columns the column data (name => value) to be inserted into the table.
422 3
     *
423
     * @return array|false primary key values or false if the command fails.
424 3
     */
425 3
    public function insert(string $table, array $columns)
426
    {
427
        $command = $this->db->createCommand()->insert($table, $columns);
428
429
        if (!$command->execute()) {
430
            return false;
431
        }
432
433
        $tableSchema = $this->getTableSchema($table);
434
        $result = [];
435
436
        foreach ($tableSchema->getPrimaryKey() as $name) {
437
            if ($tableSchema->getColumn($name)->isAutoIncrement()) {
438 6
                $result[$name] = $this->getLastInsertID($tableSchema->getSequenceName());
0 ignored issues
show
Bug introduced by
It seems like $tableSchema->getSequenceName() can also be of type null; however, parameter $sequenceName of Yiisoft\Db\Schema\Schema::getLastInsertID() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

438
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
439
                break;
440 6
            }
441 6
442
            $result[$name] = $columns[$name] ?? $tableSchema->getColumn($name)->getDefaultValue();
443
        }
444
445
        return $result;
446
    }
447
448
    /**
449
     * Quotes a string value for use in a query.
450
     *
451
     * Note that if the parameter is not a string, it will be returned without change.
452
     *
453
     * @param string|int $str string to be quoted.
454
     *
455
     * @return string|int the properly quoted string.
456
     *
457
     * {@see http://www.php.net/manual/en/function.PDO-quote.php}
458
     */
459
    public function quoteValue($str)
460
    {
461
        if (!is_string($str)) {
462
            return $str;
463
        }
464
465
        if (($value = $this->db->getSlavePdo()->quote($str)) !== false) {
466
            return $value;
467
        }
468
469
        /** the driver doesn't support quote (e.g. oci) */
470
        return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'";
471
    }
472
473
    /**
474
     * Quotes a table name for use in a query.
475
     *
476
     * If the table name contains schema prefix, the prefix will also be properly quoted. If the table name is already
477
     * quoted or contains '(' or '{{', then this method will do nothing.
478
     *
479
     * @param string $name table name.
480
     *
481
     * @return string the properly quoted table name.
482
     *
483
     * {@see quoteSimpleTableName()}
484
     */
485 376
    public function quoteTableName(string $name): string
486
    {
487 376
        if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) {
488 3
            return $name;
489
        }
490
491 376
        if (strpos($name, '{{') !== false) {
492 376
            return $name;
493
        }
494
495
        if (strpos($name, '.') === false) {
496
            return $this->quoteSimpleTableName($name);
497
        }
498
499
        $parts = $this->getTableNameParts($name);
500
501
        foreach ($parts as $i => $part) {
502
            $parts[$i] = $this->quoteSimpleTableName($part);
503
        }
504
505
        return implode('.', $parts);
506
    }
507
508
    /**
509
     * Splits full table name into parts
510
     *
511 446
     * @param string $name
512
     *
513 446
     * @return array
514 55
     */
515
    protected function getTableNameParts(string $name): array
516
    {
517 430
        return explode('.', $name);
518 426
    }
519
520
    /**
521 10
     * Quotes a column name for use in a query.
522
     *
523 10
     * If the column name contains prefix, the prefix will also be properly quoted. If the column name is already quoted
524 10
     * or contains '(', '[[' or '{{', then this method will do nothing.
525
     *
526
     * @param string $name column name.
527 10
     *
528
     * @return string the properly quoted column name.
529
     *
530
     * {@see quoteSimpleColumnName()}
531
     */
532
    public function quoteColumnName(string $name): string
533
    {
534
        if (strpos($name, '(') !== false || strpos($name, '[[') !== false) {
535
            return $name;
536
        }
537 10
538
        if (($pos = strrpos($name, '.')) !== false) {
539 10
            $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
540
            $name = substr($name, $pos + 1);
541
        } else {
542
            $prefix = '';
543
        }
544
545
        if (strpos($name, '{{') !== false) {
546
            return $name;
547
        }
548
549
        return $prefix . $this->quoteSimpleColumnName($name);
550
    }
551
552
    /**
553
     * Quotes a simple table name for use in a query.
554 562
     *
555
     * A simple table name should contain the table name only without any schema prefix. If the table name is already
556 562
     * quoted, this method will do nothing.
557 39
     *
558
     * @param string $name table name.
559
     *
560 553
     * @return string the properly quoted table name.
561 23
     */
562 23
    public function quoteSimpleTableName(string $name): string
563
    {
564 550
        if (is_string($this->tableQuoteCharacter)) {
565
            $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
566
        } else {
567 553
            [$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
568 3
        }
569
570
        return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
571 553
    }
572
573
    /**
574
     * Quotes a simple column name for use in a query.
575
     *
576
     * A simple column name should contain the column name only without any prefix. If the column name is already quoted
577
     * or is the asterisk character '*', this method will do nothing.
578
     *
579
     * @param string $name column name.
580
     *
581
     * @return string the properly quoted column name.
582
     */
583
    public function quoteSimpleColumnName(string $name): string
584 456
    {
585
        if (is_string($this->columnQuoteCharacter)) {
586 456
            $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
587 456
        } else {
588
            [$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
589
        }
590
591
        return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name
592 456
            . $endingCharacter;
593
    }
594
595
    /**
596
     * Unquotes a simple table name.
597
     *
598
     * A simple table name should contain the table name only without any schema prefix. If the table name is not
599
     * quoted, this method will do nothing.
600
     *
601
     * @param string $name table name.
602
     *
603
     * @return string unquoted table name.
604
     */
605 553
    public function unquoteSimpleTableName(string $name): string
606
    {
607 553
        if (\is_string($this->tableQuoteCharacter)) {
608 553
            $startingCharacter = $this->tableQuoteCharacter;
609
        } else {
610
            $startingCharacter = $this->tableQuoteCharacter[0];
611
        }
612
613 553
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
614 553
    }
615
616
    /**
617
     * Unquotes a simple column name.
618
     *
619
     * A simple column name should contain the column name only without any prefix. If the column name is not quoted or
620
     * is the asterisk character '*', this method will do nothing.
621
     *
622
     * @param string $name column name.
623
     *
624
     * @return string unquoted column name.
625
     */
626
    public function unquoteSimpleColumnName(string $name): string
627 2
    {
628
        if (\is_string($this->columnQuoteCharacter)) {
629 2
            $startingCharacter = $this->columnQuoteCharacter;
630 2
        } else {
631
            $startingCharacter = $this->columnQuoteCharacter[0];
632
        }
633
634
        return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
635 2
    }
636
637
    /**
638
     * Returns the actual name of a given table name.
639
     *
640
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
641
     * {@see Connection::tablePrefix}.
642
     *
643
     * @param string $name the table name to be converted.
644
     *
645
     * @return string the real name of the given table name.
646
     */
647
    public function getRawTableName(string $name): string
648
    {
649
        if (strpos($name, '{{') !== false) {
650
            $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
651
652
            return str_replace('%', $this->db->getTablePrefix(), $name);
653
        }
654
655
        return $name;
656
    }
657
658
    /**
659
     * Extracts the PHP type from abstract DB type.
660
     *
661
     * @param ColumnSchema $column the column schema information.
662
     *
663
     * @return string PHP type name.
664
     */
665
    protected function getColumnPhpType(ColumnSchema $column): string
666
    {
667
        static $typeMap = [
668
            // abstract type => php type
669 443
            self::TYPE_TINYINT  => 'integer',
670
            self::TYPE_SMALLINT => 'integer',
671 443
            self::TYPE_INTEGER  => 'integer',
672 69
            self::TYPE_BIGINT   => 'integer',
673
            self::TYPE_BOOLEAN  => 'boolean',
674 69
            self::TYPE_FLOAT    => 'double',
675
            self::TYPE_DOUBLE   => 'double',
676
            self::TYPE_BINARY   => 'resource',
677 443
            self::TYPE_JSON     => 'array',
678
        ];
679
680
        if (isset($typeMap[$column->getType()])) {
681
            if ($column->getType() === 'bigint') {
682
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
683
            }
684
685
            if ($column->getType() === 'integer') {
686
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
687 246
            }
688
689 246
            return $typeMap[$column->getType()];
690
        }
691
692
        return 'string';
693
    }
694
695
    /**
696
     * Converts a DB exception to a more concrete one if possible.
697
     *
698
     * @param \Exception $e
699
     * @param string $rawSql SQL that produced exception.
700
     *
701
     * @return Exception
702 246
     */
703 238
    public function convertException(\Exception $e, string $rawSql): Exception
704 36
    {
705
        if ($e instanceof Exception) {
706
            return $e;
707 238
        }
708 238
709
        $exceptionClass = Exception::class;
710
711 150
        foreach ($this->exceptionMap as $error => $class) {
712
            if (strpos($e->getMessage(), $error) !== false) {
713
                $exceptionClass = $class;
714 230
            }
715
        }
716
717
        $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
718
        $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
719
720
        return new $exceptionClass($message, $errorInfo, $e->getCode(), $e);
721
    }
722
723
    /**
724
     * Returns a value indicating whether a SQL statement is for read purpose.
725 28
     *
726
     * @param string $sql the SQL statement.
727 28
     *
728
     * @return bool whether a SQL statement is for read purpose.
729
     */
730
    public function isReadQuery($sql): bool
731 28
    {
732
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
733 28
734 28
        return preg_match($pattern, $sql) > 0;
735 7
    }
736
737
    /**
738
     * Returns a server version as a string comparable by {@see \version_compare()}.
739 28
     *
740 28
     * @return string server version as a string.
741
     */
742 28
    public function getServerVersion(): string
743
    {
744
        if ($this->serverVersion === null) {
745
            $this->serverVersion = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
746
        }
747
748
        return $this->serverVersion;
749
    }
750
751
    /**
752 9
     * Returns the cache key for the specified table name.
753
     *
754 9
     * @param string $name the table name.
755
     *
756 9
     * @return mixed the cache key.
757
     */
758
    protected function getCacheKey($name)
759
    {
760
        return [
761
            __CLASS__,
762
            $this->db->getDsn(),
763
            $this->db->getUsername(),
764 116
            $this->getRawTableName($name),
765
        ];
766 116
    }
767 116
768
    /**
769
     * Returns the cache tag name.
770 116
     *
771
     * This allows {@see refresh()} to invalidate all cached table schemas.
772
     *
773
     * @return string the cache tag name.
774
     */
775
    protected function getCacheTag(): string
776
    {
777
        return md5(serialize([
778
            __CLASS__,
779
            $this->db->getDsn(),
780 443
            $this->db->getUsername(),
781
        ]));
782
    }
783 443
784 443
    /**
785 443
     * Returns the metadata of the given type for the given table.
786 443
     *
787
     * If there's no metadata in the cache, this method will call a `'loadTable' . ucfirst($type)` named method with the
788
     * table name to obtain the metadata.
789
     *
790
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
791
     * @param string $type metadata type.
792
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
793
     *
794
     * @return mixed metadata.
795
     */
796
    protected function getTableMetadata(string $name, string $type, bool $refresh = false)
797 395
    {
798
        if ($this->db->isSchemaCacheEnabled() && !\in_array($name, $this->db->getSchemaCacheExclude(), true)) {
799 395
            $schemaCache = $this->cache;
800 395
        }
801 395
802 395
        $rawName = $this->getRawTableName($name);
803
804
        if (!isset($this->tableMetadata[$rawName])) {
805
            $this->loadTableMetadataFromCache($schemaCache, $rawName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $schemaCache does not seem to be defined for all execution paths leading up to this point.
Loading history...
806
        }
807
808
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
809
            $this->tableMetadata[$rawName][$type] = $this->{'loadTable' . ucfirst($type)}($rawName);
810
            $this->saveTableMetadataToCache($schemaCache, $rawName);
811
        }
812
813
        return $this->tableMetadata[$rawName][$type];
814
    }
815
816
    /**
817
     * Returns the metadata of the given type for all tables in the given schema.
818 443
     *
819
     * This method will call a `'getTable' . ucfirst($type)` named method with the table name and the refresh flag to
820 443
     * obtain the metadata.
821 443
     *
822
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
823
     * name.
824 443
     * @param string $type metadata type.
825
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
826 443
     * returned if available.
827 443
     *
828
     * @return array array of metadata.
829
     */
830 443
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
831 443
    {
832 395
        $metadata = [];
833
        $methodName = 'getTable' . ucfirst($type);
834
835 395
        foreach ($this->getTableNames($schema, $refresh) as $name) {
836
            if ($schema !== '') {
837
                $name = $schema . '.' . $name;
838
            }
839
840
            $tableMetadata = $this->$methodName($name, $refresh);
841
842
            if ($tableMetadata !== null) {
843
                $metadata[] = $tableMetadata;
844
            }
845
        }
846
847
        return $metadata;
848
    }
849
850
    /**
851
     * Sets the metadata of the given type for the given table.
852 9
     *
853
     * @param string $name table name.
854 9
     * @param string $type metadata type.
855 9
     * @param mixed  $data metadata.
856
     */
857 9
    protected function setTableMetadata(string $name, string $type, $data): void
858 9
    {
859
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
860
    }
861
862 9
    /**
863
     * Changes row's array key case to lower if PDO's one is set to uppercase.
864 9
     *
865 9
     * @param array $row row's array or an array of row's arrays.
866
     * @param bool $multiple whether multiple rows or a single row passed.
867
     *
868
     * @return array normalized row or rows.
869 9
     */
870
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
871
    {
872
        if ($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
873
            return $row;
874
        }
875
876
        if ($multiple) {
877
            return \array_map(function (array $row) {
878
                return \array_change_key_case($row, CASE_LOWER);
879 162
            }, $row);
880
        }
881 162
882 162
        return \array_change_key_case($row, CASE_LOWER);
883
    }
884
885
    /**
886
     * Tries to load and populate table metadata from cache.
887
     *
888
     * @param CacheInterface|null $cache
889
     * @param string $name
890
     */
891
    private function loadTableMetadataFromCache(?CacheInterface $cache, string $name): void
892 186
    {
893
        if ($cache === null) {
894 186
            $this->tableMetadata[$name] = [];
895 146
896
            return;
897
        }
898 40
899 40
        $metadata = $cache->get($this->getCacheKey($name));
0 ignored issues
show
Bug introduced by
$this->getCacheKey($name) of type array<integer,null|string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::get(). ( Ignorable by Annotation )

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

899
        $metadata = $cache->get(/** @scrutinizer ignore-type */ $this->getCacheKey($name));
Loading history...
900 39
901 40
        if (!\is_array($metadata) || !isset($metadata['cacheVersion']) || $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION) {
902
            $this->tableMetadata[$name] = [];
903
904
            return;
905
        }
906
907
        unset($metadata['cacheVersion']);
908
        $this->tableMetadata[$name] = $metadata;
909
    }
910
911
    /**
912
     * Saves table metadata to cache.
913 443
     *
914
     * @param CacheInterface|null $cache
915 443
     * @param string $name
916
     */
917
    private function saveTableMetadataToCache(?CacheInterface $cache, string $name): void
918
    {
919
        if ($cache === null) {
920
            return;
921 443
        }
922
923 443
        $metadata = $this->tableMetadata[$name];
924 443
925
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
926 443
927
        $cache->set(
928
            $this->getCacheKey($name),
929
            $metadata,
930
            $this->db->getSchemaCacheDuration(),
931
            new TagDependency(['tags' => $this->getCacheTag()])
932
        );
933
    }
934
935
    public function getDb(): ?Connection
936
    {
937
        return $this->db;
938
    }
939 395
940
    public function getDefaultSchema(): ?string
941 395
    {
942
        return $this->defaultSchema;
943
    }
944
}
945