Passed
Push — dbal ( a65111...92f26f )
by Greg
06:58
created

DB::schemaAssetsFilter()   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 1
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;
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     => 'BINARY',
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);
0 ignored issues
show
Unused Code introduced by
The assignment to $prefix_filter is dead and can be removed.
Loading history...
166
        $configuration = new Configuration();
167
        $configuration->setSchemaAssetsFilter(schemaAssetsFilter: self::schemaAssetsFilter(...));
168
169
        self::$dbal_connection = new Connection(params: [], driver: $dbal_driver, config: $configuration);
170
    }
171
172
    private static function schemaAssetsFilter(string $asset): bool
0 ignored issues
show
Unused Code introduced by
The method schemaAssetsFilter() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
173
    {
174
        return str_starts_with(haystack: $asset, needle: self::prefix());
175
    }
176
177
    public static function driverName(): string
178
    {
179
        return self::pdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
180
    }
181
182
    public static function exec(string $sql): int|false
183
    {
184
        return self::pdo()->exec($sql);
185
    }
186
187
    public static function lastInsertId(): int
188
    {
189
        $return = self::pdo()->lastInsertId();
190
191
        if ($return === false) {
192
            throw new RuntimeException('Unable to retrieve last insert ID');
193
        }
194
195
        // All IDs are integers in our schema.
196
        return (int) $return;
197
    }
198
199
    private static function pdo(): PDO
200
    {
201
        return parent::connection()->getPdo();
202
    }
203
204
    public static function prefix(string $identifier = ''): string
205
    {
206
        return parent::connection()->getTablePrefix() . $identifier;
207
    }
208
209
    public static function rollBack(): void
210
    {
211
        parent::connection()->rollBack();
212
    }
213
214
    /**
215
     * @internal
216
     */
217
    public static function iLike(): string
218
    {
219
        if (self::driverName() === self::POSTGRES) {
220
            return 'ILIKE';
221
        }
222
223
        if (self::driverName() === self::SQL_SERVER) {
224
            return 'COLLATE SQL_UTF8_General_CI_AI LIKE';
225
        }
226
227
        return 'LIKE';
228
    }
229
230
    /**
231
     * @internal
232
     */
233
    public static function groupConcat(string $column): string
234
    {
235
        switch (self::driverName()) {
236
            case self::POSTGRES:
237
            case self::SQL_SERVER:
238
                return 'STRING_AGG(' . $column . ", ',')";
239
240
            case self::MYSQL:
241
            case self::SQLITE:
242
            default:
243
                return 'GROUP_CONCAT(' . $column . ')';
244
        }
245
    }
246
247
    public static function binaryColumn(string $column, string|null $alias = null): Expression
248
    {
249
        if (self::driverName() === self::MYSQL) {
250
            $sql = 'CAST(' . $column . ' AS binary)';
251
        } else {
252
            $sql = $column;
253
        }
254
255
        if ($alias !== null) {
256
            $sql .= ' AS ' . $alias;
257
        }
258
259
        return new Expression($sql);
260
    }
261
262
    public static function regexOperator(): string
263
    {
264
        return self::REGEX_OPERATOR[self::driverName()];
265
    }
266
267
    /**
268
     * PHPSTAN can't detect the magic methods in the parent class.
269
     */
270
    public static function query(): Builder
271
    {
272
        return parent::connection()->query();
273
    }
274
275
    public static function getDBALConnection(): Connection
276
    {
277
        return self::$dbal_connection;
278
    }
279
280
    public static function select(string ...$expressions): QueryBuilder
281
    {
282
        return self::$dbal_connection
283
            ->createQueryBuilder()
284
            ->select(...$expressions);
285
    }
286
287
    public static function update(string $table): QueryBuilder
288
    {
289
        return parent::connection()->update(self::prefix($table));
290
    }
291
292
    /**
293
     * @param string                                                $table
294
     * @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...
295
     */
296
    public static function insert(string $table, array $rows): void
297
    {
298
        foreach ($rows as $row) {
299
            self::getDBALConnection()->insert(self::prefix($table), $row);
300
        }
301
    }
302
303
    public static function delete(string ...$expressions): QueryBuilder
304
    {
305
        return self::$dbal_connection
306
            ->createQueryBuilder()
307
            ->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

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