Passed
Pull Request — master (#240)
by Wilmer
13:14
created

Connection::open()   B

Complexity

Conditions 11
Paths 72

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 11.0099

Importance

Changes 0
Metric Value
cc 11
eloc 26
c 0
b 0
f 0
nc 72
nop 0
dl 0
loc 50
ccs 22
cts 23
cp 0.9565
crap 11.0099
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\AwareTrait\LoggerAwareTrait;
14
use Yiisoft\Db\AwareTrait\ProfilerAwareTrait;
15
use Yiisoft\Db\Cache\QueryCache;
16
use Yiisoft\Db\Command\Command;
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
    use LoggerAwareTrait;
174
    use ProfilerAwareTrait;
175
176
    private string $dsn;
177
    private ?string $username = null;
178
    private ?string $password = null;
179
    private array $attributes = [];
180
    private ?string $charset = null;
181
    private ?bool $emulatePrepare = null;
182
    private string $tablePrefix = '';
183
    private bool $enableSavepoint = true;
184
    private int $serverRetryInterval = 600;
185
    private bool $enableSlaves = true;
186
    private array $slaves = [];
187
    private array $masters = [];
188
    private bool $shuffleMasters = true;
189
    private array $quotedTableNames = [];
190
    private array $quotedColumnNames = [];
191
    private ?Connection $master = null;
192
    private ?Connection $slave = null;
193
    private ?LoggerInterface $logger = null;
194
    private ?PDO $pdo = null;
195
    private ?ProfilerInterface $profiler = null;
196 3016
    private QueryCache $queryCache;
197
    private ?Transaction $transaction = null;
198 3016
199 3016
    public function __construct(string $dsn, QueryCache $queryCache)
200 3016
    {
201
        $this->dsn = $dsn;
202
        $this->queryCache = $queryCache;
203
    }
204
205
    /**
206
     * Creates a command for execution.
207
     *
208
     * @param string|null $sql the SQL statement to be executed
209
     * @param array $params the parameters to be bound to the SQL statement
210
     *
211
     * @throws Exception|InvalidConfigException
212
     *
213
     * @return Command the DB command
214
     */
215
    abstract public function createCommand(?string $sql = null, array $params = []): Command;
216
217
    /**
218
     * Returns the schema information for the database opened by this connection.
219
     *
220
     * @return Schema the schema information for the database opened by this connection.
221
     */
222
    abstract public function getSchema(): Schema;
223
224
    /**
225
     * Creates the PDO instance.
226
     *
227
     * This method is called by {@see open} to establish a DB connection. The default implementation will create a PHP
228
     * PDO instance. You may override this method if the default PDO needs to be adapted for certain DBMS.
229
     *
230
     * @return PDO the pdo instance
231
     */
232
    abstract protected function createPdoInstance(): PDO;
233
234
    /**
235
     * Initializes the DB connection.
236
     *
237
     * This method is invoked right after the DB connection is established.
238
     *
239
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`.
240
     *
241
     * if {@see emulatePrepare} is true, and sets the database {@see charset} if it is not empty.
242
     *
243
     * It then triggers an {@see EVENT_AFTER_OPEN} event.
244
     */
245
    abstract protected function initConnection(): void;
246
247 10
    /**
248
     * Reset the connection after cloning.
249 10
     */
250 10
    public function __clone()
251 10
    {
252 10
        $this->master = null;
253
        $this->slave = null;
254 10
        $this->transaction = null;
255
256 10
        if (strncmp($this->dsn, 'sqlite::memory:', 15) !== 0) {
257
            /** reset PDO connection, unless its sqlite in-memory, which can only have one connection */
258 10
            $this->pdo = null;
259
        }
260
    }
261
262
    /**
263
     * Close the connection before serializing.
264
     *
265 6
     * @return array
266
     */
267 6
    public function __sleep(): array
268
    {
269
        $fields = (array) $this;
270 6
271 6
        unset(
272 6
            $fields["\000" . __CLASS__ . "\000" . 'pdo'],
273 6
            $fields["\000" . __CLASS__ . "\000" . 'master'],
274 6
            $fields["\000" . __CLASS__ . "\000" . 'slave'],
275
            $fields["\000" . __CLASS__ . "\000" . 'transaction'],
276
            $fields["\000" . __CLASS__ . "\000" . 'schema']
277 6
        );
278
279
        return array_keys($fields);
280
    }
281
282
    /**
283
     * Starts a transaction.
284
     *
285
     * @param string|null $isolationLevel The isolation level to use for this transaction.
286
     *
287
     * {@see Transaction::begin()} for details.
288
     *
289
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
290
     *
291 40
     * @return Transaction the transaction initiated
292
     */
293 40
    public function beginTransaction(string $isolationLevel = null): Transaction
294
    {
295 40
        $this->open();
296 40
297
        if (($transaction = $this->getTransaction()) === null) {
298
            $transaction = $this->transaction = new Transaction($this);
299 40
            if ($this->logger !== null) {
300
                $transaction->setLogger($this->logger);
301 40
            }
302
        }
303
304
        $transaction->begin($isolationLevel);
305
306
        return $transaction;
307
    }
308
309
    /**
310
     * Uses query cache for the queries performed with the callable.
311
     *
312
     * When query caching is enabled ({@see enableQueryCache} is true and {@see queryCache} refers to a valid cache),
313
     * queries performed within the callable will be cached and their results will be fetched from cache if available.
314
     *
315
     * For example,
316
     *
317
     * ```php
318
     * // The customer will be fetched from cache if available.
319
     * // If not, the query will be made against DB and cached for use next time.
320
     * $customer = $db->cache(function (Connection $db) {
321
     *     return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
322
     * });
323
     * ```
324
     *
325
     * Note that query cache is only meaningful for queries that return results. For queries performed with
326
     * {@see Command::execute()}, query cache will not be used.
327
     *
328
     * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
329
     * The signature of the callable is `function (Connection $db)`.
330
     * @param int|null $duration the number of seconds that query results can remain valid in the cache. If this is not
331
     * set, the value of {@see queryCacheDuration} will be used instead. Use 0 to indicate that the cached data will
332
     * never expire.
333
     * @param Dependency|null $dependency the cache dependency associated with the cached query
334
     * results.
335
     *
336
     * @throws Throwable if there is any exception during query
337
     *
338
     * @return mixed the return result of the callable
339 10
     *
340
     * {@see setEnableQueryCache()}
341 10
     * {@see queryCache}
342 10
     * {@see noCache()}
343
     */
344
    public function cache(callable $callable, int $duration = null, Dependency $dependency = null)
345 10
    {
346
        $this->queryCache->setInfo(
347 10
            [$duration ?? $this->queryCache->getDuration(), $dependency]
348
        );
349 10
350
        $result = $callable($this);
351
352 1853
        $this->queryCache->removeLastInfo();
353
354 1853
        return $result;
355
    }
356
357 754
    public function getAttributes(): array
358
    {
359 754
        return $this->attributes;
360
    }
361
362 1878
    public function getCharset(): ?string
363
    {
364 1878
        return $this->charset;
365
    }
366
367 1484
    public function getDsn(): string
368
    {
369 1484
        return $this->dsn;
370
    }
371
372 1717
    public function getEmulatePrepare(): ?bool
373
    {
374 1717
        return $this->emulatePrepare;
375
    }
376
377 1853
    public function isSavepointEnabled(): bool
378
    {
379 1853
        return $this->enableSavepoint;
380
    }
381
382 10
    public function areSlavesEnabled(): bool
383
    {
384 10
        return $this->enableSlaves;
385
    }
386
387 1
    /**
388
     * Returns a value indicating whether the DB connection is established.
389 1
     *
390
     * @return bool whether the DB connection is established
391
     */
392
    public function isActive(): bool
393
    {
394
        return $this->pdo !== null;
395
    }
396
397 193
    /**
398
     * Returns the ID of the last inserted row or sequence value.
399 193
     *
400
     * @param string $sequenceName name of the sequence object (required by some DBMS)
401
     *
402
     * @throws Exception
403
     * @throws InvalidCallException
404
     *
405
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
406
     *
407
     * {@see http://php.net/manual/en/pdo.lastinsertid.php'>http://php.net/manual/en/pdo.lastinsertid.php}
408
     */
409
    public function getLastInsertID(string $sequenceName = ''): string
410
    {
411
        return $this->getSchema()->getLastInsertID($sequenceName);
412
    }
413
414 10
    /**
415
     * Returns the currently active master connection.
416 10
     *
417
     * If this method is called for the first time, it will try to open a master connection.
418
     *
419
     * @return Connection the currently active master connection. `null` is returned if there is no master available.
420
     */
421
    public function getMaster(): ?self
422
    {
423
        if ($this->master === null) {
424
            $this->master = $this->shuffleMasters
425
                ? $this->openFromPool($this->masters)
426 13
                : $this->openFromPoolSequentially($this->masters);
427
        }
428 13
429 13
        return $this->master;
430 7
    }
431 8
432
    /**
433
     * Returns the PDO instance for the currently active master connection.
434 13
     *
435
     * This method will open the master DB connection and then return {@see pdo}.
436
     *
437
     * @throws Exception
438
     *
439
     * @return PDO the PDO instance for the currently active master connection.
440
     */
441
    public function getMasterPdo(): PDO
442
    {
443
        $this->open();
444
445
        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...
446 1817
    }
447
448 1817
    public function getPassword(): ?string
449
    {
450 1817
        return $this->password;
451
    }
452
453 1853
    /**
454
     * The PHP PDO instance associated with this DB connection. This property is mainly managed by {@see open()} and
455 1853
     * {@see close()} methods. When a DB connection is active, this property will represent a PDO instance; otherwise,
456
     * it will be null.
457
     *
458
     * @return PDO|null
459
     *
460
     * {@see pdoClass}
461
     */
462
    public function getPDO(): ?PDO
463
    {
464
        return $this->pdo;
465
    }
466
467 1853
    /**
468
     * Returns the query builder for the current DB connection.
469 1853
     *
470
     * @return QueryBuilder the query builder for the current DB connection.
471
     */
472
    public function getQueryBuilder(): QueryBuilder
473
    {
474
        return $this->getSchema()->getQueryBuilder();
475
    }
476
477 955
    /**
478
     * Returns a server version as a string comparable by {@see \version_compare()}.
479 955
     *
480
     * @throws Exception
481
     *
482
     * @return string server version as a string.
483
     */
484
    public function getServerVersion(): string
485
    {
486
        return $this->getSchema()->getServerVersion();
487
    }
488
489 314
    /**
490
     * Returns the currently active slave connection.
491 314
     *
492
     * If this method is called for the first time, it will try to open a slave connection when {@see setEnableSlaves()}
493
     * is true.
494
     *
495
     * @param bool $fallbackToMaster whether to return a master connection in case there is no slave connection
496
     * available.
497
     *
498
     * @return Connection the currently active slave connection. `null` is returned if there is no slave available and
499
     * `$fallbackToMaster` is false.
500
     */
501
    public function getSlave(bool $fallbackToMaster = true): ?self
502
    {
503
        if (!$this->enableSlaves) {
504
            return $fallbackToMaster ? $this : null;
505
        }
506 1799
507
        if ($this->slave === null) {
508 1799
            $this->slave = $this->openFromPool($this->slaves);
509 6
        }
510
511
        return $this->slave === null && $fallbackToMaster ? $this : $this->slave;
512 1798
    }
513 1798
514
    /**
515
     * Returns the PDO instance for the currently active slave connection.
516 1798
     *
517
     * When {@see enableSlaves} is true, one of the slaves will be used for read queries, and its PDO instance will be
518
     * returned by this method.
519
     *
520
     * @param bool $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
521
     *
522
     * @throws Exception
523
     *
524
     * @return PDO the PDO instance for the currently active slave connection. `null` is returned if no slave connection
525
     * is available and `$fallbackToMaster` is false.
526
     */
527
    public function getSlavePdo(bool $fallbackToMaster = true): ?PDO
528
    {
529
        $db = $this->getSlave(false);
530
531
        if ($db === null) {
532 1797
            return $fallbackToMaster ? $this->getMasterPdo() : null;
533
        }
534 1797
535
        return $db->getPdo();
536 1797
    }
537 1792
538
    public function getTablePrefix(): string
539
    {
540 6
        return $this->tablePrefix;
541
    }
542
543 130
    /**
544
     * Obtains the schema information for the named table.
545 130
     *
546
     * @param string $name table name.
547
     * @param bool $refresh whether to reload the table schema even if it is found in the cache.
548
     *
549
     * @return TableSchema
550
     */
551
    public function getTableSchema(string $name, $refresh = false): ?TableSchema
552
    {
553
        return $this->getSchema()->getTableSchema($name, $refresh);
554
    }
555
556 196
    /**
557
     * Returns the currently active transaction.
558 196
     *
559
     * @return Transaction|null the currently active transaction. Null if no active transaction.
560
     */
561
    public function getTransaction(): ?Transaction
562
    {
563
        return $this->transaction && $this->transaction->isActive() ? $this->transaction : null;
564
    }
565
566 1732
    public function getUsername(): ?string
567
    {
568 1732
        return $this->username;
569
    }
570
571 1977
    /**
572
     * Disables query cache temporarily.
573 1977
     *
574
     * Queries performed within the callable will not use query cache at all. For example,
575
     *
576
     * ```php
577
     * $db->cache(function (Connection $db) {
578
     *
579
     *     // ... queries that use query cache ...
580
     *
581
     *     return $db->noCache(function (Connection $db) {
582
     *         // this query will not use query cache
583
     *         return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
584
     *     });
585
     * });
586
     * ```
587
     *
588
     * @param callable $callable a PHP callable that contains DB queries which should not use query cache. The signature
589
     * of the callable is `function (Connection $db)`.
590
     *
591
     * @throws Throwable if there is any exception during query
592
     *
593
     * @return mixed the return result of the callable
594
     *
595
     * {@see enableQueryCache}
596
     * {@see queryCache}
597
     * {@see cache()}
598
     */
599
    public function noCache(callable $callable)
600
    {
601
        $queryCache = $this->queryCache;
602
603
        $queryCache->setInfo(false);
604 10
605
        $result = $callable($this);
606 10
607
        $queryCache->removeLastInfo();
608 10
609
        return $result;
610 10
    }
611
612 10
    /**
613
     * Establishes a DB connection.
614 10
     *
615
     * It does nothing if a DB connection has already been established.
616
     *
617
     * @throws Exception|InvalidConfigException if connection fails
618
     */
619
    public function open(): void
620
    {
621
        if (!empty($this->pdo)) {
622
            return;
623
        }
624 1853
625
        if (!empty($this->masters)) {
626 1853
            $db = $this->getMaster();
627 1685
628
            if ($db !== null) {
629
                $this->pdo = $db->getPDO();
630 1853
631 11
                return;
632
            }
633 11
634 8
            throw new InvalidConfigException('None of the master DB servers is available.');
635
        }
636 8
637
        if (empty($this->dsn)) {
638
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
639 10
        }
640
641
        $token = 'Opening DB connection: ' . $this->dsn;
642 1853
643
        try {
644
            if ($this->logger !== null) {
645
                $this->logger->log(LogLevel::INFO, $token);
646 1853
            }
647
648
            if ($this->profiler !== null) {
649 1853
                $this->profiler->begin($token, [__METHOD__]);
650 1853
            }
651
652
            $this->pdo = $this->createPdoInstance();
653 1853
654 1853
            $this->initConnection();
655
656
            if ($this->profiler !== null) {
657 1853
                $this->profiler->end($token, [__METHOD__]);
658
            }
659 1853
        } catch (PDOException $e) {
660
            if ($this->profiler !== null) {
661 1853
                $this->profiler->end($token, [__METHOD__]);
662 1853
            }
663
664 15
            if ($this->logger !== null) {
665 15
                $this->logger->log(LogLevel::ERROR, $token);
666 15
            }
667
668
            throw new Exception($e->getMessage(), $e->errorInfo, $e);
669 15
        }
670 15
    }
671
672
    /**
673 15
     * Closes the currently active DB connection.
674
     *
675 1853
     * It does nothing if the connection is already closed.
676
     */
677
    public function close(): void
678
    {
679
        if ($this->master) {
680
            if ($this->pdo === $this->master->getPDO()) {
681
                $this->pdo = null;
682 2922
            }
683
684 2922
            $this->master->close();
685 7
686 7
            $this->master = null;
687
        }
688
689 7
        if ($this->pdo !== null) {
690
            if ($this->logger !== null) {
691 7
                $this->logger->log(LogLevel::DEBUG, 'Closing DB connection: ' . $this->dsn . ' ' . __METHOD__);
692
            }
693
694 2922
            $this->pdo = null;
695 1853
            $this->transaction = null;
696 1843
        }
697
698
        if ($this->slave) {
699 1853
            $this->slave->close();
700 1853
            $this->slave = null;
701 1853
        }
702
    }
703
704 2922
    /**
705 5
     * Rolls back given {@see Transaction} object if it's still active and level match. In some cases rollback can fail,
706 5
     * so this method is fail safe. Exceptions thrown from rollback will be caught and just logged with
707
     * {@see logger->log()}.
708 2922
     *
709
     * @param Transaction $transaction Transaction object given from {@see beginTransaction()}.
710
     * @param int $level Transaction level just after {@see beginTransaction()} call.
711
     */
712
    private function rollbackTransactionOnLevel(Transaction $transaction, int $level): void
713
    {
714
        if ($transaction->isActive() && $transaction->getLevel() === $level) {
715
            /**
716
             * {@see https://github.com/yiisoft/yii2/pull/13347}
717
             */
718 10
            try {
719
                $transaction->rollBack();
720 10
            } catch (Exception $e) {
721
                $this->logger->log(LogLevel::ERROR, $e, [__METHOD__]);
0 ignored issues
show
Bug introduced by
The method log() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

721
                $this->logger->/** @scrutinizer ignore-call */ 
722
                               log(LogLevel::ERROR, $e, [__METHOD__]);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
722
                /** hide this exception to be able to continue throwing original exception outside */
723
            }
724
        }
725 10
    }
726
727
    /**
728
     * Opens the connection to a server in the pool.
729
     *
730
     * This method implements the load balancing among the given list of the servers.
731 10
     *
732
     * Connections will be tried in random order.
733
     *
734
     * @param array $pool the list of connection configurations in the server pool
735
     *
736
     * @return Connection|null the opened DB connection, or `null` if no server is available
737
     */
738
    protected function openFromPool(array $pool): ?self
739
    {
740
        shuffle($pool);
741
742
        return $this->openFromPoolSequentially($pool);
743
    }
744 1803
745
    /**
746 1803
     * Opens the connection to a server in the pool.
747
     *
748 1803
     * This method implements the load balancing among the given list of the servers.
749
     *
750
     * Connections will be tried in sequential order.
751
     *
752
     * @param array $pool
753
     *
754
     * @return Connection|null the opened DB connection, or `null` if no server is available
755
     */
756
    protected function openFromPoolSequentially(array $pool): ?self
757
    {
758
        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...
759
            return null;
760
        }
761
762 1808
        foreach ($pool as $config) {
763
            /* @var $db Connection */
764 1808
            $db = DatabaseFactory::connection($config);
765 1791
766
            $key = [__METHOD__, $db->getDsn()];
767
768 18
            if (
769
                $this->getSchema()->getSchemaCache()->isEnabled() &&
770 18
                $this->getSchema()->getSchemaCache()->getOrSet($key, null, $this->serverRetryInterval)
771
            ) {
772 18
                /** should not try this dead server now */
773
                continue;
774
            }
775 18
776 18
            try {
777
                $db->open();
778
779
                return $db;
780
            } catch (Exception $e) {
781
                if ($this->logger !== null) {
782
                    $this->logger->log(
783 18
                        LogLevel::WARNING,
784
                        "Connection ({$db->getDsn()}) failed: " . $e->getMessage() . ' ' . __METHOD__
785 15
                    );
786 10
                }
787 10
788 10
                if ($this->getSchema()->getSchemaCache()->isEnabled()) {
789 10
                    /** mark this server as dead and only retry it after the specified interval */
790 10
                    $this->getSchema()->getSchemaCache()->set($key, 1, $this->serverRetryInterval);
791
                }
792
793
                return null;
794 10
            }
795
        }
796 5
797
        return null;
798
    }
799 10
800
    /**
801
     * Quotes a column name for use in a query.
802
     *
803
     * If the column name contains prefix, the prefix will also be properly quoted.
804
     * If the column name is already quoted or contains special characters including '(', '[[' and '{{', then this
805
     * method will do nothing.
806
     *
807
     * @param string $name column name
808
     *
809
     * @return string the properly quoted column name
810
     */
811
    public function quoteColumnName(string $name): string
812
    {
813
        return $this->quotedColumnNames[$name]
814
            ?? ($this->quotedColumnNames[$name] = $this->getSchema()->quoteColumnName($name));
815
    }
816
817 1594
    /**
818
     * Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
819 1594
     *
820 1594
     * Tokens enclosed within double curly brackets are treated as table names, while tokens enclosed within double
821
     * square brackets are column names. They will be quoted accordingly. Also, the percentage character "%" at the
822
     * beginning or ending of a table name will be replaced with {@see tablePrefix}.
823
     *
824
     * @param string $sql the SQL to be quoted
825
     *
826
     * @return string the quoted SQL
827
     */
828
    public function quoteSql(string $sql): string
829
    {
830
        return preg_replace_callback(
831
            '/({{(%?[\w\-. ]+%?)}}|\\[\\[([\w\-. ]+)]])/',
832
            function ($matches) {
833
                if (isset($matches[3])) {
834 1842
                    return $this->quoteColumnName($matches[3]);
835
                }
836 1842
837 1842
                return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
838 1842
            },
839 689
            $sql
840 569
        );
841
    }
842
843 493
    /**
844 1842
     * Quotes a table name for use in a query.
845
     *
846
     * If the table name contains schema prefix, the prefix will also be properly quoted.
847
     * If the table name is already quoted or contains special characters including '(', '[[' and '{{', then this method
848
     * will do nothing.
849
     *
850
     * @param string $name table name
851
     *
852
     * @return string the properly quoted table name
853
     */
854
    public function quoteTableName(string $name): string
855
    {
856
        return $this->quotedTableNames[$name]
857
            ?? ($this->quotedTableNames[$name] = $this->getSchema()->quoteTableName($name));
858
    }
859
860 1388
    /**
861
     * Quotes a string value for use in a query.
862 1388
     *
863 1388
     * Note that if the parameter is not a string, it will be returned without change.
864
     *
865
     * @param int|string $value string to be quoted
866
     *
867
     * @throws Exception
868
     *
869
     * @return int|string the properly quoted string
870
     *
871
     * {@see http://php.net/manual/en/pdo.quote.php}
872
     */
873
    public function quoteValue($value)
874
    {
875
        return $this->getSchema()->quoteValue($value);
876
    }
877
878
    /**
879 1370
     * PDO attributes (name => value) that should be set when calling {@see open()} to establish a DB connection.
880
     * Please refer to the [PHP manual](http://php.net/manual/en/pdo.setattribute.php) for details about available
881 1370
     * attributes.
882
     *
883
     * @param array $value
884
     */
885
    public function setAttributes(array $value): void
886
    {
887
        $this->attributes = $value;
888
    }
889
890
    /**
891
     * The charset used for database connection. The property is only used for MySQL, PostgreSQL databases. Defaults to
892
     * null, meaning using default charset as configured by the database.
893
     *
894
     * For Oracle Database, the charset must be specified in the {@see dsn}, for example for UTF-8 by appending
895
     * `;charset=UTF-8` to the DSN string.
896
     *
897
     * The same applies for if you're using GBK or BIG5 charset with MySQL, then it's highly recommended to specify
898
     * charset via {@see dsn} like `'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'`.
899
     *
900
     * @param string|null $value
901
     */
902
    public function setCharset(?string $value): void
903
    {
904
        $this->charset = $value;
905
    }
906
907
    /**
908 885
     * Whether to turn on prepare emulation. Defaults to false, meaning PDO will use the native prepare support if
909
     * available. For some databases (such as MySQL), this may need to be set true so that PDO can emulate the prepare
910 885
     * support to bypass the buggy native prepare support. The default value is null, which means the PDO
911 885
     * ATTR_EMULATE_PREPARES value will not be changed.
912
     *
913
     * @param bool $value
914
     */
915
    public function setEmulatePrepare(bool $value): void
916
    {
917
        $this->emulatePrepare = $value;
918
    }
919
920 10
    /**
921
     * Whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint). Note that if the underlying DBMS does not
922 10
     * support savepoint, setting this property to be true will have no effect.
923 10
     *
924
     * @param bool $value
925
     */
926
    public function setEnableSavepoint(bool $value): void
927
    {
928
        $this->enableSavepoint = $value;
929
    }
930
931
    /**
932
     * Whether to enable read/write splitting by using {@see setSlaves()} to read data. Note that if {@see setSlaves()}
933 7
     * is empty, read/write splitting will NOT be enabled no matter what value this property takes.
934
     *
935 7
     * @param bool $value
936 7
     */
937
    public function setEnableSlaves(bool $value): void
938
    {
939
        $this->enableSlaves = $value;
940
    }
941
942
    /**
943
     * List of master connection. Each DSN is used to create a master DB connection. When {@see open()} is called, one
944 10
     * of these configurations will be chosen and used to create a DB connection which will be used by this object.
945
     *
946 10
     * @param string $key index master connection.
947 10
     * @param array $config The configuration that should be merged with every master configuration
948
     */
949
    public function setMasters(string $key, array $config = []): void
950
    {
951
        $this->masters[$key] = $config;
952
    }
953
954
    /**
955 5
     * The password for establishing DB connection. Defaults to `null` meaning no password to use.
956
     *
957 5
     * @param string|null $value
958 5
     */
959
    public function setPassword(?string $value): void
960
    {
961
        $this->password = $value;
962
    }
963
964
    /**
965
     * Can be used to set {@see QueryBuilder} configuration via Connection configuration array.
966
     *
967
     * @param iterable $config the {@see QueryBuilder} properties to be configured.
968
     */
969
    public function setQueryBuilder(iterable $config): void
970
    {
971
        $builder = $this->getQueryBuilder();
972
973
        foreach ($config as $key => $value) {
974
            $builder->{$key} = $value;
975
        }
976
    }
977
978 14
    /**
979
     * The retry interval in seconds for dead servers listed in {@see setMasters()} and {@see setSlaves()}.
980 14
     *
981 14
     * @param int $value
982
     */
983
    public function setServerRetryInterval(int $value): void
984
    {
985
        $this->serverRetryInterval = $value;
986
    }
987
988 2631
    /**
989
     * Whether to shuffle {@see setMasters()} before getting one.
990 2631
     *
991 2631
     * @param bool $value
992
     */
993
    public function setShuffleMasters(bool $value): void
994
    {
995
        $this->shuffleMasters = $value;
996
    }
997
998
    /**
999
     * List of slave connection. Each DSN is used to create a slave DB connection. When {@see enableSlaves} is true,
1000
     * one of these configurations will be chosen and used to create a DB connection for performing read queries only.
1001
     *
1002
     * @param string $key index slave connection.
1003
     * @param array $config The configuration that should be merged with every slave configuration
1004
     */
1005
    public function setSlaves(string $key, array $config = []): void
1006
    {
1007
        $this->slaves[$key] = $config;
1008
    }
1009
1010
    /**
1011
     * The common prefix or suffix for table names. If a table name is given as `{{%TableName}}`, then the percentage
1012
     * character `%` will be replaced with this property value. For example, `{{%post}}` becomes `{{tbl_post}}`.
1013
     *
1014
     * @param string $value
1015
     */
1016
    public function setTablePrefix(string $value): void
1017
    {
1018
        $this->tablePrefix = $value;
1019
    }
1020
1021
    /**
1022 12
     * The username for establishing DB connection. Defaults to `null` meaning no username to use.
1023
     *
1024 12
     * @param string|null $value
1025 12
     */
1026
    public function setUsername(?string $value): void
1027
    {
1028
        $this->username = $value;
1029
    }
1030
1031
    /**
1032
     * Executes callback provided in a transaction.
1033
     *
1034 9
     * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
1035
     * @param string|null $isolationLevel The isolation level to use for this transaction. {@see Transaction::begin()}
1036 9
     * for details.
1037 9
     *
1038
     * @return mixed result of callback function
1039
     *@throws Throwable if there is any exception during query. In this case the transaction will be rolled back.
1040
     *
1041
     */
1042
    public function transaction(callable $callback, string $isolationLevel = null)
1043
    {
1044
        $transaction = $this->beginTransaction($isolationLevel);
1045 24
1046
        $level = $transaction->getLevel();
1047 24
1048 24
        try {
1049
            $result = $callback($this);
1050
1051
            if ($transaction->isActive() && $transaction->getLevel() === $level) {
1052
                $transaction->commit();
1053
            }
1054
        } catch (Throwable $e) {
1055 2631
            $this->rollbackTransactionOnLevel($transaction, $level);
1056
1057 2631
            throw $e;
1058 2631
        }
1059
1060
        return $result;
1061
    }
1062
1063
    /**
1064
     * Executes the provided callback by using the master connection.
1065
     *
1066
     * This method is provided so that you can temporarily force using the master connection to perform DB operations
1067
     * even if they are read queries. For example,
1068
     *
1069
     * ```php
1070
     * $result = $db->useMaster(function ($db) {
1071 25
     *     return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
1072
     * });
1073 25
     * ```
1074
     *
1075 25
     * @param callable $callback a PHP callable to be executed by this method. Its signature is
1076
     * `function (Connection $db)`. Its return value will be returned by this method.
1077
     *
1078 25
     * @throws Throwable if there is any exception thrown from the callback
1079
     *
1080 15
     * @return mixed the return value of the callback
1081 15
     */
1082
    public function useMaster(callable $callback)
1083 10
    {
1084 10
        if ($this->enableSlaves) {
1085
            $this->enableSlaves = false;
1086 10
1087
            try {
1088
                $result = $callback($this);
1089 15
            } catch (Throwable $e) {
1090
                $this->enableSlaves = true;
1091
1092
                throw $e;
1093
            }
1094
            $this->enableSlaves = true;
1095
        } else {
1096
            $result = $callback($this);
1097
        }
1098
1099
        return $result;
1100
    }
1101
1102
    public function getQueryCache(): QueryCache
1103
    {
1104
        return $this->queryCache;
1105
    }
1106
}
1107