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

ICanBoogie / ActiveRecord / 8582028303

06 Apr 2024 03:09PM UTC coverage: 83.938%. First build
8582028303

push

github

olvlvl
Revision of how records are saved

24 of 47 new or added lines in 5 files covered. (51.06%)

1343 of 1600 relevant lines covered (83.94%)

23.53 hits per line

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

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

3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <olivier.laviale@gmail.com>
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 LogicException;
17
use PDO;
18
use PDOException;
19
use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
20
use RuntimeException;
21
use Throwable;
22

23
use function explode;
24
use function strtr;
25

26
/**
27
 * A connection to a database.
28
 *
29
 * @see self::get_last_insert_id()
30
 * @property-read int $last_insert_id
31
 *     Returns the ID of the last inserted row, or the last value from a sequence object,
32
 *     depending on the underlying driver.
33
 */
34
class Connection
35
{
36
    use AccessorTrait;
37

38
    private const array DRIVERS_MAPPING = [
39

40
        'mysql' => Driver\MySQLDriver::class,
41
        'sqlite' => Driver\SQLiteDriver::class,
42

43
    ];
44

45
    public readonly string $id;
46

47
    /**
48
     * Prefix to prepend to every table name.
49
     *
50
     * If set to "dev", all table names will be named like "dev_nodes", "dev_contents", etc.
51
     * This is a convenient way of creating a namespace for tables in a shared database.
52
     * By default, the prefix is the empty string, that is there is not prefix.
53
     *
54
     * @var non-empty-string
55
     */
56
    public readonly string $table_name_prefix;
57

58
    /**
59
     * Charset for the connection. Also used to specify the charset while creating tables.
60
     */
61
    public readonly string $charset;
62

63
    /**
64
     * Used to specify the collate while creating tables.
65
     */
66
    public readonly string $collate;
67

68
    /**
69
     * Timezone of the connection.
70
     */
71
    public readonly string $timezone;
72

73
    /**
74
     * Driver name for the connection.
75
     */
76
    public readonly string $driver_name;
77

78
    public readonly Driver $driver;
79

80
    /**
81
     * The number of database queries and executions, used for statistics purpose.
82
     */
83
    public int $queries_count = 0;
84
    public readonly PDO $pdo;
85

86
    /**
87
     * The number of micro seconds spent per request.
88
     *
89
     * @var array[]
90
     */
91
    public array $profiling = [];
92

93
    /**
94
     * Establish a connection to a database.
95
     *
96
     * Custom options can be specified using the driver-specific connection options. See
97
     * {@link Options}.
98
     *
99
     * @link http://www.php.net/manual/en/pdo.construct.php
100
     * @link http://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html
101
     */
102
    public function __construct(ConnectionDefinition $definition)
103
    {
104
        $this->id = $definition->id;
86✔
105
        $dsn = $definition->dsn;
86✔
106

107
        $this->table_name_prefix = $definition->table_name_prefix
86✔
108
            ? $definition->table_name_prefix . '_'
30✔
109
            : '';
56✔
110

111
        [ $this->charset, $this->collate ] = extract_charset_and_collate(
86✔
112
            $definition->charset_and_collate ?? $definition::DEFAULT_CHARSET_AND_COLLATE
86✔
113
        );
86✔
114

115
        $this->timezone = $definition->time_zone;
86✔
116
        $this->driver_name = $this->resolve_driver_name($dsn);
86✔
117
        $this->driver = $this->resolve_driver($this->driver_name);
86✔
118

119
        $options = $this->make_options();
86✔
120

121
        $this->pdo = new PDO($dsn, $definition->username, $definition->password, $options);
86✔
122

123
        $this->after_connection();
85✔
124
    }
125

126
    /**
127
     * Alias to {@see query}.
128
     */
129
    public function __invoke(mixed ...$args): Statement
130
    {
131
        return $this->query(...$args);
4✔
132
    }
133

134
    /**
135
     * Resolve the driver name from the DSN string.
136
     */
137
    protected function resolve_driver_name(string $dsn): string
138
    {
139
        return explode(':', $dsn, 2)[0];
86✔
140
    }
141

142
    /**
143
     * Resolves driver class.
144
     *
145
     * @throws DriverNotDefined
146
     *
147
     * @return class-string<Driver>
148
     */
149
    private function resolve_driver_class(string $driver_name): string
150
    {
151
        return self::DRIVERS_MAPPING[$driver_name]
86✔
152
            ?? throw new DriverNotDefined($driver_name);
86✔
153
    }
154

155
    /**
156
     * Resolves a {@link Driver} implementation.
157
     */
158
    private function resolve_driver(string $driver_name): Driver
159
    {
160
        $driver_class = $this->resolve_driver_class($driver_name);
86✔
161

162
        return new $driver_class(
86✔
163
            function () {
86✔
164
                return $this;
64✔
165
            }
86✔
166
        );
86✔
167
    }
168

169
    /**
170
     * Called before the connection.
171
     *
172
     * May alter the options according to the driver.
173
     *
174
     * @return array<PDO::*, mixed>
175
     */
176
    private function make_options(): array
177
    {
178
        if ($this->driver_name != 'mysql') {
86✔
179
            return [];
85✔
180
        }
181

182
        $init_command = 'SET NAMES ' . $this->charset;
1✔
183
        $init_command .= ', time_zone = "' . $this->timezone . '"';
1✔
184

185
        return [
1✔
186

187
            PDO::MYSQL_ATTR_INIT_COMMAND => $init_command,
1✔
188

189
        ];
1✔
190
    }
191

192
    private function after_connection(): void
193
    {
194
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
85✔
195
    }
196

197
    /**
198
     * Overrides the method to resolve the statement before it is prepared, then set its fetch
199
     * mode and connection.
200
     *
201
     * @param string $statement Query statement.
202
     * @param array<string, mixed> $options
203
     *
204
     * @return Statement The prepared statement.
205
     *
206
     * @throws StatementNotValid if the statement cannot be prepared.
207
     */
208
    public function prepare(string $statement, array $options = []): Statement
209
    {
210
        $statement = $this->resolve_statement($statement);
44✔
211

212
        try {
213
            $statement = $this->pdo->prepare($statement, $options);
44✔
214
        } catch (PDOException $e) {
3✔
215
            throw new StatementNotValid($statement, original: $e);
3✔
216
        }
217

218
        if (isset($options['mode'])) {
42✔
219
            $mode = (array) $options['mode'];
×
220

221
            $statement->setFetchMode(...$mode);
×
222
        }
223

224
        return new Statement($statement, $this);
42✔
225
    }
226

227
    /**
228
     * Overrides the method in order to prepare (and resolve) the statement and execute it with
229
     * the specified arguments and options.
230
     *
231
     * @param array<string|int, mixed> $args
232
     * @param array<string, mixed> $options
233
     */
234
    public function query(string $statement, array $args = [], array $options = []): Statement
235
    {
236
        $statement = $this->prepare($statement, $options);
38✔
237
        $statement->execute($args);
36✔
238

239
        return $statement;
35✔
240
    }
241

242
    /**
243
     * Executes a statement.
244
     *
245
     * The statement is resolved using the {@link resolve_statement()} method before it is
246
     * executed.
247
     *
248
     * The execution of the statement is wrapped in a try/catch block. {@link PDOException} are
249
     * caught and {@link StatementNotValid} exception are thrown with additional information
250
     * instead.
251
     *
252
     * Using this method increments the `queries_by_connection` stat.
253
     *
254
     * @return false|int @FIXME https://github.com/sebastianbergmann/phpunit/issues/4735
255
     * @throws StatementNotValid if the statement cannot be executed.
256
     */
257
    public function exec(string $statement): bool|int
258
    {
259
        $statement = $this->resolve_statement($statement);
64✔
260

261
        try {
262
            $this->queries_count++;
64✔
263

264
            return $this->pdo->exec($statement);
64✔
265
        } catch (PDOException $e) {
×
266
            throw new StatementNotValid($statement, original: $e);
×
267
        }
268
    }
269

270
    public function get_last_insert_id(): int
271
    {
272
        $id = $this->pdo->lastInsertId();
32✔
273

274
        if ($id === false) {
32✔
NEW
275
            throw new RuntimeException("Unable to retrieve last inserted ID");
×
276
        }
277

278
        return (int) $id;
32✔
279
    }
280

281
    /**
282
     * Replaces placeholders with their value.
283
     *
284
     * The following placeholders are supported:
285
     *
286
     * - `{prefix}`: replaced by the {@link $table_name_prefix} property.
287
     * - `{charset}`: replaced by the {@link $charset} property.
288
     * - `{collate}`: replaced by the {@link $collate} property.
289
     */
290
    public function resolve_statement(string $statement): string
291
    {
292
        return strtr($statement, [
68✔
293
            '{prefix}' => $this->table_name_prefix,
68✔
294
            '{charset}' => $this->charset,
68✔
295
            '{collate}' => $this->collate,
68✔
296
        ]);
68✔
297
    }
298

299
    /**
300
     * Alias for the `beginTransaction()` method.
301
     *
302
     * @see PDO::beginTransaction
303
     */
304
    public function begin(): bool
305
    {
306
        return $this->pdo->beginTransaction();
×
307
    }
308

309
    public function quote_string(string $string): string
310
    {
311
        return $this->pdo->quote($string);
×
312
    }
313

314
    public function quote_identifier(string $identifier): string
315
    {
316
        return $this->driver->quote_identifier($identifier);
1✔
317
    }
318

319
    public function cast_value(mixed $value, string $type = null): mixed
320
    {
321
        return $this->driver->cast_value($value, $type);
×
322
    }
323

324
    /**
325
     * @param non-empty-string $unprefixed_table_name
326
     *
327
     * @throws Throwable
328
     */
329
    public function create_table(string $unprefixed_table_name, Schema $schema): void
330
    {
331
        $this->driver->create_table($this->table_name_prefix . $unprefixed_table_name, $schema);
64✔
332
    }
333

334
    /**
335
     * Determines if a table exists in the database.
336
     */
337
    public function table_exists(string $unprefixed_name): bool
338
    {
339
        return $this->driver->table_exists($this->table_name_prefix . $unprefixed_name);
34✔
340
    }
341

342
    public function optimize(): void
343
    {
344
        $this->driver->optimize();
×
345
    }
346
}
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