Connection.php 35 KB
Newer Older
w  
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
w  
Qiang Xue committed
5 6 7
 * @license http://www.yiiframework.com/license/
 */

Qiang Xue committed
8
namespace yii\db;
w  
Qiang Xue committed
9

Qiang Xue committed
10 11
use PDO;
use Yii;
12
use yii\base\Component;
Qiang Xue committed
13 14
use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
15
use yii\caching\Cache;
w  
Qiang Xue committed
16

w  
Qiang Xue committed
17
/**
w  
Qiang Xue committed
18
 * Connection represents a connection to a database via [PDO](http://www.php.net/manual/en/ref.pdo.php).
w  
Qiang Xue committed
19
 *
w  
Qiang Xue committed
20 21 22
 * Connection works together with [[Command]], [[DataReader]] and [[Transaction]]
 * to provide data access to various DBMS in a common set of APIs. They are a thin wrapper
 * of the [[PDO PHP extension]](http://www.php.net/manual/en/ref.pdo.php).
w  
Qiang Xue committed
23
 *
Qiang Xue committed
24 25 26 27 28
 * Connection supports database replication and read-write splitting. In particular, a Connection component
 * can be configured with multiple [[masters]] and [[slaves]]. It will do load balancing and failover by choosing
 * appropriate servers. It will also automatically direct read operations to the slaves and write operations to
 * the masters.
 *
w  
Qiang Xue committed
29
 * To establish a DB connection, set [[dsn]], [[username]] and [[password]], and then
30
 * call [[open()]] to be true.
w  
Qiang Xue committed
31 32
 *
 * The following example shows how to create a Connection instance and establish
w  
Qiang Xue committed
33
 * the DB connection:
w  
Qiang Xue committed
34
 *
w  
Qiang Xue committed
35
 * ~~~
Alexander Makarov committed
36
 * $connection = new \yii\db\Connection([
Qiang Xue committed
37 38 39
 *     'dsn' => $dsn,
 *     'username' => $username,
 *     'password' => $password,
Alexander Makarov committed
40
 * ]);
41
 * $connection->open();
w  
Qiang Xue committed
42 43
 * ~~~
 *
Qiang Xue committed
44
 * After the DB connection is established, one can execute SQL statements like the following:
w  
Qiang Xue committed
45 46
 *
 * ~~~
47
 * $command = $connection->createCommand('SELECT * FROM post');
Qiang Xue committed
48
 * $posts = $command->queryAll();
49
 * $command = $connection->createCommand('UPDATE post SET status=1');
Qiang Xue committed
50
 * $command->execute();
w  
Qiang Xue committed
51 52
 * ~~~
 *
Qiang Xue committed
53 54 55
 * One can also do prepared SQL execution and bind parameters to the prepared SQL.
 * When the parameters are coming from user input, you should use this approach
 * to prevent SQL injection attacks. The following is an example:
w  
Qiang Xue committed
56
 *
w  
Qiang Xue committed
57
 * ~~~
58
 * $command = $connection->createCommand('SELECT * FROM post WHERE id=:id');
w  
Qiang Xue committed
59 60 61
 * $command->bindValue(':id', $_GET['id']);
 * $post = $command->query();
 * ~~~
w  
Qiang Xue committed
62
 *
Qiang Xue committed
63 64 65 66
 * For more information about how to perform various DB queries, please refer to [[Command]].
 *
 * If the underlying DBMS supports transactions, you can perform transactional SQL queries
 * like the following:
w  
Qiang Xue committed
67
 *
w  
Qiang Xue committed
68 69 70
 * ~~~
 * $transaction = $connection->beginTransaction();
 * try {
71 72 73 74
 *     $connection->createCommand($sql1)->execute();
 *     $connection->createCommand($sql2)->execute();
 *     // ... executing other SQL statements ...
 *     $transaction->commit();
75
 * } catch (Exception $e) {
76
 *     $transaction->rollBack();
w  
Qiang Xue committed
77
 * }
w  
Qiang Xue committed
78
 * ~~~
79
 *
80
 * You also can use shortcut for the above like the following:
81
 *
82 83 84 85 86 87 88
 * ~~~
 * $connection->transaction(function() {
 *     $order = new Order($customer);
 *     $order->save();
 *     $order->addItems($items);
 * });
 * ~~~
89
 *
90
 * If needed you can pass transaction isolation level as a second parameter:
91
 *
92
 * ~~~
93 94
 * $connection->transaction(function(Connection $db) {
 *     //return $db->...
95
 * }, Transaction::READ_UNCOMMITTED);
96
 * ~~~
97
 *
98
 * Connection is often used as an application component and configured in the application
Qiang Xue committed
99
 * configuration like the following:
w  
Qiang Xue committed
100 101
 *
 * ~~~
102 103 104 105 106 107 108 109 110
 * 'components' => [
 *     'db' => [
 *         'class' => '\yii\db\Connection',
 *         'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
 *         'username' => 'root',
 *         'password' => '',
 *         'charset' => 'utf8',
 *     ],
 * ],
w  
Qiang Xue committed
111
 * ~~~
w  
Qiang Xue committed
112
 *
113
 * @property string $driverName Name of the DB driver.
114
 * @property boolean $isActive Whether the DB connection is established. This property is read-only.
115 116
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
 * sequence object. This property is read-only.
Carsten Brandt committed
117 118
 * @property PDO $masterPdo The PDO instance for the currently active master connection. This property is
 * read-only.
119 120
 * @property QueryBuilder $queryBuilder The query builder for the current DB connection. This property is
 * read-only.
Carsten Brandt committed
121 122
 * @property array $queryCacheInfo The current query cache information, or null if query cache is not enabled.
 * This property is read-only.
123 124
 * @property Schema $schema The schema information for the database opened by this connection. This property
 * is read-only.
Carsten Brandt committed
125 126 127 128
 * @property Connection $slave The currently active slave connection. Null is returned if there is slave
 * available and `$fallbackToMaster` is false. This property is read-only.
 * @property PDO $slavePdo The PDO instance for the currently active slave connection. Null is returned if no
 * slave connection is available and `$fallbackToMaster` is false. This property is read-only.
129 130
 * @property Transaction $transaction The currently active transaction. Null if no active transaction. This
 * property is read-only.
Qiang Xue committed
131
 *
w  
Qiang Xue committed
132 133 134
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
135
class Connection extends Component
w  
Qiang Xue committed
136
{
137 138 139 140
    /**
     * @event Event an event that is triggered after a DB connection is established
     */
    const EVENT_AFTER_OPEN = 'afterOpen';
141 142 143 144 145 146 147 148 149 150 151 152
    /**
     * @event Event an event that is triggered right before a top-level transaction is started
     */
    const EVENT_BEGIN_TRANSACTION = 'beginTransaction';
    /**
     * @event Event an event that is triggered right after a top-level transaction is committed
     */
    const EVENT_COMMIT_TRANSACTION = 'commitTransaction';
    /**
     * @event Event an event that is triggered right after a top-level transaction is rolled back
     */
    const EVENT_ROLLBACK_TRANSACTION = 'rollbackTransaction';
153

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
    /**
     * @var string the Data Source Name, or DSN, contains the information required to connect to the database.
     * Please refer to the [PHP manual](http://www.php.net/manual/en/function.PDO-construct.php) on
     * the format of the DSN string.
     * @see charset
     */
    public $dsn;
    /**
     * @var string the username for establishing DB connection. Defaults to `null` meaning no username to use.
     */
    public $username;
    /**
     * @var string the password for establishing DB connection. Defaults to `null` meaning no password to use.
     */
    public $password;
    /**
     * @var array PDO attributes (name => value) that should be set when calling [[open()]]
     * to establish a DB connection. Please refer to the
     * [PHP manual](http://www.php.net/manual/en/function.PDO-setAttribute.php) for
     * details about available attributes.
     */
    public $attributes;
    /**
     * @var PDO the PHP PDO instance associated with this DB connection.
     * This property is mainly managed by [[open()]] and [[close()]] methods.
     * When a DB connection is active, this property will represent a PDO instance;
     * otherwise, it will be null.
     */
    public $pdo;
    /**
     * @var boolean whether to enable schema caching.
     * Note that in order to enable truly schema caching, a valid cache component as specified
     * by [[schemaCache]] must be enabled and [[enableSchemaCache]] must be set true.
     * @see schemaCacheDuration
     * @see schemaCacheExclude
     * @see schemaCache
     */
    public $enableSchemaCache = false;
    /**
     * @var integer number of seconds that table metadata can remain valid in cache.
     * Use 0 to indicate that the cached data will never expire.
     * @see enableSchemaCache
     */
    public $schemaCacheDuration = 3600;
    /**
     * @var array list of tables whose metadata should NOT be cached. Defaults to empty array.
     * The table names may contain schema prefix, if any. Do not quote the table names.
     * @see enableSchemaCache
     */
    public $schemaCacheExclude = [];
    /**
     * @var Cache|string the cache object or the ID of the cache application component that
     * is used to cache the table metadata.
     * @see enableSchemaCache
     */
    public $schemaCache = 'cache';
    /**
     * @var boolean whether to enable query caching.
     * Note that in order to enable query caching, a valid cache component as specified
     * by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true.
Qiang Xue committed
214
     * Also, only the results of the queries enclosed within [[cache()]] will be cached.
215
     * @see queryCache
216 217
     * @see cache()
     * @see noCache()
218
     */
Qiang Xue committed
219
    public $enableQueryCache = true;
220
    /**
221
     * @var integer the default number of seconds that query results can remain valid in cache.
222
     * Use 0 to indicate that the cached data will never expire.
223 224
     * Defaults to 3600, meaning 3600 seconds, or one hour. Use 0 to indicate that the cached data will never expire.
     * The value of this property will be used when [[cache()]] is called without a cache duration.
225
     * @see enableQueryCache
226
     * @see cache()
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
     */
    public $queryCacheDuration = 3600;
    /**
     * @var Cache|string the cache object or the ID of the cache application component
     * that is used for query caching.
     * @see enableQueryCache
     */
    public $queryCache = 'cache';
    /**
     * @var string the charset used for database connection. The property is only used
     * for MySQL, PostgreSQL and CUBRID databases. Defaults to null, meaning using default charset
     * as specified by the database.
     *
     * Note that if you're using GBK or BIG5 then it's highly recommended to
     * specify charset via DSN like 'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'.
     */
    public $charset;
    /**
     * @var boolean whether to turn on prepare emulation. Defaults to false, meaning PDO
     * will use the native prepare support if available. For some databases (such as MySQL),
     * this may need to be set true so that PDO can emulate the prepare support to bypass
     * the buggy native prepare support.
     * The default value is null, which means the PDO ATTR_EMULATE_PREPARES value will not be changed.
     */
    public $emulatePrepare;
    /**
     * @var string the common prefix or suffix for table names. If a table name is given
     * as `{{%TableName}}`, then the percentage character `%` will be replaced with this
     * property value. For example, `{{%post}}` becomes `{{tbl_post}}`.
     */
257
    public $tablePrefix = '';
258 259 260 261 262 263 264 265 266 267 268
    /**
     * @var array mapping between PDO driver names and [[Schema]] classes.
     * The keys of the array are PDO driver names while the values the corresponding
     * schema class name or configuration. Please refer to [[Yii::createObject()]] for
     * details on how to specify a configuration.
     *
     * This property is mainly used by [[getSchema()]] when fetching the database schema information.
     * You normally do not need to set this property unless you want to use your own
     * [[Schema]] class to support DBMS that is not supported by Yii.
     */
    public $schemaMap = [
269 270 271 272
        'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL
        'mysqli' => 'yii\db\mysql\Schema', // MySQL
        'mysql' => 'yii\db\mysql\Schema', // MySQL
        'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3
273
        'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2
274 275 276 277 278
        'sqlsrv' => 'yii\db\mssql\Schema', // newer MSSQL driver on MS Windows hosts
        'oci' => 'yii\db\oci\Schema', // Oracle driver
        'mssql' => 'yii\db\mssql\Schema', // older MSSQL driver on MS Windows hosts
        'dblib' => 'yii\db\mssql\Schema', // dblib drivers on GNU/Linux (and maybe other OSes) hosts
        'cubrid' => 'yii\db\cubrid\Schema', // CUBRID
279 280 281 282 283 284 285 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.
     */
    public $pdoClass;
    /**
     * @var boolean whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint).
     * Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect.
     */
    public $enableSavepoint = true;
289 290 291 292 293 294 295 296 297 298 299
    /**
     * @var Cache|string the cache object or the ID of the cache application component that is used to store
     * the health status of the DB servers specified in [[masters]] and [[slaves]].
     * This is used only when read/write splitting is enabled or [[masters]] is not empty.
     */
    public $serverStatusCache = 'cache';
    /**
     * @var integer the retry interval in seconds for dead servers listed in [[masters]] and [[slaves]].
     * This is used together with [[serverStatusCache]].
     */
    public $serverRetryInterval = 600;
Qiang Xue committed
300 301
    /**
     * @var boolean whether to enable read/write splitting by using [[slaves]] to read data.
302
     * Note that if [[slaves]] is empty, read/write splitting will NOT be enabled no matter what value this property takes.
Qiang Xue committed
303
     */
Qiang Xue committed
304
    public $enableSlaves = true;
Qiang Xue committed
305
    /**
306
     * @var array list of slave connection configurations. Each configuration is used to create a slave DB connection.
Qiang Xue committed
307
     * When [[enableSlaves]] is true, one of these configurations will be chosen and used to create a DB connection
308
     * for performing read queries only.
Qiang Xue committed
309
     * @see enableSlaves
310
     * @see slaveConfig
Qiang Xue committed
311 312 313
     */
    public $slaves = [];
    /**
314 315 316 317 318 319 320 321 322 323 324 325 326
     * @var array the configuration that should be merged with every slave configuration listed in [[slaves]].
     * For example,
     *
     * ```php
     * [
     *     'username' => 'slave',
     *     'password' => 'slave',
     *     'attributes' => [
     *         // use a smaller connection timeout
     *         PDO::ATTR_TIMEOUT => 10,
     *     ],
     * ]
     * ```
Qiang Xue committed
327
     */
328
    public $slaveConfig = [];
Qiang Xue committed
329
    /**
330 331 332 333 334 335
     * @var array list of master connection configurations. Each configuration is used to create a master DB connection.
     * When [[open()]] is called, one of these configurations will be chosen and used to create a DB connection
     * which will be used by this object.
     * Note that when this property is not empty, the connection setting (e.g. "dsn", "username") of this object will
     * be ignored.
     * @see masterConfig
Qiang Xue committed
336
     */
337
    public $masters = [];
Qiang Xue committed
338
    /**
339 340 341 342 343 344 345 346 347 348 349 350 351
     * @var array the configuration that should be merged with every master configuration listed in [[masters]].
     * For example,
     *
     * ```php
     * [
     *     'username' => 'master',
     *     'password' => 'master',
     *     'attributes' => [
     *         // use a smaller connection timeout
     *         PDO::ATTR_TIMEOUT => 10,
     *     ],
     * ]
     * ```
Qiang Xue committed
352
     */
353
    public $masterConfig = [];
Qiang Xue committed
354

355 356 357 358 359 360 361 362
    /**
     * @var Transaction the currently active transaction
     */
    private $_transaction;
    /**
     * @var Schema the database schema
     */
    private $_schema;
363 364 365 366
    /**
     * @var string driver name
     */
    private $_driverName;
Qiang Xue committed
367
    /**
368
     * @var Connection the currently active slave connection
Qiang Xue committed
369 370
     */
    private $_slave = false;
371 372 373 374
    /**
     * @var array query cache parameters for the [[cache()]] calls
     */
    private $_queryCacheInfo = [];
Qiang Xue committed
375

Carsten Brandt committed
376

377 378 379 380 381 382 383 384
    /**
     * Returns a value indicating whether the DB connection is established.
     * @return boolean whether the DB connection is established
     */
    public function getIsActive()
    {
        return $this->pdo !== null;
    }
385

386
    /**
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
     * Uses query cache for the queries performed with the callable.
     * When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
     * For example,
     *
     * ```php
     * // The customer will be fetched from cache if available.
     * // If not, the query will be made against DB and cached for use next time.
     * $customer = $db->cache(function (Connection $db) {
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
     * });
     * ```
     *
     * Note that query cache is only meaningful for queries that return results. For queries performed with
     * [[Command::execute()]], query cache will not be used.
     *
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
     * The signature of the callable is `function (Connection $db)`.
     * @param integer $duration the number of seconds that query results can remain valid in the cache. If this is
     * not set, the value of [[queryCacheDuration]] will be used instead.
     * Use 0 to indicate that the cached data will never expire.
     * @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query results.
     * @return mixed the return result of the callable
Qiang Xue committed
410
     * @throws \Exception if there is any exception during query
411 412 413 414 415 416 417
     * @see enableQueryCache
     * @see queryCache
     * @see noCache()
     */
    public function cache(callable $callable, $duration = null, $dependency = null)
    {
        $this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
Qiang Xue committed
418 419 420 421 422 423 424 425
        try {
            $result = call_user_func($callable, $this);
            array_pop($this->_queryCacheInfo);
            return $result;
        } catch (\Exception $e) {
            array_pop($this->_queryCacheInfo);
            throw $e;
        }
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
    }

    /**
     * Disables query cache temporarily.
     * Queries performed within the callable will not use query cache at all. For example,
     *
     * ```php
     * $db->cache(function (Connection $db) {
     *
     *     // ... queries that use query cache ...
     *
     *     return $db->noCache(function (Connection $db) {
     *         // this query will not use query cache
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
     *     });
     * });
     * ```
     *
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache.
     * The signature of the callable is `function (Connection $db)`.
     * @return mixed the return result of the callable
Qiang Xue committed
447
     * @throws \Exception if there is any exception during query
448 449 450 451 452 453 454
     * @see enableQueryCache
     * @see queryCache
     * @see cache()
     */
    public function noCache(callable $callable)
    {
        $this->_queryCacheInfo[] = false;
Qiang Xue committed
455 456 457 458 459 460 461 462
        try {
            $result = call_user_func($callable, $this);
            array_pop($this->_queryCacheInfo);
            return $result;
        } catch (\Exception $e) {
            array_pop($this->_queryCacheInfo);
            throw $e;
        }
463 464 465 466 467
    }

    /**
     * Returns the current query cache information.
     * This method is used internally by [[Command]].
468 469
     * @param integer $duration the preferred caching duration. If null, it will be ignored.
     * @param \yii\caching\Dependency $dependency the preferred caching dependency. If null, it will be ignored.
470 471
     * @return array the current query cache information, or null if query cache is not enabled.
     * @internal
472
     */
473
    public function getQueryCacheInfo($duration, $dependency)
474
    {
475 476 477 478
        if (!$this->enableQueryCache) {
            return null;
        }

479
        $info = end($this->_queryCacheInfo);
480 481 482 483 484 485 486 487 488 489
        if (is_array($info)) {
            if ($duration === null) {
                $duration = $info[0];
            }
            if ($dependency === null) {
                $dependency = $info[1];
            }
        }

        if ($duration === 0 || $duration > 0) {
Qiang Xue committed
490 491 492 493 494
            if (is_string($this->queryCache) && Yii::$app) {
                $cache = Yii::$app->get($this->queryCache, false);
            } else {
                $cache = $this->queryCache;
            }
495
            if ($cache instanceof Cache) {
496
                return [$cache, $duration, $dependency];
497
            }
498
        }
499

500
        return null;
501
    }
w  
Qiang Xue committed
502

503 504 505 506 507 508 509
    /**
     * Establishes a DB connection.
     * It does nothing if a DB connection has already been established.
     * @throws Exception if connection fails
     */
    public function open()
    {
510 511 512 513 514 515 516 517 518 519 520
        if ($this->pdo !== null) {
            return;
        }

        if (!empty($this->masters)) {
            $db = $this->openFromPool($this->masters, $this->masterConfig);
            if ($db !== null) {
                $this->pdo = $db->pdo;
                return;
            } else {
                throw new InvalidConfigException('None of the master DB servers is available.');
521 522
            }
        }
523 524 525 526 527 528

        if (empty($this->dsn)) {
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
        }
        $token = 'Opening DB connection: ' . $this->dsn;
        try {
Qiang Xue committed
529
            Yii::info($token, __METHOD__);
530 531 532 533 534 535 536 537
            Yii::beginProfile($token, __METHOD__);
            $this->pdo = $this->createPdoInstance();
            $this->initConnection();
            Yii::endProfile($token, __METHOD__);
        } catch (\PDOException $e) {
            Yii::endProfile($token, __METHOD__);
            throw new Exception($e->getMessage(), $e->errorInfo, (int)$e->getCode(), $e);
        }
538
    }
w  
Qiang Xue committed
539

540 541 542 543 544 545 546 547 548 549 550 551
    /**
     * Closes the currently active DB connection.
     * It does nothing if the connection is already closed.
     */
    public function close()
    {
        if ($this->pdo !== null) {
            Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__);
            $this->pdo = null;
            $this->_schema = null;
            $this->_transaction = null;
        }
Qiang Xue committed
552 553 554 555 556

        if ($this->_slave) {
            $this->_slave->close();
            $this->_slave = null;
        }
557
    }
w  
Qiang Xue committed
558

559 560 561 562 563 564 565 566 567 568 569 570
    /**
     * Creates the PDO instance.
     * This method is called by [[open]] to establish a DB connection.
     * The default implementation will create a PHP PDO instance.
     * You may override this method if the default PDO needs to be adapted for certain DBMS.
     * @return PDO the pdo instance
     */
    protected function createPdoInstance()
    {
        $pdoClass = $this->pdoClass;
        if ($pdoClass === null) {
            $pdoClass = 'PDO';
571 572 573
            if ($this->_driverName !== null) {
                $driver = $this->_driverName;
            } elseif (($pos = strpos($this->dsn, ':')) !== false) {
574
                $driver = strtolower(substr($this->dsn, 0, $pos));
575 576 577
            }
            if (isset($driver) && ($driver === 'mssql' || $driver === 'dblib' || $driver === 'sqlsrv')) {
                $pdoClass = 'yii\db\mssql\PDO';
578 579
            }
        }
w  
Qiang Xue committed
580

581 582
        return new $pdoClass($this->dsn, $this->username, $this->password, $this->attributes);
    }
583

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
    /**
     * Initializes the DB connection.
     * This method is invoked right after the DB connection is established.
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`
     * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty.
     * It then triggers an [[EVENT_AFTER_OPEN]] event.
     */
    protected function initConnection()
    {
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) {
            $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare);
        }
        if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'])) {
            $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset));
        }
        $this->trigger(self::EVENT_AFTER_OPEN);
    }
w  
Qiang Xue committed
602

603 604
    /**
     * Creates a command for execution.
605 606
     * @param string $sql the SQL statement to be executed
     * @param array $params the parameters to be bound to the SQL statement
607 608 609 610 611 612 613 614
     * @return Command the DB command
     */
    public function createCommand($sql = null, $params = [])
    {
        $command = new Command([
            'db' => $this,
            'sql' => $sql,
        ]);
w  
Qiang Xue committed
615

616
        return $command->bindValues($params);
617
    }
w  
Qiang Xue committed
618

619 620 621 622 623 624 625 626
    /**
     * Returns the currently active transaction.
     * @return Transaction the currently active transaction. Null if no active transaction.
     */
    public function getTransaction()
    {
        return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
    }
w  
Qiang Xue committed
627

628 629
    /**
     * Starts a transaction.
630 631
     * @param string|null $isolationLevel The isolation level to use for this transaction.
     * See [[Transaction::begin()]] for details.
632 633
     * @return Transaction the transaction initiated
     */
634
    public function beginTransaction($isolationLevel = null)
635 636
    {
        $this->open();
637

638 639 640
        if (($transaction = $this->getTransaction()) === null) {
            $transaction = $this->_transaction = new Transaction(['db' => $this]);
        }
641
        $transaction->begin($isolationLevel);
w  
Qiang Xue committed
642

643 644
        return $transaction;
    }
w  
Qiang Xue committed
645

646 647 648
    /**
     * Executes callback provided in a transaction.
     *
649
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
650 651 652 653 654
     * @param string|null $isolationLevel The isolation level to use for this transaction.
     * See [[Transaction::begin()]] for details.
     * @throws \Exception
     * @return mixed result of callback function
     */
655
    public function transaction(callable $callback, $isolationLevel = null)
656 657 658 659
    {
        $transaction = $this->beginTransaction($isolationLevel);

        try {
660
            $result = call_user_func($callback, $this);
661 662 663
            if ($transaction->isActive) {
                $transaction->commit();
            }
664 665 666 667 668 669 670 671
        } catch (\Exception $e) {
            $transaction->rollBack();
            throw $e;
        }

        return $result;
    }

672 673
    /**
     * Returns the schema information for the database opened by this connection.
674
     * @return Schema the schema information for the database opened by this connection.
675 676 677 678 679 680 681 682 683 684 685
     * @throws NotSupportedException if there is no support for the current driver type
     */
    public function getSchema()
    {
        if ($this->_schema !== null) {
            return $this->_schema;
        } else {
            $driver = $this->getDriverName();
            if (isset($this->schemaMap[$driver])) {
                $config = !is_array($this->schemaMap[$driver]) ? ['class' => $this->schemaMap[$driver]] : $this->schemaMap[$driver];
                $config['db'] = $this;
w  
Qiang Xue committed
686

687 688 689 690 691 692
                return $this->_schema = Yii::createObject($config);
            } else {
                throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS.");
            }
        }
    }
Qiang Xue committed
693

694 695 696 697 698 699 700 701
    /**
     * Returns the query builder for the current DB connection.
     * @return QueryBuilder the query builder for the current DB connection.
     */
    public function getQueryBuilder()
    {
        return $this->getSchema()->getQueryBuilder();
    }
w  
Qiang Xue committed
702

703 704
    /**
     * Obtains the schema information for the named table.
705 706
     * @param string $name table name.
     * @param boolean $refresh whether to reload the table schema even if it is found in the cache.
707 708 709 710 711 712
     * @return TableSchema table schema information. Null if the named table does not exist.
     */
    public function getTableSchema($name, $refresh = false)
    {
        return $this->getSchema()->getTableSchema($name, $refresh);
    }
w  
Qiang Xue committed
713

714 715
    /**
     * Returns the ID of the last inserted row or sequence value.
716
     * @param string $sequenceName name of the sequence object (required by some DBMS)
717 718 719 720 721 722 723
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
     */
    public function getLastInsertID($sequenceName = '')
    {
        return $this->getSchema()->getLastInsertID($sequenceName);
    }
w  
Qiang Xue committed
724

725 726 727
    /**
     * Quotes a string value for use in a query.
     * Note that if the parameter is not a string, it will be returned without change.
728
     * @param string $value string to be quoted
729 730 731
     * @return string the properly quoted string
     * @see http://www.php.net/manual/en/function.PDO-quote.php
     */
732
    public function quoteValue($value)
733
    {
734
        return $this->getSchema()->quoteValue($value);
735
    }
Qiang Xue committed
736

737 738 739 740 741
    /**
     * Quotes a table name for use in a query.
     * If the table name contains schema prefix, the prefix will also be properly quoted.
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{',
     * then this method will do nothing.
742
     * @param string $name table name
743 744 745 746 747 748
     * @return string the properly quoted table name
     */
    public function quoteTableName($name)
    {
        return $this->getSchema()->quoteTableName($name);
    }
749

750 751 752 753 754
    /**
     * Quotes a column name for use in a query.
     * If the column name contains prefix, the prefix will also be properly quoted.
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{',
     * then this method will do nothing.
755
     * @param string $name column name
756 757 758 759 760 761 762 763 764 765 766 767 768
     * @return string the properly quoted column name
     */
    public function quoteColumnName($name)
    {
        return $this->getSchema()->quoteColumnName($name);
    }

    /**
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
     * Tokens enclosed within double curly brackets are treated as table names, while
     * tokens enclosed within double square brackets are column names. They will be quoted accordingly.
     * Also, the percentage character "%" at the beginning or ending of a table name will be replaced
     * with [[tablePrefix]].
769
     * @param string $sql the SQL to be quoted
770 771 772 773
     * @return string the quoted SQL
     */
    public function quoteSql($sql)
    {
774 775
        return preg_replace_callback(
            '/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
776 777 778 779 780 781
            function ($matches) {
                if (isset($matches[3])) {
                    return $this->quoteColumnName($matches[3]);
                } else {
                    return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
                }
782 783 784
            },
            $sql
        );
785 786 787
    }

    /**
788 789
     * Returns the name of the DB driver. Based on the the current [[dsn]], in case it was not set explicitly
     * by an end user.
790 791 792 793
     * @return string name of the DB driver
     */
    public function getDriverName()
    {
794 795 796 797
        if ($this->_driverName === null) {
            if (($pos = strpos($this->dsn, ':')) !== false) {
                $this->_driverName = strtolower(substr($this->dsn, 0, $pos));
            } else {
798
                $this->_driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME));
799
            }
800
        }
801 802 803 804 805 806 807 808 809 810
        return $this->_driverName;
    }

    /**
     * Changes the current driver name.
     * @param string $driverName name of the DB driver
     */
    public function setDriverName($driverName)
    {
        $this->_driverName = strtolower($driverName);
811
    }
Qiang Xue committed
812

Qiang Xue committed
813
    /**
Qiang Xue committed
814 815 816
     * Returns the PDO instance for the currently active slave connection.
     * When [[enableSlaves]] is true, one of the slaves will be used for read queries, and its PDO instance
     * will be returned by this method.
817
     * @param boolean $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
Qiang Xue committed
818 819
     * @return PDO the PDO instance for the currently active slave connection. Null is returned if no slave connection
     * is available and `$fallbackToMaster` is false.
Qiang Xue committed
820
     */
821
    public function getSlavePdo($fallbackToMaster = true)
Qiang Xue committed
822
    {
823 824 825 826 827 828
        $db = $this->getSlave(false);
        if ($db === null) {
            return $fallbackToMaster ? $this->getMasterPdo() : null;
        } else {
            return $db->pdo;
        }
Qiang Xue committed
829 830 831
    }

    /**
Qiang Xue committed
832
     * Returns the PDO instance for the currently active master connection.
Qiang Xue committed
833
     * This method will open the master DB connection and then return [[pdo]].
Qiang Xue committed
834
     * @return PDO the PDO instance for the currently active master connection.
Qiang Xue committed
835
     */
836
    public function getMasterPdo()
Qiang Xue committed
837 838 839 840 841
    {
        $this->open();
        return $this->pdo;
    }

Qiang Xue committed
842
    /**
843
     * Returns the currently active slave connection.
Qiang Xue committed
844 845 846 847
     * If this method is called the first time, it will try to open a slave connection when [[enableSlaves]] is true.
     * @param boolean $fallbackToMaster whether to return a master connection in case there is no slave connection available.
     * @return Connection the currently active slave connection. Null is returned if there is slave available and
     * `$fallbackToMaster` is false.
Qiang Xue committed
848
     */
849
    public function getSlave($fallbackToMaster = true)
Qiang Xue committed
850
    {
Qiang Xue committed
851
        if (!$this->enableSlaves) {
852
            return $fallbackToMaster ? $this : null;
Qiang Xue committed
853 854
        }

855 856
        if ($this->_slave === false) {
            $this->_slave = $this->openFromPool($this->slaves, $this->slaveConfig);
Qiang Xue committed
857
        }
858 859

        return $this->_slave === null && $fallbackToMaster ? $this : $this->_slave;
Qiang Xue committed
860 861
    }

862 863 864 865
    /**
     * Executes the provided callback by using the master connection.
     *
     * This method is provided so that you can temporarily force using the master connection to perform
866
     * DB operations even if they are read queries. For example,
867 868 869 870 871 872 873 874
     *
     * ```php
     * $result = $db->useMaster(function ($db) {
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
     * });
     * ```
     *
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
875
     * `function (Connection $db)`. Its return value will be returned by this method.
876 877 878 879
     * @return mixed the return value of the callback
     */
    public function useMaster(callable $callback)
    {
Qiang Xue committed
880 881
        $enableSlave = $this->enableSlaves;
        $this->enableSlaves = false;
882
        $result = call_user_func($callback, $this);
Qiang Xue committed
883
        $this->enableSlaves = $enableSlave;
884 885 886
        return $result;
    }

Qiang Xue committed
887
    /**
888
     * Opens the connection to a server in the pool.
Qiang Xue committed
889
     * This method implements the load balancing among the given list of the servers.
890 891 892 893
     * @param array $pool the list of connection configurations in the server pool
     * @param array $sharedConfig the configuration common to those given in `$pool`.
     * @return Connection the opened DB connection, or null if no server is available
     * @throws InvalidConfigException if a configuration does not specify "dsn"
Qiang Xue committed
894
     */
895
    protected function openFromPool(array $pool, array $sharedConfig)
Qiang Xue committed
896
    {
897
        if (empty($pool)) {
Qiang Xue committed
898 899 900
            return null;
        }

901 902 903
        if (!isset($sharedConfig['class'])) {
            $sharedConfig['class'] = get_class($this);
        }
Qiang Xue committed
904

905
        $cache = is_string($this->serverStatusCache) ? Yii::$app->get($this->serverStatusCache, false) : $this->serverStatusCache;
Qiang Xue committed
906

907 908 909 910
        shuffle($pool);

        foreach ($pool as $config) {
            $config = array_merge($sharedConfig, $config);
Qiang Xue committed
911
            if (empty($config['dsn'])) {
912
                throw new InvalidConfigException('The "dsn" option must be specified.');
Qiang Xue committed
913 914 915 916
            }

            $key = [__METHOD__, $config['dsn']];
            if ($cache instanceof Cache && $cache->get($key)) {
917
                // should not try this dead server now
Qiang Xue committed
918 919 920
                continue;
            }

921 922
            /* @var $db Connection */
            $db = Yii::createObject($config);
Qiang Xue committed
923

Qiang Xue committed
924
            try {
925 926
                $db->open();
                return $db;
Qiang Xue committed
927
            } catch (\Exception $e) {
928
                Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__);
Qiang Xue committed
929
                if ($cache instanceof Cache) {
Qiang Xue committed
930
                    // mark this server as dead and only retry it after the specified interval
931
                    $cache->set($key, 1, $this->serverRetryInterval);
Qiang Xue committed
932 933 934 935 936 937
                }
            }
        }

        return null;
    }
w  
Qiang Xue committed
938
}