Connection::__clone()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

708
            $dsn = 'sqlite:' . /** @scrutinizer ignore-type */ Yii::getAlias(substr($dsn, 7));
Loading history...
709
        }
710
711 83
        return new $pdoClass($dsn, $this->username, $this->password, $this->attributes);
712
    }
713
714
    /**
715
     * Initializes the DB connection.
716
     * This method is invoked right after the DB connection is established.
717
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`
718
     * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty.
719
     * It then triggers an [[EVENT_AFTER_OPEN]] event.
720
     */
721 83
    protected function initConnection()
722
    {
723 83
        $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

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