Passed
Push — master ( c5569e...25d5e8 )
by Rustam
11:00
created

Connection::open()   B

Complexity

Conditions 11
Paths 96

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 11.0069

Importance

Changes 0
Metric Value
cc 11
eloc 26
c 0
b 0
f 0
nc 96
nop 0
dl 0
loc 50
ccs 25
cts 26
cp 0.9615
crap 11.0069
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Connection;
6
7
use JsonException;
8
use PDO;
9
use PDOException;
10
use Psr\Log\LoggerInterface;
11
use Psr\Log\LogLevel;
12
use Throwable;
13
use Yiisoft\Cache\Dependency\Dependency;
14
use Yiisoft\Db\Cache\QueryCache;
15
use Yiisoft\Db\Cache\SchemaCache;
16
use Yiisoft\Db\Command\Command;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Command\Command was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Yiisoft\Db\Exception\Exception;
18
use Yiisoft\Db\Exception\InvalidCallException;
19
use Yiisoft\Db\Exception\InvalidConfigException;
20
use Yiisoft\Db\Exception\NotSupportedException;
21
use Yiisoft\Db\Factory\DatabaseFactory;
22
use Yiisoft\Db\Query\QueryBuilder;
23
use Yiisoft\Db\Schema\Schema;
24
use Yiisoft\Db\Schema\TableSchema;
25
use Yiisoft\Db\Transaction\Transaction;
26
use Yiisoft\Profiler\ProfilerInterface;
27
28
use function array_keys;
29
use function str_replace;
30
use function strncmp;
31
32
/**
33
 * Connection represents a connection to a database via [PDO](http://php.net/manual/en/book.pdo.php).
34
 *
35
 * Connection works together with {@see Command}, {@see DataReader} and {@see Transaction} to provide data access to
36
 * various DBMS in a common set of APIs. They are a thin wrapper of the
37
 * [PDO PHP extension](http://php.net/manual/en/book.pdo.php).
38
 *
39
 * Connection supports database replication and read-write splitting. In particular, a Connection component can be
40
 * configured with multiple {@see setMasters()} and {@see setSlaves()}. It will do load balancing and failover by
41
 * choosing appropriate servers. It will also automatically direct read operations to the slaves and write operations
42
 * to the masters.
43
 *
44
 * To establish a DB connection, set {@see dsn}, {@see setUsername()} and {@see setPassword}, and then call
45
 * {@see open()} to connect to the database server. The current state of the connection can be checked using
46
 * {@see $isActive}.
47
 *
48
 * The following example shows how to create a Connection instance and establish the DB connection:
49
 *
50
 * ```php
51
 * $connection = new \Yiisoft\Db\Mysql\Connection(
52
 *     $cache,
53
 *     $logger,
54
 *     $profiler,
55
 *     $dsn
56
 * );
57
 * $connection->open();
58
 * ```
59
 *
60
 * After the DB connection is established, one can execute SQL statements like the following:
61
 *
62
 * ```php
63
 * $command = $connection->createCommand('SELECT * FROM post');
64
 * $posts = $command->queryAll();
65
 * $command = $connection->createCommand('UPDATE post SET status=1');
66
 * $command->execute();
67
 * ```
68
 *
69
 * One can also do prepared SQL execution and bind parameters to the prepared SQL.
70
 * When the parameters are coming from user input, you should use this approach to prevent SQL injection attacks. The
71
 * following is an example:
72
 *
73
 * ```php
74
 * $command = $connection->createCommand('SELECT * FROM post WHERE id=:id');
75
 * $command->bindValue(':id', $_GET['id']);
76
 * $post = $command->query();
77
 * ```
78
 *
79
 * For more information about how to perform various DB queries, please refer to {@see Command}.
80
 *
81
 * If the underlying DBMS supports transactions, you can perform transactional SQL queries like the following:
82
 *
83
 * ```php
84
 * $transaction = $connection->beginTransaction();
85
 * try {
86
 *     $connection->createCommand($sql1)->execute();
87
 *     $connection->createCommand($sql2)->execute();
88
 *     // ... executing other SQL statements ...
89
 *     $transaction->commit();
90
 * } catch (Exceptions $e) {
91
 *     $transaction->rollBack();
92
 * }
93
 * ```
94
 *
95
 * You also can use shortcut for the above like the following:
96
 *
97
 * ```php
98
 * $connection->transaction(function () {
99
 *     $order = new Order($customer);
100
 *     $order->save();
101
 *     $order->addItems($items);
102
 * });
103
 * ```
104
 *
105
 * If needed you can pass transaction isolation level as a second parameter:
106
 *
107
 * ```php
108
 * $connection->transaction(function (Connection $db) {
109
 *     //return $db->...
110
 * }, Transaction::READ_UNCOMMITTED);
111
 * ```
112
 *
113
 * Connection is often used as an application component and configured in the container-di configuration like the
114
 * following:
115
 *
116
 * ```php
117
 * Connection::class => static function (ContainerInterface $container) {
118
 *     $connection = new Connection(
119
 *         $container->get(CacheInterface::class),
120
 *         $container->get(LoggerInterface::class),
121
 *         $container->get(Profiler::class),
122
 *         'mysql:host=127.0.0.1;dbname=demo;charset=utf8'
123
 *     );
124
 *
125
 *     $connection->setUsername(root);
126
 *     $connection->setPassword('');
127
 *
128
 *     return $connection;
129
 * },
130
 * ```
131
 *
132
 * The {@see dsn} property can be defined via configuration {@see \Yiisoft\Db\Connection\Dsn}:
133
 *
134
 * ```php
135
 * Connection::class => static function (ContainerInterface $container) {
136
 *     $dsn = new Dsn('mysql', '127.0.0.1', 'yiitest', '3306');
137
 *
138
 *     $connection = new Connection(
139
 *         $container->get(CacheInterface::class),
140
 *         $container->get(LoggerInterface::class),
141
 *         $container->get(Profiler::class),
142
 *         $dsn->getDsn()
143
 *     );
144
 *
145
 *     $connection->setUsername(root);
146
 *     $connection->setPassword('');
147
 *
148
 *     return $connection;
149
 * },
150
 * ```
151
 *
152
 * @property string $driverName Name of the DB driver.
153
 * @property bool $isActive Whether the DB connection is established. This property is read-only.
154
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
155
 * object. This property is read-only.
156
 * @property Connection $master The currently active master connection. `null` is returned if there is no master
157
 * available. This property is read-only.
158
 * @property PDO $masterPdo The PDO instance for the currently active master connection. This property is read-only.
159
 * @property QueryBuilder $queryBuilder The query builder for the current DB connection. Note that the type of this
160
 * property differs in getter and setter. See {@see getQueryBuilder()} and {@see setQueryBuilder()} for details.
161
 * @property Schema $schema The schema information for the database opened by this connection. This property is
162
 * read-only.
163
 * @property string $serverVersion Server version as a string. This property is read-only.
164
 * @property Connection $slave The currently active slave connection. `null` is returned if there is no slave
165
 * available and `$fallbackToMaster` is false. This property is read-only.
166
 * @property PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned if no slave
167
 * connection is available and `$fallbackToMaster` is false. This property is read-only.
168
 * @property Transaction|null $transaction The currently active transaction. Null if no active transaction. This
169
 * property is read-only.
170
 */
171
abstract class Connection implements ConnectionInterface
172
{
173
    private string $dsn;
174
    private ?string $username = null;
175
    private ?string $password = null;
176
    private array $attributes = [];
177
    private ?PDO $pdo = null;
178
    private ?string $charset = null;
179
    private ?bool $emulatePrepare = null;
180
    private string $tablePrefix = '';
181
    private bool $enableSavepoint = true;
182
    private int $serverRetryInterval = 600;
183
    private bool $enableSlaves = true;
184
    private array $slaves = [];
185
    private array $masters = [];
186
    private bool $shuffleMasters = true;
187
    private bool $enableLogging = true;
188
    private array $quotedTableNames = [];
189
    private array $quotedColumnNames = [];
190
    private ?Connection $master = null;
191
    private ?Connection $slave = null;
192
    private LoggerInterface $logger;
193
    private ?Transaction $transaction = null;
194
    private ?Schema $schema = null;
195
    private ProfilerInterface $profiler;
196
    private bool $enableProfiling = true;
197
    private QueryCache $queryCache;
198
    private SchemaCache $schemaCache;
199
200 2989
    public function __construct(
201
        LoggerInterface $logger,
202
        ProfilerInterface $profiler,
203
        QueryCache $queryCache,
204
        SchemaCache $schemaCache,
205
        string $dsn
206
    ) {
207 2989
        $this->logger = $logger;
208 2989
        $this->profiler = $profiler;
209 2989
        $this->queryCache = $queryCache;
210 2989
        $this->schemaCache = $schemaCache;
211 2989
        $this->dsn = $dsn;
212 2989
    }
213
214
    /**
215
     * Creates a command for execution.
216
     *
217
     * @param string|null $sql the SQL statement to be executed
218
     * @param array $params the parameters to be bound to the SQL statement
219
     *
220
     * @throws Exception|InvalidConfigException
221
     *
222
     * @return Command the DB command
223
     */
224
    abstract public function createCommand(?string $sql = null, array $params = []): Command;
225
226
    /**
227
     * Returns the schema information for the database opened by this connection.
228
     *
229
     * @return Schema the schema information for the database opened by this connection.
230
     */
231
    abstract public function getSchema(): Schema;
232
233
    /**
234
     * Creates the PDO instance.
235
     *
236
     * This method is called by {@see open} to establish a DB connection. The default implementation will create a PHP
237
     * PDO instance. You may override this method if the default PDO needs to be adapted for certain DBMS.
238
     *
239
     * @return PDO the pdo instance
240
     */
241
    abstract protected function createPdoInstance(): PDO;
242
243
    /**
244
     * Initializes the DB connection.
245
     *
246
     * This method is invoked right after the DB connection is established.
247
     *
248
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`.
249
     *
250
     * if {@see emulatePrepare} is true, and sets the database {@see charset} if it is not empty.
251
     *
252
     * It then triggers an {@see EVENT_AFTER_OPEN} event.
253
     */
254
    abstract protected function initConnection(): void;
255
256
    /**
257
     * Reset the connection after cloning.
258
     */
259 10
    public function __clone()
260
    {
261 10
        $this->master = null;
262 10
        $this->slave = null;
263 10
        $this->schema = null;
264 10
        $this->transaction = null;
265
266 10
        if (strncmp($this->dsn, 'sqlite::memory:', 15) !== 0) {
267
            /** reset PDO connection, unless its sqlite in-memory, which can only have one connection */
268 10
            $this->pdo = null;
269
        }
270 10
    }
271
272
    /**
273
     * Close the connection before serializing.
274
     *
275
     * @return array
276
     */
277 6
    public function __sleep(): array
278
    {
279 6
        $fields = (array) $this;
280
281
        unset(
282 6
            $fields["\000" . __CLASS__ . "\000" . 'pdo'],
283 6
            $fields["\000" . __CLASS__ . "\000" . 'master'],
284 6
            $fields["\000" . __CLASS__ . "\000" . 'slave'],
285 6
            $fields["\000" . __CLASS__ . "\000" . 'transaction'],
286 6
            $fields["\000" . __CLASS__ . "\000" . 'schema']
287
        );
288
289 6
        return array_keys($fields);
290
    }
291
292
    /**
293
     * Starts a transaction.
294
     *
295
     * @param string|null $isolationLevel The isolation level to use for this transaction.
296
     *
297
     * {@see Transaction::begin()} for details.
298
     *
299
     * @throws Exception|InvalidConfigException|NotSupportedException
300
     *
301
     * @return Transaction the transaction initiated
302
     */
303 40
    public function beginTransaction($isolationLevel = null): Transaction
304
    {
305 40
        $this->open();
306
307 40
        if (($transaction = $this->getTransaction()) === null) {
308 40
            $transaction = $this->transaction = new Transaction($this, $this->logger);
309
        }
310
311 40
        $transaction->begin($isolationLevel);
312
313 40
        return $transaction;
314
    }
315
316
    /**
317
     * Uses query cache for the queries performed with the callable.
318
     *
319
     * When query caching is enabled ({@see enableQueryCache} is true and {@see queryCache} refers to a valid cache),
320
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
321
     *
322
     * For example,
323
     *
324
     * ```php
325
     * // The customer will be fetched from cache if available.
326
     * // If not, the query will be made against DB and cached for use next time.
327
     * $customer = $db->cache(function (Connection $db) {
328
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
329
     * });
330
     * ```
331
     *
332
     * Note that query cache is only meaningful for queries that return results. For queries performed with
333
     * {@see Command::execute()}, query cache will not be used.
334
     *
335
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
336
     * The signature of the callable is `function (Connection $db)`.
337
     * @param int|null $duration the number of seconds that query results can remain valid in the cache. If this is not
338
     * set, the value of {@see queryCacheDuration} will be used instead. Use 0 to indicate that the cached data will
339
     * never expire.
340
     * @param Dependency|null $dependency the cache dependency associated with the cached query
341
     * results.
342
     *
343
     * @throws Throwable if there is any exception during query
344
     *
345
     * @return mixed the return result of the callable
346
     *
347
     * {@see setEnableQueryCache()}
348
     * {@see queryCache}
349
     * {@see noCache()}
350
     */
351 10
    public function cache(callable $callable, int $duration = null, Dependency $dependency = null)
352
    {
353 10
        $this->queryCache->setInfo(
354 10
            [$duration ?? $this->queryCache->getDuration(), $dependency]
355
        );
356
357 10
        $result = $callable($this);
358
359 10
        $this->queryCache->removeLastInfo();
360
361 10
        return $result;
362
    }
363
364 1832
    public function getAttributes(): array
365
    {
366 1832
        return $this->attributes;
367
    }
368
369 733
    public function getCharset(): ?string
370
    {
371 733
        return $this->charset;
372
    }
373
374 1857
    public function getDsn(): string
375
    {
376 1857
        return $this->dsn;
377
    }
378
379 1463
    public function getEmulatePrepare(): ?bool
380
    {
381 1463
        return $this->emulatePrepare;
382
    }
383
384 1753
    public function getQueryCache(): QueryCache
385
    {
386 1753
        return $this->queryCache;
387
    }
388
389 2491
    public function getSchemaCache(): SchemaCache
390
    {
391 2491
        return $this->schemaCache;
392
    }
393
394 1699
    public function isLoggingEnabled(): bool
395
    {
396 1699
        return $this->enableLogging;
397
    }
398
399 1832
    public function isProfilingEnabled(): bool
400
    {
401 1832
        return $this->enableProfiling;
402
    }
403
404 10
    public function isSavepointEnabled(): bool
405
    {
406 10
        return $this->enableSavepoint;
407
    }
408
409 1
    public function areSlavesEnabled(): bool
410
    {
411 1
        return $this->enableSlaves;
412
    }
413
414
    /**
415
     * Returns a value indicating whether the DB connection is established.
416
     *
417
     * @return bool whether the DB connection is established
418
     */
419 193
    public function isActive(): bool
420
    {
421 193
        return $this->pdo !== null;
422
    }
423
424
    /**
425
     * Returns the ID of the last inserted row or sequence value.
426
     *
427
     * @param string $sequenceName name of the sequence object (required by some DBMS)
428
     *
429
     * @throws Exception
430
     * @throws InvalidCallException
431
     *
432
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
433
     *
434
     * {@see http://php.net/manual/en/pdo.lastinsertid.php'>http://php.net/manual/en/pdo.lastinsertid.php}
435
     */
436 10
    public function getLastInsertID($sequenceName = ''): string
437
    {
438 10
        return $this->getSchema()->getLastInsertID($sequenceName);
439
    }
440
441 1753
    public function getLogger(): LoggerInterface
442
    {
443 1753
        return $this->logger;
444
    }
445
446
    /**
447
     * Returns the currently active master connection.
448
     *
449
     * If this method is called for the first time, it will try to open a master connection.
450
     *
451
     * @return Connection the currently active master connection. `null` is returned if there is no master available.
452
     */
453 13
    public function getMaster(): ?self
454
    {
455 13
        if ($this->master === null) {
456 13
            $this->master = $this->shuffleMasters
457 7
                ? $this->openFromPool($this->masters)
458 6
                : $this->openFromPoolSequentially($this->masters);
459
        }
460
461 13
        return $this->master;
462
    }
463
464
    /**
465
     * Returns the PDO instance for the currently active master connection.
466
     *
467
     * This method will open the master DB connection and then return {@see pdo}.
468
     *
469
     * @throws Exception
470
     *
471
     * @return PDO the PDO instance for the currently active master connection.
472
     */
473 1798
    public function getMasterPdo(): PDO
474
    {
475 1798
        $this->open();
476
477 1798
        return $this->pdo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->pdo could return the type null which is incompatible with the type-hinted return PDO. Consider adding an additional type-check to rule them out.
Loading history...
478
    }
479
480 1832
    public function getPassword(): ?string
481
    {
482 1832
        return $this->password;
483
    }
484
485
    /**
486
     * The PHP PDO instance associated with this DB connection. This property is mainly managed by {@see open()} and
487
     * {@see close()} methods. When a DB connection is active, this property will represent a PDO instance; otherwise,
488
     * it will be null.
489
     *
490
     * @return PDO|null
491
     *
492
     * {@see pdoClass}
493
     */
494 1832
    public function getPDO(): ?PDO
495
    {
496 1832
        return $this->pdo;
497
    }
498
499
    /**
500
     * Returns the query builder for the current DB connection.
501
     *
502
     * @return QueryBuilder the query builder for the current DB connection.
503
     */
504 942
    public function getQueryBuilder(): QueryBuilder
505
    {
506 942
        return $this->getSchema()->getQueryBuilder();
507
    }
508
509 1753
    public function getProfiler(): ProfilerInterface
510
    {
511 1753
        return $this->profiler;
512
    }
513
514
    /**
515
     * Returns a server version as a string comparable by {@see \version_compare()}.
516
     *
517
     * @throws Exception
518
     * @throws InvalidConfigException
519
     *
520
     * @return string server version as a string.
521
     */
522 311
    public function getServerVersion(): string
523
    {
524 311
        return $this->getSchema()->getServerVersion();
525
    }
526
527
    /**
528
     * Returns the currently active slave connection.
529
     *
530
     * If this method is called for the first time, it will try to open a slave connection when {@see setEnableSlaves()}
531
     * is true.
532
     *
533
     * @param bool $fallbackToMaster whether to return a master connection in case there is no slave connection
534
     * available.
535
     *
536
     * @return Connection the currently active slave connection. `null` is returned if there is no slave available and
537
     * `$fallbackToMaster` is false.
538
     */
539 1781
    public function getSlave(bool $fallbackToMaster = true): ?self
540
    {
541 1781
        if (!$this->enableSlaves) {
542 6
            return $fallbackToMaster ? $this : null;
543
        }
544
545 1780
        if ($this->slave === null) {
546 1780
            $this->slave = $this->openFromPool($this->slaves);
547
        }
548
549 1780
        return $this->slave === null && $fallbackToMaster ? $this : $this->slave;
550
    }
551
552
    /**
553
     * Returns the PDO instance for the currently active slave connection.
554
     *
555
     * When {@see enableSlaves} is true, one of the slaves will be used for read queries, and its PDO instance will be
556
     * returned by this method.
557
     *
558
     * @param bool $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
559
     *
560
     * @throws Exception
561
     *
562
     * @return PDO the PDO instance for the currently active slave connection. `null` is returned if no slave connection
563
     * is available and `$fallbackToMaster` is false.
564
     */
565 1779
    public function getSlavePdo(bool $fallbackToMaster = true): ?PDO
566
    {
567 1779
        $db = $this->getSlave(false);
568
569 1779
        if ($db === null) {
570 1774
            return $fallbackToMaster ? $this->getMasterPdo() : null;
571
        }
572
573 6
        return $db->getPdo();
574
    }
575
576 130
    public function getTablePrefix(): string
577
    {
578 130
        return $this->tablePrefix;
579
    }
580
581
    /**
582
     * Obtains the schema information for the named table.
583
     *
584
     * @param string $name table name.
585
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
586
     *
587
     * @throws JsonException
588
     *
589
     * @return TableSchema
590
     */
591 194
    public function getTableSchema(string $name, $refresh = false): ?TableSchema
592
    {
593 194
        return $this->getSchema()->getTableSchema($name, $refresh);
594
    }
595
596
    /**
597
     * Returns the currently active transaction.
598
     *
599
     * @return Transaction|null the currently active transaction. Null if no active transaction.
600
     */
601 1714
    public function getTransaction(): ?Transaction
602
    {
603 1714
        return $this->transaction && $this->transaction->isActive() ? $this->transaction : null;
604
    }
605
606 1956
    public function getUsername(): ?string
607
    {
608 1956
        return $this->username;
609
    }
610
611
    /**
612
     * Disables query cache temporarily.
613
     *
614
     * Queries performed within the callable will not use query cache at all. For example,
615
     *
616
     * ```php
617
     * $db->cache(function (Connection $db) {
618
     *
619
     *     // ... queries that use query cache ...
620
     *
621
     *     return $db->noCache(function (Connection $db) {
622
     *         // this query will not use query cache
623
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
624
     *     });
625
     * });
626
     * ```
627
     *
628
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache. The signature
629
     * of the callable is `function (Connection $db)`.
630
     *
631
     * @throws Throwable if there is any exception during query
632
     *
633
     * @return mixed the return result of the callable
634
     *
635
     * {@see enableQueryCache}
636
     * {@see queryCache}
637
     * {@see cache()}
638
     */
639 10
    public function noCache(callable $callable)
640
    {
641 10
        $this->queryCache->setInfo(false);
642
643 10
        $result = $callable($this);
644
645 10
        $this->queryCache->removeLastInfo();
646
647 10
        return $result;
648
    }
649
650
    /**
651
     * Establishes a DB connection.
652
     *
653
     * It does nothing if a DB connection has already been established.
654
     *
655
     * @throws Exception|InvalidConfigException if connection fails
656
     */
657 1832
    public function open(): void
658
    {
659 1832
        if (!empty($this->pdo)) {
660 1669
            return;
661
        }
662
663 1832
        if (!empty($this->masters)) {
664 11
            $db = $this->getMaster();
665
666 11
            if ($db !== null) {
667 6
                $this->pdo = $db->getPDO();
668
669 6
                return;
670
            }
671
672 10
            throw new InvalidConfigException('None of the master DB servers is available.');
673
        }
674
675 1832
        if (empty($this->dsn)) {
676
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
677
        }
678
679 1832
        $token = 'Opening DB connection: ' . $this->dsn;
680
681
        try {
682 1832
            if ($this->enableLogging) {
683 1832
                $this->logger->log(LogLevel::INFO, $token);
684
            }
685
686 1832
            if ($this->isProfilingEnabled()) {
687 1832
                $this->profiler->begin($token, [__METHOD__]);
688
            }
689
690 1832
            $this->pdo = $this->createPdoInstance();
691
692 1832
            $this->initConnection();
693
694 1832
            if ($this->isProfilingEnabled()) {
695 1832
                $this->profiler->end($token, [__METHOD__]);
696
            }
697 15
        } catch (PDOException $e) {
698 15
            if ($this->isProfilingEnabled()) {
699 15
                $this->profiler->end($token, [__METHOD__]);
700
            }
701
702 15
            if ($this->enableLogging) {
703 15
                $this->logger->log(LogLevel::ERROR, $token);
704
            }
705
706 15
            throw new Exception($e->getMessage(), $e->errorInfo, $e);
707
        }
708 1832
    }
709
710
    /**
711
     * Closes the currently active DB connection.
712
     *
713
     * It does nothing if the connection is already closed.
714
     */
715 2895
    public function close(): void
716
    {
717 2895
        if ($this->master) {
718 5
            if ($this->pdo === $this->master->getPDO()) {
719 5
                $this->pdo = null;
720
            }
721
722 5
            $this->master->close();
723
724 5
            $this->master = null;
725
        }
726
727 2895
        if ($this->pdo !== null) {
728 1832
            if ($this->enableLogging) {
729 1822
                $this->logger->log(LogLevel::DEBUG, 'Closing DB connection: ' . $this->dsn . ' ' . __METHOD__);
730
            }
731
732 1832
            $this->pdo = null;
733 1832
            $this->schema = null;
734 1832
            $this->transaction = null;
735
        }
736
737 2895
        if ($this->slave) {
738 5
            $this->slave->close();
739 5
            $this->slave = null;
740
        }
741 2895
    }
742
743
    /**
744
     * Rolls back given {@see Transaction} object if it's still active and level match. In some cases rollback can fail,
745
     * so this method is fail safe. Exceptions thrown from rollback will be caught and just logged with
746
     * {@see logger->log()}.
747
     *
748
     * @param Transaction $transaction Transaction object given from {@see beginTransaction()}.
749
     * @param int $level Transaction level just after {@see beginTransaction()} call.
750
     */
751 10
    private function rollbackTransactionOnLevel(Transaction $transaction, int $level): void
752
    {
753 10
        if ($transaction->isActive() && $transaction->getLevel() === $level) {
754
            /**
755
             * {@see https://github.com/yiisoft/yii2/pull/13347}
756
             */
757
            try {
758 10
                $transaction->rollBack();
759
            } catch (Exception $e) {
760
                $this->logger->log(LogLevel::ERROR, $e, [__METHOD__]);
761
                /** hide this exception to be able to continue throwing original exception outside */
762
            }
763
        }
764 10
    }
765
766
    /**
767
     * Opens the connection to a server in the pool.
768
     *
769
     * This method implements the load balancing among the given list of the servers.
770
     *
771
     * Connections will be tried in random order.
772
     *
773
     * @param array $pool the list of connection configurations in the server pool
774
     *
775
     * @return Connection|null the opened DB connection, or `null` if no server is available
776
     */
777 1785
    protected function openFromPool(array $pool): ?self
778
    {
779 1785
        shuffle($pool);
780
781 1785
        return $this->openFromPoolSequentially($pool);
782
    }
783
784
    /**
785
     * Opens the connection to a server in the pool.
786
     *
787
     * This method implements the load balancing among the given list of the servers.
788
     *
789
     * Connections will be tried in sequential order.
790
     *
791
     * @param array $pool
792
     *
793
     * @return Connection|null the opened DB connection, or `null` if no server is available
794
     */
795 1790
    protected function openFromPoolSequentially(array $pool): ?self
796
    {
797 1790
        if (!$pool) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pool of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
798 1773
            return null;
799
        }
800
801 18
        foreach ($pool as $config) {
802
            /* @var $db Connection */
803 18
            $db = DatabaseFactory::createClass($config);
804
805 18
            $key = [__METHOD__, $db->getDsn()];
806
807 18
            if ($this->schemaCache->isEnabled() && $this->schemaCache->get($key)) {
808
                /** should not try this dead server now */
809
                continue;
810
            }
811
812
            try {
813 18
                $db->open();
814
815 13
                return $db;
816 10
            } catch (Exception $e) {
817 10
                if ($this->enableLogging) {
818 10
                    $this->logger->log(
819 10
                        LogLevel::WARNING,
820 10
                        "Connection ({$db->getDsn()}) failed: " . $e->getMessage() . ' ' . __METHOD__
821
                    );
822
                }
823
824 10
                if ($this->schemaCache->isEnabled()) {
825
                    /** mark this server as dead and only retry it after the specified interval */
826 5
                    $this->schemaCache->set($key, 1, $this->serverRetryInterval);
827
                }
828
829 10
                return null;
830
            }
831
        }
832
833
        return null;
834
    }
835
836
    /**
837
     * Quotes a column name for use in a query.
838
     *
839
     * If the column name contains prefix, the prefix will also be properly quoted.
840
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{', then this
841
     * method will do nothing.
842
     *
843
     * @param string $name column name
844
     *
845
     * @return string the properly quoted column name
846
     */
847 1588
    public function quoteColumnName(string $name): string
848
    {
849 1588
        return $this->quotedColumnNames[$name]
850 1588
            ?? ($this->quotedColumnNames[$name] = $this->getSchema()->quoteColumnName($name));
851
    }
852
853
    /**
854
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
855
     *
856
     * Tokens enclosed within double curly brackets are treated as table names, while tokens enclosed within double
857
     * square brackets are column names. They will be quoted accordingly. Also, the percentage character "%" at the
858
     * beginning or ending of a table name will be replaced with {@see tablePrefix}.
859
     *
860
     * @param string $sql the SQL to be quoted
861
     *
862
     * @return string the quoted SQL
863
     */
864 1824
    public function quoteSql(string $sql): string
865
    {
866 1824
        return preg_replace_callback(
867 1824
            '/({{(%?[\w\-. ]+%?)}}|\\[\\[([\w\-. ]+)]])/',
868 1824
            function ($matches) {
869 687
                if (isset($matches[3])) {
870 567
                    return $this->quoteColumnName($matches[3]);
871
                }
872
873 491
                return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
874 1824
            },
875
            $sql
876
        );
877
    }
878
879
    /**
880
     * Quotes a table name for use in a query.
881
     *
882
     * If the table name contains schema prefix, the prefix will also be properly quoted.
883
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{', then this method
884
     * will do nothing.
885
     *
886
     * @param string $name table name
887
     *
888
     * @return string the properly quoted table name
889
     */
890 1371
    public function quoteTableName(string $name): string
891
    {
892 1371
        return $this->quotedTableNames[$name]
893 1371
            ?? ($this->quotedTableNames[$name] = $this->getSchema()->quoteTableName($name));
894
    }
895
896
    /**
897
     * Quotes a string value for use in a query.
898
     *
899
     * Note that if the parameter is not a string, it will be returned without change.
900
     *
901
     * @param int|string $value string to be quoted
902
     *
903
     * @throws Exception|InvalidConfigException
904
     *
905
     * @return int|string the properly quoted string
906
     *
907
     * {@see http://php.net/manual/en/pdo.quote.php}
908
     */
909 1355
    public function quoteValue($value)
910
    {
911 1355
        return $this->getSchema()->quoteValue($value);
912
    }
913
914
    /**
915
     * PDO attributes (name => value) that should be set when calling {@see open()} to establish a DB connection.
916
     * Please refer to the [PHP manual](http://php.net/manual/en/pdo.setattribute.php) for details about available
917
     * attributes.
918
     *
919
     * @param array $value
920
     */
921
    public function setAttributes(array $value): void
922
    {
923
        $this->attributes = $value;
924
    }
925
926
    /**
927
     * The charset used for database connection. The property is only used for MySQL, PostgreSQL databases. Defaults to
928
     * null, meaning using default charset as configured by the database.
929
     *
930
     * For Oracle Database, the charset must be specified in the {@see dsn}, for example for UTF-8 by appending
931
     * `;charset=UTF-8` to the DSN string.
932
     *
933
     * The same applies for if you're using GBK or BIG5 charset with MySQL, then it's highly recommended to specify
934
     * charset via {@see dsn} like `'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'`.
935
     *
936
     * @param string|null $value
937
     */
938 1
    public function setCharset(?string $value): void
939
    {
940 1
        $this->charset = $value;
941 1
    }
942
943
    /**
944
     * Whether to enable profiling of opening database connection and database queries. Defaults to true. You may want
945
     * to disable this option in a production environment to gain performance if you do not need the information being
946
     * logged.
947
     *
948
     * @param bool $value
949
     */
950 10
    public function setEnableProfiling(bool $value): void
951
    {
952 10
        $this->enableProfiling = $value;
953 10
    }
954
955
    /**
956
     * Whether to turn on prepare emulation. Defaults to false, meaning PDO will use the native prepare support if
957
     * available. For some databases (such as MySQL), this may need to be set true so that PDO can emulate the prepare
958
     * support to bypass the buggy native prepare support. The default value is null, which means the PDO
959
     * ATTR_EMULATE_PREPARES value will not be changed.
960
     *
961
     * @param bool $value
962
     */
963 5
    public function setEmulatePrepare(bool $value): void
964
    {
965 5
        $this->emulatePrepare = $value;
966 5
    }
967
968
    /**
969
     * Whether to enable logging of database queries. Defaults to true. You may want to disable this option in a
970
     * production environment to gain performance if you do not need the information being logged.
971
     *
972
     * @param bool $value
973
     */
974 10
    public function setEnableLogging(bool $value): void
975
    {
976 10
        $this->enableLogging = $value;
977 10
    }
978
979
    /**
980
     * Whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint). Note that if the underlying DBMS does not
981
     * support savepoint, setting this property to be true will have no effect.
982
     *
983
     * @param bool $value
984
     */
985 5
    public function setEnableSavepoint(bool $value): void
986
    {
987 5
        $this->enableSavepoint = $value;
988 5
    }
989
990
    /**
991
     * Whether to enable read/write splitting by using {@see setSlaves()} to read data. Note that if {@see setSlaves()}
992
     * is empty, read/write splitting will NOT be enabled no matter what value this property takes.
993
     *
994
     * @param bool $value
995
     */
996
    public function setEnableSlaves(bool $value): void
997
    {
998
        $this->enableSlaves = $value;
999
    }
1000
1001
    /**
1002
     * List of master connection. Each DSN is used to create a master DB connection. When {@see open()} is called, one
1003
     * of these configurations will be chosen and used to create a DB connection which will be used by this object.
1004
     *
1005
     * @param string $key index master connection.
1006
     * @param array $config The configuration that should be merged with every master configuration
1007
     */
1008 14
    public function setMasters(string $key, array $config = []): void
1009
    {
1010 14
        $this->masters[$key] = $config;
1011 14
    }
1012
1013
    /**
1014
     * The password for establishing DB connection. Defaults to `null` meaning no password to use.
1015
     *
1016
     * @param string|null $value
1017
     */
1018 2604
    public function setPassword(?string $value): void
1019
    {
1020 2604
        $this->password = $value;
1021 2604
    }
1022
1023
    /**
1024
     * Can be used to set {@see QueryBuilder} configuration via Connection configuration array.
1025
     *
1026
     * @param iterable $config the {@see QueryBuilder} properties to be configured.
1027
     */
1028
    public function setQueryBuilder(iterable $config): void
1029
    {
1030
        $builder = $this->getQueryBuilder();
1031
1032
        foreach ($config as $key => $value) {
1033
            $builder->{$key} = $value;
1034
        }
1035
    }
1036
1037
    /**
1038
     * The retry interval in seconds for dead servers listed in {@see setMasters()} and {@see setSlaves()}.
1039
     *
1040
     * @param int $value
1041
     */
1042
    public function setServerRetryInterval(int $value): void
1043
    {
1044
        $this->serverRetryInterval = $value;
1045
    }
1046
1047
    /**
1048
     * Whether to shuffle {@see setMasters()} before getting one.
1049
     *
1050
     * @param bool $value
1051
     */
1052 12
    public function setShuffleMasters(bool $value): void
1053
    {
1054 12
        $this->shuffleMasters = $value;
1055 12
    }
1056
1057
    /**
1058
     * List of slave connection. Each DSN is used to create a slave DB connection. When {@see enableSlaves} is true,
1059
     * one of these configurations will be chosen and used to create a DB connection for performing read queries only.
1060
     *
1061
     * @param string $key index slave connection.
1062
     * @param array $config The configuration that should be merged with every slave configuration
1063
     */
1064 9
    public function setSlaves(string $key, array $config = []): void
1065
    {
1066 9
        $this->slaves[$key] = $config;
1067 9
    }
1068
1069
    /**
1070
     * The common prefix or suffix for table names. If a table name is given as `{{%TableName}}`, then the percentage
1071
     * character `%` will be replaced with this property value. For example, `{{%post}}` becomes `{{tbl_post}}`.
1072
     *
1073
     * @param string $value
1074
     */
1075 24
    public function setTablePrefix(string $value): void
1076
    {
1077 24
        $this->tablePrefix = $value;
1078 24
    }
1079
1080
    /**
1081
     * The username for establishing DB connection. Defaults to `null` meaning no username to use.
1082
     *
1083
     * @param string|null $value
1084
     */
1085 2604
    public function setUsername(?string $value): void
1086
    {
1087 2604
        $this->username = $value;
1088 2604
    }
1089
1090
    /**
1091
     * Executes callback provided in a transaction.
1092
     *
1093
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
1094
     * @param string|null $isolationLevel The isolation level to use for this transaction. {@see Transaction::begin()}
1095
     * for details.
1096
     *
1097
     * @throws Throwable if there is any exception during query. In this case the transaction will be rolled back.
1098
     *
1099
     * @return mixed result of callback function
1100
     */
1101 25
    public function transaction(callable $callback, $isolationLevel = null)
1102
    {
1103 25
        $transaction = $this->beginTransaction($isolationLevel);
1104
1105 25
        $level = $transaction->getLevel();
1106
1107
        try {
1108 25
            $result = $callback($this);
1109
1110 15
            if ($transaction->isActive() && $transaction->getLevel() === $level) {
1111 15
                $transaction->commit();
1112
            }
1113 10
        } catch (Throwable $e) {
1114 10
            $this->rollbackTransactionOnLevel($transaction, $level);
1115
1116 10
            throw $e;
1117
        }
1118
1119 15
        return $result;
1120
    }
1121
1122
    /**
1123
     * Executes the provided callback by using the master connection.
1124
     *
1125
     * This method is provided so that you can temporarily force using the master connection to perform DB operations
1126
     * even if they are read queries. For example,
1127
     *
1128
     * ```php
1129
     * $result = $db->useMaster(function ($db) {
1130
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
1131
     * });
1132
     * ```
1133
     *
1134
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
1135
     * `function (Connection $db)`. Its return value will be returned by this method.
1136
     *
1137
     * @throws Throwable if there is any exception thrown from the callback
1138
     *
1139
     * @return mixed the return value of the callback
1140
     */
1141 7
    public function useMaster(callable $callback)
1142
    {
1143 7
        if ($this->enableSlaves) {
1144 7
            $this->enableSlaves = false;
1145
1146
            try {
1147 7
                $result = $callback($this);
1148 1
            } catch (Throwable $e) {
1149 1
                $this->enableSlaves = true;
1150
1151 1
                throw $e;
1152
            }
1153 6
            $this->enableSlaves = true;
1154
        } else {
1155
            $result = $callback($this);
1156
        }
1157
1158 6
        return $result;
1159
    }
1160
}
1161