Completed
Push — master ( b5e090...980cfd )
by Alexander
13:24
created

Connection::open()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 9.0414

Importance

Changes 0
Metric Value
dl 0
loc 42
ccs 23
cts 25
cp 0.92
rs 4.909
c 0
b 0
f 0
cc 9
eloc 25
nc 24
nop 0
crap 9.0414
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use PDO;
11
use Yii;
12
use yii\base\Component;
13
use yii\base\InvalidConfigException;
14
use yii\base\NotSupportedException;
15
use yii\caching\CacheInterface;
16
17
/**
18
 * Connection represents a connection to a database via [PDO](http://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](http://php.net/manual/en/book.pdo.php).
23
 *
24
 * Connection supports database replication and read-write splitting. In particular, a Connection component
25
 * can be configured with multiple [[masters]] and [[slaves]]. It will do load balancing and failover by choosing
26
 * appropriate servers. It will also automatically direct read operations to the slaves and write operations to
27
 * the masters.
28
 *
29
 * To establish a DB connection, set [[dsn]], [[username]] and [[password]], and then
30
 * call [[open()]] to connect to the database server. The current state of the connection can be checked using [[$isActive]].
31
 *
32
 * The following example shows how to create a Connection instance and establish
33
 * the DB connection:
34
 *
35
 * ```php
36
 * $connection = new \yii\db\Connection([
37
 *     'dsn' => $dsn,
38
 *     'username' => $username,
39
 *     'password' => $password,
40
 * ]);
41
 * $connection->open();
42
 * ```
43
 *
44
 * After the DB connection is established, one can execute SQL statements like the following:
45
 *
46
 * ```php
47
 * $command = $connection->createCommand('SELECT * FROM post');
48
 * $posts = $command->queryAll();
49
 * $command = $connection->createCommand('UPDATE post SET status=1');
50
 * $command->execute();
51
 * ```
52
 *
53
 * One can also do prepared SQL execution and bind parameters to the prepared SQL.
54
 * When the parameters are coming from user input, you should use this approach
55
 * to prevent SQL injection attacks. The following is an example:
56
 *
57
 * ```php
58
 * $command = $connection->createCommand('SELECT * FROM post WHERE id=:id');
59
 * $command->bindValue(':id', $_GET['id']);
60
 * $post = $command->query();
61
 * ```
62
 *
63
 * For more information about how to perform various DB queries, please refer to [[Command]].
64
 *
65
 * If the underlying DBMS supports transactions, you can perform transactional SQL queries
66
 * like the following:
67
 *
68
 * ```php
69
 * $transaction = $connection->beginTransaction();
70
 * try {
71
 *     $connection->createCommand($sql1)->execute();
72
 *     $connection->createCommand($sql2)->execute();
73
 *     // ... executing other SQL statements ...
74
 *     $transaction->commit();
75
 * } catch (Exception $e) {
76
 *     $transaction->rollBack();
77
 * }
78
 * ```
79
 *
80
 * You also can use shortcut for the above like the following:
81
 *
82
 * ```php
83
 * $connection->transaction(function () {
84
 *     $order = new Order($customer);
85
 *     $order->save();
86
 *     $order->addItems($items);
87
 * });
88
 * ```
89
 *
90
 * If needed you can pass transaction isolation level as a second parameter:
91
 *
92
 * ```php
93
 * $connection->transaction(function (Connection $db) {
94
 *     //return $db->...
95
 * }, Transaction::READ_UNCOMMITTED);
96
 * ```
97
 *
98
 * Connection is often used as an application component and configured in the application
99
 * configuration like the following:
100
 *
101
 * ```php
102
 * 'components' => [
103
 *     'db' => [
104
 *         'class' => '\yii\db\Connection',
105
 *         'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
106
 *         'username' => 'root',
107
 *         'password' => '',
108
 *         'charset' => 'utf8',
109
 *     ],
110
 * ],
111
 * ```
112
 *
113
 * @property string $driverName Name of the DB driver.
114
 * @property bool $isActive Whether the DB connection is established. This property is read-only.
115
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
116
 * sequence object. This property is read-only.
117
 * @property Connection $master The currently active master connection. `null` is returned if there is no
118
 * master available. This property is read-only.
119
 * @property PDO $masterPdo The PDO instance for the currently active master connection. This property is
120
 * read-only.
121
 * @property QueryBuilder $queryBuilder The query builder for the current DB connection. This property is
122
 * read-only.
123
 * @property Schema $schema The schema information for the database opened by this connection. This property
124
 * is read-only.
125
 * @property Connection $slave The currently active slave connection. `null` is returned if there is no slave
126
 * available and `$fallbackToMaster` is false. This property is read-only.
127
 * @property PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned if
128
 * no slave connection is available and `$fallbackToMaster` is false. This property is read-only.
129
 * @property Transaction $transaction The currently active transaction. Null if no active transaction. This
130
 * property is read-only.
131
 *
132
 * @author Qiang Xue <[email protected]>
133
 * @since 2.0
134
 */
135
class Connection extends Component
136
{
137
    /**
138
     * @event Event an event that is triggered after a DB connection is established
139
     */
140
    const EVENT_AFTER_OPEN = 'afterOpen';
141
    /**
142
     * @event Event an event that is triggered right before a top-level transaction is started
143
     */
144
    const EVENT_BEGIN_TRANSACTION = 'beginTransaction';
145
    /**
146
     * @event Event an event that is triggered right after a top-level transaction is committed
147
     */
148
    const EVENT_COMMIT_TRANSACTION = 'commitTransaction';
149
    /**
150
     * @event 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](http://php.net/manual/en/pdo.construct.php) on
157
     * the format of the DSN string.
158
     *
159
     * For [SQLite](http://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 the username for establishing DB connection. Defaults to `null` meaning no username to use.
167
     */
168
    public $username;
169
    /**
170
     * @var string 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](http://php.net/manual/en/pdo.setattribute.php) for
177
     * details about available attributes.
178
     */
179
    public $attributes;
180
    /**
181
     * @var PDO 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 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 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 the corresponding
268
     * schema class name or configuration. 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 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
     * @see createCommand
296
     * @since 2.0.7
297
     */
298
    public $commandClass = 'yii\db\Command';
299
    /**
300
     * @var bool whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint).
301
     * Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect.
302
     */
303
    public $enableSavepoint = true;
304
    /**
305
     * @var CacheInterface|string the cache object or the ID of the cache application component that is used to store
306
     * the health status of the DB servers specified in [[masters]] and [[slaves]].
307
     * This is used only when read/write splitting is enabled or [[masters]] is not empty.
308
     */
309
    public $serverStatusCache = 'cache';
310
    /**
311
     * @var int the retry interval in seconds for dead servers listed in [[masters]] and [[slaves]].
312
     * This is used together with [[serverStatusCache]].
313
     */
314
    public $serverRetryInterval = 600;
315
    /**
316
     * @var bool whether to enable read/write splitting by using [[slaves]] to read data.
317
     * Note that if [[slaves]] is empty, read/write splitting will NOT be enabled no matter what value this property takes.
318
     */
319
    public $enableSlaves = true;
320
    /**
321
     * @var array list of slave connection configurations. Each configuration is used to create a slave DB connection.
322
     * When [[enableSlaves]] is true, one of these configurations will be chosen and used to create a DB connection
323
     * for performing read queries only.
324
     * @see enableSlaves
325
     * @see slaveConfig
326
     */
327
    public $slaves = [];
328
    /**
329
     * @var array the configuration that should be merged with every slave configuration listed in [[slaves]].
330
     * For example,
331
     *
332
     * ```php
333
     * [
334
     *     'username' => 'slave',
335
     *     'password' => 'slave',
336
     *     'attributes' => [
337
     *         // use a smaller connection timeout
338
     *         PDO::ATTR_TIMEOUT => 10,
339
     *     ],
340
     * ]
341
     * ```
342
     */
343
    public $slaveConfig = [];
344
    /**
345
     * @var array list of master connection configurations. Each configuration is used to create a master DB connection.
346
     * When [[open()]] is called, one of these configurations will be chosen and used to create a DB connection
347
     * which will be used by this object.
348
     * Note that when this property is not empty, the connection setting (e.g. "dsn", "username") of this object will
349
     * be ignored.
350
     * @see masterConfig
351
     * @see shuffleMasters
352
     */
353
    public $masters = [];
354
    /**
355
     * @var array the configuration that should be merged with every master configuration listed in [[masters]].
356
     * For example,
357
     *
358
     * ```php
359
     * [
360
     *     'username' => 'master',
361
     *     'password' => 'master',
362
     *     'attributes' => [
363
     *         // use a smaller connection timeout
364
     *         PDO::ATTR_TIMEOUT => 10,
365
     *     ],
366
     * ]
367
     * ```
368
     */
369
    public $masterConfig = [];
370
    /**
371
     * @var bool whether to shuffle [[masters]] before getting one.
372
     * @since 2.0.11
373
     * @see masters
374
     */
375
    public $shuffleMasters = true;
376
    /**
377
     * @var bool whether to enable logging of database queries. Defaults to true.
378
     * You may want to disable this option in a production environment to gain performance
379
     * if you do not need the information being logged.
380
     * @since 2.0.12
381
     * @see enableProfiling
382
     */
383
    public $enableLogging = true;
384
    /**
385
     * @var bool whether to enable profiling of opening database connection and database queries. Defaults to true.
386
     * You may want to disable this option in a production environment to gain performance
387
     * if you do not need the information being logged.
388
     * @since 2.0.12
389
     * @see enableLogging
390
     */
391
    public $enableProfiling = true;
392
393
    /**
394
     * @var Transaction the currently active transaction
395
     */
396
    private $_transaction;
397
    /**
398
     * @var Schema the database schema
399
     */
400
    private $_schema;
401
    /**
402
     * @var string driver name
403
     */
404
    private $_driverName;
405
    /**
406
     * @var Connection|false the currently active master connection
407
     */
408
    private $_master = false;
409
    /**
410
     * @var Connection|false the currently active slave connection
411
     */
412
    private $_slave = false;
413
    /**
414
     * @var array query cache parameters for the [[cache()]] calls
415
     */
416
    private $_queryCacheInfo = [];
417
418
419
    /**
420
     * Returns a value indicating whether the DB connection is established.
421
     * @return bool whether the DB connection is established
422
     */
423 294
    public function getIsActive()
424
    {
425 294
        return $this->pdo !== null;
426
    }
427
428
    /**
429
     * Uses query cache for the queries performed with the callable.
430
     *
431
     * When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
432
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
433
     * For example,
434
     *
435
     * ```php
436
     * // The customer will be fetched from cache if available.
437
     * // If not, the query will be made against DB and cached for use next time.
438
     * $customer = $db->cache(function (Connection $db) {
439
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
440
     * });
441
     * ```
442
     *
443
     * Note that query cache is only meaningful for queries that return results. For queries performed with
444
     * [[Command::execute()]], query cache will not be used.
445
     *
446
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
447
     * The signature of the callable is `function (Connection $db)`.
448
     * @param int $duration the number of seconds that query results can remain valid in the cache. If this is
449
     * not set, the value of [[queryCacheDuration]] will be used instead.
450
     * Use 0 to indicate that the cached data will never expire.
451
     * @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query results.
452
     * @return mixed the return result of the callable
453
     * @throws \Exception|\Throwable if there is any exception during query
454
     * @see enableQueryCache
455
     * @see queryCache
456
     * @see noCache()
457
     */
458 3
    public function cache(callable $callable, $duration = null, $dependency = null)
459
    {
460 3
        $this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
461
        try {
462 3
            $result = call_user_func($callable, $this);
463 3
            array_pop($this->_queryCacheInfo);
464 3
            return $result;
465
        } catch (\Exception $e) {
466
            array_pop($this->_queryCacheInfo);
467
            throw $e;
468
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
469
            array_pop($this->_queryCacheInfo);
470
            throw $e;
471
        }
472
    }
473
474
    /**
475
     * Disables query cache temporarily.
476
     *
477
     * Queries performed within the callable will not use query cache at all. For example,
478
     *
479
     * ```php
480
     * $db->cache(function (Connection $db) {
481
     *
482
     *     // ... queries that use query cache ...
483
     *
484
     *     return $db->noCache(function (Connection $db) {
485
     *         // this query will not use query cache
486
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
487
     *     });
488
     * });
489
     * ```
490
     *
491
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache.
492
     * The signature of the callable is `function (Connection $db)`.
493
     * @return mixed the return result of the callable
494
     * @throws \Exception|\Throwable if there is any exception during query
495
     * @see enableQueryCache
496
     * @see queryCache
497
     * @see cache()
498
     */
499 19
    public function noCache(callable $callable)
500
    {
501 19
        $this->_queryCacheInfo[] = false;
502
        try {
503 19
            $result = call_user_func($callable, $this);
504 19
            array_pop($this->_queryCacheInfo);
505 19
            return $result;
506 3
        } catch (\Exception $e) {
507 3
            array_pop($this->_queryCacheInfo);
508 3
            throw $e;
509
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
510
            array_pop($this->_queryCacheInfo);
511
            throw $e;
512
        }
513
    }
514
515
    /**
516
     * Returns the current query cache information.
517
     * This method is used internally by [[Command]].
518
     * @param int $duration the preferred caching duration. If null, it will be ignored.
519
     * @param \yii\caching\Dependency $dependency the preferred caching dependency. If null, it will be ignored.
520
     * @return array the current query cache information, or null if query cache is not enabled.
521
     * @internal
522
     */
523 1060
    public function getQueryCacheInfo($duration, $dependency)
524
    {
525 1060
        if (!$this->enableQueryCache) {
526 23
            return null;
527
        }
528
529 1059
        $info = end($this->_queryCacheInfo);
530 1059
        if (is_array($info)) {
531 3
            if ($duration === null) {
532 3
                $duration = $info[0];
533
            }
534 3
            if ($dependency === null) {
535 3
                $dependency = $info[1];
536
            }
537
        }
538
539 1059
        if ($duration === 0 || $duration > 0) {
540 3
            if (is_string($this->queryCache) && Yii::$app) {
541
                $cache = Yii::$app->get($this->queryCache, false);
542
            } else {
543 3
                $cache = $this->queryCache;
544
            }
545 3
            if ($cache instanceof CacheInterface) {
546 3
                return [$cache, $duration, $dependency];
547
            }
548
        }
549
550 1059
        return null;
551
    }
552
553
    /**
554
     * Establishes a DB connection.
555
     * It does nothing if a DB connection has already been established.
556
     * @throws Exception if connection fails
557
     */
558 1348
    public function open()
559
    {
560 1348
        if ($this->pdo !== null) {
561 1136
            return;
562
        }
563
564 1295
        if (!empty($this->masters)) {
565 1
            $db = $this->getMaster();
566 1
            if ($db !== null) {
567 1
                $this->pdo = $db->pdo;
568 1
                return;
569
            }
570
571
            throw new InvalidConfigException('None of the master DB servers is available.');
572
        }
573
574 1295
        if (empty($this->dsn)) {
575
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
576
        }
577
578 1295
        $token = 'Opening DB connection: ' . $this->dsn;
579 1295
        $enableProfiling = $this->enableProfiling;
580
        try {
581 1295
            Yii::info($token, __METHOD__);
582 1295
            if ($enableProfiling) {
583 1295
                Yii::beginProfile($token, __METHOD__);
584
            }
585
586 1295
            $this->pdo = $this->createPdoInstance();
587 1295
            $this->initConnection();
588
589 1295
            if ($enableProfiling) {
590 1295
                Yii::endProfile($token, __METHOD__);
591
            }
592 4
        } catch (\PDOException $e) {
593 4
            if ($enableProfiling) {
594 4
                Yii::endProfile($token, __METHOD__);
595
            }
596
597 4
            throw new Exception($e->getMessage(), $e->errorInfo, (int) $e->getCode(), $e);
598
        }
599 1295
    }
600
601
    /**
602
     * Closes the currently active DB connection.
603
     * It does nothing if the connection is already closed.
604
     */
605 1561
    public function close()
606
    {
607 1561
        if ($this->_master) {
608
            if ($this->pdo === $this->_master->pdo) {
609
                $this->pdo = null;
610
            }
611
612
            $this->_master->close();
613
            $this->_master = false;
614
        }
615
616 1561
        if ($this->pdo !== null) {
617 1227
            Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__);
618 1227
            $this->pdo = null;
619 1227
            $this->_schema = null;
620 1227
            $this->_transaction = null;
621
        }
622
623 1561
        if ($this->_slave) {
624 4
            $this->_slave->close();
625 4
            $this->_slave = false;
626
        }
627 1561
    }
628
629
    /**
630
     * Creates the PDO instance.
631
     * This method is called by [[open]] to establish a DB connection.
632
     * The default implementation will create a PHP PDO instance.
633
     * You may override this method if the default PDO needs to be adapted for certain DBMS.
634
     * @return PDO the pdo instance
635
     */
636 1295
    protected function createPdoInstance()
637
    {
638 1295
        $pdoClass = $this->pdoClass;
639 1295
        if ($pdoClass === null) {
640 1295
            $pdoClass = 'PDO';
641 1295
            if ($this->_driverName !== null) {
642 116
                $driver = $this->_driverName;
643 1180
            } elseif (($pos = strpos($this->dsn, ':')) !== false) {
644 1180
                $driver = strtolower(substr($this->dsn, 0, $pos));
645
            }
646 1295
            if (isset($driver)) {
647 1295
                if ($driver === 'mssql' || $driver === 'dblib') {
648
                    $pdoClass = 'yii\db\mssql\PDO';
649 1295
                } elseif ($driver === 'sqlsrv') {
650
                    $pdoClass = 'yii\db\mssql\SqlsrvPDO';
651
                }
652
            }
653
        }
654
655 1295
        $dsn = $this->dsn;
656 1295
        if (strncmp('sqlite:@', $dsn, 8) === 0) {
657 1
            $dsn = 'sqlite:' . Yii::getAlias(substr($dsn, 7));
658
        }
659
660 1295
        return new $pdoClass($dsn, $this->username, $this->password, $this->attributes);
661
    }
662
663
    /**
664
     * Initializes the DB connection.
665
     * This method is invoked right after the DB connection is established.
666
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`
667
     * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty.
668
     * It then triggers an [[EVENT_AFTER_OPEN]] event.
669
     */
670 1295
    protected function initConnection()
671
    {
672 1295
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
673 1295
        if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) {
674
            $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare);
675
        }
676 1295
        if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'], true)) {
677
            $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset));
678
        }
679 1295
        $this->trigger(self::EVENT_AFTER_OPEN);
680 1295
    }
681
682
    /**
683
     * Creates a command for execution.
684
     * @param string $sql the SQL statement to be executed
685
     * @param array $params the parameters to be bound to the SQL statement
686
     * @return Command the DB command
687
     */
688 1131
    public function createCommand($sql = null, $params = [])
689
    {
690
        /** @var Command $command */
691 1131
        $command = new $this->commandClass([
692 1131
            'db' => $this,
693 1131
            'sql' => $sql,
694
        ]);
695
696 1131
        return $command->bindValues($params);
697
    }
698
699
    /**
700
     * Returns the currently active transaction.
701
     * @return Transaction the currently active transaction. Null if no active transaction.
702
     */
703 1105
    public function getTransaction()
704
    {
705 1105
        return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
706
    }
707
708
    /**
709
     * Starts a transaction.
710
     * @param string|null $isolationLevel The isolation level to use for this transaction.
711
     * See [[Transaction::begin()]] for details.
712
     * @return Transaction the transaction initiated
713
     */
714 32
    public function beginTransaction($isolationLevel = null)
715
    {
716 32
        $this->open();
717
718 32
        if (($transaction = $this->getTransaction()) === null) {
719 32
            $transaction = $this->_transaction = new Transaction(['db' => $this]);
720
        }
721 32
        $transaction->begin($isolationLevel);
722
723 32
        return $transaction;
724
    }
725
726
    /**
727
     * Executes callback provided in a transaction.
728
     *
729
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
730
     * @param string|null $isolationLevel The isolation level to use for this transaction.
731
     * See [[Transaction::begin()]] for details.
732
     * @throws \Exception|\Throwable if there is any exception during query. In this case the transaction will be rolled back.
733
     * @return mixed result of callback function
734
     */
735 16
    public function transaction(callable $callback, $isolationLevel = null)
736
    {
737 16
        $transaction = $this->beginTransaction($isolationLevel);
738 16
        $level = $transaction->level;
739
740
        try {
741 16
            $result = call_user_func($callback, $this);
742 12
            if ($transaction->isActive && $transaction->level === $level) {
743 12
                $transaction->commit();
744
            }
745 4
        } catch (\Exception $e) {
746 4
            $this->rollbackTransactionOnLevel($transaction, $level);
747 4
            throw $e;
748
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
749
            $this->rollbackTransactionOnLevel($transaction, $level);
750
            throw $e;
751
        }
752
753 12
        return $result;
754
    }
755
756
    /**
757
     * Rolls back given [[Transaction]] object if it's still active and level match.
758
     * In some cases rollback can fail, so this method is fail safe. Exception thrown
759
     * from rollback will be caught and just logged with [[\Yii::error()]].
760
     * @param Transaction $transaction Transaction object given from [[beginTransaction()]].
761
     * @param int $level Transaction level just after [[beginTransaction()]] call.
762
     */
763 4
    private function rollbackTransactionOnLevel($transaction, $level)
764
    {
765 4
        if ($transaction->isActive && $transaction->level === $level) {
766
            // https://github.com/yiisoft/yii2/pull/13347
767
            try {
768 4
                $transaction->rollBack();
769
            } catch (\Exception $e) {
770
                \Yii::error($e, __METHOD__);
771
                // hide this exception to be able to continue throwing original exception outside
772
            }
773
        }
774 4
    }
775
776
    /**
777
     * Returns the schema information for the database opened by this connection.
778
     * @return Schema the schema information for the database opened by this connection.
779
     * @throws NotSupportedException if there is no support for the current driver type
780
     */
781 1429
    public function getSchema()
782
    {
783 1429
        if ($this->_schema !== null) {
784 1182
            return $this->_schema;
785
        }
786
787 1376
        $driver = $this->getDriverName();
788 1376
        if (isset($this->schemaMap[$driver])) {
789 1376
            $config = !is_array($this->schemaMap[$driver]) ? ['class' => $this->schemaMap[$driver]] : $this->schemaMap[$driver];
790 1376
            $config['db'] = $this;
791
792 1376
            return $this->_schema = Yii::createObject($config);
793
        }
794
795
        throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS.");
796
    }
797
798
    /**
799
     * Returns the query builder for the current DB connection.
800
     * @return QueryBuilder the query builder for the current DB connection.
801
     */
802 842
    public function getQueryBuilder()
803
    {
804 842
        return $this->getSchema()->getQueryBuilder();
805
    }
806
807
    /**
808
     * Obtains the schema information for the named table.
809
     * @param string $name table name.
810
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
811
     * @return TableSchema table schema information. Null if the named table does not exist.
812
     */
813 119
    public function getTableSchema($name, $refresh = false)
814
    {
815 119
        return $this->getSchema()->getTableSchema($name, $refresh);
816
    }
817
818
    /**
819
     * Returns the ID of the last inserted row or sequence value.
820
     * @param string $sequenceName name of the sequence object (required by some DBMS)
821
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
822
     * @see http://php.net/manual/en/pdo.lastinsertid.php
823
     */
824
    public function getLastInsertID($sequenceName = '')
825
    {
826
        return $this->getSchema()->getLastInsertID($sequenceName);
827
    }
828
829
    /**
830
     * Quotes a string value for use in a query.
831
     * Note that if the parameter is not a string, it will be returned without change.
832
     * @param string $value string to be quoted
833
     * @return string the properly quoted string
834
     * @see http://php.net/manual/en/pdo.quote.php
835
     */
836 802
    public function quoteValue($value)
837
    {
838 802
        return $this->getSchema()->quoteValue($value);
839
    }
840
841
    /**
842
     * Quotes a table name for use in a query.
843
     * If the table name contains schema prefix, the prefix will also be properly quoted.
844
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{',
845
     * then this method will do nothing.
846
     * @param string $name table name
847
     * @return string the properly quoted table name
848
     */
849 954
    public function quoteTableName($name)
850
    {
851 954
        return $this->getSchema()->quoteTableName($name);
852
    }
853
854
    /**
855
     * Quotes a column name for use in a query.
856
     * If the column name contains prefix, the prefix will also be properly quoted.
857
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{',
858
     * then this method will do nothing.
859
     * @param string $name column name
860
     * @return string the properly quoted column name
861
     */
862 980
    public function quoteColumnName($name)
863
    {
864 980
        return $this->getSchema()->quoteColumnName($name);
865
    }
866
867
    /**
868
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
869
     * Tokens enclosed within double curly brackets are treated as table names, while
870
     * tokens enclosed within double square brackets are column names. They will be quoted accordingly.
871
     * Also, the percentage character "%" at the beginning or ending of a table name will be replaced
872
     * with [[tablePrefix]].
873
     * @param string $sql the SQL to be quoted
874
     * @return string the quoted SQL
875
     */
876 1174
    public function quoteSql($sql)
877
    {
878 1174
        return preg_replace_callback(
879 1174
            '/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
880 1174
            function ($matches) {
881 570
                if (isset($matches[3])) {
882 412
                    return $this->quoteColumnName($matches[3]);
883
                }
884
885 529
                return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
886 1174
            },
887 1174
            $sql
888
        );
889
    }
890
891
    /**
892
     * Returns the name of the DB driver. Based on the the current [[dsn]], in case it was not set explicitly
893
     * by an end user.
894
     * @return string name of the DB driver
895
     */
896 1387
    public function getDriverName()
897
    {
898 1387
        if ($this->_driverName === null) {
899 1363
            if (($pos = strpos($this->dsn, ':')) !== false) {
900 1363
                $this->_driverName = strtolower(substr($this->dsn, 0, $pos));
901
            } else {
902
                $this->_driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME));
903
            }
904
        }
905
906 1387
        return $this->_driverName;
907
    }
908
909
    /**
910
     * Changes the current driver name.
911
     * @param string $driverName name of the DB driver
912
     */
913
    public function setDriverName($driverName)
914
    {
915
        $this->_driverName = strtolower($driverName);
916
    }
917
918
    /**
919
     * Returns the PDO instance for the currently active slave connection.
920
     * When [[enableSlaves]] is true, one of the slaves will be used for read queries, and its PDO instance
921
     * will be returned by this method.
922
     * @param bool $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
923
     * @return PDO the PDO instance for the currently active slave connection. `null` is returned if no slave connection
924
     * is available and `$fallbackToMaster` is false.
925
     */
926 1119
    public function getSlavePdo($fallbackToMaster = true)
927
    {
928 1119
        $db = $this->getSlave(false);
929 1119
        if ($db === null) {
930 1115
            return $fallbackToMaster ? $this->getMasterPdo() : null;
931
        }
932
933 5
        return $db->pdo;
934
    }
935
936
    /**
937
     * Returns the PDO instance for the currently active master connection.
938
     * This method will open the master DB connection and then return [[pdo]].
939
     * @return PDO the PDO instance for the currently active master connection.
940
     */
941 1146
    public function getMasterPdo()
942
    {
943 1146
        $this->open();
944 1146
        return $this->pdo;
945
    }
946
947
    /**
948
     * Returns the currently active slave connection.
949
     * If this method is called for the first time, it will try to open a slave connection when [[enableSlaves]] is true.
950
     * @param bool $fallbackToMaster whether to return a master connection in case there is no slave connection available.
951
     * @return Connection the currently active slave connection. `null` is returned if there is no slave available and
952
     * `$fallbackToMaster` is false.
953
     */
954 1121
    public function getSlave($fallbackToMaster = true)
955
    {
956 1121
        if (!$this->enableSlaves) {
957 90
            return $fallbackToMaster ? $this : null;
958
        }
959
960 1049
        if ($this->_slave === false) {
961 1049
            $this->_slave = $this->openFromPool($this->slaves, $this->slaveConfig);
962
        }
963
964 1049
        return $this->_slave === null && $fallbackToMaster ? $this : $this->_slave;
965
    }
966
967
    /**
968
     * Returns the currently active master connection.
969
     * If this method is called for the first time, it will try to open a master connection.
970
     * @return Connection the currently active master connection. `null` is returned if there is no master available.
971
     * @since 2.0.11
972
     */
973 3
    public function getMaster()
974
    {
975 3
        if ($this->_master === false) {
976 3
            $this->_master = ($this->shuffleMasters)
977 2
                ? $this->openFromPool($this->masters, $this->masterConfig)
978 1
                : $this->openFromPoolSequentially($this->masters, $this->masterConfig);
979
        }
980
981 3
        return $this->_master;
982
    }
983
984
    /**
985
     * Executes the provided callback by using the master connection.
986
     *
987
     * This method is provided so that you can temporarily force using the master connection to perform
988
     * DB operations even if they are read queries. For example,
989
     *
990
     * ```php
991
     * $result = $db->useMaster(function ($db) {
992
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
993
     * });
994
     * ```
995
     *
996
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
997
     * `function (Connection $db)`. Its return value will be returned by this method.
998
     * @return mixed the return value of the callback
999
     * @throws \Exception|\Throwable if there is any exception thrown from the callback
1000
     */
1001 7
    public function useMaster(callable $callback)
1002
    {
1003 7
        if ($this->enableSlaves) {
1004 7
            $this->enableSlaves = false;
1005
            try {
1006 7
                $result = call_user_func($callback, $this);
1007 1
            } catch (\Exception $e) {
1008 1
                $this->enableSlaves = true;
1009 1
                throw $e;
1010
            } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1011
                $this->enableSlaves = true;
1012
                throw $e;
1013
            }
1014
            // TODO: use "finally" keyword when miminum required PHP version is >= 5.5
1015 6
            $this->enableSlaves = true;
1016
        } else {
1017
            $result = call_user_func($callback, $this);
1018
        }
1019
1020 6
        return $result;
1021
    }
1022
1023
    /**
1024
     * Opens the connection to a server in the pool.
1025
     * This method implements the load balancing among the given list of the servers.
1026
     * Connections will be tried in random order.
1027
     * @param array $pool the list of connection configurations in the server pool
1028
     * @param array $sharedConfig the configuration common to those given in `$pool`.
1029
     * @return Connection the opened DB connection, or `null` if no server is available
1030
     * @throws InvalidConfigException if a configuration does not specify "dsn"
1031
     */
1032 1049
    protected function openFromPool(array $pool, array $sharedConfig)
1033
    {
1034 1049
        shuffle($pool);
1035 1049
        return $this->openFromPoolSequentially($pool, $sharedConfig);
1036
    }
1037
1038
    /**
1039
     * Opens the connection to a server in the pool.
1040
     * This method implements the load balancing among the given list of the servers.
1041
     * Connections will be tried in sequential order.
1042
     * @param array $pool the list of connection configurations in the server pool
1043
     * @param array $sharedConfig the configuration common to those given in `$pool`.
1044
     * @return Connection the opened DB connection, or `null` if no server is available
1045
     * @throws InvalidConfigException if a configuration does not specify "dsn"
1046
     * @since 2.0.11
1047
     */
1048 1049
    protected function openFromPoolSequentially(array $pool, array $sharedConfig)
1049
    {
1050 1049
        if (empty($pool)) {
1051 1043
            return null;
1052
        }
1053
1054 7
        if (!isset($sharedConfig['class'])) {
1055 7
            $sharedConfig['class'] = get_class($this);
1056
        }
1057
1058 7
        $cache = is_string($this->serverStatusCache) ? Yii::$app->get($this->serverStatusCache, false) : $this->serverStatusCache;
1059
1060 7
        foreach ($pool as $config) {
1061 7
            $config = array_merge($sharedConfig, $config);
1062 7
            if (empty($config['dsn'])) {
1063
                throw new InvalidConfigException('The "dsn" option must be specified.');
1064
            }
1065
1066 7
            $key = [__METHOD__, $config['dsn']];
1067 7
            if ($cache instanceof CacheInterface && $cache->get($key)) {
1068
                // should not try this dead server now
1069
                continue;
1070
            }
1071
1072
            /* @var $db Connection */
1073 7
            $db = Yii::createObject($config);
1074
1075
            try {
1076 7
                $db->open();
1077 7
                return $db;
1078
            } catch (\Exception $e) {
1079
                Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__);
1080
                if ($cache instanceof CacheInterface) {
1081
                    // mark this server as dead and only retry it after the specified interval
1082
                    $cache->set($key, 1, $this->serverRetryInterval);
1083
                }
1084
            }
1085
        }
1086
1087
        return null;
1088
    }
1089
1090
    /**
1091
     * Close the connection before serializing.
1092
     * @return array
1093
     */
1094 17
    public function __sleep()
1095
    {
1096 17
        $fields = (array) $this;
1097
1098 17
        unset($fields['pdo']);
1099 17
        unset($fields["\000" . __CLASS__ . "\000" . '_master']);
1100 17
        unset($fields["\000" . __CLASS__ . "\000" . '_slave']);
1101 17
        unset($fields["\000" . __CLASS__ . "\000" . '_transaction']);
1102 17
        unset($fields["\000" . __CLASS__ . "\000" . '_schema']);
1103
1104 17
        return array_keys($fields);
1105
    }
1106
1107
    /**
1108
     * Reset the connection after cloning.
1109
     */
1110 7
    public function __clone()
1111
    {
1112 7
        parent::__clone();
1113
1114 7
        $this->_master = false;
1115 7
        $this->_slave = false;
1116 7
        $this->_schema = null;
1117 7
        $this->_transaction = null;
1118 7
        if (strncmp($this->dsn, 'sqlite::memory:', 15) !== 0) {
1119
            // reset PDO connection, unless its sqlite in-memory, which can only have one connection
1120 6
            $this->pdo = null;
1121
        }
1122 7
    }
1123
}
1124