Issues (910)

framework/db/Connection.php (2 issues)

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, PostgreSQL and CUBRID 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
        'cubrid' => 'yii\db\cubrid\Schema', // CUBRID
286
    ];
287
    /**
288
     * @var string|null Custom PDO wrapper class. If not set, it will use [[PDO]] or [[\yii\db\mssql\PDO]] when MSSQL is used.
289
     * @see pdo
290
     */
291
    public $pdoClass;
292
    /**
293
     * @var string the class used to create new database [[Command]] objects. If you want to extend the [[Command]] class,
294
     * you may configure this property to use your extended version of the class.
295
     * Since version 2.0.14 [[$commandMap]] is used if this property is set to its default value.
296
     * @see createCommand
297
     * @since 2.0.7
298
     * @deprecated since 2.0.14. Use [[$commandMap]] for precise configuration.
299
     */
300
    public $commandClass = 'yii\db\Command';
301
    /**
302
     * @var array mapping between PDO driver names and [[Command]] classes.
303
     * The keys of the array are PDO driver names while the values are either the corresponding
304
     * command class names or configurations. Please refer to [[Yii::createObject()]] for
305
     * details on how to specify a configuration.
306
     *
307
     * This property is mainly used by [[createCommand()]] to create new database [[Command]] objects.
308
     * You normally do not need to set this property unless you want to use your own
309
     * [[Command]] class or support DBMS that is not supported by Yii.
310
     * @since 2.0.14
311
     */
312
    public $commandMap = [
313
        'pgsql' => 'yii\db\Command', // PostgreSQL
314
        'mysqli' => 'yii\db\Command', // MySQL
315
        'mysql' => 'yii\db\Command', // MySQL
316
        'sqlite' => 'yii\db\sqlite\Command', // sqlite 3
317
        'sqlite2' => 'yii\db\sqlite\Command', // sqlite 2
318
        'sqlsrv' => 'yii\db\Command', // newer MSSQL driver on MS Windows hosts
319
        'oci' => 'yii\db\oci\Command', // Oracle driver
320
        'mssql' => 'yii\db\Command', // older MSSQL driver on MS Windows hosts
321
        'dblib' => 'yii\db\Command', // dblib drivers on GNU/Linux (and maybe other OSes) hosts
322
        'cubrid' => 'yii\db\Command', // CUBRID
323
    ];
324
    /**
325
     * @var bool whether to enable [savepoint](https://en.wikipedia.org/wiki/Savepoint).
326
     * Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect.
327
     */
328
    public $enableSavepoint = true;
329
    /**
330
     * @var CacheInterface|string|false the cache object or the ID of the cache application component that is used to store
331
     * the health status of the DB servers specified in [[masters]] and [[slaves]].
332
     * This is used only when read/write splitting is enabled or [[masters]] is not empty.
333
     * Set boolean `false` to disabled server status caching.
334
     * @see openFromPoolSequentially() for details about the failover behavior.
335
     * @see serverRetryInterval
336
     */
337
    public $serverStatusCache = 'cache';
338
    /**
339
     * @var int the retry interval in seconds for dead servers listed in [[masters]] and [[slaves]].
340
     * This is used together with [[serverStatusCache]].
341
     */
342
    public $serverRetryInterval = 600;
343
    /**
344
     * @var bool whether to enable read/write splitting by using [[slaves]] to read data.
345
     * Note that if [[slaves]] is empty, read/write splitting will NOT be enabled no matter what value this property takes.
346
     */
347
    public $enableSlaves = true;
348
    /**
349
     * @var array list of slave connection configurations. Each configuration is used to create a slave DB connection.
350
     * When [[enableSlaves]] is true, one of these configurations will be chosen and used to create a DB connection
351
     * for performing read queries only.
352
     * @see enableSlaves
353
     * @see slaveConfig
354
     */
355
    public $slaves = [];
356
    /**
357
     * @var array the configuration that should be merged with every slave configuration listed in [[slaves]].
358
     * For example,
359
     *
360
     * ```php
361
     * [
362
     *     'username' => 'slave',
363
     *     'password' => 'slave',
364
     *     'attributes' => [
365
     *         // use a smaller connection timeout
366
     *         PDO::ATTR_TIMEOUT => 10,
367
     *     ],
368
     * ]
369
     * ```
370
     */
371
    public $slaveConfig = [];
372
    /**
373
     * @var array list of master connection configurations. Each configuration is used to create a master DB connection.
374
     * When [[open()]] is called, one of these configurations will be chosen and used to create a DB connection
375
     * which will be used by this object.
376
     * Note that when this property is not empty, the connection setting (e.g. "dsn", "username") of this object will
377
     * be ignored.
378
     * @see masterConfig
379
     * @see shuffleMasters
380
     */
381
    public $masters = [];
382
    /**
383
     * @var array the configuration that should be merged with every master configuration listed in [[masters]].
384
     * For example,
385
     *
386
     * ```php
387
     * [
388
     *     'username' => 'master',
389
     *     'password' => 'master',
390
     *     'attributes' => [
391
     *         // use a smaller connection timeout
392
     *         PDO::ATTR_TIMEOUT => 10,
393
     *     ],
394
     * ]
395
     * ```
396
     */
397
    public $masterConfig = [];
398
    /**
399
     * @var bool whether to shuffle [[masters]] before getting one.
400
     * @since 2.0.11
401
     * @see masters
402
     */
403
    public $shuffleMasters = true;
404
    /**
405
     * @var bool whether to enable logging of database queries. Defaults to true.
406
     * You may want to disable this option in a production environment to gain performance
407
     * if you do not need the information being logged.
408
     * @since 2.0.12
409
     * @see enableProfiling
410
     */
411
    public $enableLogging = true;
412
    /**
413
     * @var bool whether to enable profiling of opening database connection and database queries. Defaults to true.
414
     * You may want to disable this option in a production environment to gain performance
415
     * if you do not need the information being logged.
416
     * @since 2.0.12
417
     * @see enableLogging
418
     */
419
    public $enableProfiling = true;
420
    /**
421
     * @var bool If the database connected via pdo_dblib is SyBase.
422
     * @since 2.0.38
423
     */
424
    public $isSybase = false;
425
426
    /**
427
     * @var array An array of [[setQueryBuilder()]] calls, holding the passed arguments.
428
     * Is used to restore a QueryBuilder configuration after the connection close/open cycle.
429
     *
430
     * @see restoreQueryBuilderConfiguration()
431
     */
432
    private $_queryBuilderConfigurations = [];
433
    /**
434
     * @var Transaction the currently active transaction
435
     */
436
    private $_transaction;
437
    /**
438
     * @var Schema the database schema
439
     */
440
    private $_schema;
441
    /**
442
     * @var string driver name
443
     */
444
    private $_driverName;
445
    /**
446
     * @var Connection|false the currently active master connection
447
     */
448
    private $_master = false;
449
    /**
450
     * @var Connection|false the currently active slave connection
451
     */
452
    private $_slave = false;
453
    /**
454
     * @var array query cache parameters for the [[cache()]] calls
455
     */
456
    private $_queryCacheInfo = [];
457
    /**
458
     * @var string[] quoted table name cache for [[quoteTableName()]] calls
459
     */
460
    private $_quotedTableNames;
461
    /**
462
     * @var string[] quoted column name cache for [[quoteColumnName()]] calls
463
     */
464
    private $_quotedColumnNames;
465
466
467
    /**
468
     * Returns a value indicating whether the DB connection is established.
469
     * @return bool whether the DB connection is established
470
     */
471 346
    public function getIsActive()
472
    {
473 346
        return $this->pdo !== null;
474
    }
475
476
    /**
477
     * Uses query cache for the queries performed with the callable.
478
     *
479
     * When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
480
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
481
     * For example,
482
     *
483
     * ```php
484
     * // The customer will be fetched from cache if available.
485
     * // If not, the query will be made against DB and cached for use next time.
486
     * $customer = $db->cache(function (Connection $db) {
487
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
488
     * });
489
     * ```
490
     *
491
     * Note that query cache is only meaningful for queries that return results. For queries performed with
492
     * [[Command::execute()]], query cache will not be used.
493
     *
494
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
495
     * The signature of the callable is `function (Connection $db)`.
496
     * @param int|null $duration the number of seconds that query results can remain valid in the cache. If this is
497
     * not set, the value of [[queryCacheDuration]] will be used instead.
498
     * Use 0 to indicate that the cached data will never expire.
499
     * @param \yii\caching\Dependency|null $dependency the cache dependency associated with the cached query results.
500
     * @return mixed the return result of the callable
501
     * @throws \Throwable if there is any exception during query
502
     * @see enableQueryCache
503
     * @see queryCache
504
     * @see noCache()
505
     */
506 6
    public function cache(callable $callable, $duration = null, $dependency = null)
507
    {
508 6
        $this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
509
        try {
510 6
            $result = call_user_func($callable, $this);
511 6
            array_pop($this->_queryCacheInfo);
512 6
            return $result;
513
        } catch (\Exception $e) {
514
            array_pop($this->_queryCacheInfo);
515
            throw $e;
516
        } catch (\Throwable $e) {
517
            array_pop($this->_queryCacheInfo);
518
            throw $e;
519
        }
520
    }
521
522
    /**
523
     * Disables query cache temporarily.
524
     *
525
     * Queries performed within the callable will not use query cache at all. For example,
526
     *
527
     * ```php
528
     * $db->cache(function (Connection $db) {
529
     *
530
     *     // ... queries that use query cache ...
531
     *
532
     *     return $db->noCache(function (Connection $db) {
533
     *         // this query will not use query cache
534
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
535
     *     });
536
     * });
537
     * ```
538
     *
539
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache.
540
     * The signature of the callable is `function (Connection $db)`.
541
     * @return mixed the return result of the callable
542
     * @throws \Throwable if there is any exception during query
543
     * @see enableQueryCache
544
     * @see queryCache
545
     * @see cache()
546
     */
547 40
    public function noCache(callable $callable)
548
    {
549 40
        $this->_queryCacheInfo[] = false;
550
        try {
551 40
            $result = call_user_func($callable, $this);
552 40
            array_pop($this->_queryCacheInfo);
553 40
            return $result;
554 4
        } catch (\Exception $e) {
555 4
            array_pop($this->_queryCacheInfo);
556 4
            throw $e;
557
        } catch (\Throwable $e) {
558
            array_pop($this->_queryCacheInfo);
559
            throw $e;
560
        }
561
    }
562
563
    /**
564
     * Returns the current query cache information.
565
     * This method is used internally by [[Command]].
566
     * @param int|null $duration the preferred caching duration. If null, it will be ignored.
567
     * @param \yii\caching\Dependency|null $dependency the preferred caching dependency. If null, it will be ignored.
568
     * @return array|null the current query cache information, or null if query cache is not enabled.
569
     * @internal
570
     */
571 1613
    public function getQueryCacheInfo($duration, $dependency)
572
    {
573 1613
        if (!$this->enableQueryCache) {
574 48
            return null;
575
        }
576
577 1609
        $info = end($this->_queryCacheInfo);
578 1609
        if (is_array($info)) {
579 6
            if ($duration === null) {
580 6
                $duration = $info[0];
581
            }
582 6
            if ($dependency === null) {
583 6
                $dependency = $info[1];
584
            }
585
        }
586
587 1609
        if ($duration === 0 || $duration > 0) {
588 6
            if (is_string($this->queryCache) && Yii::$app) {
589
                $cache = Yii::$app->get($this->queryCache, false);
590
            } else {
591 6
                $cache = $this->queryCache;
592
            }
593 6
            if ($cache instanceof CacheInterface) {
594 6
                return [$cache, $duration, $dependency];
595
            }
596
        }
597
598 1609
        return null;
599
    }
600
601
    /**
602
     * Establishes a DB connection.
603
     * It does nothing if a DB connection has already been established.
604
     * @throws Exception if connection fails
605
     */
606 2178
    public function open()
607
    {
608 2178
        if ($this->pdo !== null) {
609 1738
            return;
610
        }
611
612 2122
        if (!empty($this->masters)) {
613 9
            $db = $this->getMaster();
614 9
            if ($db !== null) {
615 9
                $this->pdo = $db->pdo;
616 9
                return;
617
            }
618
619 8
            throw new InvalidConfigException('None of the master DB servers is available.');
620
        }
621
622 2122
        if (empty($this->dsn)) {
623
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
624
        }
625
626 2122
        $token = 'Opening DB connection: ' . $this->dsn;
627 2122
        $enableProfiling = $this->enableProfiling;
628
        try {
629 2122
            if ($this->enableLogging) {
630 2122
                Yii::info($token, __METHOD__);
631
            }
632
633 2122
            if ($enableProfiling) {
634 2122
                Yii::beginProfile($token, __METHOD__);
635
            }
636
637 2122
            $this->pdo = $this->createPdoInstance();
638 2122
            $this->initConnection();
639
640 2122
            if ($enableProfiling) {
641 2122
                Yii::endProfile($token, __METHOD__);
642
            }
643 12
        } catch (\PDOException $e) {
644 12
            if ($enableProfiling) {
645 12
                Yii::endProfile($token, __METHOD__);
646
            }
647
648 12
            throw new Exception($e->getMessage(), $e->errorInfo, $e->getCode(), $e);
649
        }
650
    }
651
652
    /**
653
     * Closes the currently active DB connection.
654
     * It does nothing if the connection is already closed.
655
     */
656 2262
    public function close()
657
    {
658 2262
        if ($this->_master) {
659 8
            if ($this->pdo === $this->_master->pdo) {
660 8
                $this->pdo = null;
661
            }
662
663 8
            $this->_master->close();
664 8
            $this->_master = false;
665
        }
666
667 2262
        if ($this->pdo !== null) {
668 1907
            Yii::debug('Closing DB connection: ' . $this->dsn, __METHOD__);
669 1907
            $this->pdo = null;
670
        }
671
672 2262
        if ($this->_slave) {
673 4
            $this->_slave->close();
674 4
            $this->_slave = false;
675
        }
676
677 2262
        $this->_schema = null;
678 2262
        $this->_transaction = null;
679 2262
        $this->_driverName = null;
680 2262
        $this->_queryCacheInfo = [];
681 2262
        $this->_quotedTableNames = null;
682 2262
        $this->_quotedColumnNames = null;
683
    }
684
685
    /**
686
     * Creates the PDO instance.
687
     * This method is called by [[open]] to establish a DB connection.
688
     * The default implementation will create a PHP PDO instance.
689
     * You may override this method if the default PDO needs to be adapted for certain DBMS.
690
     * @return PDO the pdo instance
691
     */
692 2122
    protected function createPdoInstance()
693
    {
694 2122
        $pdoClass = $this->pdoClass;
695 2122
        if ($pdoClass === null) {
696 2122
            $driver = null;
697 2122
            if ($this->_driverName !== null) {
698 261
                $driver = $this->_driverName;
699 1870
            } elseif (($pos = strpos($this->dsn, ':')) !== false) {
700 1870
                $driver = strtolower(substr($this->dsn, 0, $pos));
701
            }
702
            switch ($driver) {
703 2122
                case 'mssql':
704
                    $pdoClass = 'yii\db\mssql\PDO';
705
                    break;
706 2122
                case 'dblib':
707
                    $pdoClass = 'yii\db\mssql\DBLibPDO';
708
                    break;
709 2122
                case 'sqlsrv':
710
                    $pdoClass = 'yii\db\mssql\SqlsrvPDO';
711
                    break;
712
                default:
713 2122
                    $pdoClass = 'PDO';
714
            }
715
        }
716
717 2122
        $dsn = $this->dsn;
718 2122
        if (strncmp('sqlite:@', $dsn, 8) === 0) {
719 1
            $dsn = 'sqlite:' . Yii::getAlias(substr($dsn, 7));
720
        }
721
722 2122
        return new $pdoClass($dsn, $this->username, $this->password, $this->attributes);
723
    }
724
725
    /**
726
     * Initializes the DB connection.
727
     * This method is invoked right after the DB connection is established.
728
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`
729
     * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty.
730
     * It then triggers an [[EVENT_AFTER_OPEN]] event.
731
     */
732 2122
    protected function initConnection()
733
    {
734 2122
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
735 2122
        if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) {
736
            if ($this->driverName !== 'sqlsrv') {
737
                $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare);
738
            }
739
        }
740
741 2122
        if (PHP_VERSION_ID >= 80100 && $this->getDriverName() === 'sqlite') {
742 643
            $this->pdo->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
743
        }
744
745 2122
        if (!$this->isSybase && in_array($this->getDriverName(), ['mssql', 'dblib'], true)) {
746
            $this->pdo->exec('SET ANSI_NULL_DFLT_ON ON');
747
        }
748 2122
        if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'], true)) {
749
            $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset));
750
        }
751 2122
        $this->trigger(self::EVENT_AFTER_OPEN);
752
    }
753
754
    /**
755
     * Creates a command for execution.
756
     * @param string|null $sql the SQL statement to be executed
757
     * @param array $params the parameters to be bound to the SQL statement
758
     * @return Command the DB command
759
     */
760 1718
    public function createCommand($sql = null, $params = [])
761
    {
762 1718
        $driver = $this->getDriverName();
763 1718
        $config = ['class' => 'yii\db\Command'];
764 1718
        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

764
        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...
765
            $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

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