Passed
Push — dbal ( 620c4a...2c143b )
by Greg
23:32 queued 07:20
created

DB   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 137
c 1
b 0
f 0
dl 0
loc 367
rs 8.48
wmc 49

31 Methods

Rating   Name   Duplication   Size   Complexity  
A query() 0 3 1
A index() 0 3 1
A integer() 0 3 1
A primaryKey() 0 3 1
A binaryColumn() 0 13 3
A text() 0 3 1
A iLike() 0 11 3
A update() 0 3 1
A rollBack() 0 3 1
A lastInsertId() 0 10 2
A schemaAssetsFilter() 0 3 1
A prefix() 0 3 1
A pdo() 0 3 1
A exec() 0 3 1
A varchar() 0 7 1
A driverName() 0 3 1
A char() 0 8 1
A expression() 0 3 1
A regexOperator() 0 3 1
A timestamp() 0 3 1
A nchar() 0 8 1
A foreignKey() 0 6 1
B connect() 0 79 9
A nvarchar() 0 7 1
A select() 0 5 1
A groupConcat() 0 11 5
A uniqueIndex() 0 3 1
A insert() 0 4 2
A float() 0 3 1
A delete() 0 5 1
A getDBALConnection() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DB often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DB, and based on these observations, apply Extract Interface, too.

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

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