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
Bug
introduced
by
![]() |
|||
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 |