Passed
Push — develop ( a25cb7...a43744 )
by nguereza
03:28
created

Connection   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 568
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 62
eloc 235
c 1
b 0
f 0
dl 0
loc 568
rs 3.44

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 2
B connect() 0 54 9
A getSchema() 0 3 1
A count() 0 9 1
A transaction() 0 30 4
A getDsn() 0 3 1
B replaceParameters() 0 22 7
D setConnectionParams() 0 112 16
A getParams() 0 3 1
A getLogs() 0 3 1
A column() 0 9 1
A persistent() 0 5 1
A query() 0 6 1
A getPDO() 0 3 1
A getConfig() 0 3 1
A exec() 0 3 1
A execute() 0 36 3
A bindValues() 0 13 6
A getDriver() 0 3 1
A setLogger() 0 5 1
A prepare() 0 21 2

How to fix   Complexity   

Complex Class

Complex classes like Connection 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 Connection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Platine Database
5
 *
6
 * Platine Database is the abstraction layer using PDO with support of query and schema builder
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Database
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file Connection.php
33
 *
34
 *  The Database Connection class
35
 *
36
 *  @package    Platine\Database
37
 *  @author Platine Developers Team
38
 *  @copyright  Copyright (c) 2020
39
 *  @license    http://opensource.org/licenses/MIT  MIT License
40
 *  @link   http://www.iacademy.cf
41
 *  @version 1.0.0
42
 *  @filesource
43
 */
44
declare(strict_types=1);
45
46
namespace Platine\Database;
47
48
use InvalidArgumentException;
49
use PDO;
50
use PDOException;
51
use PDOStatement;
52
use Platine\Database\Driver\Driver;
53
use Platine\Database\Exception\ConnectionException;
54
use Platine\Database\Exception\QueryException;
55
use Platine\Database\Exception\TransactionException;
56
use Platine\Logger\Logger;
57
use Platine\Logger\NullHandler;
58
59
/**
60
 * Class Connection
61
 * @package Platine\Database
62
 */
63
class Connection
64
{
65
66
    /**
67
     * The PDO instance
68
     * @var PDO
69
     */
70
    protected PDO $pdo;
71
72
    /**
73
     * The PDO data source name
74
     * @var string
75
     */
76
    protected string $dsn = '';
77
78
    /**
79
     * The list of execution query logs
80
     * @var array<int, array<string, mixed>>
81
     */
82
    protected array $logs = [];
83
84
    /**
85
     * The driver to use
86
     * @var Driver
87
     */
88
    protected Driver $driver;
89
90
    /**
91
     * The Schema instance to use
92
     * @var Schema
93
     */
94
    protected Schema $schema;
95
96
    /**
97
     * The connection configuration
98
     * @var ConfigurationInterface
99
     */
100
    protected ConfigurationInterface $config;
101
102
    /**
103
     * The connection parameters
104
     * @var array<int|string, mixed>
105
     */
106
    protected array $params = [];
107
108
    /**
109
     * @var Logger
110
     */
111
    protected Logger $logger;
112
113
    /**
114
     * Connection constructor.
115
     * @param ConfigurationInterface $config
116
     * @param Logger $logger
117
     * @throws ConnectionException
118
     */
119
    public function __construct(
120
        ConfigurationInterface $config,
121
        ?Logger $logger = null
122
    ) {
123
        $this->config = $config;
124
125
        $this->logger = $logger ? $logger : new Logger(new NullHandler());
126
        $this->logger->setChannel(__CLASS__);
127
128
        $driverClass = $this->config->getDriverClassName();
129
        $this->driver = new $driverClass($this);
130
131
        $this->schema = new Schema($this);
132
133
        $this->connect();
134
    }
135
136
    /**
137
     * Connect to the database
138
     * @return void
139
     */
140
    public function connect(): void
141
    {
142
        $this->setConnectionParams();
143
144
        if ($this->config->isPersistent()) {
145
            $this->persistent(true);
146
        }
147
148
        $attr = $this->params;
149
150
        if (empty($attr)) {
151
            throw new InvalidArgumentException('Invalid database options supplied');
152
        }
153
154
        $driver = $attr['driver'];
155
        unset($attr['driver']);
156
157
        $params = [];
158
        foreach ($attr as $key => $value) {
159
            $params[] = is_int($key) ? $value : $key . '=' . $value;
160
        }
161
162
        $dsn = $driver . ':' . implode(';', $params);
163
        if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv'])) {
164
            $charset = $this->config->getCharset();
165
            $this->config->addCommand('SET NAMES "' . $charset . '"' . (
166
                    $this->config->getDriverName() === 'mysql'
167
                            ? ' COLLATE "' . $this->config->getCollation() . '"'
168
                            : ''
169
                    ));
170
        }
171
172
        $this->dsn = $dsn;
173
174
        try {
175
            $this->pdo = new PDO(
176
                $this->dsn,
177
                $this->config->getUsername(),
178
                $this->config->getPassword(),
179
                $this->config->getOptions()
180
            );
181
182
            foreach ($this->config->getCommands() as $command) {
183
                $this->pdo->exec($command);
184
            }
185
        } catch (PDOException $exception) {
186
            $this->logger->emergency('Can not connect to database. Error message: {error}', [
187
                'exception' => $exception,
188
                'error' => $exception->getMessage()
189
            ]);
190
            throw new ConnectionException(
191
                'Can not connect to database',
192
                (int) $exception->getCode(),
193
                $exception->getPrevious()
194
            );
195
        }
196
    }
197
198
    /**
199
     *
200
     * @param Logger $logger
201
     * @return self
202
     */
203
    public function setLogger(Logger $logger): self
204
    {
205
        $this->logger = $logger;
206
207
        return $this;
208
    }
209
210
    /**
211
     * Return the query execution logs
212
     * @return array<int, array<string, mixed>>
213
     */
214
    public function getLogs(): array
215
    {
216
        return $this->logs;
217
    }
218
219
    /**
220
     * Return the current connection parameters
221
     * @return array<int|string, mixed>
222
     */
223
    public function getParams(): array
224
    {
225
        return $this->params;
226
    }
227
228
    /**
229
     * Return the current connection configuration
230
     * @return ConfigurationInterface
231
     */
232
    public function getConfig(): ConfigurationInterface
233
    {
234
        return $this->config;
235
    }
236
237
    /**
238
     * Return the current driver instance
239
     * @return Driver
240
     */
241
    public function getDriver(): Driver
242
    {
243
        return $this->driver;
244
    }
245
246
    /**
247
     * Return the current Schema instance
248
     * @return Schema
249
     */
250
    public function getSchema(): Schema
251
    {
252
        return $this->schema;
253
    }
254
255
    /**
256
     * Set connection to be persistent
257
     * @param bool $value
258
     * @return self
259
     */
260
    public function persistent(bool $value = true): self
261
    {
262
        $this->config->setOption(PDO::ATTR_PERSISTENT, $value);
263
264
        return $this;
265
    }
266
267
    /**
268
     * @return string
269
     */
270
    public function getDsn(): string
271
    {
272
        return $this->dsn;
273
    }
274
275
    /**
276
     * Return the instance of the PDO
277
     * @return PDO
278
     */
279
    public function getPDO(): PDO
280
    {
281
        return $this->pdo;
282
    }
283
284
    /**
285
     * Execute the SQL query and return the result
286
     * @param string $sql
287
     * @param array<int, mixed> $params the query parameters
288
     * @return ResultSet
289
     * @throws QueryException
290
     */
291
    public function query(string $sql, array $params = []): ResultSet
292
    {
293
        $prepared = $this->prepare($sql, $params);
294
        $this->execute($prepared);
295
296
        return new ResultSet($prepared['statement']);
297
    }
298
299
    /**
300
     * Direct execute the SQL query
301
     * @param string $sql
302
     * @param array<int, mixed> $params the query parameters
303
     * @return bool
304
     * @throws QueryException
305
     */
306
    public function exec(string $sql, array $params = []): bool
307
    {
308
        return $this->execute($this->prepare($sql, $params));
309
    }
310
311
    /**
312
     *  Execute the SQL query and return the number
313
     * of affected rows
314
     * @param string $sql
315
     * @param array<int, mixed> $params the query parameters
316
     * @return int
317
     * @throws QueryException
318
     */
319
    public function count(string $sql, array $params = []): int
320
    {
321
        $prepared = $this->prepare($sql, $params);
322
        $this->execute($prepared);
323
324
        $result = $prepared['statement']->rowCount();
325
        $prepared['statement']->closeCursor();
326
327
        return $result;
328
    }
329
330
    /**
331
     *  Execute the SQL query and return the first column result
332
     * @param string $sql
333
     * @param array<int, mixed> $params the query parameters
334
     * @return mixed
335
     * @throws QueryException
336
     */
337
    public function column(string $sql, array $params = [])
338
    {
339
        $prepared = $this->prepare($sql, $params);
340
        $this->execute($prepared);
341
342
        $result = $prepared['statement']->fetchColumn();
343
        $prepared['statement']->closeCursor();
344
345
        return $result;
346
    }
347
348
    /**
349
     * @param callable $callback
350
     * @param mixed|null $that
351
     *
352
     * @return mixed
353
     *
354
     * @throws ConnectionException
355
     */
356
    public function transaction(
357
        callable $callback,
358
        $that = null
359
    ) {
360
        if ($that === null) {
361
            $that = $this;
362
        }
363
364
        if ($this->pdo->inTransaction()) {
365
            return $callback($that);
366
        }
367
368
        try {
369
            $this->pdo->beginTransaction();
370
            $result = $callback($that);
371
            $this->pdo->commit();
372
        } catch (PDOException $exception) {
373
            $this->pdo->rollBack();
374
            $this->logger->error('Database transaction error. Error message: {error}', [
375
                'exception' => $exception,
376
                'error' => $exception->getMessage()
377
            ]);
378
            throw new TransactionException(
379
                $exception->getMessage(),
380
                (int) $exception->getCode(),
381
                $exception->getPrevious()
382
            );
383
        }
384
385
        return $result;
386
    }
387
388
    /**
389
     * Change the query parameters placeholder with the value
390
     * @param string $query
391
     * @param array<int, mixed> $params
392
     * @return string
393
     */
394
    protected function replaceParameters(string $query, array $params): string
395
    {
396
        $driver = $this->driver;
397
398
        return (string) preg_replace_callback(
399
            '/\?/',
400
            function () use ($driver, &$params) {
401
                $param = array_shift($params);
402
403
                $value = is_object($param) ? get_class($param) : $param;
404
                if (is_int($value) || is_float($value)) {
405
                    return $value;
406
                }
407
                if ($value === null) {
408
                    return 'NULL';
409
                }
410
                if (is_bool($value)) {
411
                    return $value ? 'TRUE' : 'FALSE';
412
                }
413
                return $driver->quote($value);
414
            },
415
            $query
416
        );
417
    }
418
419
    /**
420
     * Prepare the query
421
     * @param string $query
422
     * @param array<mixed> $params
423
     * @return array<string, mixed>
424
     * @throws QueryException
425
     */
426
    protected function prepare(string $query, array $params): array
427
    {
428
        try {
429
            $statement = $this->pdo->prepare($query);
430
        } catch (PDOException $exception) {
431
            $this->logger->error('Error when prepare query [{query}]. Error message: {error}', [
432
                'exception' => $exception,
433
                'error' => $exception->getMessage(),
434
                'query' => $query
435
            ]);
436
            throw new QueryException(
437
                $exception->getMessage() . ' [' . $query . ']',
438
                (int) $exception->getCode(),
439
                $exception->getPrevious()
440
            );
441
        }
442
443
        return [
444
            'statement' => $statement,
445
            'query' => $query,
446
            'params' => $params
447
        ];
448
    }
449
450
    /**
451
     * Execute the prepared query
452
     * @param array<string, mixed> $prepared
453
     * @return bool the status of the execution
454
     * @throws QueryException
455
     */
456
    protected function execute(array $prepared): bool
457
    {
458
        $sql = $this->replaceParameters($prepared['query'], $prepared['params']);
459
        $sqlLog = [
460
            'query' => $prepared['query'],
461
            'parameters' => implode(', ', $prepared['params'])
462
        ];
463
464
        try {
465
            if ($prepared['params']) {
466
                $this->bindValues($prepared['statement'], $prepared['params']);
467
            }
468
            $start = microtime(true);
469
            $result = $prepared['statement']->execute();
470
            $sqlLog['time'] = number_format(microtime(true) - $start, 6);
471
472
            $this->logs[] = $sqlLog;
473
474
            $this->logger->info(
475
                'Execute Query: [{query}], parameters: [{parameters}], time: [{time}]',
476
                $sqlLog
477
            );
478
        } catch (PDOException $exception) {
479
            $this->logger->error('Error when execute query [{sql}]. Error message: {error}', [
480
                'exception' => $exception,
481
                'error' => $exception->getMessage(),
482
                'sql' => $sql
483
            ]);
484
            throw new QueryException(
485
                $exception->getMessage() . ' [' . $sql . ']',
486
                (int) $exception->getCode(),
487
                $exception->getPrevious()
488
            );
489
        }
490
491
        return $result;
492
    }
493
494
    /**
495
     * Bind the parameters values
496
     * @param PDOStatement $statement
497
     * @param array<int, mixed> $values
498
     */
499
    protected function bindValues(PDOStatement $statement, array $values): void
500
    {
501
        foreach ($values as $key => $value) {
502
            $param = PDO::PARAM_STR;
503
            if (is_null($value)) {
504
                $param = PDO::PARAM_NULL;
505
            } elseif (is_int($value) || is_float($value)) {
506
                $param = PDO::PARAM_INT;
507
            } elseif (is_bool($value)) {
508
                $param = PDO::PARAM_BOOL;
509
            }
510
511
            $statement->bindValue($key + 1, $value, $param);
512
        }
513
    }
514
515
    /**
516
     * Set the PDO connection parameters to use
517
     * @return void
518
     */
519
    protected function setConnectionParams(): void
520
    {
521
        $port = $this->config->getPort();
522
        $database = $this->config->getDatabase();
523
        $hostname = $this->config->getHostname();
524
        $attr = [];
525
526
        $driverName = $this->config->getDriverName();
527
        switch ($driverName) {
528
            case 'mysql':
529
            case 'pgsql':
530
                $attr = [
531
                    'driver' => $driverName,
532
                    'dbname' => $database,
533
                    'host' => $hostname,
534
                ];
535
536
                if ($port > 0) {
537
                    $attr['port'] = $port;
538
                }
539
540
                if ($driverName === 'mysql') {
541
                    //Make MySQL using standard quoted identifier
542
                    $this->config->addCommand('SET SQL_MODE=ANSI_QUOTES');
543
                    $this->config->addCommand('SET CHARACTER SET "' . $this->config->getCharset() . '"');
544
545
                    $socket = $this->config->getSocket();
546
                    if (!empty($socket)) {
547
                        $attr['unix_socket'] = $socket;
548
549
                        unset($attr['host']);
550
                        unset($attr['port']);
551
                    }
552
                }
553
                break;
554
            case 'sqlsrv':
555
                //Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting
556
                $this->config->addCommand('SET QUOTED_IDENTIFIER ON');
557
558
                //Make ANSI_NULLS is ON for NULL value
559
                $this->config->addCommand('SET ANSI_NULLS ON');
560
561
                $attr = [
562
                    'driver' => 'sqlsrv',
563
                    'Server' => $hostname
564
                        . ($port > 0 ? ':' . $port : ''),
565
                    'Database' => $database
566
                ];
567
568
                $appName = $this->config->getAppname();
569
                if (!empty($appName)) {
570
                    $attr['APP'] = $appName;
571
                }
572
573
                $attributes = [
574
                    'ApplicationIntent',
575
                    'AttachDBFileName',
576
                    'Authentication',
577
                    'ColumnEncryption',
578
                    'ConnectionPooling',
579
                    'Encrypt',
580
                    'Failover_Partner',
581
                    'KeyStoreAuthentication',
582
                    'KeyStorePrincipalId',
583
                    'KeyStoreSecret',
584
                    'LoginTimeout',
585
                    'MultipleActiveResultSets',
586
                    'MultiSubnetFailover',
587
                    'Scrollable',
588
                    'TraceFile',
589
                    'TraceOn',
590
                    'TransactionIsolation',
591
                    'TransparentNetworkIPResolution',
592
                    'TrustServerCertificate',
593
                    'WSID',
594
                ];
595
596
                foreach ($attributes as $attribute) {
597
                    $str = preg_replace(
598
                        ['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'],
599
                        '$1_$2',
600
                        $attribute
601
                    );
602
603
                    if (is_string($str)) {
604
                        $keyname = strtolower($str);
605
606
                        if ($this->config->hasAttribute($keyname)) {
607
                            $attr[$attribute] = $this->config->getAttribute($keyname);
608
                        }
609
                    }
610
                }
611
                break;
612
            case 'oci':
613
            case 'oracle':
614
                $attr = [
615
                    'driver' => 'oci',
616
                    'dbname' => '//' . $hostname
617
                    . ($port > 0 ? ':' . $port : ':1521') . '/' . $database
618
                ];
619
620
                $attr['charset'] = $this->config->getCharset();
621
                break;
622
            case 'sqlite':
623
                $attr = [
624
                    'driver' => 'sqlite',
625
                    $database
626
                ];
627
                break;
628
        }
629
630
        $this->params = $attr;
631
    }
632
}
633