Passed
Pull Request — master (#19371)
by Wilmer
19:12
created

Connection   F

Complexity

Total Complexity 126

Size/Duplication

Total Lines 1151
Duplicated Lines 0 %

Test Coverage

Coverage 85.33%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 335
dl 0
loc 1151
ccs 256
cts 300
cp 0.8533
rs 2
c 1
b 0
f 0
wmc 126

35 Methods

Rating   Name   Duplication   Size   Complexity  
A getIsActive() 0 3 1
A quoteValue() 0 3 1
A restoreQueryBuilderConfiguration() 0 10 3
A quoteColumnName() 0 6 2
A getMasterPdo() 0 4 1
B initConnection() 0 20 10
A transaction() 0 19 5
C openFromPoolSequentially() 0 64 13
A getQueryBuilder() 0 3 1
A quoteSql() 0 12 2
A getLastInsertID() 0 3 1
A getSlavePdo() 0 8 3
A getSchema() 0 18 4
A getDriverName() 0 11 3
A rollbackTransactionOnLevel() 0 8 4
A quoteTableName() 0 6 2
A noCache() 0 13 3
A getMaster() 0 9 3
B getQueryCacheInfo() 0 28 10
A getTableSchema() 0 3 1
A useMaster() 0 20 4
A getSlave() 0 11 6
A getTransaction() 0 3 3
A __sleep() 0 11 1
A beginTransaction() 0 10 2
A getServerVersion() 0 3 1
A __clone() 0 11 2
A setQueryBuilder() 0 4 1
A cache() 0 13 4
A close() 0 27 5
A openFromPool() 0 4 1
A createCommand() 0 14 4
A setDriverName() 0 3 1
B createPdoInstance() 0 31 8
B open() 0 43 10

How to fix   Complexity   

Complex Class

Complex classes like Connection 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 Connection, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use PDO;
11
use Yii;
12
use yii\base\Component;
13
use yii\base\InvalidConfigException;
14
use yii\base\NotSupportedException;
15
use yii\caching\CacheInterface;
16
17
/**
18
 * Connection represents a connection to a database via [PDO](https://www.php.net/manual/en/book.pdo.php).
19
 *
20
 * Connection works together with [[Command]], [[DataReader]] and [[Transaction]]
21
 * to provide data access to various DBMS in a common set of APIs. They are a thin wrapper
22
 * of the [PDO PHP extension](https://www.php.net/manual/en/book.pdo.php).
23
 *
24
 * Connection supports database replication and read-write splitting. In particular, a Connection component
25
 * can be configured with multiple [[masters]] and [[slaves]]. It will do load balancing and failover by choosing
26
 * appropriate servers. It will also automatically direct read operations to the slaves and write operations to
27
 * the masters.
28
 *
29
 * To establish a DB connection, set [[dsn]], [[username]] and [[password]], and then
30
 * call [[open()]] to connect to the database server. The current state of the connection can be checked using [[$isActive]].
31
 *
32
 * The following example shows how to create a Connection instance and establish
33
 * the DB connection:
34
 *
35
 * ```php
36
 * $connection = new \yii\db\Connection([
37
 *     'dsn' => $dsn,
38
 *     'username' => $username,
39
 *     'password' => $password,
40
 * ]);
41
 * $connection->open();
42
 * ```
43
 *
44
 * After the DB connection is established, one can execute SQL statements like the following:
45
 *
46
 * ```php
47
 * $command = $connection->createCommand('SELECT * FROM post');
48
 * $posts = $command->queryAll();
49
 * $command = $connection->createCommand('UPDATE post SET status=1');
50
 * $command->execute();
51
 * ```
52
 *
53
 * One can also do prepared SQL execution and bind parameters to the prepared SQL.
54
 * When the parameters are coming from user input, you should use this approach
55
 * to prevent SQL injection attacks. The following is an example:
56
 *
57
 * ```php
58
 * $command = $connection->createCommand('SELECT * FROM post WHERE id=:id');
59
 * $command->bindValue(':id', $_GET['id']);
60
 * $post = $command->query();
61
 * ```
62
 *
63
 * For more information about how to perform various DB queries, please refer to [[Command]].
64
 *
65
 * If the underlying DBMS supports transactions, you can perform transactional SQL queries
66
 * like the following:
67
 *
68
 * ```php
69
 * $transaction = $connection->beginTransaction();
70
 * try {
71
 *     $connection->createCommand($sql1)->execute();
72
 *     $connection->createCommand($sql2)->execute();
73
 *     // ... executing other SQL statements ...
74
 *     $transaction->commit();
75
 * } catch (Exception $e) {
76
 *     $transaction->rollBack();
77
 * }
78
 * ```
79
 *
80
 * You also can use shortcut for the above like the following:
81
 *
82
 * ```php
83
 * $connection->transaction(function () {
84
 *     $order = new Order($customer);
85
 *     $order->save();
86
 *     $order->addItems($items);
87
 * });
88
 * ```
89
 *
90
 * If needed you can pass transaction isolation level as a second parameter:
91
 *
92
 * ```php
93
 * $connection->transaction(function (Connection $db) {
94
 *     //return $db->...
95
 * }, Transaction::READ_UNCOMMITTED);
96
 * ```
97
 *
98
 * Connection is often used as an application component and configured in the application
99
 * configuration like the following:
100
 *
101
 * ```php
102
 * 'components' => [
103
 *     'db' => [
104
 *         'class' => '\yii\db\Connection',
105
 *         'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
106
 *         'username' => 'root',
107
 *         'password' => '',
108
 *         'charset' => 'utf8',
109
 *     ],
110
 * ],
111
 * ```
112
 *
113
 * @property string $driverName Name of the DB driver.
114
 * @property-read bool $isActive Whether the DB connection is established.
115
 * @property-read string $lastInsertID The row ID of the last row inserted, or the last value retrieved from
116
 * the sequence object.
117
 * @property-read Connection $master The currently active master connection. `null` is returned if there is no
118
 * master available.
119
 * @property-read PDO $masterPdo The PDO instance for the currently active master connection.
120
 * @property QueryBuilder $queryBuilder The query builder for the current DB connection. Note that the type of
121
 * this property differs in getter and setter. See [[getQueryBuilder()]] and [[setQueryBuilder()]] for details.
122
 * @property-read Schema $schema The schema information for the database opened by this connection.
123
 * @property-read string $serverVersion Server version as a string.
124
 * @property-read Connection $slave The currently active slave connection. `null` is returned if there is no
125
 * slave available and `$fallbackToMaster` is false.
126
 * @property-read PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned
127
 * if no slave connection is available and `$fallbackToMaster` is false.
128
 * @property-read Transaction|null $transaction The currently active transaction. Null if no active
129
 * transaction.
130
 *
131
 * @author Qiang Xue <[email protected]>
132
 * @since 2.0
133
 */
134
class Connection extends Component
135
{
136
    /**
137
     * @event \yii\base\Event an event that is triggered after a DB connection is established
138
     */
139
    const EVENT_AFTER_OPEN = 'afterOpen';
140
    /**
141
     * @event \yii\base\Event an event that is triggered right before a top-level transaction is started
142
     */
143
    const EVENT_BEGIN_TRANSACTION = 'beginTransaction';
144
    /**
145
     * @event \yii\base\Event an event that is triggered right after a top-level transaction is committed
146
     */
147
    const EVENT_COMMIT_TRANSACTION = 'commitTransaction';
148
    /**
149
     * @event \yii\base\Event an event that is triggered right after a top-level transaction is rolled back
150
     */
151
    const EVENT_ROLLBACK_TRANSACTION = 'rollbackTransaction';
152
153
    /**
154
     * @var string the Data Source Name, or DSN, contains the information required to connect to the database.
155
     * Please refer to the [PHP manual](https://www.php.net/manual/en/pdo.construct.php) on
156
     * the format of the DSN string.
157
     *
158
     * For [SQLite](https://www.php.net/manual/en/ref.pdo-sqlite.connection.php) you may use a [path alias](guide:concept-aliases)
159
     * for specifying the database path, e.g. `sqlite:@app/data/db.sql`.
160
     *
161
     * @see charset
162
     */
163
    public $dsn;
164
    /**
165
     * @var string|null the username for establishing DB connection. Defaults to `null` meaning no username to use.
166
     */
167
    public $username;
168
    /**
169
     * @var string|null the password for establishing DB connection. Defaults to `null` meaning no password to use.
170
     */
171
    public $password;
172
    /**
173
     * @var array PDO attributes (name => value) that should be set when calling [[open()]]
174
     * to establish a DB connection. Please refer to the
175
     * [PHP manual](https://www.php.net/manual/en/pdo.setattribute.php) for
176
     * details about available attributes.
177
     */
178
    public $attributes;
179
    /**
180
     * @var PDO|null the PHP PDO instance associated with this DB connection.
181
     * This property is mainly managed by [[open()]] and [[close()]] methods.
182
     * When a DB connection is active, this property will represent a PDO instance;
183
     * otherwise, it will be null.
184
     * @see pdoClass
185
     */
186
    public $pdo;
187
    /**
188
     * @var bool whether to enable schema caching.
189
     * Note that in order to enable truly schema caching, a valid cache component as specified
190
     * by [[schemaCache]] must be enabled and [[enableSchemaCache]] must be set true.
191
     * @see schemaCacheDuration
192
     * @see schemaCacheExclude
193
     * @see schemaCache
194
     */
195
    public $enableSchemaCache = false;
196
    /**
197
     * @var int number of seconds that table metadata can remain valid in cache.
198
     * Use 0 to indicate that the cached data will never expire.
199
     * @see enableSchemaCache
200
     */
201
    public $schemaCacheDuration = 3600;
202
    /**
203
     * @var array list of tables whose metadata should NOT be cached. Defaults to empty array.
204
     * The table names may contain schema prefix, if any. Do not quote the table names.
205
     * @see enableSchemaCache
206
     */
207
    public $schemaCacheExclude = [];
208
    /**
209
     * @var CacheInterface|string the cache object or the ID of the cache application component that
210
     * is used to cache the table metadata.
211
     * @see enableSchemaCache
212
     */
213
    public $schemaCache = 'cache';
214
    /**
215
     * @var bool whether to enable query caching.
216
     * Note that in order to enable query caching, a valid cache component as specified
217
     * by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true.
218
     * Also, only the results of the queries enclosed within [[cache()]] will be cached.
219
     * @see queryCache
220
     * @see cache()
221
     * @see noCache()
222
     */
223
    public $enableQueryCache = true;
224
    /**
225
     * @var int the default number of seconds that query results can remain valid in cache.
226
     * Defaults to 3600, meaning 3600 seconds, or one hour. Use 0 to indicate that the cached data will never expire.
227
     * The value of this property will be used when [[cache()]] is called without a cache duration.
228
     * @see enableQueryCache
229
     * @see cache()
230
     */
231
    public $queryCacheDuration = 3600;
232
    /**
233
     * @var CacheInterface|string the cache object or the ID of the cache application component
234
     * that is used for query caching.
235
     * @see enableQueryCache
236
     */
237
    public $queryCache = 'cache';
238
    /**
239
     * @var string|null the charset used for database connection. The property is only used
240
     * for MySQL, PostgreSQL and CUBRID databases. Defaults to null, meaning using default charset
241
     * as configured by the database.
242
     *
243
     * For Oracle Database, the charset must be specified in the [[dsn]], for example for UTF-8 by appending `;charset=UTF-8`
244
     * to the DSN string.
245
     *
246
     * The same applies for if you're using GBK or BIG5 charset with MySQL, then it's highly recommended to
247
     * specify charset via [[dsn]] like `'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'`.
248
     */
249
    public $charset;
250
    /**
251
     * @var bool|null whether to turn on prepare emulation. Defaults to false, meaning PDO
252
     * will use the native prepare support if available. For some databases (such as MySQL),
253
     * this may need to be set true so that PDO can emulate the prepare support to bypass
254
     * the buggy native prepare support.
255
     * The default value is null, which means the PDO ATTR_EMULATE_PREPARES value will not be changed.
256
     */
257
    public $emulatePrepare;
258
    /**
259
     * @var string the common prefix or suffix for table names. If a table name is given
260
     * as `{{%TableName}}`, then the percentage character `%` will be replaced with this
261
     * property value. For example, `{{%post}}` becomes `{{tbl_post}}`.
262
     */
263
    public $tablePrefix = '';
264
    /**
265
     * @var array mapping between PDO driver names and [[Schema]] classes.
266
     * The keys of the array are PDO driver names while the values are either the corresponding
267
     * schema class names or configurations. Please refer to [[Yii::createObject()]] for
268
     * details on how to specify a configuration.
269
     *
270
     * This property is mainly used by [[getSchema()]] when fetching the database schema information.
271
     * You normally do not need to set this property unless you want to use your own
272
     * [[Schema]] class to support DBMS that is not supported by Yii.
273
     */
274
    public $schemaMap = [
275
        'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL
276
        'mysqli' => 'yii\db\mysql\Schema', // MySQL
277
        'mysql' => 'yii\db\mysql\Schema', // MySQL
278
        'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3
279
        'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2
280
        'sqlsrv' => 'yii\db\mssql\Schema', // newer MSSQL driver on MS Windows hosts
281
        'oci' => 'yii\db\oci\Schema', // Oracle driver
282
        'mssql' => 'yii\db\mssql\Schema', // older MSSQL driver on MS Windows hosts
283
        'dblib' => 'yii\db\mssql\Schema', // dblib drivers on GNU/Linux (and maybe other OSes) hosts
284
        'cubrid' => 'yii\db\cubrid\Schema', // CUBRID
285
    ];
286
    /**
287
     * @var string|null Custom PDO wrapper class. If not set, it will use [[PDO]] or [[\yii\db\mssql\PDO]] when MSSQL is used.
288
     * @see pdo
289
     */
290
    public $pdoClass;
291
    /**
292
     * @var string the class used to create new database [[Command]] objects. If you want to extend the [[Command]] class,
293
     * you may configure this property to use your extended version of the class.
294
     * Since version 2.0.14 [[$commandMap]] is used if this property is set to its default value.
295
     * @see createCommand
296
     * @since 2.0.7
297
     * @deprecated since 2.0.14. Use [[$commandMap]] for precise configuration.
298
     */
299
    public $commandClass = 'yii\db\Command';
300
    /**
301
     * @var array mapping between PDO driver names and [[Command]] classes.
302
     * The keys of the array are PDO driver names while the values are either the corresponding
303
     * command class names or configurations. Please refer to [[Yii::createObject()]] for
304
     * details on how to specify a configuration.
305
     *
306
     * This property is mainly used by [[createCommand()]] to create new database [[Command]] objects.
307
     * You normally do not need to set this property unless you want to use your own
308
     * [[Command]] class or support DBMS that is not supported by Yii.
309
     * @since 2.0.14
310
     */
311
    public $commandMap = [
312
        'pgsql' => 'yii\db\Command', // PostgreSQL
313
        'mysqli' => 'yii\db\Command', // MySQL
314
        'mysql' => 'yii\db\Command', // MySQL
315
        'sqlite' => 'yii\db\sqlite\Command', // sqlite 3
316
        'sqlite2' => 'yii\db\sqlite\Command', // sqlite 2
317
        'sqlsrv' => 'yii\db\Command', // newer MSSQL driver on MS Windows hosts
318
        'oci' => 'yii\db\oci\Command', // Oracle driver
319
        'mssql' => 'yii\db\Command', // older MSSQL driver on MS Windows hosts
320
        'dblib' => 'yii\db\Command', // dblib drivers on GNU/Linux (and maybe other OSes) hosts
321
        'cubrid' => 'yii\db\Command', // CUBRID
322
    ];
323
    /**
324
     * @var bool whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint).
325
     * Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect.
326
     */
327
    public $enableSavepoint = true;
328
    /**
329
     * @var CacheInterface|string|false the cache object or the ID of the cache application component that is used to store
330
     * the health status of the DB servers specified in [[masters]] and [[slaves]].
331
     * This is used only when read/write splitting is enabled or [[masters]] is not empty.
332
     * Set boolean `false` to disabled server status caching.
333
     * @see openFromPoolSequentially() for details about the failover behavior.
334
     * @see serverRetryInterval
335
     */
336
    public $serverStatusCache = 'cache';
337
    /**
338
     * @var int the retry interval in seconds for dead servers listed in [[masters]] and [[slaves]].
339
     * This is used together with [[serverStatusCache]].
340
     */
341
    public $serverRetryInterval = 600;
342
    /**
343
     * @var bool whether to enable read/write splitting by using [[slaves]] to read data.
344
     * Note that if [[slaves]] is empty, read/write splitting will NOT be enabled no matter what value this property takes.
345
     */
346
    public $enableSlaves = true;
347
    /**
348
     * @var array list of slave connection configurations. Each configuration is used to create a slave DB connection.
349
     * When [[enableSlaves]] is true, one of these configurations will be chosen and used to create a DB connection
350
     * for performing read queries only.
351
     * @see enableSlaves
352
     * @see slaveConfig
353
     */
354
    public $slaves = [];
355
    /**
356
     * @var array the configuration that should be merged with every slave configuration listed in [[slaves]].
357
     * For example,
358
     *
359
     * ```php
360
     * [
361
     *     'username' => 'slave',
362
     *     'password' => 'slave',
363
     *     'attributes' => [
364
     *         // use a smaller connection timeout
365
     *         PDO::ATTR_TIMEOUT => 10,
366
     *     ],
367
     * ]
368
     * ```
369
     */
370
    public $slaveConfig = [];
371
    /**
372
     * @var array list of master connection configurations. Each configuration is used to create a master DB connection.
373
     * When [[open()]] is called, one of these configurations will be chosen and used to create a DB connection
374
     * which will be used by this object.
375
     * Note that when this property is not empty, the connection setting (e.g. "dsn", "username") of this object will
376
     * be ignored.
377
     * @see masterConfig
378
     * @see shuffleMasters
379
     */
380
    public $masters = [];
381
    /**
382
     * @var array the configuration that should be merged with every master configuration listed in [[masters]].
383
     * For example,
384
     *
385
     * ```php
386
     * [
387
     *     'username' => 'master',
388
     *     'password' => 'master',
389
     *     'attributes' => [
390
     *         // use a smaller connection timeout
391
     *         PDO::ATTR_TIMEOUT => 10,
392
     *     ],
393
     * ]
394
     * ```
395
     */
396
    public $masterConfig = [];
397
    /**
398
     * @var bool whether to shuffle [[masters]] before getting one.
399
     * @since 2.0.11
400
     * @see masters
401
     */
402
    public $shuffleMasters = true;
403
    /**
404
     * @var bool whether to enable logging of database queries. Defaults to true.
405
     * You may want to disable this option in a production environment to gain performance
406
     * if you do not need the information being logged.
407
     * @since 2.0.12
408
     * @see enableProfiling
409
     */
410
    public $enableLogging = true;
411
    /**
412
     * @var bool whether to enable profiling of opening database connection and database queries. Defaults to true.
413
     * You may want to disable this option in a production environment to gain performance
414
     * if you do not need the information being logged.
415
     * @since 2.0.12
416
     * @see enableLogging
417
     */
418
    public $enableProfiling = true;
419
    /**
420
     * @var bool If the database connected via pdo_dblib is SyBase.
421
     * @since 2.0.38
422
     */
423
    public $isSybase = false;
424
425
    /**
426
     * @var array An array of [[setQueryBuilder()]] calls, holding the passed arguments.
427
     * Is used to restore a QueryBuilder configuration after the connection close/open cycle.
428
     *
429
     * @see restoreQueryBuilderConfiguration()
430
     */
431
    private $_queryBuilderConfigurations = [];
432
    /**
433
     * @var Transaction the currently active transaction
434
     */
435
    private $_transaction;
436
    /**
437
     * @var Schema the database schema
438
     */
439
    private $_schema;
440
    /**
441
     * @var string driver name
442
     */
443
    private $_driverName;
444
    /**
445
     * @var Connection|false the currently active master connection
446
     */
447
    private $_master = false;
448
    /**
449
     * @var Connection|false the currently active slave connection
450
     */
451
    private $_slave = false;
452
    /**
453
     * @var array query cache parameters for the [[cache()]] calls
454
     */
455
    private $_queryCacheInfo = [];
456
    /**
457
     * @var string[] quoted table name cache for [[quoteTableName()]] calls
458
     */
459
    private $_quotedTableNames;
460
    /**
461
     * @var string[] quoted column name cache for [[quoteColumnName()]] calls
462
     */
463
    private $_quotedColumnNames;
464
465
466
    /**
467
     * Returns a value indicating whether the DB connection is established.
468
     * @return bool whether the DB connection is established
469
     */
470 334
    public function getIsActive()
471
    {
472 334
        return $this->pdo !== null;
473
    }
474
475
    /**
476
     * Uses query cache for the queries performed with the callable.
477
     *
478
     * When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
479
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
480
     * For example,
481
     *
482
     * ```php
483
     * // The customer will be fetched from cache if available.
484
     * // If not, the query will be made against DB and cached for use next time.
485
     * $customer = $db->cache(function (Connection $db) {
486
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
487
     * });
488
     * ```
489
     *
490
     * Note that query cache is only meaningful for queries that return results. For queries performed with
491
     * [[Command::execute()]], query cache will not be used.
492
     *
493
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
494
     * The signature of the callable is `function (Connection $db)`.
495
     * @param int|null $duration the number of seconds that query results can remain valid in the cache. If this is
496
     * not set, the value of [[queryCacheDuration]] will be used instead.
497
     * Use 0 to indicate that the cached data will never expire.
498
     * @param \yii\caching\Dependency|null $dependency the cache dependency associated with the cached query results.
499
     * @return mixed the return result of the callable
500
     * @throws \Throwable if there is any exception during query
501
     * @see enableQueryCache
502
     * @see queryCache
503
     * @see noCache()
504
     */
505 6
    public function cache(callable $callable, $duration = null, $dependency = null)
506
    {
507 6
        $this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
508
        try {
509 6
            $result = call_user_func($callable, $this);
510 6
            array_pop($this->_queryCacheInfo);
511 6
            return $result;
512
        } catch (\Exception $e) {
513
            array_pop($this->_queryCacheInfo);
514
            throw $e;
515
        } catch (\Throwable $e) {
516
            array_pop($this->_queryCacheInfo);
517
            throw $e;
518
        }
519
    }
520
521
    /**
522
     * Disables query cache temporarily.
523
     *
524
     * Queries performed within the callable will not use query cache at all. For example,
525
     *
526
     * ```php
527
     * $db->cache(function (Connection $db) {
528
     *
529
     *     // ... queries that use query cache ...
530
     *
531
     *     return $db->noCache(function (Connection $db) {
532
     *         // this query will not use query cache
533
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
534
     *     });
535
     * });
536
     * ```
537
     *
538
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache.
539
     * The signature of the callable is `function (Connection $db)`.
540
     * @return mixed the return result of the callable
541
     * @throws \Throwable if there is any exception during query
542
     * @see enableQueryCache
543
     * @see queryCache
544
     * @see cache()
545
     */
546 40
    public function noCache(callable $callable)
547
    {
548 40
        $this->_queryCacheInfo[] = false;
549
        try {
550 40
            $result = call_user_func($callable, $this);
551 40
            array_pop($this->_queryCacheInfo);
552 40
            return $result;
553 4
        } catch (\Exception $e) {
554 4
            array_pop($this->_queryCacheInfo);
555 4
            throw $e;
556
        } catch (\Throwable $e) {
557
            array_pop($this->_queryCacheInfo);
558
            throw $e;
559
        }
560
    }
561
562
    /**
563
     * Returns the current query cache information.
564
     * This method is used internally by [[Command]].
565
     * @param int|null $duration the preferred caching duration. If null, it will be ignored.
566
     * @param \yii\caching\Dependency|null $dependency the preferred caching dependency. If null, it will be ignored.
567
     * @return array|null the current query cache information, or null if query cache is not enabled.
568
     * @internal
569
     */
570 1544
    public function getQueryCacheInfo($duration, $dependency)
571
    {
572 1544
        if (!$this->enableQueryCache) {
573 45
            return null;
574
        }
575
576 1542
        $info = end($this->_queryCacheInfo);
577 1542
        if (is_array($info)) {
578 6
            if ($duration === null) {
579 6
                $duration = $info[0];
580
            }
581 6
            if ($dependency === null) {
582 6
                $dependency = $info[1];
583
            }
584
        }
585
586 1542
        if ($duration === 0 || $duration > 0) {
587 6
            if (is_string($this->queryCache) && Yii::$app) {
588
                $cache = Yii::$app->get($this->queryCache, false);
589
            } else {
590 6
                $cache = $this->queryCache;
591
            }
592 6
            if ($cache instanceof CacheInterface) {
593 6
                return [$cache, $duration, $dependency];
594
            }
595
        }
596
597 1542
        return null;
598
    }
599
600
    /**
601
     * Establishes a DB connection.
602
     * It does nothing if a DB connection has already been established.
603
     * @throws Exception if connection fails
604
     */
605 2096
    public function open()
606
    {
607 2096
        if ($this->pdo !== null) {
608 1658
            return;
609
        }
610
611 2041
        if (!empty($this->masters)) {
612 9
            $db = $this->getMaster();
613 9
            if ($db !== null) {
614 9
                $this->pdo = $db->pdo;
615 9
                return;
616
            }
617
618 8
            throw new InvalidConfigException('None of the master DB servers is available.');
619
        }
620
621 2041
        if (empty($this->dsn)) {
622
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
623
        }
624
625 2041
        $token = 'Opening DB connection: ' . $this->dsn;
626 2041
        $enableProfiling = $this->enableProfiling;
627
        try {
628 2041
            if ($this->enableLogging) {
629 2041
                Yii::info($token, __METHOD__);
630
            }
631
632 2041
            if ($enableProfiling) {
633 2041
                Yii::beginProfile($token, __METHOD__);
634
            }
635
636 2041
            $this->pdo = $this->createPdoInstance();
637 2041
            $this->initConnection();
638
639 2041
            if ($enableProfiling) {
640 2041
                Yii::endProfile($token, __METHOD__);
641
            }
642 12
        } catch (\PDOException $e) {
643 12
            if ($enableProfiling) {
644 12
                Yii::endProfile($token, __METHOD__);
645
            }
646
647 12
            throw new Exception($e->getMessage(), $e->errorInfo, (int) $e->getCode(), $e);
648
        }
649 2041
    }
650
651
    /**
652
     * Closes the currently active DB connection.
653
     * It does nothing if the connection is already closed.
654
     */
655 2186
    public function close()
656
    {
657 2186
        if ($this->_master) {
658 8
            if ($this->pdo === $this->_master->pdo) {
659 8
                $this->pdo = null;
660
            }
661
662 8
            $this->_master->close();
663 8
            $this->_master = false;
664
        }
665
666 2186
        if ($this->pdo !== null) {
667 1831
            Yii::debug('Closing DB connection: ' . $this->dsn, __METHOD__);
668 1831
            $this->pdo = null;
669
        }
670
671 2186
        if ($this->_slave) {
672 4
            $this->_slave->close();
673 4
            $this->_slave = false;
674
        }
675
676 2186
        $this->_schema = null;
677 2186
        $this->_transaction = null;
678 2186
        $this->_driverName = null;
679 2186
        $this->_queryCacheInfo = [];
680 2186
        $this->_quotedTableNames = null;
681 2186
        $this->_quotedColumnNames = null;
682 2186
    }
683
684
    /**
685
     * Creates the PDO instance.
686
     * This method is called by [[open]] to establish a DB connection.
687
     * The default implementation will create a PHP PDO instance.
688
     * You may override this method if the default PDO needs to be adapted for certain DBMS.
689
     * @return PDO the pdo instance
690
     */
691 2041
    protected function createPdoInstance()
692
    {
693 2041
        $pdoClass = $this->pdoClass;
694 2041
        if ($pdoClass === null) {
695 2041
            $driver = null;
696 2041
            if ($this->_driverName !== null) {
697 258
                $driver = $this->_driverName;
698 1792
            } elseif (($pos = strpos($this->dsn, ':')) !== false) {
699 1792
                $driver = strtolower(substr($this->dsn, 0, $pos));
700
            }
701 2041
            switch ($driver) {
702 2041
                case 'mssql':
703
                    $pdoClass = 'yii\db\mssql\PDO';
704
                    break;
705 2041
                case 'dblib':
706
                    $pdoClass = 'yii\db\mssql\DBLibPDO';
707
                    break;
708 2041
                case 'sqlsrv':
709
                    $pdoClass = 'yii\db\mssql\SqlsrvPDO';
710
                    break;
711
                default:
712 2041
                    $pdoClass = 'PDO';
713
            }
714
        }
715
716 2041
        $dsn = $this->dsn;
717 2041
        if (strncmp('sqlite:@', $dsn, 8) === 0) {
718 1
            $dsn = 'sqlite:' . Yii::getAlias(substr($dsn, 7));
0 ignored issues
show
Bug introduced by
Are you sure Yii::getAlias(substr($dsn, 7)) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

718
            $dsn = 'sqlite:' . /** @scrutinizer ignore-type */ Yii::getAlias(substr($dsn, 7));
Loading history...
719
        }
720
721 2041
        return new $pdoClass($dsn, $this->username, $this->password, $this->attributes);
722
    }
723
724
    /**
725
     * Initializes the DB connection.
726
     * This method is invoked right after the DB connection is established.
727
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`
728
     * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty.
729
     * It then triggers an [[EVENT_AFTER_OPEN]] event.
730
     */
731 2041
    protected function initConnection()
732
    {
733 2041
        $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

733
        $this->pdo->/** @scrutinizer ignore-call */ 
734
                    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...
734 2041
        if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) {
735
            if ($this->driverName !== 'sqlsrv') {
736
                $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare);
737
            }
738
        }
739
740 2041
        if (PHP_VERSION_ID >= 80100 && $this->getDriverName() === 'sqlite') {
741
            $this->pdo->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
742
        }
743
744 2041
        if (!$this->isSybase && in_array($this->getDriverName(), ['mssql', 'dblib'], true)) {
745
            $this->pdo->exec('SET ANSI_NULL_DFLT_ON ON');
746
        }
747 2041
        if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'], true)) {
748
            $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset));
749
        }
750 2041
        $this->trigger(self::EVENT_AFTER_OPEN);
751 2041
    }
752
753
    /**
754
     * Creates a command for execution.
755
     * @param string|null $sql the SQL statement to be executed
756
     * @param array $params the parameters to be bound to the SQL statement
757
     * @return Command the DB command
758
     */
759 1646
    public function createCommand($sql = null, $params = [])
760
    {
761 1646
        $driver = $this->getDriverName();
762 1646
        $config = ['class' => 'yii\db\Command'];
763 1646
        if ($this->commandClass !== $config['class']) {
0 ignored issues
show
Deprecated Code introduced by
The property yii\db\Connection::$commandClass has been deprecated: since 2.0.14. Use [[$commandMap]] for precise configuration. ( Ignorable by Annotation )

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

763
        if (/** @scrutinizer ignore-deprecated */ $this->commandClass !== $config['class']) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
764
            $config['class'] = $this->commandClass;
0 ignored issues
show
Deprecated Code introduced by
The property yii\db\Connection::$commandClass has been deprecated: since 2.0.14. Use [[$commandMap]] for precise configuration. ( Ignorable by Annotation )

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

764
            $config['class'] = /** @scrutinizer ignore-deprecated */ $this->commandClass;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
765 1646
        } elseif (isset($this->commandMap[$driver])) {
766 1646
            $config = !is_array($this->commandMap[$driver]) ? ['class' => $this->commandMap[$driver]] : $this->commandMap[$driver];
767
        }
768 1646
        $config['db'] = $this;
769 1646
        $config['sql'] = $sql;
770
        /** @var Command $command */
771 1646
        $command = Yii::createObject($config);
772 1646
        return $command->bindValues($params);
773
    }
774
775
    /**
776
     * Returns the currently active transaction.
777
     * @return Transaction|null the currently active transaction. Null if no active transaction.
778
     */
779 1611
    public function getTransaction()
780
    {
781 1611
        return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
782
    }
783
784
    /**
785
     * Starts a transaction.
786
     * @param string|null $isolationLevel The isolation level to use for this transaction.
787
     * See [[Transaction::begin()]] for details.
788
     * @return Transaction the transaction initiated
789
     */
790 41
    public function beginTransaction($isolationLevel = null)
791
    {
792 41
        $this->open();
793
794 41
        if (($transaction = $this->getTransaction()) === null) {
795 41
            $transaction = $this->_transaction = new Transaction(['db' => $this]);
796
        }
797 41
        $transaction->begin($isolationLevel);
798
799 41
        return $transaction;
800
    }
801
802
    /**
803
     * Executes callback provided in a transaction.
804
     *
805
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
806
     * @param string|null $isolationLevel The isolation level to use for this transaction.
807
     * See [[Transaction::begin()]] for details.
808
     * @throws \Throwable if there is any exception during query. In this case the transaction will be rolled back.
809
     * @return mixed result of callback function
810
     */
811 25
    public function transaction(callable $callback, $isolationLevel = null)
812
    {
813 25
        $transaction = $this->beginTransaction($isolationLevel);
814 25
        $level = $transaction->level;
815
816
        try {
817 25
            $result = call_user_func($callback, $this);
818 17
            if ($transaction->isActive && $transaction->level === $level) {
819 17
                $transaction->commit();
820
            }
821 8
        } catch (\Exception $e) {
822 8
            $this->rollbackTransactionOnLevel($transaction, $level);
823 8
            throw $e;
824
        } catch (\Throwable $e) {
825
            $this->rollbackTransactionOnLevel($transaction, $level);
826
            throw $e;
827
        }
828
829 17
        return $result;
830
    }
831
832
    /**
833
     * Rolls back given [[Transaction]] object if it's still active and level match.
834
     * In some cases rollback can fail, so this method is fail safe. Exception thrown
835
     * from rollback will be caught and just logged with [[\Yii::error()]].
836
     * @param Transaction $transaction Transaction object given from [[beginTransaction()]].
837
     * @param int $level Transaction level just after [[beginTransaction()]] call.
838
     */
839 8
    private function rollbackTransactionOnLevel($transaction, $level)
840
    {
841 8
        if ($transaction->isActive && $transaction->level === $level) {
842
            // https://github.com/yiisoft/yii2/pull/13347
843
            try {
844 8
                $transaction->rollBack();
845
            } catch (\Exception $e) {
846
                \Yii::error($e, __METHOD__);
847
                // hide this exception to be able to continue throwing original exception outside
848
            }
849
        }
850 8
    }
851
852
    /**
853
     * Returns the schema information for the database opened by this connection.
854
     * @return Schema the schema information for the database opened by this connection.
855
     * @throws NotSupportedException if there is no support for the current driver type
856
     */
857 2128
    public function getSchema()
858
    {
859 2128
        if ($this->_schema !== null) {
860 1740
            return $this->_schema;
861
        }
862
863 2073
        $driver = $this->getDriverName();
864 2073
        if (isset($this->schemaMap[$driver])) {
865 2073
            $config = !is_array($this->schemaMap[$driver]) ? ['class' => $this->schemaMap[$driver]] : $this->schemaMap[$driver];
866 2073
            $config['db'] = $this;
867
868 2073
            $this->_schema = Yii::createObject($config);
869 2073
            $this->restoreQueryBuilderConfiguration();
870
871 2073
            return $this->_schema;
872
        }
873
874
        throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS.");
875
    }
876
877
    /**
878
     * Returns the query builder for the current DB connection.
879
     * @return QueryBuilder the query builder for the current DB connection.
880
     */
881 1139
    public function getQueryBuilder()
882
    {
883 1139
        return $this->getSchema()->getQueryBuilder();
884
    }
885
886
    /**
887
     * Can be used to set [[QueryBuilder]] configuration via Connection configuration array.
888
     *
889
     * @param array $value the [[QueryBuilder]] properties to be configured.
890
     * @since 2.0.14
891
     */
892 4
    public function setQueryBuilder($value)
893
    {
894 4
        Yii::configure($this->getQueryBuilder(), $value);
895 4
        $this->_queryBuilderConfigurations[] = $value;
896 4
    }
897
898
    /**
899
     * Restores custom QueryBuilder configuration after the connection close/open cycle
900
     */
901 2073
    private function restoreQueryBuilderConfiguration()
902
    {
903 2073
        if ($this->_queryBuilderConfigurations === []) {
904 2073
            return;
905
        }
906
907 4
        $queryBuilderConfigurations = $this->_queryBuilderConfigurations;
908 4
        $this->_queryBuilderConfigurations = [];
909 4
        foreach ($queryBuilderConfigurations as $queryBuilderConfiguration) {
910 4
            $this->setQueryBuilder($queryBuilderConfiguration);
911
        }
912 4
    }
913
914
    /**
915
     * Obtains the schema information for the named table.
916
     * @param string $name table name.
917
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
918
     * @return TableSchema|null table schema information. Null if the named table does not exist.
919
     */
920 250
    public function getTableSchema($name, $refresh = false)
921
    {
922 250
        return $this->getSchema()->getTableSchema($name, $refresh);
923
    }
924
925
    /**
926
     * Returns the ID of the last inserted row or sequence value.
927
     * @param string $sequenceName name of the sequence object (required by some DBMS)
928
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
929
     * @see https://www.php.net/manual/en/pdo.lastinsertid.php
930
     */
931 6
    public function getLastInsertID($sequenceName = '')
932
    {
933 6
        return $this->getSchema()->getLastInsertID($sequenceName);
934
    }
935
936
    /**
937
     * Quotes a string value for use in a query.
938
     * Note that if the parameter is not a string, it will be returned without change.
939
     * @param string $value string to be quoted
940
     * @return string the properly quoted string
941
     * @see https://www.php.net/manual/en/pdo.quote.php
942
     */
943 1098
    public function quoteValue($value)
944
    {
945 1098
        return $this->getSchema()->quoteValue($value);
946
    }
947
948
    /**
949
     * Quotes a table name for use in a query.
950
     * If the table name contains schema prefix, the prefix will also be properly quoted.
951
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{',
952
     * then this method will do nothing.
953
     * @param string $name table name
954
     * @return string the properly quoted table name
955
     */
956 1400
    public function quoteTableName($name)
957
    {
958 1400
        if (isset($this->_quotedTableNames[$name])) {
959 1031
            return $this->_quotedTableNames[$name];
960
        }
961 1334
        return $this->_quotedTableNames[$name] = $this->getSchema()->quoteTableName($name);
962
    }
963
964
    /**
965
     * Quotes a column name for use in a query.
966
     * If the column name contains prefix, the prefix will also be properly quoted.
967
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{',
968
     * then this method will do nothing.
969
     * @param string $name column name
970
     * @return string the properly quoted column name
971
     */
972 1471
    public function quoteColumnName($name)
973
    {
974 1471
        if (isset($this->_quotedColumnNames[$name])) {
975 898
            return $this->_quotedColumnNames[$name];
976
        }
977 1416
        return $this->_quotedColumnNames[$name] = $this->getSchema()->quoteColumnName($name);
978
    }
979
980
    /**
981
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
982
     * Tokens enclosed within double curly brackets are treated as table names, while
983
     * tokens enclosed within double square brackets are column names. They will be quoted accordingly.
984
     * Also, the percentage character "%" at the beginning or ending of a table name will be replaced
985
     * with [[tablePrefix]].
986
     * @param string $sql the SQL to be quoted
987
     * @return string the quoted SQL
988
     */
989 1690
    public function quoteSql($sql)
990
    {
991 1690
        return preg_replace_callback(
992 1690
            '/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
993 1690
            function ($matches) {
994 879
                if (isset($matches[3])) {
995 657
                    return $this->quoteColumnName($matches[3]);
996
                }
997
998 739
                return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
999 1690
            },
1000
            $sql
1001
        );
1002
    }
1003
1004
    /**
1005
     * Returns the name of the DB driver. Based on the the current [[dsn]], in case it was not set explicitly
1006
     * by an end user.
1007
     * @return string|null name of the DB driver
1008
     */
1009 2366
    public function getDriverName()
1010
    {
1011 2366
        if ($this->_driverName === null) {
1012 2297
            if (($pos = strpos((string)$this->dsn, ':')) !== false) {
1013 2297
                $this->_driverName = strtolower(substr($this->dsn, 0, $pos));
1014
            } else {
1015
                $this->_driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME));
1016
            }
1017
        }
1018
1019 2366
        return $this->_driverName;
1020
    }
1021
1022
    /**
1023
     * Changes the current driver name.
1024
     * @param string $driverName name of the DB driver
1025
     */
1026
    public function setDriverName($driverName)
1027
    {
1028
        $this->_driverName = strtolower($driverName);
1029
    }
1030
1031
    /**
1032
     * Returns a server version as a string comparable by [[\version_compare()]].
1033
     * @return string server version as a string.
1034
     * @since 2.0.14
1035
     */
1036 422
    public function getServerVersion()
1037
    {
1038 422
        return $this->getSchema()->getServerVersion();
1039
    }
1040
1041
    /**
1042
     * Returns the PDO instance for the currently active slave connection.
1043
     * When [[enableSlaves]] is true, one of the slaves will be used for read queries, and its PDO instance
1044
     * will be returned by this method.
1045
     * @param bool $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
1046
     * @return PDO|null the PDO instance for the currently active slave connection. `null` is returned if no slave connection
1047
     * is available and `$fallbackToMaster` is false.
1048
     */
1049 1795
    public function getSlavePdo($fallbackToMaster = true)
1050
    {
1051 1795
        $db = $this->getSlave(false);
1052 1795
        if ($db === null) {
1053 1791
            return $fallbackToMaster ? $this->getMasterPdo() : null;
1054
        }
1055
1056 5
        return $db->pdo;
1057
    }
1058
1059
    /**
1060
     * Returns the PDO instance for the currently active master connection.
1061
     * This method will open the master DB connection and then return [[pdo]].
1062
     * @return PDO the PDO instance for the currently active master connection.
1063
     */
1064 1830
    public function getMasterPdo()
1065
    {
1066 1830
        $this->open();
1067 1830
        return $this->pdo;
1068
    }
1069
1070
    /**
1071
     * Returns the currently active slave connection.
1072
     * If this method is called for the first time, it will try to open a slave connection when [[enableSlaves]] is true.
1073
     * @param bool $fallbackToMaster whether to return a master connection in case there is no slave connection available.
1074
     * @return Connection|null the currently active slave connection. `null` is returned if there is no slave available and
1075
     * `$fallbackToMaster` is false.
1076
     */
1077 1797
    public function getSlave($fallbackToMaster = true)
1078
    {
1079 1797
        if (!$this->enableSlaves) {
1080 221
            return $fallbackToMaster ? $this : null;
1081
        }
1082
1083 1692
        if ($this->_slave === false) {
1084 1692
            $this->_slave = $this->openFromPool($this->slaves, $this->slaveConfig);
1085
        }
1086
1087 1686
        return $this->_slave === null && $fallbackToMaster ? $this : $this->_slave;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_slave ===...? $this : $this->_slave also could return the type true which is incompatible with the documented return type null|yii\db\Connection.
Loading history...
1088
    }
1089
1090
    /**
1091
     * Returns the currently active master connection.
1092
     * If this method is called for the first time, it will try to open a master connection.
1093
     * @return Connection|null the currently active master connection. `null` is returned if there is no master available.
1094
     * @since 2.0.11
1095
     */
1096 11
    public function getMaster()
1097
    {
1098 11
        if ($this->_master === false) {
1099 11
            $this->_master = $this->shuffleMasters
1100 2
                ? $this->openFromPool($this->masters, $this->masterConfig)
1101 9
                : $this->openFromPoolSequentially($this->masters, $this->masterConfig);
1102
        }
1103
1104 11
        return $this->_master;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_master also could return the type true which is incompatible with the documented return type null|yii\db\Connection.
Loading history...
1105
    }
1106
1107
    /**
1108
     * Executes the provided callback by using the master connection.
1109
     *
1110
     * This method is provided so that you can temporarily force using the master connection to perform
1111
     * DB operations even if they are read queries. For example,
1112
     *
1113
     * ```php
1114
     * $result = $db->useMaster(function ($db) {
1115
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
1116
     * });
1117
     * ```
1118
     *
1119
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
1120
     * `function (Connection $db)`. Its return value will be returned by this method.
1121
     * @return mixed the return value of the callback
1122
     * @throws \Throwable if there is any exception thrown from the callback
1123
     */
1124 112
    public function useMaster(callable $callback)
1125
    {
1126 112
        if ($this->enableSlaves) {
1127 106
            $this->enableSlaves = false;
1128
            try {
1129 106
                $result = call_user_func($callback, $this);
1130 4
            } catch (\Exception $e) {
1131 4
                $this->enableSlaves = true;
1132 4
                throw $e;
1133
            } catch (\Throwable $e) {
1134
                $this->enableSlaves = true;
1135
                throw $e;
1136
            }
1137
            // TODO: use "finally" keyword when miminum required PHP version is >= 5.5
1138 102
            $this->enableSlaves = true;
1139
        } else {
1140 6
            $result = call_user_func($callback, $this);
1141
        }
1142
1143 108
        return $result;
1144
    }
1145
1146
    /**
1147
     * Opens the connection to a server in the pool.
1148
     *
1149
     * This method implements load balancing and failover among the given list of the servers.
1150
     * Connections will be tried in random order.
1151
     * For details about the failover behavior, see [[openFromPoolSequentially]].
1152
     *
1153
     * @param array $pool the list of connection configurations in the server pool
1154
     * @param array $sharedConfig the configuration common to those given in `$pool`.
1155
     * @return Connection|null the opened DB connection, or `null` if no server is available
1156
     * @throws InvalidConfigException if a configuration does not specify "dsn"
1157
     * @see openFromPoolSequentially
1158
     */
1159 1692
    protected function openFromPool(array $pool, array $sharedConfig)
1160
    {
1161 1692
        shuffle($pool);
1162 1692
        return $this->openFromPoolSequentially($pool, $sharedConfig);
1163
    }
1164
1165
    /**
1166
     * Opens the connection to a server in the pool.
1167
     *
1168
     * This method implements failover among the given list of servers.
1169
     * Connections will be tried in sequential order. The first successful connection will return.
1170
     *
1171
     * If [[serverStatusCache]] is configured, this method will cache information about
1172
     * unreachable servers and does not try to connect to these for the time configured in [[serverRetryInterval]].
1173
     * This helps to keep the application stable when some servers are unavailable. Avoiding
1174
     * connection attempts to unavailable servers saves time when the connection attempts fail due to timeout.
1175
     *
1176
     * If none of the servers are available the status cache is ignored and connection attempts are made to all
1177
     * servers (Since version 2.0.35). This is to avoid downtime when all servers are unavailable for a short time.
1178
     * After a successful connection attempt the server is marked as available again.
1179
     *
1180
     * @param array $pool the list of connection configurations in the server pool
1181
     * @param array $sharedConfig the configuration common to those given in `$pool`.
1182
     * @return Connection|null the opened DB connection, or `null` if no server is available
1183
     * @throws InvalidConfigException if a configuration does not specify "dsn"
1184
     * @since 2.0.11
1185
     * @see openFromPool
1186
     * @see serverStatusCache
1187
     */
1188 1700
    protected function openFromPoolSequentially(array $pool, array $sharedConfig)
1189
    {
1190 1700
        if (empty($pool)) {
1191 1680
            return null;
1192
        }
1193
1194 21
        if (!isset($sharedConfig['class'])) {
1195 21
            $sharedConfig['class'] = get_class($this);
1196
        }
1197
1198 21
        $cache = is_string($this->serverStatusCache) ? Yii::$app->get($this->serverStatusCache, false) : $this->serverStatusCache;
1199
1200 21
        foreach ($pool as $i => $config) {
1201 21
            $pool[$i] = $config = array_merge($sharedConfig, $config);
1202 21
            if (empty($config['dsn'])) {
1203 6
                throw new InvalidConfigException('The "dsn" option must be specified.');
1204
            }
1205
1206 15
            $key = [__METHOD__, $config['dsn']];
1207 15
            if ($cache instanceof CacheInterface && $cache->get($key)) {
1208
                // should not try this dead server now
1209
                continue;
1210
            }
1211
1212
            /* @var $db Connection */
1213 15
            $db = Yii::createObject($config);
1214
1215
            try {
1216 15
                $db->open();
1217 15
                return $db;
1218 8
            } catch (\Exception $e) {
1219 8
                Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__);
1220 8
                if ($cache instanceof CacheInterface) {
1221
                    // mark this server as dead and only retry it after the specified interval
1222 4
                    $cache->set($key, 1, $this->serverRetryInterval);
1223
                }
1224
                // exclude server from retry below
1225 8
                unset($pool[$i]);
1226
            }
1227
        }
1228
1229 8
        if ($cache instanceof CacheInterface) {
1230
            // if server status cache is enabled and no server is available
1231
            // ignore the cache and try to connect anyway
1232
            // $pool now only contains servers we did not already try in the loop above
1233 4
            foreach ($pool as $config) {
1234
1235
                /* @var $db Connection */
1236
                $db = Yii::createObject($config);
1237
                try {
1238
                    $db->open();
1239
                } catch (\Exception $e) {
1240
                    Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__);
1241
                    continue;
1242
                }
1243
1244
                // mark this server as available again after successful connection
1245
                $cache->delete([__METHOD__, $config['dsn']]);
1246
1247
                return $db;
1248
            }
1249
        }
1250
1251 8
        return null;
1252
    }
1253
1254
    /**
1255
     * Close the connection before serializing.
1256
     * @return array
1257
     */
1258 20
    public function __sleep()
1259
    {
1260 20
        $fields = (array) $this;
1261
1262 20
        unset($fields['pdo']);
1263 20
        unset($fields["\000" . __CLASS__ . "\000" . '_master']);
1264 20
        unset($fields["\000" . __CLASS__ . "\000" . '_slave']);
1265 20
        unset($fields["\000" . __CLASS__ . "\000" . '_transaction']);
1266 20
        unset($fields["\000" . __CLASS__ . "\000" . '_schema']);
1267
1268 20
        return array_keys($fields);
1269
    }
1270
1271
    /**
1272
     * Reset the connection after cloning.
1273
     */
1274 7
    public function __clone()
1275
    {
1276 7
        parent::__clone();
1277
1278 7
        $this->_master = false;
1279 7
        $this->_slave = false;
1280 7
        $this->_schema = null;
1281 7
        $this->_transaction = null;
1282 7
        if (strncmp($this->dsn, 'sqlite::memory:', 15) !== 0) {
1283
            // reset PDO connection, unless its sqlite in-memory, which can only have one connection
1284 6
            $this->pdo = null;
1285
        }
1286 7
    }
1287
}
1288