Issues (43)

src/Driver/Pdo/AbstractPdoConnection.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Driver\Pdo;
6
7
use PDO;
8
use PDOException;
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerAwareTrait;
11
use Psr\Log\LogLevel;
12
use Throwable;
13
use Yiisoft\Db\Cache\SchemaCache;
14
use Yiisoft\Db\Connection\AbstractConnection;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidCallException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Profiler\Context\ConnectionContext;
19
use Yiisoft\Db\Profiler\ProfilerAwareInterface;
20
use Yiisoft\Db\Profiler\ProfilerAwareTrait;
21
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
22
use Yiisoft\Db\Schema\QuoterInterface;
23
use Yiisoft\Db\Schema\SchemaInterface;
24
use Yiisoft\Db\Transaction\TransactionInterface;
25
26
use function array_keys;
27
use function is_string;
28
use function method_exists;
29
30
/**
31
 * Represents a connection to a database using the PDO (PHP Data Objects) extension.
32
 *
33
 * It provides a set of methods for interacting with a database using PDO, such as executing SQL statements, preparing
34
 * and executing statements, and managing transactions.
35
 *
36
 * The ConnectionPDO classes extend from this class, which is a base class for representing a connection to a database.
37
 *
38
 * It implements the ConnectionInterface, which defines the interface for interacting with a database connection.
39
 */
40
abstract class AbstractPdoConnection extends AbstractConnection implements PdoConnectionInterface, LoggerAwareInterface, ProfilerAwareInterface
41
{
42
    use LoggerAwareTrait;
43
    use ProfilerAwareTrait;
44
45
    protected PDO|null $pdo = null;
46
    protected string $serverVersion = '';
47
    protected bool|null $emulatePrepare = null;
48
    protected QueryBuilderInterface|null $queryBuilder = null;
49
    protected QuoterInterface|null $quoter = null;
50
    protected SchemaInterface|null $schema = null;
51
52
    public function __construct(protected PdoDriverInterface $driver, protected SchemaCache $schemaCache)
53
    {
54
    }
55
56
    /**
57
     * Reset the connection after cloning.
58
     */
59
    public function __clone()
60
    {
61
        $this->transaction = null;
62
        $this->pdo = null;
63
    }
64
65
    /**
66
     * Close the connection before serializing.
67
     */
68
    public function __sleep(): array
69
    {
70
        $fields = (array) $this;
71
72
        unset(
73
            $fields["\000*\000" . 'pdo'],
74
            $fields["\000*\000" . 'transaction'],
75
            $fields["\000*\000" . 'schema']
76
        );
77
78
        return array_keys($fields);
79
    }
80
81
    public function beginTransaction(string $isolationLevel = null): TransactionInterface
82
    {
83
        $transaction = parent::beginTransaction($isolationLevel);
84
        if ($this->logger !== null && $transaction instanceof LoggerAwareInterface) {
85
            $transaction->setLogger($this->logger);
86
        }
87
88
        return $transaction;
89
    }
90
91
    public function open(): void
92
    {
93
        if ($this->pdo instanceof PDO) {
94
            return;
95
        }
96
97
        if ($this->driver->getDsn() === '') {
98
            throw new InvalidConfigException('Connection::dsn cannot be empty.');
99
        }
100
101
        $token = 'Opening DB connection: ' . $this->driver->getDsn();
102
        $connectionContext = new ConnectionContext(__METHOD__);
103
104
        try {
105
            $this->logger?->log(LogLevel::INFO, $token, ['type' => LogType::CONNECTION]);
106
            $this->profiler?->begin($token, $connectionContext);
107
            $this->initConnection();
108
            $this->profiler?->end($token, $connectionContext);
109
        } catch (PDOException $e) {
110
            $this->profiler?->end($token, $connectionContext->setException($e));
111
            $this->logger?->log(LogLevel::ERROR, $token, ['type' => LogType::CONNECTION]);
112
113
            throw new Exception($e->getMessage(), (array) $e->errorInfo, $e);
114
        }
115
    }
116
117
    public function close(): void
118
    {
119
        if ($this->pdo !== null) {
120
            $this->logger?->log(
121
                LogLevel::DEBUG,
122
                'Closing DB connection: ' . $this->driver->getDsn() . ' ' . __METHOD__,
123
                ['type' => LogType::CONNECTION],
124
            );
125
126
            $this->pdo = null;
127
            $this->transaction = null;
128
        }
129
    }
130
131
    public function getDriver(): PdoDriverInterface
132
    {
133
        return $this->driver;
134
    }
135
136
    public function getEmulatePrepare(): bool|null
137
    {
138
        return $this->emulatePrepare;
139
    }
140
141
    public function getActivePDO(string|null $sql = '', bool|null $forRead = null): PDO
142
    {
143
        $this->open();
144
        $pdo = $this->getPDO();
145
146
        if ($pdo === null) {
147
            throw new Exception('PDO cannot be initialized.');
148
        }
149
150
        return $pdo;
151
    }
152
153
    public function getPDO(): PDO|null
154
    {
155
        return $this->pdo;
156
    }
157
158
    public function getLastInsertID(string $sequenceName = null): string
159
    {
160
        if ($this->pdo !== null) {
161
            return $this->pdo->lastInsertID($sequenceName ?? null);
162
        }
163
164
        throw new InvalidCallException('DB Connection is not active.');
165
    }
166
167
    public function getDriverName(): string
168
    {
169
        return $this->driver->getDriverName();
170
    }
171
172
    public function getServerVersion(): string
173
    {
174
        if ($this->serverVersion === '') {
175
            /** @psalm-var mixed $version */
176
            $version = $this->getActivePDO()->getAttribute(PDO::ATTR_SERVER_VERSION);
177
            $this->serverVersion = is_string($version) ? $version : 'Version could not be determined.';
178
        }
179
180
        return $this->serverVersion;
181
    }
182
183
    public function isActive(): bool
184
    {
185
        return $this->pdo !== null;
186
    }
187
188
    public function quoteValue(mixed $value): mixed
189
    {
190
        if (is_string($value) === false) {
191
            return $value;
192
        }
193
194
        return $this->getActivePDO()->quote($value);
195
    }
196
197
    public function setEmulatePrepare(bool $value): void
198
    {
199
        $this->emulatePrepare = $value;
200
    }
201
202
    public function setTablePrefix(string $value): void
203
    {
204
        parent::setTablePrefix($value);
205
        if ($this->quoter !== null && method_exists($this->quoter, 'setTablePrefix')) {
206
            $this->quoter->setTablePrefix($value);
0 ignored issues
show
The method setTablePrefix() does not exist on Yiisoft\Db\Schema\QuoterInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Schema\QuoterInterface. ( Ignorable by Annotation )

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

206
            $this->quoter->/** @scrutinizer ignore-call */ 
207
                           setTablePrefix($value);
Loading history...
207
        }
208
    }
209
210
    /**
211
     * Initializes the DB connection.
212
     *
213
     * This method is invoked right after the DB connection is established.
214
     *
215
     * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES`, if {@see getEmulatePrepare()} is `true`.
216
     */
217
    protected function initConnection(): void
218
    {
219
        if ($this->getEmulatePrepare() !== null) {
220
            $this->driver->attributes([PDO::ATTR_EMULATE_PREPARES => $this->getEmulatePrepare()]);
221
        }
222
223
        $this->pdo = $this->driver->createConnection();
224
    }
225
226
    /*
227
     * Exceptions thrown from rollback will be caught and just logged with {@see logger->log()}.
228
     */
229
    protected function rollbackTransactionOnLevel(TransactionInterface $transaction, int $level): void
230
    {
231
        if ($transaction->isActive() && $transaction->getLevel() === $level) {
232
            /**
233
             * @link https://github.com/yiisoft/yii2/pull/13347
234
             */
235
            try {
236
                $transaction->rollBack();
237
            } catch (Throwable $e) {
238
                $this->logger?->log(LogLevel::ERROR, (string) $e, [__METHOD__, 'type' => LogType::TRANSACTION]);
239
                /** hide this exception to be able to continue throwing original exception outside */
240
            }
241
        }
242
    }
243
}
244