Passed
Push — master ( a8d37b...dabdd0 )
by Wilmer
10: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 PDO;
8
use PDOException;
9
use Psr\Log\LogLevel;
10
use Psr\Log\LoggerInterface;
11
use Throwable;
12
use Yiisoft\Cache\Dependency\Dependency;
13
use Yiisoft\Db\Cache\QueryCache;
14
use Yiisoft\Db\Cache\SchemaCache;
15
use Yiisoft\Db\Command\Command;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidCallException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Factory\DatabaseFactory;
21
use Yiisoft\Db\Query\QueryBuilder;
22
use Yiisoft\Db\Schema\Schema;
23
use Yiisoft\Db\Schema\TableSchema;
24
use Yiisoft\Db\Transaction\Transaction;
25
use Yiisoft\Profiler\ProfilerInterface;
26
27
use function array_keys;
28
use function str_replace;
29
use function strncmp;
30
31
/**
32
 * Connection represents a connection to a database via [PDO](http://php.net/manual/en/book.pdo.php).
33
 *
34
 * Connection works together with {@see Command}, {@see DataReader} and {@see Transaction} to provide data access to
35
 * various DBMS in a common set of APIs. They are a thin wrapper of the
36
 * [PDO PHP extension](http://php.net/manual/en/book.pdo.php).
37
 *
38
 * Connection supports database replication and read-write splitting. In particular, a Connection component can be
39
 * configured with multiple {@see setMasters()} and {@see setSlaves()}. It will do load balancing and failover by
40
 * choosing appropriate servers. It will also automatically direct read operations to the slaves and write operations
41
 * to the masters.
42
 *
43
 * To establish a DB connection, set {@see dsn}, {@see setUsername()} and {@see setPassword}, and then call
44
 * {@see open()} to connect to the database server. The current state of the connection can be checked using
45
 * {@see $isActive}.
46
 *
47
 * The following example shows how to create a Connection instance and establish the DB connection:
48
 *
49
 * ```php
50
 * $connection = new \Yiisoft\Db\Mysql\Connection(
51
 *     $cache,
52
 *     $logger,
53
 *     $profiler,
54
 *     $dsn
55
 * );
56
 * $connection->open();
57
 * ```
58
 *
59
 * After the DB connection is established, one can execute SQL statements like the following:
60
 *
61
 * ```php
62
 * $command = $connection->createCommand('SELECT * FROM post');
63
 * $posts = $command->queryAll();
64
 * $command = $connection->createCommand('UPDATE post SET status=1');
65
 * $command->execute();
66
 * ```
67
 *
68
 * One can also do prepared SQL execution and bind parameters to the prepared SQL.
69
 * When the parameters are coming from user input, you should use this approach to prevent SQL injection attacks. The
70
 * following is an example:
71
 *
72
 * ```php
73
 * $command = $connection->createCommand('SELECT * FROM post WHERE id=:id');
74
 * $command->bindValue(':id', $_GET['id']);
75
 * $post = $command->query();
76
 * ```
77
 *
78
 * For more information about how to perform various DB queries, please refer to {@see Command}.
79
 *
80
 * If the underlying DBMS supports transactions, you can perform transactional SQL queries like the following:
81
 *
82
 * ```php
83
 * $transaction = $connection->beginTransaction();
84
 * try {
85
 *     $connection->createCommand($sql1)->execute();
86
 *     $connection->createCommand($sql2)->execute();
87
 *     // ... executing other SQL statements ...
88
 *     $transaction->commit();
89
 * } catch (Exceptions $e) {
90
 *     $transaction->rollBack();
91
 * }
92
 * ```
93
 *
94
 * You also can use shortcut for the above like the following:
95
 *
96
 * ```php
97
 * $connection->transaction(function () {
98
 *     $order = new Order($customer);
99
 *     $order->save();
100
 *     $order->addItems($items);
101
 * });
102
 * ```
103
 *
104
 * If needed you can pass transaction isolation level as a second parameter:
105
 *
106
 * ```php
107
 * $connection->transaction(function (Connection $db) {
108
 *     //return $db->...
109
 * }, Transaction::READ_UNCOMMITTED);
110
 * ```
111
 *
112
 * Connection is often used as an application component and configured in the container-di configuration like the
113
 * following:
114
 *
115
 * ```php
116
 * Connection::class => static function (ContainerInterface $container) {
117
 *     $connection = new Connection(
118
 *         $container->get(CacheInterface::class),
119
 *         $container->get(LoggerInterface::class),
120
 *         $container->get(Profiler::class),
121
 *         'mysql:host=127.0.0.1;dbname=demo;charset=utf8'
122
 *     );
123
 *
124
 *     $connection->setUsername(root);
125
 *     $connection->setPassword('');
126
 *
127
 *     return $connection;
128
 * },
129
 * ```
130
 *
131
 * The {@see dsn} property can be defined via configuration {@see \Yiisoft\Db\Connection\Dsn}:
132
 *
133
 * ```php
134
 * Connection::class => static function (ContainerInterface $container) {
135
 *     $dsn = new Dsn('mysql', '127.0.0.1', 'yiitest', '3306');
136
 *
137
 *     $connection = new Connection(
138
 *         $container->get(CacheInterface::class),
139
 *         $container->get(LoggerInterface::class),
140
 *         $container->get(Profiler::class),
141
 *         $dsn->getDsn()
142
 *     );
143
 *
144
 *     $connection->setUsername(root);
145
 *     $connection->setPassword('');
146
 *
147
 *     return $connection;
148
 * },
149
 * ```
150
 *
151
 * @property string $driverName Name of the DB driver.
152
 * @property bool $isActive Whether the DB connection is established. This property is read-only.
153
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence
154
 * object. This property is read-only.
155
 * @property Connection $master The currently active master connection. `null` is returned if there is no master
156
 * available. This property is read-only.
157
 * @property PDO $masterPdo The PDO instance for the currently active master connection. This property is read-only.
158
 * @property QueryBuilder $queryBuilder The query builder for the current DB connection. Note that the type of this
159
 * property differs in getter and setter. See {@see getQueryBuilder()} and {@see setQueryBuilder()} for details.
160
 * @property Schema $schema The schema information for the database opened by this connection. This property is
161
 * read-only.
162
 * @property string $serverVersion Server version as a string. This property is read-only.
163
 * @property Connection $slave The currently active slave connection. `null` is returned if there is no slave
164
 * available and `$fallbackToMaster` is false. This property is read-only.
165
 * @property PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned if no slave
166
 * connection is available and `$fallbackToMaster` is false. This property is read-only.
167
 * @property Transaction|null $transaction The currently active transaction. Null if no active transaction. This
168
 * property is read-only.
169
 */
170
abstract class Connection implements ConnectionInterface
171
{
172
    private string $dsn;
173
    private ?string $username = null;
174
    private ?string $password = null;
175
    private array $attributes = [];
176
    private ?PDO $pdo = null;
177
    private ?string $charset = null;
178
    private ?bool $emulatePrepare = null;
179
    private string $tablePrefix = '';
180
    private bool $enableSavepoint = true;
181
    private int $serverRetryInterval = 600;
182
    private bool $enableSlaves = true;
183
    private array $slaves = [];
184
    private array $masters = [];
185
    private bool $shuffleMasters = true;
186
    private bool $enableLogging = true;
187
    private array $quotedTableNames = [];
188
    private array $quotedColumnNames = [];
189
    private ?Connection $master = null;
190
    private ?Connection $slave = null;
191
    private ?Transaction $transaction = null;
192
    private ?Schema $schema = null;
193
    private bool $enableProfiling = true;
194
    private LazyConnectionDependencies $dependencies;
195
196 2989
    public function __construct(string $dsn, LazyConnectionDependencies $dependencies)
197
    {
198 2989
        $this->dsn = $dsn;
199 2989
        $this->dependencies = $dependencies;
200 2989
    }
201
202
    /**
203
     * Creates a command for execution.
204
     *
205
     * @param string|null $sql the SQL statement to be executed
206
     * @param array $params the parameters to be bound to the SQL statement
207
     *
208
     * @throws Exception|InvalidConfigException
209
     *
210
     * @return Command the DB command
211
     */
212
    abstract public function createCommand(?string $sql = null, array $params = []): Command;
213
214
    /**
215
     * Returns the schema information for the database opened by this connection.
216
     *
217
     * @return Schema the schema information for the database opened by this connection.
218
     */
219
    abstract public function getSchema(): Schema;
220
221
    /**
222
     * Creates the PDO instance.
223
     *
224
     * This method is called by {@see open} to establish a DB connection. The default implementation will create a PHP
225
     * PDO instance. You may override this method if the default PDO needs to be adapted for certain DBMS.
226
     *
227
     * @return PDO the pdo instance
228
     */
229
    abstract protected function createPdoInstance(): PDO;
230
231
    /**
232
     * Initializes the DB connection.
233
     *
234
     * This method is invoked right after the DB connection is established.
235
     *
236
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`.
237
     *
238
     * if {@see emulatePrepare} is true, and sets the database {@see charset} if it is not empty.
239
     *
240
     * It then triggers an {@see EVENT_AFTER_OPEN} event.
241
     */
242
    abstract protected function initConnection(): void;
243
244
    /**
245
     * Reset the connection after cloning.
246
     */
247 10
    public function __clone()
248
    {
249 10
        $this->master = null;
250 10
        $this->slave = null;
251 10
        $this->schema = null;
252 10
        $this->transaction = null;
253
254 10
        if (strncmp($this->dsn, 'sqlite::memory:', 15) !== 0) {
255
            /** reset PDO connection, unless its sqlite in-memory, which can only have one connection */
256 10
            $this->pdo = null;
257
        }
258 10
    }
259
260
    /**
261
     * Close the connection before serializing.
262
     *
263
     * @return array
264
     */
265 6
    public function __sleep(): array
266
    {
267 6
        $fields = (array) $this;
268
269
        unset(
270 6
            $fields["\000" . __CLASS__ . "\000" . 'pdo'],
271 6
            $fields["\000" . __CLASS__ . "\000" . 'master'],
272 6
            $fields["\000" . __CLASS__ . "\000" . 'slave'],
273 6
            $fields["\000" . __CLASS__ . "\000" . 'transaction'],
274 6
            $fields["\000" . __CLASS__ . "\000" . 'schema']
275
        );
276
277 6
        return array_keys($fields);
278
    }
279
280
    /**
281
     * Starts a transaction.
282
     *
283
     * @param string|null $isolationLevel The isolation level to use for this transaction.
284
     *
285
     * {@see Transaction::begin()} for details.
286
     *
287
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
288
     *
289
     * @return Transaction the transaction initiated
290
     */
291 40
    public function beginTransaction($isolationLevel = null): Transaction
292
    {
293 40
        $this->open();
294
295 40
        if (($transaction = $this->getTransaction()) === null) {
296 40
            $transaction = $this->transaction = new Transaction($this);
297
        }
298
299 40
        $transaction->begin($isolationLevel);
300
301 40
        return $transaction;
302
    }
303
304
    /**
305
     * Uses query cache for the queries performed with the callable.
306
     *
307
     * When query caching is enabled ({@see enableQueryCache} is true and {@see queryCache} refers to a valid cache),
308
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
309
     *
310
     * For example,
311
     *
312
     * ```php
313
     * // The customer will be fetched from cache if available.
314
     * // If not, the query will be made against DB and cached for use next time.
315
     * $customer = $db->cache(function (Connection $db) {
316
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
317
     * });
318
     * ```
319
     *
320
     * Note that query cache is only meaningful for queries that return results. For queries performed with
321
     * {@see Command::execute()}, query cache will not be used.
322
     *
323
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
324
     * The signature of the callable is `function (Connection $db)`.
325
     * @param int|null $duration the number of seconds that query results can remain valid in the cache. If this is not
326
     * set, the value of {@see queryCacheDuration} will be used instead. Use 0 to indicate that the cached data will
327
     * never expire.
328
     * @param Dependency|null $dependency the cache dependency associated with the cached query
329
     * results.
330
     *
331
     * @throws Throwable if there is any exception during query
332
     *
333
     * @return mixed the return result of the callable
334
     *
335
     * {@see setEnableQueryCache()}
336
     * {@see queryCache}
337
     * {@see noCache()}
338
     */
339 10
    public function cache(callable $callable, int $duration = null, Dependency $dependency = null)
340
    {
341 10
        $this->getQueryCache()->setInfo(
342 10
            [$duration ?? $this->getQueryCache()->getDuration(), $dependency]
343
        );
344
345 10
        $result = $callable($this);
346
347 10
        $this->getQueryCache()->removeLastInfo();
348
349 10
        return $result;
350
    }
351
352 1832
    public function getAttributes(): array
353
    {
354 1832
        return $this->attributes;
355
    }
356
357 733
    public function getCharset(): ?string
358
    {
359 733
        return $this->charset;
360
    }
361
362 1857
    public function getDsn(): string
363
    {
364 1857
        return $this->dsn;
365
    }
366
367 1463
    public function getEmulatePrepare(): ?bool
368
    {
369 1463
        return $this->emulatePrepare;
370
    }
371
372 1699
    public function isLoggingEnabled(): bool
373
    {
374 1699
        return $this->enableLogging;
375
    }
376
377 1832
    public function isProfilingEnabled(): bool
378
    {
379 1832
        return $this->enableProfiling;
380
    }
381
382 10
    public function isSavepointEnabled(): bool
383
    {
384 10
        return $this->enableSavepoint;
385
    }
386
387 1
    public function areSlavesEnabled(): bool
388
    {
389 1
        return $this->enableSlaves;
390
    }
391
392
    /**
393
     * Returns a value indicating whether the DB connection is established.
394
     *
395
     * @return bool whether the DB connection is established
396
     */
397 193
    public function isActive(): bool
398
    {
399 193
        return $this->pdo !== null;
400
    }
401
402
    /**
403
     * Returns the ID of the last inserted row or sequence value.
404
     *
405
     * @param string $sequenceName name of the sequence object (required by some DBMS)
406
     *
407
     * @throws Exception
408
     * @throws InvalidCallException
409
     *
410
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
411
     *
412
     * {@see http://php.net/manual/en/pdo.lastinsertid.php'>http://php.net/manual/en/pdo.lastinsertid.php}
413
     */
414 10
    public function getLastInsertID($sequenceName = ''): string
415
    {
416 10
        return $this->getSchema()->getLastInsertID($sequenceName);
417
    }
418
419
    /**
420
     * Returns the currently active master connection.
421
     *
422
     * If this method is called for the first time, it will try to open a master connection.
423
     *
424
     * @return Connection the currently active master connection. `null` is returned if there is no master available.
425
     */
426 13
    public function getMaster(): ?self
427
    {
428 13
        if ($this->master === null) {
429 13
            $this->master = $this->shuffleMasters
430 7
                ? $this->openFromPool($this->masters)
431 8
                : $this->openFromPoolSequentially($this->masters);
432
        }
433
434 13
        return $this->master;
435
    }
436
437
    /**
438
     * Returns the PDO instance for the currently active master connection.
439
     *
440
     * This method will open the master DB connection and then return {@see pdo}.
441
     *
442
     * @throws Exception
443
     *
444
     * @return PDO the PDO instance for the currently active master connection.
445
     */
446 1798
    public function getMasterPdo(): PDO
447
    {
448 1798
        $this->open();
449
450 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...
451
    }
452
453 1832
    public function getPassword(): ?string
454
    {
455 1832
        return $this->password;
456
    }
457
458
    /**
459
     * The PHP PDO instance associated with this DB connection. This property is mainly managed by {@see open()} and
460
     * {@see close()} methods. When a DB connection is active, this property will represent a PDO instance; otherwise,
461
     * it will be null.
462
     *
463
     * @return PDO|null
464
     *
465
     * {@see pdoClass}
466
     */
467 1832
    public function getPDO(): ?PDO
468
    {
469 1832
        return $this->pdo;
470
    }
471
472
    /**
473
     * Returns the query builder for the current DB connection.
474
     *
475
     * @return QueryBuilder the query builder for the current DB connection.
476
     */
477 942
    public function getQueryBuilder(): QueryBuilder
478
    {
479 942
        return $this->getSchema()->getQueryBuilder();
480
    }
481
482
    /**
483
     * Returns a server version as a string comparable by {@see \version_compare()}.
484
     *
485
     * @throws Exception
486
     *
487
     * @return string server version as a string.
488
     */
489 311
    public function getServerVersion(): string
490
    {
491 311
        return $this->getSchema()->getServerVersion();
492
    }
493
494
    /**
495
     * Returns the currently active slave connection.
496
     *
497
     * If this method is called for the first time, it will try to open a slave connection when {@see setEnableSlaves()}
498
     * is true.
499
     *
500
     * @param bool $fallbackToMaster whether to return a master connection in case there is no slave connection
501
     * available.
502
     *
503
     * @return Connection the currently active slave connection. `null` is returned if there is no slave available and
504
     * `$fallbackToMaster` is false.
505
     */
506 1781
    public function getSlave(bool $fallbackToMaster = true): ?self
507
    {
508 1781
        if (!$this->enableSlaves) {
509 6
            return $fallbackToMaster ? $this : null;
510
        }
511
512 1780
        if ($this->slave === null) {
513 1780
            $this->slave = $this->openFromPool($this->slaves);
514
        }
515
516 1780
        return $this->slave === null && $fallbackToMaster ? $this : $this->slave;
517
    }
518
519
    /**
520
     * Returns the PDO instance for the currently active slave connection.
521
     *
522
     * When {@see enableSlaves} is true, one of the slaves will be used for read queries, and its PDO instance will be
523
     * returned by this method.
524
     *
525
     * @param bool $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
526
     *
527
     * @throws Exception
528
     *
529
     * @return PDO the PDO instance for the currently active slave connection. `null` is returned if no slave connection
530
     * is available and `$fallbackToMaster` is false.
531
     */
532 1779
    public function getSlavePdo(bool $fallbackToMaster = true): ?PDO
533
    {
534 1779
        $db = $this->getSlave(false);
535
536 1779
        if ($db === null) {
537 1774
            return $fallbackToMaster ? $this->getMasterPdo() : null;
538
        }
539
540 6
        return $db->getPdo();
541
    }
542
543 130
    public function getTablePrefix(): string
544
    {
545 130
        return $this->tablePrefix;
546
    }
547
548
    /**
549
     * Obtains the schema information for the named table.
550
     *
551
     * @param string $name table name.
552
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
553
     *
554
     * @return TableSchema
555
     */
556 194
    public function getTableSchema(string $name, $refresh = false): ?TableSchema
557
    {
558 194
        return $this->getSchema()->getTableSchema($name, $refresh);
559
    }
560
561
    /**
562
     * Returns the currently active transaction.
563
     *
564
     * @return Transaction|null the currently active transaction. Null if no active transaction.
565
     */
566 1714
    public function getTransaction(): ?Transaction
567
    {
568 1714
        return $this->transaction && $this->transaction->isActive() ? $this->transaction : null;
569
    }
570
571 1956
    public function getUsername(): ?string
572
    {
573 1956
        return $this->username;
574
    }
575
576
    /**
577
     * Disables query cache temporarily.
578
     *
579
     * Queries performed within the callable will not use query cache at all. For example,
580
     *
581
     * ```php
582
     * $db->cache(function (Connection $db) {
583
     *
584
     *     // ... queries that use query cache ...
585
     *
586
     *     return $db->noCache(function (Connection $db) {
587
     *         // this query will not use query cache
588
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
589
     *     });
590
     * });
591
     * ```
592
     *
593
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache. The signature
594
     * of the callable is `function (Connection $db)`.
595
     *
596
     * @throws Throwable if there is any exception during query
597
     *
598
     * @return mixed the return result of the callable
599
     *
600
     * {@see enableQueryCache}
601
     * {@see queryCache}
602
     * {@see cache()}
603
     */
604 10
    public function noCache(callable $callable)
605
    {
606 10
        $queryCache = $this->dependencies->queryCache();
607
608 10
        $queryCache->setInfo(false);
609
610 10
        $result = $callable($this);
611
612 10
        $queryCache->removeLastInfo();
613
614 10
        return $result;
615
    }
616
617
    /**
618
     * Establishes a DB connection.
619
     *
620
     * It does nothing if a DB connection has already been established.
621
     *
622
     * @throws Exception|InvalidConfigException if connection fails
623
     */
624 1832
    public function open(): void
625
    {
626 1832
        if (!empty($this->pdo)) {
627 1667
            return;
628
        }
629
630 1832
        if (!empty($this->masters)) {
631 11
            $db = $this->getMaster();
632
633 11
            if ($db !== null) {
634 8
                $this->pdo = $db->getPDO();
635
636 8
                return;
637
            }
638
639 10
            throw new InvalidConfigException('None of the master DB servers is available.');
640
        }
641
642 1832
        if (empty($this->dsn)) {
643
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
644
        }
645
646 1832
        $token = 'Opening DB connection: ' . $this->dsn;
647
648
        try {
649 1832
            if ($this->enableLogging) {
650 1832
                $this->getLogger()->log(LogLevel::INFO, $token);
651
            }
652
653 1832
            if ($this->isProfilingEnabled()) {
654 1832
                $this->getProfiler()->begin($token, [__METHOD__]);
655
            }
656
657 1832
            $this->pdo = $this->createPdoInstance();
658
659 1832
            $this->initConnection();
660
661 1832
            if ($this->isProfilingEnabled()) {
662 1832
                $this->getProfiler()->end($token, [__METHOD__]);
663
            }
664 15
        } catch (PDOException $e) {
665 15
            if ($this->isProfilingEnabled()) {
666 15
                $this->getProfiler()->end($token, [__METHOD__]);
667
            }
668
669 15
            if ($this->enableLogging) {
670 15
                $this->getLogger()->log(LogLevel::ERROR, $token);
671
            }
672
673 15
            throw new Exception($e->getMessage(), $e->errorInfo, $e);
674
        }
675 1832
    }
676
677
    /**
678
     * Closes the currently active DB connection.
679
     *
680
     * It does nothing if the connection is already closed.
681
     */
682 2895
    public function close(): void
683
    {
684 2895
        if ($this->master) {
685 7
            if ($this->pdo === $this->master->getPDO()) {
686 7
                $this->pdo = null;
687
            }
688
689 7
            $this->master->close();
690
691 7
            $this->master = null;
692
        }
693
694 2895
        if ($this->pdo !== null) {
695 1832
            if ($this->enableLogging) {
696 1822
                $this->getLogger()->log(LogLevel::DEBUG, 'Closing DB connection: ' . $this->dsn . ' ' . __METHOD__);
697
            }
698
699 1832
            $this->pdo = null;
700 1832
            $this->schema = null;
701 1832
            $this->transaction = null;
702
        }
703
704 2895
        if ($this->slave) {
705 5
            $this->slave->close();
706 5
            $this->slave = null;
707
        }
708 2895
    }
709
710
    /**
711
     * Rolls back given {@see Transaction} object if it's still active and level match. In some cases rollback can fail,
712
     * so this method is fail safe. Exceptions thrown from rollback will be caught and just logged with
713
     * {@see logger->log()}.
714
     *
715
     * @param Transaction $transaction Transaction object given from {@see beginTransaction()}.
716
     * @param int $level Transaction level just after {@see beginTransaction()} call.
717
     */
718 10
    private function rollbackTransactionOnLevel(Transaction $transaction, int $level): void
719
    {
720 10
        if ($transaction->isActive() && $transaction->getLevel() === $level) {
721
            /**
722
             * {@see https://github.com/yiisoft/yii2/pull/13347}
723
             */
724
            try {
725 10
                $transaction->rollBack();
726
            } catch (Exception $e) {
727
                $this->getLogger()->log(LogLevel::ERROR, $e, [__METHOD__]);
728
                /** hide this exception to be able to continue throwing original exception outside */
729
            }
730
        }
731 10
    }
732
733
    /**
734
     * Opens the connection to a server in the pool.
735
     *
736
     * This method implements the load balancing among the given list of the servers.
737
     *
738
     * Connections will be tried in random order.
739
     *
740
     * @param array $pool the list of connection configurations in the server pool
741
     *
742
     * @return Connection|null the opened DB connection, or `null` if no server is available
743
     */
744 1785
    protected function openFromPool(array $pool): ?self
745
    {
746 1785
        shuffle($pool);
747
748 1785
        return $this->openFromPoolSequentially($pool);
749
    }
750
751
    /**
752
     * Opens the connection to a server in the pool.
753
     *
754
     * This method implements the load balancing among the given list of the servers.
755
     *
756
     * Connections will be tried in sequential order.
757
     *
758
     * @param array $pool
759
     *
760
     * @return Connection|null the opened DB connection, or `null` if no server is available
761
     */
762 1790
    protected function openFromPoolSequentially(array $pool): ?self
763
    {
764 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...
765 1773
            return null;
766
        }
767
768 18
        foreach ($pool as $config) {
769
            /* @var $db Connection */
770 18
            $db = DatabaseFactory::createClass($config);
771
772 18
            $key = [__METHOD__, $db->getDsn()];
773
774
            if (
775 18
                $this->getSchemaCache()->isEnabled() &&
776 18
                $this->getSchemaCache()->getOrSet($key, null, $this->serverRetryInterval)
777
            ) {
778
                /** should not try this dead server now */
779
                continue;
780
            }
781
782
            try {
783 18
                $db->open();
784
785 15
                return $db;
786 10
            } catch (Exception $e) {
787 10
                if ($this->enableLogging) {
788 10
                    $this->getLogger()->log(
789 10
                        LogLevel::WARNING,
790 10
                        "Connection ({$db->getDsn()}) failed: " . $e->getMessage() . ' ' . __METHOD__
791
                    );
792
                }
793
794 10
                if ($this->getSchemaCache()->isEnabled()) {
795
                    /** mark this server as dead and only retry it after the specified interval */
796 5
                    $this->getSchemaCache()->set($key, 1, $this->serverRetryInterval);
797
                }
798
799 10
                return null;
800
            }
801
        }
802
803
        return null;
804
    }
805
806
    /**
807
     * Quotes a column name for use in a query.
808
     *
809
     * If the column name contains prefix, the prefix will also be properly quoted.
810
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{', then this
811
     * method will do nothing.
812
     *
813
     * @param string $name column name
814
     *
815
     * @return string the properly quoted column name
816
     */
817 1588
    public function quoteColumnName(string $name): string
818
    {
819 1588
        return $this->quotedColumnNames[$name]
820 1588
            ?? ($this->quotedColumnNames[$name] = $this->getSchema()->quoteColumnName($name));
821
    }
822
823
    /**
824
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
825
     *
826
     * Tokens enclosed within double curly brackets are treated as table names, while tokens enclosed within double
827
     * square brackets are column names. They will be quoted accordingly. Also, the percentage character "%" at the
828
     * beginning or ending of a table name will be replaced with {@see tablePrefix}.
829
     *
830
     * @param string $sql the SQL to be quoted
831
     *
832
     * @return string the quoted SQL
833
     */
834 1824
    public function quoteSql(string $sql): string
835
    {
836 1824
        return preg_replace_callback(
837 1824
            '/({{(%?[\w\-. ]+%?)}}|\\[\\[([\w\-. ]+)]])/',
838 1824
            function ($matches) {
839 687
                if (isset($matches[3])) {
840 567
                    return $this->quoteColumnName($matches[3]);
841
                }
842
843 491
                return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
844 1824
            },
845
            $sql
846
        );
847
    }
848
849
    /**
850
     * Quotes a table name for use in a query.
851
     *
852
     * If the table name contains schema prefix, the prefix will also be properly quoted.
853
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{', then this method
854
     * will do nothing.
855
     *
856
     * @param string $name table name
857
     *
858
     * @return string the properly quoted table name
859
     */
860 1371
    public function quoteTableName(string $name): string
861
    {
862 1371
        return $this->quotedTableNames[$name]
863 1371
            ?? ($this->quotedTableNames[$name] = $this->getSchema()->quoteTableName($name));
864
    }
865
866
    /**
867
     * Quotes a string value for use in a query.
868
     *
869
     * Note that if the parameter is not a string, it will be returned without change.
870
     *
871
     * @param int|string $value string to be quoted
872
     *
873
     * @throws Exception
874
     *
875
     * @return int|string the properly quoted string
876
     *
877
     * {@see http://php.net/manual/en/pdo.quote.php}
878
     */
879 1355
    public function quoteValue($value)
880
    {
881 1355
        return $this->getSchema()->quoteValue($value);
882
    }
883
884
    /**
885
     * PDO attributes (name => value) that should be set when calling {@see open()} to establish a DB connection.
886
     * Please refer to the [PHP manual](http://php.net/manual/en/pdo.setattribute.php) for details about available
887
     * attributes.
888
     *
889
     * @param array $value
890
     */
891
    public function setAttributes(array $value): void
892
    {
893
        $this->attributes = $value;
894
    }
895
896
    /**
897
     * The charset used for database connection. The property is only used for MySQL, PostgreSQL databases. Defaults to
898
     * null, meaning using default charset as configured by the database.
899
     *
900
     * For Oracle Database, the charset must be specified in the {@see dsn}, for example for UTF-8 by appending
901
     * `;charset=UTF-8` to the DSN string.
902
     *
903
     * The same applies for if you're using GBK or BIG5 charset with MySQL, then it's highly recommended to specify
904
     * charset via {@see dsn} like `'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'`.
905
     *
906
     * @param string|null $value
907
     */
908 1
    public function setCharset(?string $value): void
909
    {
910 1
        $this->charset = $value;
911 1
    }
912
913
    /**
914
     * Whether to enable profiling of opening database connection and database queries. Defaults to true. You may want
915
     * to disable this option in a production environment to gain performance if you do not need the information being
916
     * logged.
917
     *
918
     * @param bool $value
919
     */
920 10
    public function setEnableProfiling(bool $value): void
921
    {
922 10
        $this->enableProfiling = $value;
923 10
    }
924
925
    /**
926
     * Whether to turn on prepare emulation. Defaults to false, meaning PDO will use the native prepare support if
927
     * available. For some databases (such as MySQL), this may need to be set true so that PDO can emulate the prepare
928
     * support to bypass the buggy native prepare support. The default value is null, which means the PDO
929
     * ATTR_EMULATE_PREPARES value will not be changed.
930
     *
931
     * @param bool $value
932
     */
933 5
    public function setEmulatePrepare(bool $value): void
934
    {
935 5
        $this->emulatePrepare = $value;
936 5
    }
937
938
    /**
939
     * Whether to enable logging of database queries. Defaults to true. You may want to disable this option in a
940
     * production environment to gain performance if you do not need the information being logged.
941
     *
942
     * @param bool $value
943
     */
944 10
    public function setEnableLogging(bool $value): void
945
    {
946 10
        $this->enableLogging = $value;
947 10
    }
948
949
    /**
950
     * Whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint). Note that if the underlying DBMS does not
951
     * support savepoint, setting this property to be true will have no effect.
952
     *
953
     * @param bool $value
954
     */
955 5
    public function setEnableSavepoint(bool $value): void
956
    {
957 5
        $this->enableSavepoint = $value;
958 5
    }
959
960
    /**
961
     * Whether to enable read/write splitting by using {@see setSlaves()} to read data. Note that if {@see setSlaves()}
962
     * is empty, read/write splitting will NOT be enabled no matter what value this property takes.
963
     *
964
     * @param bool $value
965
     */
966
    public function setEnableSlaves(bool $value): void
967
    {
968
        $this->enableSlaves = $value;
969
    }
970
971
    /**
972
     * List of master connection. Each DSN is used to create a master DB connection. When {@see open()} is called, one
973
     * of these configurations will be chosen and used to create a DB connection which will be used by this object.
974
     *
975
     * @param string $key index master connection.
976
     * @param array $config The configuration that should be merged with every master configuration
977
     */
978 14
    public function setMasters(string $key, array $config = []): void
979
    {
980 14
        $this->masters[$key] = $config;
981 14
    }
982
983
    /**
984
     * The password for establishing DB connection. Defaults to `null` meaning no password to use.
985
     *
986
     * @param string|null $value
987
     */
988 2604
    public function setPassword(?string $value): void
989
    {
990 2604
        $this->password = $value;
991 2604
    }
992
993
    /**
994
     * Can be used to set {@see QueryBuilder} configuration via Connection configuration array.
995
     *
996
     * @param iterable $config the {@see QueryBuilder} properties to be configured.
997
     */
998
    public function setQueryBuilder(iterable $config): void
999
    {
1000
        $builder = $this->getQueryBuilder();
1001
1002
        foreach ($config as $key => $value) {
1003
            $builder->{$key} = $value;
1004
        }
1005
    }
1006
1007
    /**
1008
     * The retry interval in seconds for dead servers listed in {@see setMasters()} and {@see setSlaves()}.
1009
     *
1010
     * @param int $value
1011
     */
1012
    public function setServerRetryInterval(int $value): void
1013
    {
1014
        $this->serverRetryInterval = $value;
1015
    }
1016
1017
    /**
1018
     * Whether to shuffle {@see setMasters()} before getting one.
1019
     *
1020
     * @param bool $value
1021
     */
1022 12
    public function setShuffleMasters(bool $value): void
1023
    {
1024 12
        $this->shuffleMasters = $value;
1025 12
    }
1026
1027
    /**
1028
     * List of slave connection. Each DSN is used to create a slave DB connection. When {@see enableSlaves} is true,
1029
     * one of these configurations will be chosen and used to create a DB connection for performing read queries only.
1030
     *
1031
     * @param string $key index slave connection.
1032
     * @param array $config The configuration that should be merged with every slave configuration
1033
     */
1034 9
    public function setSlaves(string $key, array $config = []): void
1035
    {
1036 9
        $this->slaves[$key] = $config;
1037 9
    }
1038
1039
    /**
1040
     * The common prefix or suffix for table names. If a table name is given as `{{%TableName}}`, then the percentage
1041
     * character `%` will be replaced with this property value. For example, `{{%post}}` becomes `{{tbl_post}}`.
1042
     *
1043
     * @param string $value
1044
     */
1045 24
    public function setTablePrefix(string $value): void
1046
    {
1047 24
        $this->tablePrefix = $value;
1048 24
    }
1049
1050
    /**
1051
     * The username for establishing DB connection. Defaults to `null` meaning no username to use.
1052
     *
1053
     * @param string|null $value
1054
     */
1055 2604
    public function setUsername(?string $value): void
1056
    {
1057 2604
        $this->username = $value;
1058 2604
    }
1059
1060
    /**
1061
     * Executes callback provided in a transaction.
1062
     *
1063
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
1064
     * @param string|null $isolationLevel The isolation level to use for this transaction. {@see Transaction::begin()}
1065
     * for details.
1066
     *
1067
     * @throws Throwable if there is any exception during query. In this case the transaction will be rolled back.
1068
     *
1069
     * @return mixed result of callback function
1070
     */
1071 25
    public function transaction(callable $callback, $isolationLevel = null)
1072
    {
1073 25
        $transaction = $this->beginTransaction($isolationLevel);
1074
1075 25
        $level = $transaction->getLevel();
1076
1077
        try {
1078 25
            $result = $callback($this);
1079
1080 15
            if ($transaction->isActive() && $transaction->getLevel() === $level) {
1081 15
                $transaction->commit();
1082
            }
1083 10
        } catch (Throwable $e) {
1084 10
            $this->rollbackTransactionOnLevel($transaction, $level);
1085
1086 10
            throw $e;
1087
        }
1088
1089 15
        return $result;
1090
    }
1091
1092
    /**
1093
     * Executes the provided callback by using the master connection.
1094
     *
1095
     * This method is provided so that you can temporarily force using the master connection to perform DB operations
1096
     * even if they are read queries. For example,
1097
     *
1098
     * ```php
1099
     * $result = $db->useMaster(function ($db) {
1100
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
1101
     * });
1102
     * ```
1103
     *
1104
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
1105
     * `function (Connection $db)`. Its return value will be returned by this method.
1106
     *
1107
     * @throws Throwable if there is any exception thrown from the callback
1108
     *
1109
     * @return mixed the return value of the callback
1110
     */
1111 7
    public function useMaster(callable $callback)
1112
    {
1113 7
        if ($this->enableSlaves) {
1114 7
            $this->enableSlaves = false;
1115
1116
            try {
1117 7
                $result = $callback($this);
1118 1
            } catch (Throwable $e) {
1119 1
                $this->enableSlaves = true;
1120
1121 1
                throw $e;
1122
            }
1123 6
            $this->enableSlaves = true;
1124
        } else {
1125
            $result = $callback($this);
1126
        }
1127
1128 6
        return $result;
1129
    }
1130
1131 1832
    public function getLogger(): LoggerInterface
1132
    {
1133 1832
        return $this->dependencies->logger();
1134
    }
1135
1136 1832
    public function getProfiler(): ProfilerInterface
1137
    {
1138 1832
        return $this->dependencies->profiler();
1139
    }
1140
1141 1680
    public function getQueryCache(): QueryCache
1142
    {
1143 1680
        return $this->dependencies->queryCache();
1144
    }
1145
1146 1614
    public function getSchemaCache(): SchemaCache
1147
    {
1148 1614
        return $this->dependencies->schemaCache();
1149
    }
1150
}
1151