Passed
Pull Request — master (#215)
by Wilmer
13:47
created

Connection::open()   B

Complexity

Conditions 11
Paths 96

Size

Total Lines 53
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 11.0245

Importance

Changes 0
Metric Value
cc 11
eloc 28
c 0
b 0
f 0
nc 96
nop 0
dl 0
loc 53
rs 7.3166
ccs 16
cts 17
cp 0.9412
crap 11.0245

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