• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / ActiveRecord / 11642569649

02 Nov 2024 12:10PM UTC coverage: 83.671% (-0.4%) from 84.036%
11642569649

push

github

olvlvl
Add ConnectionTelemetry

30 of 41 new or added lines in 4 files covered. (73.17%)

5 existing lines in 3 files now uncovered.

1404 of 1678 relevant lines covered (83.67%)

21.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.16
/lib/ActiveRecord/Connection.php
1
<?php
2

3
namespace ICanBoogie\ActiveRecord;
4

5
use ICanBoogie\Accessor\AccessorTrait;
6
use ICanBoogie\ActiveRecord\Config\ConnectionDefinition;
7
use InvalidArgumentException;
8
use PDO;
9
use PDOException;
10
use RuntimeException;
11
use Throwable;
12

13
use function explode;
14
use function strtr;
15

16
/**
17
 * A connection to a database.
18
 *
19
 * @see self::get_last_insert_id()
20
 * @property-read int $last_insert_id
21
 *     Returns the ID of the last inserted row, or the last value from a sequence object,
22
 *     depending on the underlying driver.
23
 */
24
class Connection
25
{
26
    use AccessorTrait;
27

28
    private const DRIVERS_MAPPING = [
29

30
        'mysql' => Driver\MySQLDriver::class,
31
        'sqlite' => Driver\SQLiteDriver::class,
32

33
    ];
34

35
    public readonly string $id;
36

37
    /**
38
     * Prefix to prepend to every table name.
39
     *
40
     * If set to "dev", all table names will be named like "dev_nodes", "dev_contents", etc.
41
     * This is a convenient way of creating a namespace for tables in a shared database.
42
     * By default, the prefix is the empty string, that is there is not prefix.
43
     */
44
    public readonly string $table_name_prefix;
45

46
    /**
47
     * Charset for the connection. Also used to specify the charset while creating tables.
48
     */
49
    public readonly string $charset;
50

51
    /**
52
     * Used to specify the collate while creating tables.
53
     */
54
    public readonly string $collate;
55

56
    /**
57
     * Timezone of the connection.
58
     */
59
    public readonly string $timezone;
60

61
    /**
62
     * Driver name for the connection.
63
     */
64
    public readonly string $driver_name;
65
    public readonly Driver $driver;
66

67
    public readonly PDO $pdo;
68
    public ConnectionTelemetry $telemetry;
69

70
    /**
71
     * Establish a connection to a database.
72
     *
73
     * Custom options can be specified using the driver-specific connection options. See
74
     * {@see Options}.
75
     *
76
     * @link http://www.php.net/manual/en/pdo.construct.php
77
     * @link http://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html
78
     */
79
    public function __construct(ConnectionDefinition $definition)
80
    {
81
        $this->id = $definition->id;
82✔
82
        $dsn = $definition->dsn;
82✔
83

84
        $this->table_name_prefix = $definition->table_name_prefix
82✔
85
            ? $definition->table_name_prefix . '_'
28✔
86
            : '';
54✔
87

88
        [ $this->charset, $this->collate ] = extract_charset_and_collate(
82✔
89
            $definition->charset_and_collate ?? $definition::DEFAULT_CHARSET_AND_COLLATE
82✔
90
        );
82✔
91

92
        $this->timezone = $definition->time_zone;
82✔
93
        $this->driver_name = $this->resolve_driver_name($dsn);
82✔
94
        $this->driver = $this->resolve_driver($this->driver_name);
82✔
95
        $this->telemetry = new ConnectionTelemetry($this->id);
82✔
96

97
        $options = $this->make_options();
82✔
98

99
        $this->pdo = new PDO($dsn, $definition->username, $definition->password, $options);
82✔
100

101
        $this->after_connection();
81✔
102
    }
103

104
    /**
105
     * Resolve the driver name from the DSN string.
106
     */
107
    protected function resolve_driver_name(string $dsn): string
108
    {
109
        return explode(':', $dsn, 2)[0];
82✔
110
    }
111

112
    /**
113
     * Resolves driver class.
114
     *
115
     * @return class-string<Driver>
116
     * @throws DriverNotDefined
117
     *
118
     */
119
    private function resolve_driver_class(string $driver_name): string
120
    {
121
        return self::DRIVERS_MAPPING[$driver_name]
82✔
122
            ?? throw new DriverNotDefined($driver_name); // @phpstan-ignore-line
82✔
123
    }
124

125
    /**
126
     * Resolves a {@link Driver} implementation.
127
     */
128
    private function resolve_driver(string $driver_name): Driver
129
    {
130
        $driver_class = $this->resolve_driver_class($driver_name);
82✔
131

132
        return new $driver_class(
82✔
133
            function () {
82✔
134
                return $this;
51✔
135
            }
82✔
136
        );
82✔
137
    }
138

139
    /**
140
     * Called before the connection.
141
     *
142
     * May alter the options according to the driver.
143
     *
144
     * @return array<PDO::*, mixed>
145
     */
146
    private function make_options(): array
147
    {
148
        if ($this->driver_name != 'mysql') {
82✔
149
            return [];
81✔
150
        }
151

152
        $init_command = 'SET NAMES ' . $this->charset;
1✔
153
        $init_command .= ', time_zone = "' . $this->timezone . '"';
1✔
154

155
        return [
1✔
156

157
            PDO::MYSQL_ATTR_INIT_COMMAND => $init_command,
1✔
158

159
        ];
1✔
160
    }
161

162
    private function after_connection(): void
163
    {
164
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
81✔
165
    }
166

167
    /**
168
     * Overrides the method to resolve the statement before it is prepared, then set its fetch
169
     * mode and connection.
170
     *
171
     * @throws StatementNotValid if the statement cannot be prepared.
172
     */
173
    public function prepare(string $statement): Statement
174
    {
175
        $statement = $this->resolve_statement($statement);
42✔
176

177
        try {
178
            $statement = $this->pdo->prepare($statement);
42✔
179
        } catch (PDOException $e) {
3✔
180
            throw new StatementNotValid($statement, original: $e);
3✔
181
        }
182

183
        return new Statement($statement, $this->telemetry);
40✔
184
    }
185

186
    /**
187
     * Overrides the method to prepare (and resolve) the statement and execute it with
188
     * the specified arguments and options.
189
     *
190
     * @param mixed[] $args
191
     */
192
    public function query(string $statement, array $args = []): Statement
193
    {
194
        $statement = $this->prepare($statement);
41✔
195

196
        $this->telemetry->record_execute_duration(
39✔
197
            $statement,
39✔
198
            static fn() => $statement->execute($args)
39✔
199
        );
39✔
200

201
        return $statement;
38✔
202
    }
203

204
    /**
205
     * Executes a statement.
206
     *
207
     * The statement is resolved using the {@see resolve_statement()} method before it is
208
     * executed.
209
     *
210
     * The execution of the statement is wrapped in a try/catch block.
211
     * When a {@see PDOException} is caught, it is wrapped with {@see StatementNotValid}.
212
     *
213
     * Using this method increments the `queries_count` stat.
214
     *
215
     * @return false|int @FIXME https://github.com/sebastianbergmann/phpunit/issues/4735
216
     * @throws StatementNotValid if the statement cannot be executed.
217
     */
218
    public function exec(string $statement): bool|int
219
    {
220
        $statement = $this->resolve_statement($statement);
51✔
221

222
        try {
223
            // @phpstan-ignore-next-line
224
            return $this->telemetry->record_execute_duration(
51✔
225
                $statement,
51✔
226
                fn() => $this->pdo->exec($statement)
51✔
227
            );
51✔
228
        } catch (PDOException $e) {
×
229
            throw new StatementNotValid($statement, original: $e);
×
230
        }
231
    }
232

233
    public function get_last_insert_id(): int
234
    {
NEW
235
        $id = $this->pdo->lastInsertId();
×
236

NEW
237
        if ($id === false) {
×
NEW
238
            throw new RuntimeException("Unable to retrieve last inserted ID");
×
239
        }
240

NEW
241
        return (int)$id;
×
242
    }
243

244
    /**
245
     * Replaces placeholders with their value.
246
     *
247
     * The following placeholders are supported:
248
     *
249
     * - `{prefix}`: replaced by the {@link $table_name_prefix} property.
250
     * - `{charset}`: replaced by the {@link $charset} property.
251
     * - `{collate}`: replaced by the {@link $collate} property.
252
     */
253
    public function resolve_statement(string $statement): string
254
    {
255
        return strtr($statement, [
55✔
256
            '{prefix}' => $this->table_name_prefix,
55✔
257
            '{charset}' => $this->charset,
55✔
258
            '{collate}' => $this->collate,
55✔
259
        ]);
55✔
260
    }
261

262
    /**
263
     * Alias for the `beginTransaction()` method.
264
     *
265
     * @see PDO::beginTransaction
266
     */
267
    public function begin(): bool
268
    {
269
        return $this->pdo->beginTransaction();
×
270
    }
271

272
    /**
273
     * @see PDO::quote()
274
     */
275
    public function quote(string $string, int $type = PDO::PARAM_STR): string
276
    {
NEW
277
        $quoted = $this->pdo->quote($string, $type);
×
278

279
        // @phpstan-ignore-next-line
NEW
280
        if ($quoted === false) {
×
NEW
281
            throw new InvalidArgumentException("Unsupported quote type: $type");
×
282
        }
283

NEW
284
        return $quoted;
×
285
    }
286

287
    public function quote_identifier(string $identifier): string
288
    {
289
        return $this->driver->quote_identifier($identifier);
1✔
290
    }
291

292
    public function cast_value(mixed $value, string $type = null): mixed
293
    {
294
        return $this->driver->cast_value($value, $type);
×
295
    }
296

297
    /**
298
     * @throws Throwable
299
     */
300
    public function create_table(string $unprefixed_table_name, Schema $schema): void
301
    {
302
        $this->driver->create_table($this->table_name_prefix . $unprefixed_table_name, $schema);
51✔
303
    }
304

305
    /**
306
     * Determines if a table exists in the database.
307
     */
308
    public function table_exists(string $unprefixed_name): bool
309
    {
310
        return $this->driver->table_exists($this->table_name_prefix . $unprefixed_name);
37✔
311
    }
312

313
    public function optimize(): void
314
    {
UNCOV
315
        $this->driver->optimize();
×
316
    }
317
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc