1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the ICanBoogie package. |
||
5 | * |
||
6 | * (c) Olivier Laviale <[email protected]> |
||
7 | * |
||
8 | * For the full copyright and license information, please view the LICENSE |
||
9 | * file that was distributed with this source code. |
||
10 | */ |
||
11 | |||
12 | namespace ICanBoogie\ActiveRecord; |
||
13 | |||
14 | use ICanBoogie\Accessor\AccessorTrait; |
||
15 | use ICanBoogie\ActiveRecord\Config\ConnectionDefinition; |
||
16 | use PDO; |
||
17 | use PDOException; |
||
18 | use Throwable; |
||
19 | |||
20 | use function explode; |
||
21 | use function strtr; |
||
22 | |||
23 | /** |
||
24 | * A connection to a database. |
||
25 | */ |
||
26 | class Connection |
||
27 | { |
||
28 | /** |
||
29 | * @uses lazy_get_driver |
||
30 | */ |
||
31 | use AccessorTrait; |
||
32 | |||
33 | private const DRIVERS_MAPPING = [ |
||
34 | |||
35 | 'mysql' => Driver\MySQLDriver::class, |
||
36 | 'sqlite' => Driver\SQLiteDriver::class, |
||
37 | |||
38 | ]; |
||
39 | |||
40 | public readonly string $id; |
||
41 | |||
42 | /** |
||
43 | * Prefix to prepend to every table name. |
||
44 | * |
||
45 | * If set to "dev", all table names will be named like "dev_nodes", "dev_contents", etc. |
||
46 | * This is a convenient way of creating a namespace for tables in a shared database. |
||
47 | * By default, the prefix is the empty string, that is there is not prefix. |
||
48 | */ |
||
49 | public readonly string $table_name_prefix; |
||
50 | |||
51 | /** |
||
52 | * Charset for the connection. Also used to specify the charset while creating tables. |
||
53 | */ |
||
54 | public readonly string $charset; |
||
55 | |||
56 | /** |
||
57 | * Used to specify the collate while creating tables. |
||
58 | */ |
||
59 | public readonly string $collate; |
||
60 | |||
61 | /** |
||
62 | * Timezone of the connection. |
||
63 | */ |
||
64 | public readonly string $timezone; |
||
65 | |||
66 | /** |
||
67 | * Driver name for the connection. |
||
68 | */ |
||
69 | public readonly string $driver_name; |
||
70 | |||
71 | private Driver $driver; |
||
72 | |||
73 | private function lazy_get_driver(): Driver |
||
74 | { |
||
75 | return $this->resolve_driver($this->driver_name); |
||
76 | } |
||
77 | |||
78 | /** |
||
79 | * The number of database queries and executions, used for statistics purpose. |
||
80 | */ |
||
81 | public int $queries_count = 0; |
||
82 | public readonly PDO $pdo; |
||
83 | |||
84 | /** |
||
85 | * The number of micro seconds spent per request. |
||
86 | * |
||
87 | * @var array[] |
||
88 | */ |
||
89 | public array $profiling = []; |
||
90 | |||
91 | /** |
||
92 | * Establish a connection to a database. |
||
93 | * |
||
94 | * Custom options can be specified using the driver-specific connection options. See |
||
95 | * {@link Options}. |
||
96 | * |
||
97 | * @link http://www.php.net/manual/en/pdo.construct.php |
||
98 | * @link http://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html |
||
99 | */ |
||
100 | public function __construct(ConnectionDefinition $definition) |
||
101 | { |
||
102 | unset($this->driver); // to trigger lazy loading |
||
103 | |||
104 | $this->id = $definition->id; |
||
105 | $dsn = $definition->dsn; |
||
106 | |||
107 | $this->table_name_prefix = $definition->table_name_prefix |
||
108 | ? $definition->table_name_prefix . '_' |
||
109 | : ''; |
||
110 | |||
111 | [ $this->charset, $this->collate ] = extract_charset_and_collate( |
||
112 | $definition->charset_and_collate ?? $definition::DEFAULT_CHARSET_AND_COLLATE |
||
113 | ); |
||
114 | |||
115 | $this->timezone = $definition->time_zone; |
||
116 | $this->driver_name = $this->resolve_driver_name($dsn); |
||
117 | |||
118 | $options = $this->make_options(); |
||
119 | |||
120 | $this->pdo = new PDO($dsn, $definition->username, $definition->password, $options); |
||
121 | |||
122 | $this->after_connection(); |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * Alias to {@link query}. |
||
127 | */ |
||
128 | public function __invoke(mixed ...$args): Statement |
||
129 | { |
||
130 | return $this->query(...$args); |
||
131 | } |
||
132 | |||
133 | /** |
||
134 | * Resolve the driver name from the DSN string. |
||
135 | */ |
||
136 | protected function resolve_driver_name(string $dsn): string |
||
137 | { |
||
138 | return explode(':', $dsn, 2)[0]; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * Resolves driver class. |
||
143 | * |
||
144 | * @throws DriverNotDefined |
||
145 | * |
||
146 | * @return class-string<Driver> |
||
147 | */ |
||
148 | private function resolve_driver_class(string $driver_name): string |
||
149 | { |
||
150 | return self::DRIVERS_MAPPING[$driver_name] |
||
151 | ?? throw new DriverNotDefined($driver_name); |
||
152 | } |
||
153 | |||
154 | /** |
||
155 | * Resolves a {@link Driver} implementation. |
||
156 | */ |
||
157 | private function resolve_driver(string $driver_name): Driver |
||
158 | { |
||
159 | $driver_class = $this->resolve_driver_class($driver_name); |
||
160 | |||
161 | return new $driver_class( |
||
162 | function () { |
||
163 | return $this; |
||
164 | } |
||
165 | ); |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * Called before the connection. |
||
170 | * |
||
171 | * May alter the options according to the driver. |
||
172 | * |
||
173 | * @return array<PDO::*, mixed> |
||
174 | */ |
||
175 | private function make_options(): array |
||
176 | { |
||
177 | if ($this->driver_name != 'mysql') { |
||
178 | return []; |
||
179 | } |
||
180 | |||
181 | $init_command = 'SET NAMES ' . $this->charset; |
||
182 | $init_command .= ', time_zone = "' . $this->timezone . '"'; |
||
183 | |||
184 | return [ |
||
185 | |||
186 | PDO::MYSQL_ATTR_INIT_COMMAND => $init_command, |
||
187 | |||
188 | ]; |
||
189 | } |
||
190 | |||
191 | private function after_connection(): void |
||
192 | { |
||
193 | $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * Overrides the method to resolve the statement before it is prepared, then set its fetch |
||
198 | * mode and connection. |
||
199 | * |
||
200 | * @param string $statement Query statement. |
||
201 | * @param array<string, mixed> $options |
||
202 | * |
||
203 | * @return Statement The prepared statement. |
||
204 | * |
||
205 | * @throws StatementNotValid if the statement cannot be prepared. |
||
206 | */ |
||
207 | public function prepare(string $statement, array $options = []): Statement |
||
208 | { |
||
209 | $statement = $this->resolve_statement($statement); |
||
210 | |||
211 | try { |
||
212 | $statement = $this->pdo->prepare($statement, $options); |
||
213 | } catch (PDOException $e) { |
||
214 | throw new StatementNotValid($statement, 500, $e); |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
215 | } |
||
216 | |||
217 | if (isset($options['mode'])) { |
||
218 | $mode = (array) $options['mode']; |
||
219 | |||
220 | $statement->setFetchMode(...$mode); |
||
221 | } |
||
222 | |||
223 | return new Statement($statement, $this); |
||
224 | } |
||
225 | |||
226 | /** |
||
227 | * Overrides the method in order to prepare (and resolve) the statement and execute it with |
||
228 | * the specified arguments and options. |
||
229 | * |
||
230 | * @param array<string|int, mixed> $args |
||
231 | * @param array<string, mixed> $options |
||
232 | */ |
||
233 | public function query(string $statement, array $args = [], array $options = []): Statement |
||
234 | { |
||
235 | $statement = $this->prepare($statement, $options); |
||
236 | $statement->execute($args); |
||
237 | |||
238 | return $statement; |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Executes a statement. |
||
243 | * |
||
244 | * The statement is resolved using the {@link resolve_statement()} method before it is |
||
245 | * executed. |
||
246 | * |
||
247 | * The execution of the statement is wrapped in a try/catch block. {@link PDOException} are |
||
248 | * caught and {@link StatementNotValid} exception are thrown with additional information |
||
249 | * instead. |
||
250 | * |
||
251 | * Using this method increments the `queries_by_connection` stat. |
||
252 | * |
||
253 | * @return false|int @FIXME https://github.com/sebastianbergmann/phpunit/issues/4735 |
||
254 | * @throws StatementNotValid if the statement cannot be executed. |
||
255 | */ |
||
256 | public function exec(string $statement): bool|int |
||
257 | { |
||
258 | $statement = $this->resolve_statement($statement); |
||
259 | |||
260 | try { |
||
261 | $this->queries_count++; |
||
262 | |||
263 | return $this->pdo->exec($statement); |
||
264 | } catch (PDOException $e) { |
||
265 | throw new StatementNotValid($statement, 500, $e); |
||
266 | } |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * Replaces placeholders with their value. |
||
271 | * |
||
272 | * The following placeholders are supported: |
||
273 | * |
||
274 | * - `{prefix}`: replaced by the {@link $table_name_prefix} property. |
||
275 | * - `{charset}`: replaced by the {@link $charset} property. |
||
276 | * - `{collate}`: replaced by the {@link $collate} property. |
||
277 | */ |
||
278 | public function resolve_statement(string $statement): string |
||
279 | { |
||
280 | return strtr($statement, [ |
||
281 | '{prefix}' => $this->table_name_prefix, |
||
282 | '{charset}' => $this->charset, |
||
283 | '{collate}' => $this->collate, |
||
284 | ]); |
||
285 | } |
||
286 | |||
287 | /** |
||
288 | * Alias for the `beginTransaction()` method. |
||
289 | * |
||
290 | * @see PDO::beginTransaction |
||
291 | */ |
||
292 | public function begin(): bool |
||
293 | { |
||
294 | return $this->pdo->beginTransaction(); |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * @codeCoverageIgnore |
||
299 | */ |
||
300 | public function quote_string(string $string): string |
||
301 | { |
||
302 | return $this->pdo->quote($string); |
||
303 | } |
||
304 | |||
305 | public function quote_identifier(string $identifier): string |
||
306 | { |
||
307 | return $this->driver->quote_identifier($identifier); |
||
308 | } |
||
309 | |||
310 | public function cast_value(mixed $value, string $type = null): mixed |
||
311 | { |
||
312 | return $this->driver->cast_value($value, $type); |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * @param non-empty-string $unprefixed_table_name |
||
317 | * |
||
318 | * @throws Throwable |
||
319 | */ |
||
320 | public function create_table(string $unprefixed_table_name, Schema $schema): void |
||
321 | { |
||
322 | $this->driver->create_table($this->table_name_prefix . $unprefixed_table_name, $schema); |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | * @codeCoverageIgnore |
||
327 | */ |
||
328 | public function table_exists(string $unprefixed_name): bool |
||
329 | { |
||
330 | return $this->driver->table_exists($this->table_name_prefix . $unprefixed_name); |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * @codeCoverageIgnore |
||
335 | */ |
||
336 | public function optimize(): void |
||
337 | { |
||
338 | $this->driver->optimize(); |
||
339 | } |
||
340 | } |
||
341 |