Passed
Pull Request — master (#163)
by Wilmer
13:10
created

Connection::open()   B

Complexity

Conditions 11
Paths 72

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 11.0099

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 26
c 1
b 0
f 0
nc 72
nop 0
dl 0
loc 50
ccs 22
cts 23
cp 0.9565
crap 11.0099
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Connection;
6
7
use PDO;
8
use Psr\Log\LoggerInterface;
9
use Psr\Log\LogLevel;
10
use Yiisoft\Cache\CacheInterface;
11
use Yiisoft\Cache\Dependency\Dependency;
12
use Yiisoft\Db\Command\Command;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Exception\InvalidCallException;
15
use Yiisoft\Db\Exception\InvalidConfigException;
16
use Yiisoft\Db\Exception\NotSupportedException;
17
use Yiisoft\Db\Factory\DatabaseFactory;
18
use Yiisoft\Db\Query\QueryBuilder;
19
use Yiisoft\Db\Schema\Schema;
20
use Yiisoft\Db\Schema\TableSchema;
21
use Yiisoft\Db\Transaction\Transaction;
22
use Yiisoft\Profiler\Profiler;
23
24
/**
25
 * Connection represents a connection to a database via [PDO](http://php.net/manual/en/book.pdo.php).
26
 *
27
 * Connection works together with {@see Command}, {@see DataReader} and {@see Transaction} to provide data access to
28
 * various DBMS in a common set of APIs. They are a thin wrapper of the
29
 * [PDO PHP extension](http://php.net/manual/en/book.pdo.php).
30
 *
31
 * Connection supports database replication and read-write splitting. In particular, a Connection component can be
32
 * configured with multiple {@see setMasters()} and {@see setSlaves()}. It will do load balancing and failover by
33
 * choosing appropriate servers. It will also automatically direct read operations to the slaves and write operations
34
 * to the masters.
35
 *
36
 * To establish a DB connection, set {@see dsn}, {@see setUsername()} and {@see setPassword}, and then call
37
 * {@see open()} to connect to the database server. The current state of the connection can be checked using
38
 * {@see $isActive}.
39
 *
40
 * The following example shows how to create a Connection instance and establish the DB connection:
41
 *
42
 * ```php
43
 * $connection = new \Yiisoft\Db\Connection\Connection(
44
 *     $cache,
45
 *     $logger,
46
 *     $profiler,
47
 *     $dsn
48
 * );
49
 * $connection->open();
50
 * ```
51
 *
52
 * After the DB connection is established, one can execute SQL statements like the following:
53
 *
54
 * ```php
55
 * $command = $connection->createCommand('SELECT * FROM post');
56
 * $posts = $command->queryAll();
57
 * $command = $connection->createCommand('UPDATE post SET status=1');
58
 * $command->execute();
59
 * ```
60
 *
61
 * One can also do prepared SQL execution and bind parameters to the prepared SQL.
62
 * When the parameters are coming from user input, you should use this approach to prevent SQL injection attacks. The
63
 * following is an example:
64
 *
65
 * ```php
66
 * $command = $connection->createCommand('SELECT * FROM post WHERE id=:id');
67
 * $command->bindValue(':id', $_GET['id']);
68
 * $post = $command->query();
69
 * ```
70
 *
71
 * For more information about how to perform various DB queries, please refer to {@see Command}.
72
 *
73
 * If the underlying DBMS supports transactions, you can perform transactional SQL queries like the following:
74
 *
75
 * ```php
76
 * $transaction = $connection->beginTransaction();
77
 * try {
78
 *     $connection->createCommand($sql1)->execute();
79
 *     $connection->createCommand($sql2)->execute();
80
 *     // ... executing other SQL statements ...
81
 *     $transaction->commit();
82
 * } catch (Exceptions $e) {
83
 *     $transaction->rollBack();
84
 * }
85
 * ```
86
 *
87
 * You also can use shortcut for the above like the following:
88
 *
89
 * ```php
90
 * $connection->transaction(function () {
91
 *     $order = new Order($customer);
92
 *     $order->save();
93
 *     $order->addItems($items);
94
 * });
95
 * ```
96
 *
97
 * If needed you can pass transaction isolation level as a second parameter:
98
 *
99
 * ```php
100
 * $connection->transaction(function (Connection $db) {
101
 *     //return $db->...
102
 * }, Transaction::READ_UNCOMMITTED);
103
 * ```
104
 *
105
 * Connection is often used as an application component and configured in the container-di configuration like the
106
 * following:
107
 *
108
 * ```php
109
 * Connection::class => static function (ContainerInterface $container) {
110
 *     $connection = new Connection(
111
 *         $container->get(CacheInterface::class),
112
 *         $container->get(LoggerInterface::class),
113
 *         $container->get(Profiler::class),
114
 *         'mysql:host=127.0.0.1;dbname=demo;charset=utf8'
115
 *     );
116
 *
117
 *     $connection->setUsername(root);
118
 *     $connection->setPassword('');
119
 *
120
 *     return $connection;
121
 * },
122
 * ```
123
 *
124
 * The {@see dsn} property can be defined via configuration {@see \Yiisoft\Db\Helper\Dsn}:
125
 *
126
 * ```php
127
 * Connection::class => static function (ContainerInterface $container) {
128
 *     $dsn = new Dsn('mysql', '127.0.0.1', 'yiitest', '3306');
129
 *
130
 *     $connection = new Connection(
131
 *         $container->get(CacheInterface::class),
132
 *         $container->get(LoggerInterface::class),
133
 *         $container->get(Profiler::class),
134
 *         $dsn->getDsn()
135
 *     );
136
 *
137
 *     $connection->setUsername(root);
138
 *     $connection->setPassword('');
139
 *
140
 *     return $connection;
141
 * },
142
 * ```
143
 *
144
 * @property string $driverName Name of the DB driver.
145
 * @property bool $isActive Whether the DB connection is established. This property is read-only.
146
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
147
 * object. This property is read-only.
148
 * @property Connection $master The currently active master connection. `null` is returned if there is no master
149
 * available. This property is read-only.
150
 * @property PDO $masterPdo The PDO instance for the currently active master connection. This property is read-only.
151
 * @property QueryBuilder $queryBuilder The query builder for the current DB connection. Note that the type of this
152
 * property differs in getter and setter. See {@see getQueryBuilder()} and {@see setQueryBuilder()} for details.
153
 * @property Schema $schema The schema information for the database opened by this connection. This property is
154
 * read-only.
155
 * @property string $serverVersion Server version as a string. This property is read-only.
156
 * @property Connection $slave The currently active slave connection. `null` is returned if there is no slave
157
 * available and `$fallbackToMaster` is false. This property is read-only.
158
 * @property PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned if no slave
159
 * connection is available and `$fallbackToMaster` is false. This property is read-only.
160
 * @property Transaction|null $transaction The currently active transaction. Null if no active transaction. This
161
 * property is read-only.
162
 */
163
class Connection
164
{
165
    private ?string $driverName = null;
166
    private ?string $dsn = null;
167
    private ?string $username = null;
168
    private ?string $password = null;
169
    private array $attributes = [];
170
    private ?PDO $pdo = null;
171
    private bool $enableSchemaCache = true;
172
    private int $schemaCacheDuration = 3600;
173
    private array $schemaCacheExclude = [];
174
    private ?CacheInterface $schemaCache = null;
175
    private bool $enableQueryCache = true;
176
    private ?CacheInterface $queryCache = null;
177
    private ?string $charset = null;
178
    private ?bool $emulatePrepare = null;
179
    private string $tablePrefix = '';
180
181
    /**
182
     * @var array mapping between PDO driver names and {@see Schema} classes. The keys of the array are PDO driver names
183
     * while the values are either the corresponding schema class names or configurations.
184
     *
185
     * This property is mainly used by {@see getSchema()} when fetching the database schema information. You normally do
186
     * not need to set this property unless you want to use your own {@see Schema} class to support DBMS that is not
187
     * supported by Yii.
188
     */
189
    private array $schemaMap = [
190
        'pgsql' => \Yiisoft\Db\Pgsql\Schema\Schema::class, // PostgreSQL
191
        'mysqli' => \Yiisoft\Db\Mysql\Schema\Schema::class, // MySQL
192
        'mysql' => \Yiisoft\Db\Mysql\Schema\Schema::class, // MySQL
193
        'sqlite' => \Yiisoft\Db\Sqlite\Schema\Schema::class, // sqlite 3
194
        'sqlite2' => \Yiisoft\Db\Sqlite\Schema\Schema::class, // sqlite 2
195
        'sqlsrv' => \Yiisoft\Db\Mssql\Schema::class, // newer MSSQL driver on MS Windows hosts
196
        'oci' => \Yiisoft\Db\Oci\Schema::class, // Oracle driver
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Oci\Schema was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
197
        'mssql' => \Yiisoft\Db\Mssql\Schema::class, // older MSSQL driver on MS Windows hosts
198
        'dblib' => \Yiisoft\Db\Mssql\Schema::class, // dblib drivers on GNU/Linux (and maybe other OSes) hosts
199
    ];
200
201
    /**
202
     * @var string Custom PDO wrapper class. If not set, it will use {@see PDO} or {@see \Yiisoft\Db\mssql\PDO}
203
     * when MSSQL is used.
204
     *
205
     * @see pdo
206
     */
207
    private ?string $pdoClass = null;
208
209
    /**
210
     * @var array mapping between PDO driver names and {@see Command} classes. The keys of the array are PDO driver
211
     * names while the values are either the corresponding command class names or configurations.
212
     *
213
     * This property is mainly used by {@see createCommand()} to create new database {@see Command} objects. You
214
     * normally do not need to set this property unless you want to use your own {@see Command} class or support
215
     * DBMS that is not supported by Yii.
216
     */
217
    private array $commandMap = [
218
        'pgsql'   => Command::class, // PostgreSQL
219
        'mysqli'  => Command::class, // MySQL
220
        'mysql'   => Command::class, // MySQL
221
        'mariadb' => Command::class, // MySQL
222
        'sqlite'  => \Yiisoft\Db\Sqlite\Command\Command::class, // sqlite 3
223
        'sqlite2' => \Yiisoft\Db\Sqlite\Command\Command::class, // sqlite 2
224
        'sqlsrv'  => Command::class, // newer MSSQL driver on MS Windows hosts
225
        'oci'     => Command::class, // Oracle driver
226
        'mssql'   => Command::class, // older MSSQL driver on MS Windows hosts
227
        'dblib'   => Command::class, // dblib drivers on GNU/Linux (and maybe other OSes) hosts
228
    ];
229
230
    /**
231
     * @var array query cache parameters for the {cache()} calls
232
     */
233
    private array $queryCacheInfo = [];
234
235
    private bool $enableSavepoint = true;
236
    private int $serverRetryInterval = 600;
237
    private bool $enableSlaves = true;
238
    private array $slaves = [];
239
    private array $masters = [];
240
    private bool $shuffleMasters = true;
241
    private bool $enableLogging = true;
242
    private bool $enableProfiling = true;
243
    private int $queryCacheDuration = 3600;
244
    private array $quotedTableNames = [];
245
    private array $quotedColumnNames = [];
246
    private ?Connection $master = null;
247
    private ?Connection $slave = null;
248
    private ?LoggerInterface $logger = null;
249
    private ?Profiler $profiler = null;
250
    private ?Transaction $transaction = null;
251
    private ?Schema $schema = null;
252
253 1147
    public function __construct(CacheInterface $cache, LoggerInterface $logger, Profiler $profiler, string $dsn)
254
    {
255 1147
        $this->schemaCache = $cache;
256 1147
        $this->logger = $logger;
257 1147
        $this->profiler = $profiler;
258 1147
        $this->dsn = $dsn;
259 1147
    }
260
261
    /**
262
     * Reset the connection after cloning.
263
     */
264 3
    public function __clone()
265
    {
266 3
        $this->master = null;
267 3
        $this->slave = null;
268 3
        $this->schema = null;
269 3
        $this->transaction = null;
270
271 3
        if (strncmp($this->dsn, 'sqlite::memory:', 15) !== 0) {
272
            /* reset PDO connection, unless its sqlite in-memory, which can only have one connection */
273 3
            $this->pdo = null;
274
        }
275 3
    }
276
277
    /**
278
     * Close the connection before serializing.
279
     *
280
     * @return array
281
     */
282 4
    public function __sleep(): array
283
    {
284 4
        $fields = (array) $this;
285
286
        unset(
287 4
            $fields["\000" . __CLASS__ . "\000" . 'pdo'],
288 4
            $fields["\000" . __CLASS__ . "\000" . 'master'],
289 4
            $fields["\000" . __CLASS__ . "\000" . 'slave'],
290 4
            $fields["\000" . __CLASS__ . "\000" . 'transaction'],
291 4
            $fields["\000" . __CLASS__ . "\000" . 'schema']
292
        );
293
294 4
        return array_keys($fields);
295
    }
296
297
    /**
298
     * Starts a transaction.
299
     *
300
     * @param string|null $isolationLevel The isolation level to use for this transaction.
301
     *
302
     * {@see Transaction::begin()} for details.
303
     *
304
     * @throws Exception
305
     * @throws InvalidConfigException
306
     * @throws NotSupportedException
307
     *
308
     * @return Transaction the transaction initiated
309
     */
310 24
    public function beginTransaction($isolationLevel = null): Transaction
311
    {
312 24
        $this->open();
313
314 24
        if (($transaction = $this->getTransaction()) === null) {
315 24
            $transaction = $this->transaction = new Transaction($this, $this->logger);
316
        }
317
318 24
        $transaction->begin($isolationLevel);
319
320 24
        return $transaction;
321
    }
322
323
    /**
324
     * Uses query cache for the queries performed with the callable.
325
     *
326
     * When query caching is enabled ({@see enableQueryCache} is true and {@see queryCache} refers to a valid cache),
327
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
328
     *
329
     * For example,
330
     *
331
     * ```php
332
     * // The customer will be fetched from cache if available.
333
     * // If not, the query will be made against DB and cached for use next time.
334
     * $customer = $db->cache(function (Connection $db) {
335
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
336
     * });
337
     * ```
338
     *
339
     * Note that query cache is only meaningful for queries that return results. For queries performed with
340
     * {@see Command::execute()}, query cache will not be used.
341
     *
342
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
343
     * The signature of the callable is `function (Connection $db)`.
344
     * @param int $duration the number of seconds that query results can remain valid in the cache. If this is not set,
345
     * the value of {@see queryCacheDuration} will be used instead. Use 0 to indicate that the cached data will never
346
     * expire.
347
     * @param Dependency $dependency the cache dependency associated with the cached query
348
     * results.
349
     *
350
     * @throws \Throwable if there is any exception during query
351
     *
352
     * @return mixed the return result of the callable
353
     *
354
     * {@see setEnableQueryCache()}
355
     * {@see queryCache}
356
     * {@see noCache()}
357
     */
358 6
    public function cache(callable $callable, $duration = null, $dependency = null)
359
    {
360 6
        $this->queryCacheInfo[] = [$duration ?? $this->queryCacheDuration, $dependency];
361
362
        try {
363 6
            $result = $callable($this);
364
365 6
            array_pop($this->queryCacheInfo);
366
367 6
            return $result;
368
        } catch (\Throwable $e) {
369
            array_pop($this->queryCacheInfo);
370
371
            throw $e;
372
        }
373
    }
374
375
    /**
376
     * Creates a command for execution.
377
     *
378
     * @param string $sql the SQL statement to be executed
379
     * @param array $params the parameters to be bound to the SQL statement
380
     *
381
     * @throws Exception
382
     * @throws InvalidConfigException
383
     *
384
     * @return Command the DB command
385
     */
386 525
    public function createCommand($sql = null, $params = []): Command
387
    {
388 525
        $driver = $this->getDriverName();
389
390 525
        if ($sql !== null) {
391 525
            $sql = $this->quoteSql($sql);
392
        }
393
394
        /** @var Command $command */
395 525
        $command = new $this->commandMap[$driver]($this->profiler, $this->logger, $this, $sql);
396
397 525
        return $command->bindValues($params);
398
    }
399
400
    public function getAttributes(): array
401
    {
402
        return $this->attributes;
403
    }
404
405
    /**
406
     * Returns the name of the DB driver. Based on the the current {@see dsn}, in case it was not set explicitly by an
407
     * end user.
408
     *
409
     * @throws Exception
410
     * @throws InvalidConfigException
411
     *
412
     * @return string|null name of the DB driver
413
     */
414 918
    public function getDriverName(): ?string
415
    {
416 918
        if ($this->driverName === null) {
417 918
            if (($pos = strpos($this->dsn, ':')) !== false) {
418 918
                $this->driverName = strtolower(substr($this->dsn, 0, $pos));
419
            } else {
420
                $this->driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME));
421
            }
422
        }
423
424 918
        return $this->driverName;
425
    }
426
427 462
    public function getDsn(): ?string
428
    {
429 462
        return $this->dsn;
430
    }
431
432 495
    public function isLoggingEnabled(): bool
433
    {
434 495
        return $this->enableLogging;
435
    }
436
437 495
    public function isProfilingEnabled(): bool
438
    {
439 495
        return $this->enableProfiling;
440
    }
441
442
    public function isQueryCacheEnabled(): bool
443
    {
444
        return $this->enableQueryCache;
445
    }
446
447 6
    public function isSavepointEnabled(): bool
448
    {
449 6
        return $this->enableSavepoint;
450
    }
451
452 443
    public function isSchemaCacheEnabled(): bool
453
    {
454 443
        return $this->enableSchemaCache;
455
    }
456
457 1
    public function areSlavesEnabled(): bool
458
    {
459 1
        return $this->enableSlaves;
460
    }
461
462
    /**
463
     * Returns a value indicating whether the DB connection is established.
464
     *
465
     * @return bool whether the DB connection is established
466
     */
467 31
    public function isActive(): bool
468
    {
469 31
        return $this->pdo !== null;
470
    }
471
472
    /**
473
     * Returns the ID of the last inserted row or sequence value.
474
     *
475
     * @param string $sequenceName name of the sequence object (required by some DBMS)
476
     *
477
     * @throws Exception
478
     * @throws InvalidConfigException
479
     * @throws NotSupportedException
480
     * @throws InvalidCallException
481
     *
482
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
483
     *
484
     * {@see http://php.net/manual/en/pdo.lastinsertid.php'>http://php.net/manual/en/pdo.lastinsertid.php}
485
     */
486
    public function getLastInsertID($sequenceName = ''): string
487
    {
488
        return $this->getSchema()->getLastInsertID($sequenceName);
489
    }
490
491
    public function getLogger(): LoggerInterface
492
    {
493
        return $this->logger;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->logger could return the type null which is incompatible with the type-hinted return Psr\Log\LoggerInterface. Consider adding an additional type-check to rule them out.
Loading history...
494
    }
495
496
    /**
497
     * Returns the currently active master connection.
498
     *
499
     * If this method is called for the first time, it will try to open a master connection.
500 9
     *
501
     * @throws InvalidConfigException
502 9
     *
503 9
     * @return Connection the currently active master connection. `null` is returned if there is no master available.
504 5
     */
505 7
    public function getMaster(): ?Connection
506
    {
507
        if ($this->master === null) {
508 9
            $this->master = $this->shuffleMasters
509
                ? $this->openFromPool($this->masters)
510
                : $this->openFromPoolSequentially($this->masters);
511
        }
512
513
        return $this->master;
514
    }
515
516
    /**
517
     * Returns the PDO instance for the currently active master connection.
518
     *
519
     * This method will open the master DB connection and then return {@see pdo}.
520 540
     *
521
     * @throws Exception
522 540
     *
523
     * @return PDO the PDO instance for the currently active master connection.
524 540
     */
525
    public function getMasterPdo(): PDO
526
    {
527 6
        $this->open();
528
529 6
        return $this->pdo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->pdo could return the type null which is incompatible with the type-hinted return PDO. Consider adding an additional type-check to rule them out.
Loading history...
530
    }
531
532
    public function getPassword(): ?string
533
    {
534
        return $this->password;
535
    }
536
537
    /**
538
     * The PHP PDO instance associated with this DB connection. This property is mainly managed by {@see open()} and
539
     * {@see close()} methods. When a DB connection is active, this property will represent a PDO instance; otherwise,
540
     * it will be null.
541 111
     *
542
     * @return PDO|null
543 111
     *
544
     * {@see pdoClass}
545
     */
546
    public function getPDO(): ?PDO
547
    {
548
        return $this->pdo;
549
    }
550
551
    public function getProfiler(): profiler
552
    {
553
        return $this->profiler;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->profiler could return the type null which is incompatible with the type-hinted return Yiisoft\Profiler\Profiler. Consider adding an additional type-check to rule them out.
Loading history...
554
    }
555 179
556
    /**
557 179
     * Returns the query builder for the current DB connection.
558
     *
559
     * @throws Exception
560 6
     * @throws InvalidConfigException
561
     * @throws NotSupportedException
562 6
     *
563
     * @return QueryBuilder the query builder for the current DB connection.
564
     */
565
    public function getQueryBuilder(): QueryBuilder
566
    {
567
        return $this->getSchema()->getQueryBuilder();
568
    }
569
570
    public function getQueryCacheDuration(): ?int
571
    {
572
        return $this->queryCacheDuration;
573
    }
574
575
    /**
576 477
     * Returns the current query cache information.
577
     *
578 477
     * This method is used internally by {@see Command}.
579
     *
580 477
     * @param int|null $duration the preferred caching duration. If null, it will be ignored.
581 477
     * @param Dependency|null $dependency the preferred caching dependency. If null, it will be
582
     * ignored.
583 477
     *
584 6
     * @return array|null the current query cache information, or null if query cache is not enabled.
585 6
     */
586
    public function getQueryCacheInfo(?int $duration, ?Dependency $dependency): ?array
587
    {
588 6
        $result = null;
589 6
590
        if ($this->enableQueryCache) {
591
            $info = \end($this->queryCacheInfo);
592
593 477
            if (\is_array($info)) {
594 6
                if ($duration === null) {
595 6
                    $duration = $info[0];
596
                }
597
598
                if ($dependency === null) {
599
                    $dependency = $info[1];
600 477
                }
601
            }
602
603
            if ($duration === 0 || $duration > 0) {
604
                if ($this->schemaCache instanceof CacheInterface) {
605
                    $result = [$this->schemaCache, $duration, $dependency];
606
                }
607
            }
608
        }
609
610
        return $result;
611
    }
612 899
613
    /**
614 899
     * Returns the schema information for the database opened by this connection.
615 594
     *
616
     * @throws Exception
617
     * @throws InvalidConfigException
618 899
     * @throws NotSupportedException if there is no support for the current driver type
619
     *
620 899
     * @return Schema the schema information for the database opened by this connection.
621 899
     */
622
    public function getSchema(): Schema
623 899
    {
624
        if ($this->schema !== null) {
625
            return $this->schema;
626
        }
627
628
        $driver = $this->getDriverName();
629 900
630
        if (isset($this->schemaMap[$driver])) {
631 900
            $class = $this->schemaMap[$driver];
632
633
            return $this->schema = new $class($this);
634 395
        }
635
636 395
        throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS.");
637
    }
638
639 443
    public function getSchemaCache(): CacheInterface
640
    {
641 443
        return $this->schemaCache;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->schemaCache could return the type null which is incompatible with the type-hinted return Yiisoft\Cache\CacheInterface. Consider adding an additional type-check to rule them out.
Loading history...
642
    }
643
644
    public function getSchemaCacheDuration(): int
645
    {
646
        return $this->schemaCacheDuration;
647
    }
648
649
    public function getSchemaCacheExclude(): array
650
    {
651
        return $this->schemaCacheExclude;
652
    }
653 116
654
    /**
655 116
     * Returns a server version as a string comparable by {@see \version_compare()}.
656
     *
657
     * @throws Exception
658
     * @throws InvalidConfigException
659
     * @throws NotSupportedException
660
     *
661
     * @return string server version as a string.
662
     */
663
    public function getServerVersion(): string
664
    {
665
        return $this->getSchema()->getServerVersion();
666
    }
667
668
    /**
669
     * Returns the currently active slave connection.
670
     *
671
     * If this method is called for the first time, it will try to open a slave connection when {@see setEnableSlaves()}
672 530
     * is true.
673
     *
674 530
     * @param bool $fallbackToMaster whether to return a master connection in case there is no slave connection
675 2
     * available.
676
     *
677
     * @throws InvalidConfigException
678 530
     *
679 530
     * @return Connection the currently active slave connection. `null` is returned if there is no slave available and
680
     * `$fallbackToMaster` is false.
681
     */
682 530
    public function getSlave(bool $fallbackToMaster = true): ?Connection
683
    {
684
        if (!$this->enableSlaves) {
685
            return $fallbackToMaster ? $this : null;
686
        }
687
688
        if ($this->slave === null) {
689
            $this->slave = $this->openFromPool($this->slaves);
690
        }
691
692
        return $this->slave === null && $fallbackToMaster ? $this : $this->slave;
693
    }
694
695
    /**
696
     * Returns the PDO instance for the currently active slave connection.
697
     *
698
     * When {@see enableSlaves} is true, one of the slaves will be used for read queries, and its PDO instance will be
699 528
     * returned by this method.
700
     *
701 528
     * @param bool $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
702
     *
703 528
     * @throws Exception
704 525
     * @throws InvalidConfigException
705
     *
706
     * @return PDO the PDO instance for the currently active slave connection. `null` is returned if no slave connection
707 4
     * is available and `$fallbackToMaster` is false.
708
     */
709
    public function getSlavePdo(bool $fallbackToMaster = true): PDO
710 69
    {
711
        $db = $this->getSlave(false);
712 69
713
        if ($db === null) {
714
            return $fallbackToMaster ? $this->getMasterPdo() : null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fallbackToMaster...->getMasterPdo() : null could return the type null which is incompatible with the type-hinted return PDO. Consider adding an additional type-check to rule them out.
Loading history...
715
        }
716
717
        return $db->getPdo();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $db->getPdo() could return the type null which is incompatible with the type-hinted return PDO. Consider adding an additional type-check to rule them out.
Loading history...
718
    }
719
720
    public function getTablePrefix(): string
721
    {
722
        return $this->tablePrefix;
723
    }
724
725
    /**
726
     * Obtains the schema information for the named table.
727 53
     *
728
     * @param string $name table name.
729 53
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
730
     *
731
     * @throws Exception
732
     * @throws InvalidConfigException
733
     * @throws NotSupportedException
734
     *
735
     * @return TableSchema
736
     */
737 504
    public function getTableSchema($name, $refresh = false): ?TableSchema
738
    {
739 504
        return $this->getSchema()->getTableSchema($name, $refresh);
740
    }
741
742 455
    /**
743
     * Returns the currently active transaction.
744 455
     *
745
     * @return Transaction|null the currently active transaction. Null if no active transaction.
746
     */
747
    public function getTransaction(): ?Transaction
748
    {
749
        return $this->transaction && $this->transaction->isActive() ? $this->transaction : null;
750
    }
751
752
    public function getUsername(): ?string
753
    {
754
        return $this->username;
755
    }
756
757
    /**
758
     * Disables query cache temporarily.
759
     *
760
     * Queries performed within the callable will not use query cache at all. For example,
761
     *
762
     * ```php
763
     * $db->cache(function (Connection $db) {
764
     *
765
     *     // ... queries that use query cache ...
766
     *
767
     *     return $db->noCache(function (Connection $db) {
768
     *         // this query will not use query cache
769
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
770
     *     });
771
     * });
772
     * ```
773
     *
774
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache. The signature
775 6
     * of the callable is `function (Connection $db)`.
776
     *
777 6
     * @throws \Throwable if there is any exception during query
778
     *
779
     * @return mixed the return result of the callable
780 6
     *
781 6
     * {@see enableQueryCache}
782
     * {@see queryCache}
783 6
     * {@see cache()}
784
     */
785
    public function noCache(callable $callable)
786
    {
787
        $this->queryCacheInfo[] = false;
788
789
        try {
790
            $result = $callable($this);
791
            array_pop($this->queryCacheInfo);
792
793
            return $result;
794
        } catch (\Throwable $e) {
795
            array_pop($this->queryCacheInfo);
796
797
            throw $e;
798
        }
799 1124
    }
800
801 1124
    /**
802 538
     * Establishes a DB connection.
803
     *
804
     * It does nothing if a DB connection has already been established.
805 1124
     *
806 7
     * @throws Exception if connection fails
807
     * @throws InvalidArgumentException
808 7
     */
809 7
    public function open()
810
    {
811 7
        if (!empty($this->pdo)) {
812
            return null;
813
        }
814 6
815
        if (!empty($this->masters)) {
816
            $db = $this->getMaster();
817 1124
818
            if ($db !== null) {
819
                $this->pdo = $db->getPDO();
820
821 1124
                return null;
822
            }
823
824 1124
            throw new InvalidConfigException('None of the master DB servers is available.');
825
        }
826 1124
827 1124
        if (empty($this->dsn)) {
828
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
829
        }
830 1124
831
        $token = 'Opening DB connection: ' . $this->dsn;
832 1124
833
        try {
834 1124
            if ($this->enableLogging) {
835 1124
                $this->logger->log(LogLevel::INFO, $token);
0 ignored issues
show
Bug introduced by
The method log() 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

835
                $this->logger->/** @scrutinizer ignore-call */ 
836
                               log(LogLevel::INFO, $token);

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...
836
            }
837 9
838 9
            if ($this->enableProfiling) {
839 9
                $this->profiler->begin($token, [__METHOD__]);
0 ignored issues
show
Bug introduced by
The method begin() 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

839
                $this->profiler->/** @scrutinizer ignore-call */ 
840
                                 begin($token, [__METHOD__]);

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...
840 9
            }
841
842
            $this->pdo = $this->createPdoInstance();
843 9
844
            $this->initConnection();
845 1124
846
            if ($this->enableProfiling) {
847
                $this->profiler->end($token, [__METHOD__]);
848
            }
849
        } catch (\PDOException $e) {
850
            if ($this->enableProfiling) {
851
                $this->profiler->end($token, [__METHOD__]);
852 1145
            }
853
854 1145
            if ($this->enableLogging) {
855 9
                $this->logger->log(LogLevel::ERROR, $token);
856 7
            }
857
858
            throw new Exception($e->getMessage(), $e->errorInfo, (string) $e->getCode(), $e);
859 9
        }
860
    }
861 9
862
    /**
863
     * Closes the currently active DB connection.
864 1145
     *
865 738
     * It does nothing if the connection is already closed.
866
     */
867 738
    public function close(): void
868 738
    {
869 738
        if ($this->master) {
870
            if ($this->pdo === $this->master->getPDO()) {
871
                $this->pdo = null;
872 1145
            }
873 6
874 6
            $this->master->close();
875
876 1145
            $this->master = null;
877
        }
878
879
        if ($this->pdo !== null) {
880
            if ($this->enableLogging) {
881
                $this->logger->log(LogLevel::DEBUG, 'Closing DB connection: ' . $this->dsn . ' ' . __METHOD__);
882
            }
883
884
            $this->pdo = null;
885
            $this->schema = null;
886 1124
            $this->transaction = null;
887
        }
888 1124
889
        if ($this->slave) {
890 1124
            $this->slave->close();
891 1124
            $this->slave = null;
892
        }
893 1124
    }
894 64
895 1064
    /**
896 1064
     * Creates the PDO instance.
897
     *
898
     * This method is called by {@see open} to establish a DB connection. The default implementation will create a PHP
899 1124
     * PDO instance. You may override this method if the default PDO needs to be adapted for certain DBMS.
900 1124
     *
901
     * @return PDO the pdo instance
902 1124
     */
903
    protected function createPdoInstance(): PDO
904
    {
905
        $pdoClass = $this->pdoClass;
906
907
        if ($pdoClass === null) {
908 1124
            $pdoClass = 'PDO';
909
910 1124
            if ($this->driverName !== null) {
911
                $driver = $this->driverName;
912
            } elseif (($pos = strpos($this->dsn, ':')) !== false) {
913
                $driver = strtolower(substr($this->dsn, 0, $pos));
914 1124
            }
915
916
            if (isset($driver)) {
917
                if ($driver === 'mssql' || $driver === 'dblib') {
918
                    $pdoClass = \Yiisoft\Db\Mssql\Pdo\PDO::class;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Mssql\Pdo\PDO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
919
                } elseif ($driver === 'sqlsrv') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $driver does not seem to be defined for all execution paths leading up to this point.
Loading history...
920
                    $pdoClass = \Yiisoft\Db\Mssql\Pdo\SqlsrvPDO::class;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Mssql\Pdo\SqlsrvPDO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
921
                }
922
            }
923 1124
        }
924
925 1124
        $dsn = $this->dsn;
926
927 1124
        if (strncmp('sqlite:@', $dsn, 8) === 0) {
928
            $dsn = 'sqlite:' . substr($dsn, 7);
929
        }
930
931 1124
        return new $pdoClass($dsn, $this->username, $this->password, $this->attributes);
932
    }
933
934
    /**
935
     * Initializes the DB connection.
936 1124
     *
937
     * This method is invoked right after the DB connection is established. The default implementation turns on
938
     * `PDO::ATTR_EMULATE_PREPARES` if {@see emulatePrepare} is true. It then triggers an {@see EVENT_AFTER_OPEN} event.
939
     */
940
    protected function initConnection(): void
941
    {
942
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
0 ignored issues
show
Bug introduced by
The method setAttribute() 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

942
        $this->pdo->/** @scrutinizer ignore-call */ 
943
                    setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

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...
943
944
        if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) {
945
            $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare);
946
        }
947
948 3
        if (
949
            $this->charset !== null &&
950 3
            in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid', 'mariadb'], true)
951
        ) {
952
            $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset));
953 3
        }
954
955
        //$this->trigger(self::EVENT_AFTER_OPEN);
956
    }
957
958
    /**
959 3
     * Rolls back given {@see Transaction} object if it's still active and level match. In some cases rollback can fail,
960
     * so this method is fail safe. Exceptions thrown from rollback will be caught and just logged with
961
     * {@see logger->log()}.
962
     *
963
     * @param Transaction $transaction Transaction object given from {@see beginTransaction()}.
964
     * @param int $level Transaction level just after {@see beginTransaction()} call.
965
     *
966
     * @return void
967
     */
968
    private function rollbackTransactionOnLevel(Transaction $transaction, int $level): void
969
    {
970
        if ($transaction->isActive() && $transaction->getLevel() === $level) {
971
            /* https://github.com/yiisoft/yii2/pull/13347 */
972
            try {
973
                $transaction->rollBack();
974
            } catch (Exception $e) {
975 533
                $this->logger->log(LogLevel::ERROR, $e, [__METHOD__]);
976
                /* hide this exception to be able to continue throwing original exception outside */
977 533
            }
978
        }
979 533
    }
980
981
    /**
982
     * Opens the connection to a server in the pool.
983
     *
984
     * This method implements the load balancing among the given list of the servers.
985
     *
986
     * Connections will be tried in random order.
987
     *
988
     * @param array $pool the list of connection configurations in the server pool
989
     * @param array $sharedConfig the configuration common to those given in `$pool`.
990
     *
991
     * @throws InvalidConfigException
992
     *
993
     * @return Connection|null the opened DB connection, or `null` if no server is available
994
     */
995 536
    protected function openFromPool(array $pool): ?Connection
996
    {
997 536
        shuffle($pool);
998 525
999
        return $this->openFromPoolSequentially($pool);
1000
    }
1001 12
1002 12
    /**
1003
     * Opens the connection to a server in the pool.
1004
     *
1005
     * This method implements the load balancing among the given list of the servers.
1006 12
     *
1007
     * Connections will be tried in sequential order.
1008 12
     *
1009
     * @param array $pool
1010 12
     *
1011
     * @throws InvalidConfigException if a configuration does not specify "dsn"
1012 12
     *
1013
     * @return Connection|null the opened DB connection, or `null` if no server is available
1014 12
     */
1015
    protected function openFromPoolSequentially(array $pool): ?Connection
1016 12
    {
1017 12
        if (!$pool) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pool of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1018 12
            return null;
1019 12
        }
1020
1021
        foreach ($pool as $config) {
1022
            /* @var $db Connection */
1023
            $db = DatabaseFactory::createClass($config);
1024
1025
            $key = [__METHOD__, $db->getDsn()];
1026 12
1027
            if ($this->schemaCache instanceof CacheInterface && $this->schemaCache->get($key)) {
0 ignored issues
show
Bug introduced by
$key 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

1027
            if ($this->schemaCache instanceof CacheInterface && $this->schemaCache->get(/** @scrutinizer ignore-type */ $key)) {
Loading history...
1028 12
                // should not try this dead server now
1029
                continue;
1030
            }
1031
1032
            try {
1033
                $db->open();
1034 12
1035
                return $db;
1036 12
            } catch (Exception $e) {
1037 6
                if ($this->enableLogging) {
1038 6
                    $this->logger->log(
1039 6
                        LogLevel::WARNING,
1040 6
                        "Connection ({$db->getDsn()}) failed: " . $e->getMessage() . ' ' . __METHOD__
1041
                    );
1042
                }
1043 6
1044
                if ($this->schemaCache instanceof CacheInterface) {
1045 3
                    // mark this server as dead and only retry it after the specified interval
1046
                    $this->schemaCache->set($key, 1, $this->serverRetryInterval);
1047
                }
1048 6
1049
                return null;
1050
            }
1051
        }
1052
    }
1053
1054
    /**
1055
     * Quotes a column name for use in a query.
1056
     *
1057
     * If the column name contains prefix, the prefix will also be properly quoted.
1058
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{', then this
1059
     * method will do nothing.
1060
     *
1061
     * @param string $name column name
1062
     *
1063
     * @throws Exception
1064
     * @throws InvalidConfigException
1065
     * @throws NotSupportedException
1066
     * @throws InvalidArgumentException
1067
     *
1068
     * @return string the properly quoted column name
1069 510
     */
1070
    public function quoteColumnName(string $name): string
1071 510
    {
1072 187
        if (isset($this->quotedColumnNames[$name])) {
1073
            return $this->quotedColumnNames[$name];
1074
        }
1075 510
1076
        return $this->quotedColumnNames[$name] = $this->getSchema()->quoteColumnName($name);
1077
    }
1078
1079
    /**
1080
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
1081
     *
1082
     * Tokens enclosed within double curly brackets are treated as table names, while tokens enclosed within double
1083
     * square brackets are column names. They will be quoted accordingly. Also, the percentage character "%" at the
1084
     * beginning or ending of a table name will be replaced with {@see tablePrefix}.
1085
     *
1086
     * @param string $sql the SQL to be quoted
1087
     *
1088
     * @return string the quoted SQL
1089 567
     */
1090
    public function quoteSql(string $sql): string
1091 567
    {
1092 567
        return preg_replace_callback(
1093 567
            '/(\\{\\{(%?[\w\-\. ]+%?)}}|\\[\\[([\w\-\. ]+)]])/',
1094 172
            function ($matches) {
1095 142
                if (isset($matches[3])) {
1096
                    return $this->quoteColumnName($matches[3]);
1097
                }
1098 161
1099 567
                return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
1100
            },
1101
            $sql
1102
        );
1103
    }
1104
1105
    /**
1106
     * Quotes a table name for use in a query.
1107
     *
1108
     * If the table name contains schema prefix, the prefix will also be properly quoted.
1109
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{', then this method
1110
     * will do nothing.
1111
     *
1112
     * @param string $name table name
1113
     *
1114
     * @return string the properly quoted table name
1115
     *
1116
     * @throws Exception
1117
     * @throws InvalidConfigException
1118
     * @throws NotSupportedException
1119
     * @throws InvalidArgumentException
1120 395
     */
1121
    public function quoteTableName(string $name): string
1122 395
    {
1123 194
        if (isset($this->quotedTableNames[$name])) {
1124
            return $this->quotedTableNames[$name];
1125
        }
1126 395
1127
        return $this->quotedTableNames[$name] = $this->getSchema()->quoteTableName($name);
1128
    }
1129
1130
    /**
1131
     * Quotes a string value for use in a query.
1132
     *
1133
     * Note that if the parameter is not a string, it will be returned without change.
1134
     *
1135
     * @param string|int $value string to be quoted
1136
     *
1137
     * @throws Exception
1138
     * @throws InvalidConfigException
1139
     * @throws NotSupportedException
1140
     *
1141
     * @return string|int the properly quoted string
1142
     *
1143
     * {@see http://php.net/manual/en/pdo.quote.php}
1144 318
     */
1145
    public function quoteValue($value)
1146 318
    {
1147
        return $this->getSchema()->quoteValue($value);
1148
    }
1149
1150
    /**
1151
     * PDO attributes (name => value) that should be set when calling {@see open()} to establish a DB connection.
1152
     * Please refer to the [PHP manual](http://php.net/manual/en/pdo.setattribute.php) for details about available
1153
     * attributes.
1154
     *
1155
     * @param array $value
1156
     *
1157
     * @return void
1158
     */
1159
    public function setAttributes(array $value): void
1160
    {
1161
        $this->attributes = $value;
1162
    }
1163
1164
    /**
1165
     * The charset used for database connection. The property is only used for MySQL, PostgreSQL databases. Defaults to
1166
     * null, meaning using default charset as configured by the database.
1167
     *
1168
     * For Oracle Database, the charset must be specified in the {@see dsn}, for example for UTF-8 by appending
1169
     * `;charset=UTF-8` to the DSN string.
1170
     *
1171
     * The same applies for if you're using GBK or BIG5 charset with MySQL, then it's highly recommended to specify
1172
     * charset via {@see dsn} like `'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'`.
1173
     *
1174
     * @param string $value
1175
     *
1176
     * @return void
1177
     */
1178
    public function setCharset(?string $value): void
1179
    {
1180
        $this->charset = $value;
1181
    }
1182
1183
    /**
1184
     * Changes the current driver name.
1185
     *
1186
     * @param string $driverName name of the DB driver
1187
     */
1188
    public function setDriverName(string $driverName): void
1189
    {
1190
        $this->driverName = strtolower($driverName);
1191
    }
1192
1193
    /**
1194
     * Whether to turn on prepare emulation. Defaults to false, meaning PDO will use the native prepare support if
1195
     * available. For some databases (such as MySQL), this may need to be set true so that PDO can emulate the prepare
1196
     * support to bypass the buggy native prepare support. The default value is null, which means the PDO
1197
     * ATTR_EMULATE_PREPARES value will not be changed.
1198
     *
1199
     * @param bool $value
1200
     *
1201
     * @return void
1202 2
     */
1203
    public function setEmulatePrepare(bool $value): void
1204 2
    {
1205 2
        $this->emulatePrepare = $value;
1206
    }
1207
1208
    /**
1209
     * Whether to enable logging of database queries. Defaults to true. You may want to disable this option in a
1210
     * production environment to gain performance if you do not need the information being logged.
1211
     *
1212
     * @param bool $value
1213
     *
1214
     * @return void
1215
     *
1216
     * {@see setEnableProfiling()}
1217 2
     */
1218
    public function setEnableLogging(bool $value): void
1219 2
    {
1220 2
        $this->enableLogging = $value;
1221
    }
1222
1223
    /**
1224
     * Whether to enable profiling of opening database connection and database queries. Defaults to true. You may want
1225
     * to disable this option in a production environment to gain performance if you do not need the information being
1226
     * logged.
1227
     *
1228
     * @param bool $value
1229
     *
1230
     * @return void
1231
     *
1232
     * {@see setEnableLogging()}
1233 2
     */
1234
    public function setEnableProfiling(bool $value): void
1235 2
    {
1236 2
        $this->enableProfiling = $value;
1237
    }
1238
1239
    /**
1240
     * Whether to enable query caching. Note that in order to enable query caching, a valid cache component as specified
1241
     * by {@see setQueryCache()} must be enabled and {@see enableQueryCache} must be set true. Also, only the results of
1242
     * the queries enclosed within {@see cache()} will be cached.
1243
     *
1244
     * @param bool $value
1245
     *
1246
     * @return void
1247
     *
1248
     * {@see setQueryCache()}
1249
     * {@see cache()}
1250
     * {@see noCache()}
1251 6
     */
1252
    public function setEnableQueryCache(bool $value): void
1253 6
    {
1254 6
        $this->enableQueryCache = $value;
1255
    }
1256
1257
    /**
1258
     * Whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint). Note that if the underlying DBMS does not
1259
     * support savepoint, setting this property to be true will have no effect.
1260
     *
1261
     * @param bool $value
1262
     *
1263
     * @return void
1264 3
     */
1265
    public function setEnableSavepoint(bool $value): void
1266 3
    {
1267 3
        $this->enableSavepoint = $value;
1268
    }
1269
1270
    /**
1271
     * Whether to enable schema caching. Note that in order to enable truly schema caching, a valid cache component as
1272
     * specified by {@see setSchemaCache()} must be enabled and {@see setEnableSchemaCache()} must be set true.
1273
     *
1274
     * @param bool $value
1275
     *
1276
     * @return void
1277
     *
1278
     * {@see setSchemaCacheDuration()}
1279
     * {@see setSchemaCacheExclude()}
1280
     * {@see setSchemaCache()}
1281 24
     */
1282
    public function setEnableSchemaCache(bool $value): void
1283 24
    {
1284 24
        $this->enableSchemaCache = $value;
1285
    }
1286
1287
    /**
1288
     * Whether to enable read/write splitting by using {@see setSlaves()} to read data. Note that if {@see setSlaves()}
1289
     * is empty, read/write splitting will NOT be enabled no matter what value this property takes.
1290
     *
1291
     * @param bool $value
1292
     *
1293
     * @return void
1294
     */
1295
    public function setEnableSlaves(bool $value): void
1296
    {
1297
        $this->enableSlaves = $value;
1298
    }
1299
1300
    /**
1301
     * List of master connection. Each DSN is used to create a master DB connection. When {@see open()} is called, one
1302
     * of these configurations will be chosen and used to create a DB connection which will be used by this object.
1303
     *
1304
     * @param string $key index master connection.
1305
     * @param string $dsn the Data Source Name, or DSN, contains the information required to connect to the database
1306
     * @param array $config The configuration that should be merged with every master configuration
1307
     *
1308
     * @return void
1309
     *
1310
     * For example,
1311
     *
1312
     * ```php
1313
     * $connection->setMasters(
1314
     *     '1',
1315
     *     [
1316
     *         '__construct()' => ['mysql:host=127.0.0.1;dbname=yiitest;port=3306'],
1317
     *         'setUsername()' => [$connection->getUsername()],
1318
     *         'setPassword()' => [$connection->getPassword()],
1319
     *     ]
1320
     * );
1321
     * ```
1322
     *
1323
     * {@see setShuffleMasters()}
1324 10
     */
1325
    public function setMasters(string $key, array $config = []): void
1326 10
    {
1327 10
        $this->masters[$key] = $config;
1328
    }
1329
1330
    /**
1331
     * The password for establishing DB connection. Defaults to `null` meaning no password to use.
1332
     *
1333
     * @param string|null $value
1334
     *
1335
     * @return void
1336 795
     */
1337
    public function setPassword(?string $value): void
1338 795
    {
1339 795
        $this->password = $value;
1340
    }
1341
1342
    /**
1343
     * Can be used to set {@see QueryBuilder} configuration via Connection configuration array.
1344
     *
1345
     * @throws Exception
1346
     * @throws InvalidConfigException
1347
     * @throws NotSupportedException
1348
     *
1349
     * @param iterable $config the {@see QueryBuilder} properties to be configured.
1350
     *
1351
     * @return void
1352
     */
1353
    public function setQueryBuilder(iterable $config): void
1354
    {
1355
        $builder = $this->getQueryBuilder();
1356
1357
        foreach ($config as $key => $value) {
1358
            $builder->{$key} = $value;
1359
        }
1360
    }
1361
1362
    /**
1363
     * The cache object or the ID of the cache application component that is used for query caching.
1364
     *
1365
     * @param CacheInterface $value
1366
     *
1367
     * @return void
1368
     *
1369
     * {@see setEnableQueryCache()}
1370 6
     */
1371
    public function setQueryCache(CacheInterface $value): void
1372 6
    {
1373 6
        $this->queryCache = $value;
1374
    }
1375
1376
    /**
1377
     * The default number of seconds that query results can remain valid in cache. Defaults to 3600, meaning 3600
1378
     * seconds, or one hour. Use 0 to indicate that the cached data will never expire. The value of this property will
1379
     * be used when {@see cache()} is called without a cache duration.
1380
     *
1381
     * @param int $value
1382
     *
1383
     * @return void
1384
     *
1385
     * {@see setEnableQueryCache()}
1386
     * {@see cache()}
1387
     */
1388
    public function setQueryCacheDuration(int $value): void
1389
    {
1390
        $this->queryCacheDuration = $value;
1391
    }
1392
1393
    /**
1394
     * The cache object or the ID of the cache application component that is used to cache the table metadata.
1395
     *
1396
     * @param $value
1397
     *
1398
     * @return void
1399
     *
1400
     * {@see setEnableSchemaCache()}
1401 27
     */
1402
    public function setSchemaCache(?CacheInterface $value): void
1403 27
    {
1404 27
        $this->schemaCache = $value;
1405
    }
1406
1407
    /**
1408
     * Number of seconds that table metadata can remain valid in cache. Use 0 to indicate that the cached data will
1409
     * never expire.
1410
     *
1411
     * @param int $value
1412
     *
1413
     * @return void
1414
     *
1415
     * {@see setEnableSchemaCache()}
1416
     */
1417
    public function setSchemaCacheDuration(int $value): void
1418
    {
1419
        $this->schemaCacheDuration = $value;
1420
    }
1421
1422
    /**
1423
     * List of tables whose metadata should NOT be cached. Defaults to empty array. The table names may contain schema
1424
     * prefix, if any. Do not quote the table names.
1425
     *
1426
     * @param array $value
1427
     *
1428
     * @return void
1429
     *
1430
     * {@see setEnableSchemaCache()}
1431
     */
1432
    public function setSchemaCacheExclude(array $value): void
1433
    {
1434
        $this->schemaCacheExclude = $value;
1435
    }
1436
1437
    /**
1438
     * The retry interval in seconds for dead servers listed in {@see setMasters()} and {@see setSlaves()}.
1439
     *
1440
     * @param int $value
1441
     *
1442
     * @return void
1443
     */
1444
    public function setServerRetryInterval(int $value): void
1445
    {
1446
        $this->serverRetryInterval = $value;
1447
    }
1448
1449
    /**
1450
     * Whether to shuffle {@see setMasters()} before getting one.
1451
     *
1452
     * @param bool $value
1453
     *
1454
     * @return void
1455
     *
1456
     * {@see setMasters()}
1457 8
     */
1458
    public function setShuffleMasters(bool $value): void
1459 8
    {
1460 8
        $this->shuffleMasters = $value;
1461
    }
1462
1463
    /**
1464
     * List of slave connection. Each DSN is used to create a slave DB connection. When {@see enableSlaves} is true,
1465
     * one of these configurations will be chosen and used to create a DB connection for performing read queries only.
1466
     *
1467
     * @param string $key index slave connection.
1468
     * @param string $dsn the Data Source Name, or DSN, contains the information required to connect to the database
1469
     * @param array $config The configuration that should be merged with every slave configuration
1470
     *
1471
     * @return void
1472
     *
1473
     * For example,
1474
     *
1475
     * ```php
1476
     * $connection->setSlaves(
1477
     *     '1',
1478
     *     [
1479
     *         '__construct()' => ['mysql:host=127.0.0.1;dbname=yiitest;port=3306'],
1480
     *         'setUsername()' => [$connection->getUsername()],
1481
     *         'setPassword()' => [$connection->getPassword()]
1482
     *     ]
1483
     * );
1484
     * ```
1485
     *
1486
     * {@see setEnableSlaves()}
1487 7
     */
1488
    public function setSlaves(string $key, array $config = []): void
1489 7
    {
1490 7
        $this->slaves[$key] = $config;
1491
    }
1492
1493
    /**
1494
     * The common prefix or suffix for table names. If a table name is given as `{{%TableName}}`, then the percentage
1495
     * character `%` will be replaced with this property value. For example, `{{%post}}` becomes `{{tbl_post}}`.
1496
     *
1497
     * @param string $value
1498
     *
1499
     * @return void
1500 18
     */
1501
    public function setTablePrefix(string $value): void
1502 18
    {
1503 18
        $this->tablePrefix = $value;
1504
    }
1505
1506
    /**
1507
     * The username for establishing DB connection. Defaults to `null` meaning no username to use.
1508
     *
1509
     * @param string|null $value
1510
     *
1511
     * @return void
1512 795
     */
1513
    public function setUsername(?string $value): void
1514 795
    {
1515 795
        $this->username = $value;
1516
    }
1517
1518
    /**
1519
     * Executes callback provided in a transaction.
1520
     *
1521
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
1522
     * @param string|null $isolationLevel The isolation level to use for this transaction. {@see Transaction::begin()}
1523
     * for details.
1524
     *
1525
     * @throws \Throwable if there is any exception during query. In this case the transaction will be rolled back.
1526
     *
1527
     * @return mixed result of callback function
1528 15
     */
1529
    public function transaction(callable $callback, $isolationLevel = null)
1530 15
    {
1531
        $transaction = $this->beginTransaction($isolationLevel);
1532 15
1533
        $level = $transaction->getLevel();
1534
1535 15
        try {
1536
            $result = $callback($this);
1537 12
1538 12
            if ($transaction->isActive() && $transaction->getLevel() === $level) {
1539
                $transaction->commit();
1540 3
            }
1541 3
        } catch (\Throwable $e) {
1542
            $this->rollbackTransactionOnLevel($transaction, $level);
1543 3
1544
            throw $e;
1545
        }
1546 12
1547
        return $result;
1548
    }
1549
1550
    /**
1551
     * Executes the provided callback by using the master connection.
1552
     *
1553
     * This method is provided so that you can temporarily force using the master connection to perform DB operations
1554
     * even if they are read queries. For example,
1555
     *
1556
     * ```php
1557
     * $result = $db->useMaster(function ($db) {
1558
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
1559
     * });
1560
     * ```
1561
     *
1562
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
1563
     * `function (Connection $db)`. Its return value will be returned by this method.
1564
     *
1565
     * @throws \Throwable if there is any exception thrown from the callback
1566
     *
1567
     * @return mixed the return value of the callback
1568 3
     */
1569
    public function useMaster(callable $callback)
1570 3
    {
1571 3
        if ($this->enableSlaves) {
1572
            $this->enableSlaves = false;
1573
1574 3
            try {
1575 1
                $result = $callback($this);
1576 1
            } catch (\Throwable $e) {
1577
                $this->enableSlaves = true;
1578 1
1579
                throw $e;
1580 2
            }
1581
            $this->enableSlaves = true;
1582
        } else {
1583
            $result = $callback($this);
1584
        }
1585 2
1586
        return $result;
1587
    }
1588
}
1589