Completed
Push — 2.1 ( 75dda6...7ef2cf )
by Alexander
04:23
created

Connection::cache()   A

Complexity

Conditions 3
Paths 6

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.125

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 5
cts 10
cp 0.5
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 6
nop 3
crap 4.125
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\Cache;
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::class,
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 Cache|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 Cache|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' => pgsql\Schema::class, // PostgreSQL
277
        'mysqli' => mysql\Schema::class, // MySQL
278
        'mysql' => mysql\Schema::class, // MySQL
279
        'sqlite' => sqlite\Schema::class, // sqlite 3
280
        'sqlite2' => sqlite\Schema::class, // sqlite 2
281
        'sqlsrv' => mssql\Schema::class, // newer MSSQL driver on MS Windows hosts
282
        'oci' => oci\Schema::class, // Oracle driver
283
        'mssql' => mssql\Schema::class, // older MSSQL driver on MS Windows hosts
284
        'dblib' => mssql\Schema::class, // dblib drivers on GNU/Linux (and maybe other OSes) hosts
285
        'cubrid' => cubrid\Schema::class, // 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 = Command::class;
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 Cache|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 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 the currently active master connection
407
     */
408
    private $_master = false;
409
    /**
410
     * @var Connection 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 198
    public function getIsActive()
424
    {
425 198
        return $this->pdo !== null;
426
    }
427
428
    /**
429
     * Uses query cache for the queries performed with the callable.
430
     * When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
431
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
432
     * For example,
433
     *
434
     * ```php
435
     * // The customer will be fetched from cache if available.
436
     * // If not, the query will be made against DB and cached for use next time.
437
     * $customer = $db->cache(function (Connection $db) {
438
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
439
     * });
440
     * ```
441
     *
442
     * Note that query cache is only meaningful for queries that return results. For queries performed with
443
     * [[Command::execute()]], query cache will not be used.
444
     *
445
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
446
     * The signature of the callable is `function (Connection $db)`.
447
     * @param int $duration the number of seconds that query results can remain valid in the cache. If this is
448
     * not set, the value of [[queryCacheDuration]] will be used instead.
449
     * Use 0 to indicate that the cached data will never expire.
450
     * @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query results.
451
     * @return mixed the return result of the callable
452
     * @throws \Throwable if there is any exception during query
453
     * @see enableQueryCache
454
     * @see queryCache
455
     * @see noCache()
456
     */
457 3
    public function cache(callable $callable, $duration = null, $dependency = null)
458
    {
459 3
        $this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
460
        try {
461 3
            $result = call_user_func($callable, $this);
462 3
            array_pop($this->_queryCacheInfo);
463 3
            return $result;
464
        } catch (\Throwable $e) {
465
            array_pop($this->_queryCacheInfo);
466
            throw $e;
467
        }
468
    }
469
470
    /**
471
     * Disables query cache temporarily.
472
     * Queries performed within the callable will not use query cache at all. For example,
473
     *
474
     * ```php
475
     * $db->cache(function (Connection $db) {
476
     *
477
     *     // ... queries that use query cache ...
478
     *
479
     *     return $db->noCache(function (Connection $db) {
480
     *         // this query will not use query cache
481
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
482
     *     });
483
     * });
484
     * ```
485
     *
486
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache.
487
     * The signature of the callable is `function (Connection $db)`.
488
     * @return mixed the return result of the callable
489
     * @throws \Throwable if there is any exception during query
490
     * @see enableQueryCache
491
     * @see queryCache
492
     * @see cache()
493
     */
494
    public function noCache(callable $callable)
495
    {
496
        $this->_queryCacheInfo[] = false;
497 19
        try {
498
            $result = call_user_func($callable, $this);
499 19
            array_pop($this->_queryCacheInfo);
500
            return $result;
501 19
        } catch (\Throwable $e) {
502 19
            array_pop($this->_queryCacheInfo);
503 19
            throw $e;
504 3
        }
505 3
    }
506 3
507
    /**
508
     * Returns the current query cache information.
509
     * This method is used internally by [[Command]].
510
     * @param int $duration the preferred caching duration. If null, it will be ignored.
511
     * @param \yii\caching\Dependency $dependency the preferred caching dependency. If null, it will be ignored.
512
     * @return array the current query cache information, or null if query cache is not enabled.
513
     * @internal
514
     */
515
    public function getQueryCacheInfo($duration, $dependency)
516
    {
517
        if (!$this->enableQueryCache) {
518
            return null;
519
        }
520
521 761
        $info = end($this->_queryCacheInfo);
522
        if (is_array($info)) {
523 761
            if ($duration === null) {
524 23
                $duration = $info[0];
525
            }
526
            if ($dependency === null) {
527 760
                $dependency = $info[1];
528 760
            }
529 3
        }
530 3
531
        if ($duration === 0 || $duration > 0) {
532 3
            if (is_string($this->queryCache) && Yii::$app) {
533 3
                $cache = Yii::$app->get($this->queryCache, false);
534
            } else {
535
                $cache = $this->queryCache;
536
            }
537 760
            if ($cache instanceof Cache) {
538 3
                return [$cache, $duration, $dependency];
539
            }
540
        }
541 3
542
        return null;
543 3
    }
544 3
545
    /**
546
     * Establishes a DB connection.
547
     * It does nothing if a DB connection has already been established.
548 760
     * @throws Exception if connection fails
549
     */
550
    public function open()
551
    {
552
        if ($this->pdo !== null) {
553
            return;
554
        }
555
556 963
        if (!empty($this->masters)) {
557
            $db = $this->getMaster();
558 963
            if ($db !== null) {
559 800
                $this->pdo = $db->pdo;
560
                return;
561
            } else {
562 932
                throw new InvalidConfigException('None of the master DB servers is available.');
563 1
            }
564 1
        }
565 1
566 1
        if (empty($this->dsn)) {
567
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
568
        }
569
        $token = 'Opening DB connection: ' . $this->dsn;
570
        try {
571
            Yii::info($token, __METHOD__);
572 932
            Yii::beginProfile($token, __METHOD__);
573
            $this->pdo = $this->createPdoInstance();
574
            $this->initConnection();
575 932
            Yii::endProfile($token, __METHOD__);
576
        } catch (\PDOException $e) {
577 932
            Yii::endProfile($token, __METHOD__);
578 932
            throw new Exception($e->getMessage(), $e->errorInfo, (int) $e->getCode(), $e);
579 932
        }
580 932
    }
581 932
582 4
    /**
583 4
     * Closes the currently active DB connection.
584 4
     * It does nothing if the connection is already closed.
585
     */
586 932
    public function close()
587
    {
588
        if ($this->_master) {
589
            if ($this->pdo === $this->_master->pdo) {
590
                $this->pdo = null;
591
            }
592 1203
593
            $this->_master->close();
594 1203
            $this->_master = null;
595
        }
596
597
        if ($this->pdo !== null) {
598
            Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__);
599
            $this->pdo = null;
600
            $this->_schema = null;
601
            $this->_transaction = null;
602
        }
603 1203
604 878
        if ($this->_slave) {
605 878
            $this->_slave->close();
606 878
            $this->_slave = null;
607 878
        }
608
    }
609
610 1203
    /**
611
     * Creates the PDO instance.
612
     * This method is called by [[open]] to establish a DB connection.
613
     * The default implementation will create a PHP PDO instance.
614 1203
     * You may override this method if the default PDO needs to be adapted for certain DBMS.
615
     * @return PDO the pdo instance
616
     */
617
    protected function createPdoInstance()
618
    {
619
        $pdoClass = $this->pdoClass;
620
        if ($pdoClass === null) {
621
            $pdoClass = 'PDO';
622
            if ($this->_driverName !== null) {
623 932
                $driver = $this->_driverName;
624
            } elseif (($pos = strpos($this->dsn, ':')) !== false) {
625 932
                $driver = strtolower(substr($this->dsn, 0, $pos));
626 932
            }
627 932
            if (isset($driver)) {
628 932
                if ($driver === 'mssql' || $driver === 'dblib') {
629 95
                    $pdoClass = mssql\PDO::class;
630 839
                } elseif ($driver === 'sqlsrv') {
631 839
                    $pdoClass = mssql\SqlsrvPDO::class;
632
                }
633 932
            }
634 932
        }
635
636 932
        $dsn = $this->dsn;
637
        if (strncmp('sqlite:@', $dsn, 8) === 0) {
638
            $dsn = 'sqlite:' . Yii::getAlias(substr($dsn, 7));
639
        }
640
        return new $pdoClass($dsn, $this->username, $this->password, $this->attributes);
641
    }
642 932
643 932
    /**
644 1
     * Initializes the DB connection.
645
     * This method is invoked right after the DB connection is established.
646 932
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`
647
     * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty.
648
     * It then triggers an [[EVENT_AFTER_OPEN]] event.
649
     */
650
    protected function initConnection()
651
    {
652
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
653
        if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) {
654
            $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare);
655
        }
656 932
        if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'], true)) {
657
            $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset));
658 932
        }
659 932
        $this->trigger(self::EVENT_AFTER_OPEN);
660
    }
661
662 932
    /**
663
     * Creates a command for execution.
664
     * @param string $sql the SQL statement to be executed
665 932
     * @param array $params the parameters to be bound to the SQL statement
666 932
     * @return Command the DB command
667
     */
668
    public function createCommand($sql = null, $params = [])
669
    {
670
        /** @var Command $command */
671
        $command = new $this->commandClass([
672
            'db' => $this,
673
            'sql' => $sql,
674 824
        ]);
675
676
        return $command->bindValues($params);
677 824
    }
678 824
679 824
    /**
680
     * Returns the currently active transaction.
681
     * @return Transaction the currently active transaction. Null if no active transaction.
682 824
     */
683
    public function getTransaction()
684
    {
685
        return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
686
    }
687
688
    /**
689 798
     * Starts a transaction.
690
     * @param string|null $isolationLevel The isolation level to use for this transaction.
691 798
     * See [[Transaction::begin()]] for details.
692
     * @return Transaction the transaction initiated
693
     */
694
    public function beginTransaction($isolationLevel = null)
695
    {
696
        $this->open();
697
698
        if (($transaction = $this->getTransaction()) === null) {
699
            $transaction = $this->_transaction = new Transaction(['db' => $this]);
700 32
        }
701
        $transaction->begin($isolationLevel);
702 32
703
        return $transaction;
704 32
    }
705 32
706
    /**
707 32
     * Executes callback provided in a transaction.
708
     *
709 32
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
710
     * @param string|null $isolationLevel The isolation level to use for this transaction.
711
     * See [[Transaction::begin()]] for details.
712
     * @throws \Throwable if there is any exception during query. In this case the transaction will be rolled back.
713
     * @return mixed result of callback function
714
     */
715
    public function transaction(callable $callback, $isolationLevel = null)
716
    {
717
        $transaction = $this->beginTransaction($isolationLevel);
718
        $level = $transaction->level;
719
720
        try {
721 16
            $result = call_user_func($callback, $this);
722
            if ($transaction->isActive && $transaction->level === $level) {
723 16
                $transaction->commit();
724 16
            }
725
        } catch (\Throwable $e) {
726
            $this->rollbackTransactionOnLevel($transaction, $level);
727 16
            throw $e;
728 12
        }
729 12
730
        return $result;
731 4
    }
732 4
733 4
    /**
734
     * Rolls back given [[Transaction]] object if it's still active and level match.
735
     * In some cases rollback can fail, so this method is fail safe. Exception thrown
736
     * from rollback will be caught and just logged with [[\Yii::error()]].
737
     * @param Transaction $transaction Transaction object given from [[beginTransaction()]].
738
     * @param int $level Transaction level just after [[beginTransaction()]] call.
739 12
     */
740
    private function rollbackTransactionOnLevel($transaction, $level)
741
    {
742
        if ($transaction->isActive && $transaction->level === $level) {
743
            // https://github.com/yiisoft/yii2/pull/13347
744
            try {
745
                $transaction->rollBack();
746
            } catch (\Exception $e) {
747
                \Yii::error($e, __METHOD__);
748
                // hide this exception to be able to continue throwing original exception outside
749 4
            }
750
        }
751 4
    }
752
753
    /**
754 4
     * Returns the schema information for the database opened by this connection.
755
     * @return Schema the schema information for the database opened by this connection.
756
     * @throws NotSupportedException if there is no support for the current driver type
757
     */
758
    public function getSchema()
759
    {
760 4
        if ($this->_schema !== null) {
761
            return $this->_schema;
762
        } else {
763
            $driver = $this->getDriverName();
764
            if (isset($this->schemaMap[$driver])) {
765
                $config = !is_array($this->schemaMap[$driver]) ? ['class' => $this->schemaMap[$driver]] : $this->schemaMap[$driver];
766
                $config['db'] = $this;
767 1044
768
                return $this->_schema = Yii::createObject($config);
769 1044
            } else {
770 883
                throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS.");
771
            }
772 1005
        }
773 1005
    }
774 1005
775 1005
    /**
776
     * Returns the query builder for the current DB connection.
777 1005
     * @return QueryBuilder the query builder for the current DB connection.
778
     */
779
    public function getQueryBuilder()
780
    {
781
        return $this->getSchema()->getQueryBuilder();
782
    }
783
784
    /**
785
     * Obtains the schema information for the named table.
786
     * @param string $name table name.
787
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
788 679
     * @return TableSchema table schema information. Null if the named table does not exist.
789
     */
790 679
    public function getTableSchema($name, $refresh = false)
791
    {
792
        return $this->getSchema()->getTableSchema($name, $refresh);
793
    }
794
795
    /**
796
     * Returns the ID of the last inserted row or sequence value.
797
     * @param string $sequenceName name of the sequence object (required by some DBMS)
798
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
799 103
     * @see http://php.net/manual/en/pdo.lastinsertid.php
800
     */
801 103
    public function getLastInsertID($sequenceName = '')
802
    {
803
        return $this->getSchema()->getLastInsertID($sequenceName);
804
    }
805
806
    /**
807
     * Quotes a string value for use in a query.
808
     * Note that if the parameter is not a string, it will be returned without change.
809
     * @param string $value string to be quoted
810
     * @return string the properly quoted string
811
     * @see http://php.net/manual/en/pdo.quote.php
812
     */
813
    public function quoteValue($value)
814
    {
815
        return $this->getSchema()->quoteValue($value);
816
    }
817
818
    /**
819
     * Quotes a table name for use in a query.
820
     * If the table name contains schema prefix, the prefix will also be properly quoted.
821
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{',
822 556
     * then this method will do nothing.
823
     * @param string $name table name
824 556
     * @return string the properly quoted table name
825
     */
826
    public function quoteTableName($name)
827
    {
828
        return $this->getSchema()->quoteTableName($name);
829
    }
830
831
    /**
832
     * Quotes a column name for use in a query.
833
     * If the column name contains prefix, the prefix will also be properly quoted.
834
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{',
835 750
     * then this method will do nothing.
836
     * @param string $name column name
837 750
     * @return string the properly quoted column name
838
     */
839
    public function quoteColumnName($name)
840
    {
841
        return $this->getSchema()->quoteColumnName($name);
842
    }
843
844
    /**
845
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
846
     * Tokens enclosed within double curly brackets are treated as table names, while
847
     * tokens enclosed within double square brackets are column names. They will be quoted accordingly.
848 805
     * Also, the percentage character "%" at the beginning or ending of a table name will be replaced
849
     * with [[tablePrefix]].
850 805
     * @param string $sql the SQL to be quoted
851
     * @return string the quoted SQL
852
     */
853
    public function quoteSql($sql)
854
    {
855
        return preg_replace_callback(
856
            '/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
857
            function ($matches) {
858
                if (isset($matches[3])) {
859
                    return $this->quoteColumnName($matches[3]);
860
                } else {
861
                    return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
862 832
                }
863
            },
864 832
            $sql
865 832
        );
866 832
    }
867 388
868 324
    /**
869
     * Returns the name of the DB driver. Based on the the current [[dsn]], in case it was not set explicitly
870 352
     * by an end user.
871
     * @return string name of the DB driver
872 832
     */
873 832
    public function getDriverName()
874
    {
875
        if ($this->_driverName === null) {
876
            if (($pos = strpos($this->dsn, ':')) !== false) {
877
                $this->_driverName = strtolower(substr($this->dsn, 0, $pos));
878
            } else {
879
                $this->_driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME));
880
            }
881
        }
882 1015
        return $this->_driverName;
883
    }
884 1015
885 995
    /**
886 995
     * Changes the current driver name.
887
     * @param string $driverName name of the DB driver
888
     */
889
    public function setDriverName($driverName)
890
    {
891 1015
        $this->_driverName = strtolower($driverName);
892
    }
893
894
    /**
895
     * Returns the PDO instance for the currently active slave connection.
896
     * When [[enableSlaves]] is true, one of the slaves will be used for read queries, and its PDO instance
897
     * will be returned by this method.
898
     * @param bool $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
899
     * @return PDO the PDO instance for the currently active slave connection. `null` is returned if no slave connection
900
     * is available and `$fallbackToMaster` is false.
901
     */
902
    public function getSlavePdo($fallbackToMaster = true)
903
    {
904
        $db = $this->getSlave(false);
905
        if ($db === null) {
906
            return $fallbackToMaster ? $this->getMasterPdo() : null;
907
        } else {
908
            return $db->pdo;
909
        }
910
    }
911 776
912
    /**
913 776
     * Returns the PDO instance for the currently active master connection.
914 776
     * This method will open the master DB connection and then return [[pdo]].
915 776
     * @return PDO the PDO instance for the currently active master connection.
916
     */
917 1
    public function getMasterPdo()
918
    {
919
        $this->open();
920
        return $this->pdo;
921
    }
922
923
    /**
924
     * Returns the currently active slave connection.
925
     * If this method is called for the first time, it will try to open a slave connection when [[enableSlaves]] is true.
926 803
     * @param bool $fallbackToMaster whether to return a master connection in case there is no slave connection available.
927
     * @return Connection the currently active slave connection. `null` is returned if there is no slave available and
928 803
     * `$fallbackToMaster` is false.
929 803
     */
930
    public function getSlave($fallbackToMaster = true)
931
    {
932
        if (!$this->enableSlaves) {
933
            return $fallbackToMaster ? $this : null;
934
        }
935
936
        if ($this->_slave === false) {
937
            $this->_slave = $this->openFromPool($this->slaves, $this->slaveConfig);
938
        }
939 778
940
        return $this->_slave === null && $fallbackToMaster ? $this : $this->_slave;
941 778
    }
942 60
943
    /**
944
     * Returns the currently active master connection.
945 735
     * If this method is called for the first time, it will try to open a master connection.
946 735
     * @return Connection the currently active master connection. `null` is returned if there is no master available.
947
     * @since 2.0.11
948
     */
949 735
    public function getMaster()
950
    {
951
        if ($this->_master === false) {
952
            $this->_master = ($this->shuffleMasters)
953
                ? $this->openFromPool($this->masters, $this->masterConfig)
954
                : $this->openFromPoolSequentially($this->masters, $this->masterConfig);
955
        }
956
957
        return $this->_master;
958 3
    }
959
960 3
    /**
961 3
     * Executes the provided callback by using the master connection.
962 2
     *
963 1
     * This method is provided so that you can temporarily force using the master connection to perform
964
     * DB operations even if they are read queries. For example,
965
     *
966 3
     * ```php
967
     * $result = $db->useMaster(function ($db) {
968
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
969
     * });
970
     * ```
971
     *
972
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
973
     * `function (Connection $db)`. Its return value will be returned by this method.
974
     * @return mixed the return value of the callback
975
     * @throws \Throwable if there is any exception thrown from the callback
976
     */
977
    public function useMaster(callable $callback)
978
    {
979
        if ($this->enableSlaves) {
980
            $this->enableSlaves = false;
981
            try {
982
                $result = call_user_func($callback, $this);
983
            } catch (\Throwable $e) {
984
                $this->enableSlaves = true;
985
                throw $e;
986 3
            }
987
            // TODO: use "finally" keyword when miminum required PHP version is >= 5.5
988 3
            $this->enableSlaves = true;
989 3
        } else {
990
            $result = call_user_func($callback, $this);
991 3
        }
992 1
993 1
        return $result;
994 1
    }
995
996
    /**
997
     * Opens the connection to a server in the pool.
998
     * This method implements the load balancing among the given list of the servers.
999
     * Connections will be tried in random order.
1000 2
     * @param array $pool the list of connection configurations in the server pool
1001
     * @param array $sharedConfig the configuration common to those given in `$pool`.
1002
     * @return Connection the opened DB connection, or `null` if no server is available
1003
     * @throws InvalidConfigException if a configuration does not specify "dsn"
1004
     */
1005 2
    protected function openFromPool(array $pool, array $sharedConfig)
1006
    {
1007
        shuffle($pool);
1008
        return $this->openFromPoolSequentially($pool, $sharedConfig);
1009
    }
1010
1011
    /**
1012
     * Opens the connection to a server in the pool.
1013
     * This method implements the load balancing among the given list of the servers.
1014
     * Connections will be tried in sequential order.
1015
     * @param array $pool the list of connection configurations in the server pool
1016
     * @param array $sharedConfig the configuration common to those given in `$pool`.
1017 735
     * @return Connection the opened DB connection, or `null` if no server is available
1018
     * @throws InvalidConfigException if a configuration does not specify "dsn"
1019 735
     * @since 2.0.11
1020 735
     */
1021
    protected function openFromPoolSequentially(array $pool, array $sharedConfig)
1022
    {
1023
        if (empty($pool)) {
1024
            return null;
1025
        }
1026
1027
        if (!isset($sharedConfig['class'])) {
1028
            $sharedConfig['class'] = get_class($this);
1029
        }
1030
1031
        $cache = is_string($this->serverStatusCache) ? Yii::$app->get($this->serverStatusCache, false) : $this->serverStatusCache;
1032
1033 735
        foreach ($pool as $config) {
1034
            $config = array_merge($sharedConfig, $config);
1035 735
            if (empty($config['dsn'])) {
1036 733
                throw new InvalidConfigException('The "dsn" option must be specified.');
1037
            }
1038
1039 3
            $key = [__METHOD__, $config['dsn']];
1040 3
            if ($cache instanceof Cache && $cache->get($key)) {
1041
                // should not try this dead server now
1042
                continue;
1043 3
            }
1044
1045 3
            /* @var $db Connection */
1046 3
            $db = Yii::createObject($config);
1047 3
1048
            try {
1049
                $db->open();
1050
                return $db;
1051 3
            } catch (\Exception $e) {
1052 3
                Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__);
1053
                if ($cache instanceof Cache) {
1054
                    // mark this server as dead and only retry it after the specified interval
1055
                    $cache->set($key, 1, $this->serverRetryInterval);
1056
                }
1057
            }
1058 3
        }
1059
1060
        return null;
1061 3
    }
1062 3
1063
    /**
1064
     * Close the connection before serializing.
1065
     * @return array
1066
     */
1067
    public function __sleep()
1068
    {
1069
        $this->close();
1070
        return array_keys((array) $this);
1071
    }
1072
1073
    /**
1074
     * Reset the connection after cloning.
1075
     */
1076
    public function __clone()
1077
    {
1078
        parent::__clone();
1079 14
1080
        $this->_master = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<yii\db\Connection> of property $_master.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1081 14
        $this->_slave = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<yii\db\Connection> of property $_slave.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1082 14
        $this->_schema = null;
1083
        $this->_transaction = null;
1084
        if (strncmp($this->dsn, 'sqlite::memory:', 15) !== 0) {
1085
            // reset PDO connection, unless its sqlite in-memory, which can only have one connection
1086
            $this->pdo = null;
1087
        }
1088 7
    }
1089
}
1090