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

DB::connect()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 80
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 37
nc 12
nop 11
dl 0
loc 80
rs 7.7724
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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