Passed
Push — 6.0 ( 7ab78c...8e0f01 )
by Olivier
01:35
created

lib/ActiveRecord/Connection.php (1 issue)

Labels
Severity
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
$statement of type PDOStatement is incompatible with the type array|string expected by parameter $statement of ICanBoogie\ActiveRecord\...NotValid::__construct(). ( Ignorable by Annotation )

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

214
            throw new StatementNotValid(/** @scrutinizer ignore-type */ $statement, 500, $e);
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