Passed
Push — dbal ( 545eb7...a65111 )
by Greg
13:50 queued 06:40
created

DB::pdo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2023 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Doctrine\DBAL\Configuration;
23
use Doctrine\DBAL\Connection;
24
use Doctrine\DBAL\Driver;
25
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
26
use Doctrine\DBAL\Query\QueryBuilder;
27
use DomainException;
28
use Fisharebest\Webtrees\DB\Column;
29
use Fisharebest\Webtrees\DB\ColumnType;
30
use Fisharebest\Webtrees\DB\Drivers\DriverInterface;
31
use Fisharebest\Webtrees\DB\Drivers\MySQLDriver;
32
use Fisharebest\Webtrees\DB\Drivers\PostgreSQLDriver;
33
use Fisharebest\Webtrees\DB\Drivers\SQLiteDriver;
34
use Fisharebest\Webtrees\DB\Drivers\SQLServerDriver;
35
use Fisharebest\Webtrees\DB\ForeignKey;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\DB\ForeignKey was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
use Fisharebest\Webtrees\DB\Index;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\DB\Index was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
use Fisharebest\Webtrees\DB\PrimaryKey;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\DB\PrimaryKey was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
use Fisharebest\Webtrees\DB\UniqueIndex;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\DB\UniqueIndex was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
39
use Illuminate\Database\Capsule\Manager;
40
use Illuminate\Database\Query\Builder;
41
use Illuminate\Database\Query\Expression;
42
use PDO;
43
use PDOException;
44
use RuntimeException;
45
use SensitiveParameter;
1 ignored issue
show
Bug introduced by
The type SensitiveParameter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
46
47
use function str_starts_with;
48
49
/**
50
 * Database abstraction
51
 */
52
class DB extends Manager
53
{
54
    // Supported drivers
55
    public const MYSQL      = 'mysql';
56
    public const POSTGRES   = 'pgsql';
57
    public const SQLITE     = 'sqlite';
58
    public const SQL_SERVER = 'sqlsrv';
59
60
    private const COLLATION_ASCII = [
61
        self::MYSQL      => 'ascii_bin',
62
        self::POSTGRES   => 'C',
63
        self::SQLITE     => 'C',
64
        self::SQL_SERVER => 'Latin1_General_Bin',
65
    ];
66
67
    private const COLLATION_UTF8 = [
68
        self::MYSQL      => 'utf8mb4_unicode_ci',
69
        self::POSTGRES   => 'und-x-icu',
70
        self::SQLITE     => 'nocase',
71
        self::SQL_SERVER => 'utf8_CI_AI',
72
    ];
73
74
    private const REGEX_OPERATOR = [
75
        self::MYSQL      => 'REGEXP',
76
        self::POSTGRES   => '~',
77
        self::SQLITE     => 'REGEXP',
78
        self::SQL_SERVER => 'REGEXP',
79
    ];
80
81
    private const DRIVER_INITIALIZATION = [
82
        self::MYSQL      => "SET NAMES utf8mb4, sql_mode := 'ANSI,STRICT_ALL_TABLES', TIME_ZONE := '+00:00', SQL_BIG_SELECTS := 1, GROUP_CONCAT_MAX_LEN := 1048576",
83
        self::POSTGRES   => '',
84
        self::SQLITE     => 'PRAGMA foreign_keys = ON',
85
        self::SQL_SERVER => 'SET language us_english', // For timestamp columns
86
    ];
87
88
    private static Connection $dbal_connection;
89
90
    public static function connect(
91
        #[SensitiveParameter]
92
        string $driver,
93
        #[SensitiveParameter]
94
        string $host,
95
        #[SensitiveParameter]
96
        string $port,
97
        #[SensitiveParameter]
98
        string $database,
99
        #[SensitiveParameter]
100
        string $username,
101
        #[SensitiveParameter]
102
        string $password,
103
        #[SensitiveParameter]
104
        string $prefix,
105
        #[SensitiveParameter]
106
        string $key,
107
        #[SensitiveParameter]
108
        string $certificate,
109
        #[SensitiveParameter]
110
        string $ca,
111
        #[SensitiveParameter]
112
        bool $verify_certificate,
113
    ): void {
114
        $options = [
115
            // Some drivers do this and some don't. Make them consistent.
116
            PDO::ATTR_STRINGIFY_FETCHES => true,
117
        ];
118
119
        // MySQL/MariaDB support encrypted connections
120
        if ($driver === self::MYSQL && $key !== '' && $certificate !== '' && $ca !== '') {
121
            $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $verify_certificate;
122
            $options[PDO::MYSQL_ATTR_SSL_KEY]                = Webtrees::ROOT_DIR . 'data/' . $key;
123
            $options[PDO::MYSQL_ATTR_SSL_CERT]               = Webtrees::ROOT_DIR . 'data/' . $certificate;
124
            $options[PDO::MYSQL_ATTR_SSL_CA]                 = Webtrees::ROOT_DIR . 'data/' . $ca;
125
        }
126
127
        if ($driver === self::SQLITE && $database !== ':memory:') {
128
            $database = Webtrees::ROOT_DIR . 'data/' . $database . '.sqlite';
129
        }
130
131
        $capsule = new self();
132
        $capsule->addConnection([
133
            'driver'         => $driver,
134
            'host'           => $host,
135
            'port'           => $port,
136
            'database'       => $database,
137
            'username'       => $username,
138
            'password'       => $password,
139
            'prefix'         => $prefix,
140
            'prefix_indexes' => true,
141
            'options'        => $options,
142
        ]);
143
        $capsule->setAsGlobal();
144
145
        // Eager-load the connection, to prevent database credentials appearing in error logs.
146
        try {
147
            self::pdo();
148
        } catch (PDOException $exception) {
149
            throw new RuntimeException($exception->getMessage());
150
        }
151
152
        $sql = self::DRIVER_INITIALIZATION[$driver];
153
154
        if ($sql !== '') {
155
            self::exec($sql);
156
        }
157
158
        $dbal_driver = match ($driver) {
159
            self::MYSQL      => new MySQLDriver(pdo: self::pdo()),
160
            self::POSTGRES   => new PostgreSQLDriver(pdo: self::pdo()),
161
            self::SQLITE     => new SQLiteDriver(pdo: self::pdo()),
162
            self::SQL_SERVER => new SQLServerDriver(pdo: self::pdo()),
163
        };
164
165
        $prefix_filter = static fn (string $name): bool => str_starts_with(haystack: $name, needle: $prefix);
166
        $configuration = new Configuration();
167
        $configuration->setSchemaAssetsFilter(schemaAssetsFilter: $prefix_filter);
168
169
        self::$dbal_connection = new Connection(params: [], driver: $dbal_driver, config: $configuration);
170
    }
171
172
    public static function driverName(): string
173
    {
174
        return self::pdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
175
    }
176
177
    public static function exec(string $sql): int|false
178
    {
179
        return self::pdo()->exec($sql);
180
    }
181
182
    public static function lastInsertId(): int
183
    {
184
        $return = self::pdo()->lastInsertId();
185
186
        if ($return === false) {
187
            throw new RuntimeException('Unable to retrieve last insert ID');
188
        }
189
190
        // All IDs are integers in our schema.
191
        return (int) $return;
192
    }
193
194
    private static function pdo(): PDO
195
    {
196
        return parent::connection()->getPdo();
197
    }
198
199
    public static function prefix(string $identifier = ''): string
200
    {
201
        return parent::connection()->getTablePrefix() . $identifier;
202
    }
203
204
    public static function rollBack(): void
205
    {
206
        parent::connection()->rollBack();
207
    }
208
209
    /**
210
     * @internal
211
     */
212
    public static function iLike(): string
213
    {
214
        if (self::driverName() === self::POSTGRES) {
215
            return 'ILIKE';
216
        }
217
218
        if (self::driverName() === self::SQL_SERVER) {
219
            return 'COLLATE SQL_UTF8_General_CI_AI LIKE';
220
        }
221
222
        return 'LIKE';
223
    }
224
225
    /**
226
     * @internal
227
     */
228
    public static function groupConcat(string $column): string
229
    {
230
        switch (self::driverName()) {
231
            case self::POSTGRES:
232
            case self::SQL_SERVER:
233
                return 'STRING_AGG(' . $column . ", ',')";
234
235
            case self::MYSQL:
236
            case self::SQLITE:
237
            default:
238
                return 'GROUP_CONCAT(' . $column . ')';
239
        }
240
    }
241
242
    public static function binaryColumn(string $column, string|null $alias = null): Expression
243
    {
244
        if (self::driverName() === self::MYSQL) {
245
            $sql = 'CAST(' . $column . ' AS binary)';
246
        } else {
247
            $sql = $column;
248
        }
249
250
        if ($alias !== null) {
251
            $sql .= ' AS ' . $alias;
252
        }
253
254
        return new Expression($sql);
255
    }
256
257
    public static function regexOperator(): string
258
    {
259
        return self::REGEX_OPERATOR[self::driverName()];
260
    }
261
262
    /**
263
     * PHPSTAN can't detect the magic methods in the parent class.
264
     */
265
    public static function query(): Builder
266
    {
267
        return parent::connection()->query();
268
    }
269
270
    public static function getDBALConnection(): Connection
271
    {
272
        return self::$dbal_connection;
273
    }
274
275
    public static function select(string ...$expressions): QueryBuilder
276
    {
277
        return self::$dbal_connection
278
            ->createQueryBuilder()
279
            ->select(...$expressions);
280
    }
281
282
    public static function update(string $table): QueryBuilder
283
    {
284
        return parent::connection()->update(self::prefix($table));
285
    }
286
287
    /**
288
     * @param string                                                $table
289
     * @param array<array-key,array<string,int|float|string|null>>  $rows
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key,array<st...int|float|string|null>> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key,array<string,int|float|string|null>>.
Loading history...
290
     */
291
    public static function insert(string $table, array $rows): void
292
    {
293
        foreach ($rows as $row) {
294
            self::getDBALConnection()->insert(self::prefix($table), $row);
295
        }
296
    }
297
298
    public static function delete(string ...$expressions): QueryBuilder
299
    {
300
        return self::$dbal_connection
301
            ->createQueryBuilder()
302
            ->delete(...$expressions);
0 ignored issues
show
Bug introduced by
$expressions is expanded, but the parameter $table of Doctrine\DBAL\Query\QueryBuilder::delete() does not expect variable arguments. ( Ignorable by Annotation )

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

302
            ->delete(/** @scrutinizer ignore-type */ ...$expressions);
Loading history...
303
    }
304
305
    public static function expression(): ExpressionBuilder
306
    {
307
        return self::$dbal_connection->createExpressionBuilder();
308
    }
309
310
    public static function char(string $name, int $length): Column
311
    {
312
        return new Column(
313
            name: $name,
314
            type: ColumnType::Char,
315
            length: $length,
316
            fixed: true,
317
            collation: self::COLLATION_ASCII[self::driverName()],
318
        );
319
    }
320
321
    public static function varchar(string $name, int $length): Column
322
    {
323
        return new Column(
324
            name: $name,
325
            type: ColumnType::Char,
326
            length: $length,
327
            collation: self::COLLATION_ASCII[self::driverName()],
328
        );
329
    }
330
331
    public static function nchar(string $name, int $length): Column
332
    {
333
        return new Column(
334
            name: $name,
335
            type: ColumnType::NChar,
336
            length: $length,
337
            fixed: true,
338
            collation: self::COLLATION_UTF8[self::driverName()],
339
        );
340
    }
341
342
    public static function nvarchar(string $name, int $length): Column
343
    {
344
        return new Column(
345
            name: $name,
346
            type: ColumnType::NVarChar,
347
            length: $length,
348
            collation: self::COLLATION_UTF8[self::driverName()],
349
        );
350
    }
351
352
    public static function integer(string $name): Column
353
    {
354
        return new Column(name: $name, type: ColumnType::Integer);
355
    }
356
357
    public static function float(string $name): Column
358
    {
359
        return new Column(name: $name, type: ColumnType::Float);
360
    }
361
362
    public static function text(string $name): Column
363
    {
364
        return new Column(name: $name, type: ColumnType::Text, collation: self::COLLATION_UTF8[self::driverName()]);
365
    }
366
367
    public static function timestamp(string $name, int $precision = 0): Column
368
    {
369
        return new Column(name: $name, type: ColumnType::Timestamp, precision: $precision);
370
    }
371
372
    /**
373
     * @param array<array-key,string> $columns
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key,string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key,string>.
Loading history...
374
     *
375
     * @return PrimaryKey
376
     */
377
    public static function primaryKey(array $columns): PrimaryKey
378
    {
379
        return new PrimaryKey(columns: $columns);
380
    }
381
382
    /**
383
     * @param array<array-key,string> $columns
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key,string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key,string>.
Loading history...
384
     *
385
     * @return Index
386
     */
387
    public static function index(array $columns): Index
388
    {
389
        return new Index(columns: $columns);
390
    }
391
392
    /**
393
     * @param array<array-key,string> $columns
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key,string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key,string>.
Loading history...
394
     *
395
     * @return UniqueIndex
396
     */
397
    public static function uniqueIndex(array $columns): UniqueIndex
398
    {
399
        return new UniqueIndex(columns: $columns);
400
    }
401
402
    /**
403
     * @param array<array-key,string> $local_columns
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key,string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key,string>.
Loading history...
404
     * @param string                  $foreign_table
405
     * @param array<array-key,string> $foreign_columns
406
     *
407
     * @return ForeignKey
408
     */
409
    public static function foreignKey(array $local_columns, string $foreign_table, array $foreign_columns = null): ForeignKey
410
    {
411
        return new ForeignKey(
412
            local_columns: $local_columns,
413
            foreign_table: $foreign_table,
414
            foreign_columns: $foreign_columns ?? $local_columns,
415
        );
416
    }
417
}
418