Passed
Push — develop ( f12fb2...40c14d )
by nguereza
02:31
created

Connection   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 662
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 253
dl 0
loc 662
rs 2.88
c 2
b 0
f 0
wmc 69

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getSchema() 0 3 1
A __construct() 0 15 1
A count() 0 9 1
A transaction() 0 29 4
A getDsn() 0 3 1
B replaceParameters() 0 22 7
A createPDO() 0 22 3
C setConnectionParams() 0 109 16
A getParams() 0 3 1
A getLogs() 0 3 1
A column() 0 9 1
A getSql() 0 3 1
A persistent() 0 5 1
A query() 0 6 1
A setEmulate() 0 4 1
A __wakeup() 0 3 1
A __sleep() 0 9 1
A getPDO() 0 3 1
A exec() 0 3 1
A getConfig() 0 3 1
B connect() 0 35 7
A execute() 0 35 3
A getEmulate() 0 3 1
A bindValues() 0 13 6
A getDriver() 0 3 1
A setLogger() 0 5 1
A prepare() 0 30 3
A getValues() 0 3 1

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\QueryPrepareException;
56
use Platine\Database\Exception\TransactionException;
57
use Platine\Logger\Logger;
58
use Platine\Logger\LoggerInterface;
59
60
/**
61
 * Class Connection
62
 * @package Platine\Database
63
 */
64
class Connection
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 Configuration
99
     */
100
    protected Configuration $config;
101
102
    /**
103
     * The connection parameters
104
     * @var array<int|string, mixed>
105
     */
106
    protected array $params = [];
107
108
    /**
109
     * The logger interface test
110
     * @var LoggerInterface
111
     */
112
    protected LoggerInterface $logger;
113
114
    /**
115
     * The last execution SQL query
116
     * @var string
117
     */
118
    protected string $sql = '';
119
120
    /**
121
     * The last execution query parameters values
122
     * @var array<mixed>
123
     */
124
    protected array $values = [];
125
126
    /**
127
     * Emulate instead of execute the query
128
     * @var bool
129
     */
130
    protected bool $emulate = false;
131
132
    /**
133
     * Connection constructor.
134
     * @param Configuration $config
135
     * @param LoggerInterface|null $logger
136
     * @throws ConnectionException
137
     */
138
    public function __construct(
139
        Configuration $config,
140
        ?LoggerInterface $logger = null
141
    ) {
142
        $this->config = $config;
143
144
        $this->logger = $logger ?? new Logger();
145
        $this->logger->setChannel('db');
146
147
        $driverClass = $this->config->getDriverClassName();
148
        $this->driver = new $driverClass($this);
149
150
        $this->schema = new Schema($this);
151
152
        $this->connect();
153
    }
154
155
    /**
156
     * Connect to the database
157
     * @return void
158
     */
159
    public function connect(): void
160
    {
161
        $this->setConnectionParams();
162
163
        if ($this->config->isPersistent()) {
164
            $this->persistent(true);
165
        }
166
167
        $attr = $this->params;
168
169
        if (empty($attr)) {
170
            throw new InvalidArgumentException('Invalid database options supplied');
171
        }
172
173
        $driver = $attr['driver'];
174
        unset($attr['driver']);
175
176
        $params = [];
177
        foreach ($attr as $key => $value) {
178
            $params[] = is_int($key) ? $value : $key . '=' . $value;
179
        }
180
181
        $dsn = $driver . ':' . implode(';', $params);
182
        if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv'])) {
183
            $charset = $this->config->getCharset();
184
            $this->config->addCommand('SET NAMES "' . $charset . '"' . (
185
                    $this->config->getDriverName() === 'mysql'
186
                            ? ' COLLATE "' . $this->config->getCollation() . '"'
187
                            : ''
188
                    ));
189
        }
190
191
        $this->dsn = $dsn;
192
193
        $this->createPDO();
194
    }
195
196
    /**
197
     *
198
     * @param LoggerInterface $logger
199
     * @return $this
200
     */
201
    public function setLogger(LoggerInterface $logger): self
202
    {
203
        $this->logger = $logger;
204
205
        return $this;
206
    }
207
208
    /**
209
     * Return the query execution logs
210
     * @return array<int, array<string, mixed>>
211
     */
212
    public function getLogs(): array
213
    {
214
        return $this->logs;
215
    }
216
217
    /**
218
     * Return the current connection parameters
219
     * @return array<int|string, mixed>
220
     */
221
    public function getParams(): array
222
    {
223
        return $this->params;
224
    }
225
226
    /**
227
     * Return the current connection configuration
228
     * @return Configuration
229
     */
230
    public function getConfig(): Configuration
231
    {
232
        return $this->config;
233
    }
234
235
    /**
236
     * Return the current driver instance
237
     * @return Driver
238
     */
239
    public function getDriver(): Driver
240
    {
241
        return $this->driver;
242
    }
243
244
    /**
245
     * Return the current Schema instance
246
     * @return Schema
247
     */
248
    public function getSchema(): Schema
249
    {
250
        return $this->schema;
251
    }
252
253
    /**
254
     * Set connection to be persistent
255
     * @param bool $value
256
     * @return self
257
     */
258
    public function persistent(bool $value = true): self
259
    {
260
        $this->config->setOption(PDO::ATTR_PERSISTENT, $value);
261
262
        return $this;
263
    }
264
265
    /**
266
     * @return string
267
     */
268
    public function getDsn(): string
269
    {
270
        return $this->dsn;
271
    }
272
273
    /**
274
     * Return the instance of the PDO
275
     * @return PDO
276
     */
277
    public function getPDO(): PDO
278
    {
279
        return $this->pdo;
280
    }
281
282
    /**
283
     * Execute the SQL query and return the result
284
     * @param string $sql
285
     * @param array<int, mixed> $params the query parameters
286
     * @return ResultSet
287
     * @throws QueryException
288
     */
289
    public function query(string $sql, array $params = []): ResultSet
290
    {
291
        $prepared = $this->prepare($sql, $params);
292
        $this->execute($prepared);
293
294
        return new ResultSet($prepared['statement']);
295
    }
296
297
    /**
298
     * Direct execute the SQL query
299
     * @param string $sql
300
     * @param array<int, mixed> $params the query parameters
301
     * @return bool
302
     * @throws QueryException
303
     */
304
    public function exec(string $sql, array $params = []): bool
305
    {
306
        return $this->execute($this->prepare($sql, $params));
307
    }
308
309
    /**
310
     *  Execute the SQL query and return the number
311
     * of affected rows
312
     * @param string $sql
313
     * @param array<int, mixed> $params the query parameters
314
     * @return int
315
     * @throws QueryException
316
     */
317
    public function count(string $sql, array $params = []): int
318
    {
319
        $prepared = $this->prepare($sql, $params);
320
        $this->execute($prepared);
321
322
        $result = $prepared['statement']->rowCount();
323
        $prepared['statement']->closeCursor();
324
325
        return $result;
326
    }
327
328
    /**
329
     *  Execute the SQL query and return the first column result
330
     * @param string $sql
331
     * @param array<int, mixed> $params the query parameters
332
     * @return mixed
333
     * @throws QueryException
334
     */
335
    public function column(string $sql, array $params = [])
336
    {
337
        $prepared = $this->prepare($sql, $params);
338
        $this->execute($prepared);
339
340
        $result = $prepared['statement']->fetchColumn();
341
        $prepared['statement']->closeCursor();
342
343
        return $result;
344
    }
345
346
    /**
347
     * @param callable $callback
348
     * @param mixed|null $that
349
     *
350
     * @return mixed
351
     *
352
     * @throws ConnectionException
353
     */
354
    public function transaction(
355
        callable $callback,
356
        $that = null
357
    ) {
358
        if ($that === null) {
359
            $that = $this;
360
        }
361
362
        if ($this->pdo->inTransaction()) {
363
            return $callback($that);
364
        }
365
366
        try {
367
            $this->pdo->beginTransaction();
368
            $result = $callback($that);
369
            $this->pdo->commit();
370
        } catch (PDOException $exception) {
371
            $this->pdo->rollBack();
372
            $this->logger->error('Database transaction error. Error message: {error}', [
373
                'error' => $exception->getMessage()
374
            ]);
375
            throw new TransactionException(
376
                $exception->getMessage(),
377
                (int) $exception->getCode(),
378
                $exception->getPrevious()
379
            );
380
        }
381
382
        return $result;
383
    }
384
385
    /**
386
     * {@inheritdoc}
387
     */
388
    public function __sleep()
389
    {
390
        return [
391
            'dsn',
392
            'driver',
393
            'schema',
394
            'config',
395
            'params',
396
            'logger',
397
        ];
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403
    public function __wakeup()
404
    {
405
        $this->createPDO();
406
    }
407
408
    /**
409
     * Return the last SQL query
410
     * @return string
411
     */
412
    public function getSql(): string
413
    {
414
        return $this->sql;
415
    }
416
417
    /**
418
     * Return the last query parameters values
419
     * @return array<mixed>
420
     */
421
    public function getValues(): array
422
    {
423
        return $this->values;
424
    }
425
426
    /**
427
     * Return the emulation status
428
     * @return bool
429
     */
430
    public function getEmulate(): bool
431
    {
432
        return $this->emulate;
433
    }
434
435
    /**
436
     * Set the emulation status
437
     * @param bool $emulate
438
     * @return $this
439
     */
440
    public function setEmulate(bool $emulate): self
441
    {
442
        $this->emulate = $emulate;
443
        return $this;
444
    }
445
446
447
    /**
448
     * Create PDO connection
449
     * @return void
450
     * @throws ConnectionException
451
     */
452
    protected function createPDO(): void
453
    {
454
        try {
455
            $this->pdo = new PDO(
456
                $this->dsn,
457
                $this->config->getUsername(),
458
                $this->config->getPassword(),
459
                $this->config->getOptions()
460
            );
461
462
            foreach ($this->config->getCommands() as $command) {
463
                $this->pdo->exec($command);
464
            }
465
        } catch (PDOException $exception) {
466
            $this->logger->emergency('Can not connect to database. Error message: {error}', [
467
                'error' => $exception->getMessage()
468
            ]);
469
470
            throw new ConnectionException(
471
                'Can not connect to database',
472
                (int) $exception->getCode(),
473
                $exception->getPrevious()
474
            );
475
        }
476
    }
477
478
     /**
479
     * Change the query parameters placeholder with the value
480
     * @param string $query
481
     * @param array<int, mixed> $params
482
     * @return string
483
     */
484
    protected function replaceParameters(string $query, array $params): string
485
    {
486
        $driver = $this->driver;
487
488
        return (string) preg_replace_callback(
489
            '/\?/',
490
            function () use ($driver, &$params) {
491
                $param = array_shift($params);
492
493
                $value = is_object($param) ? get_class($param) : $param;
494
                if (is_int($value) || is_float($value)) {
495
                    return $value;
496
                }
497
                if ($value === null) {
498
                    return 'NULL';
499
                }
500
                if (is_bool($value)) {
501
                    return $value ? 'TRUE' : 'FALSE';
502
                }
503
                return $driver->quote($value);
504
            },
505
            $query
506
        );
507
    }
508
509
    /**
510
     * Prepare the query
511
     * @param string $query
512
     * @param array<mixed> $params
513
     * @return array<string, mixed>
514
     * @throws QueryException
515
     */
516
    protected function prepare(string $query, array $params): array
517
    {
518
        $this->sql = $query;
519
        $this->values = $params;
520
        if ($this->emulate) {
521
            return [
522
                'statement' => new ConnectionStatement(),
523
                'query' => $query,
524
                'params' => $params
525
            ];
526
        }
527
528
        try {
529
            $statement = $this->pdo->prepare($query);
530
        } catch (PDOException $exception) {
531
            $this->logger->error('Error when prepare query [{query}]. Error message: {error}', [
532
                'error' => $exception->getMessage(),
533
                'query' => $query
534
            ]);
535
            throw new QueryPrepareException(
536
                $exception->getMessage() . ' [' . $query . ']',
537
                (int) $exception->getCode(),
538
                $exception->getPrevious()
539
            );
540
        }
541
542
        return [
543
            'statement' => $statement,
544
            'query' => $query,
545
            'params' => $params
546
        ];
547
    }
548
549
    /**
550
     * Execute the prepared query
551
     * @param array<string, mixed> $prepared
552
     * @return bool the status of the execution
553
     * @throws QueryException
554
     */
555
    protected function execute(array $prepared): bool
556
    {
557
        $sql = $this->replaceParameters($prepared['query'], $prepared['params']);
558
        $sqlLog = [
559
            'query' => $prepared['query'],
560
            'parameters' => implode(', ', $prepared['params'])
561
        ];
562
563
        try {
564
            if ($prepared['params']) {
565
                $this->bindValues($prepared['statement'], $prepared['params']);
566
            }
567
            $start = microtime(true);
568
            $result = $prepared['statement']->execute();
569
            $sqlLog['time'] = number_format(microtime(true) - $start, 6);
570
571
            $this->logs[] = $sqlLog;
572
573
            $this->logger->info(
574
                'Execute Query: [{query}], parameters: [{parameters}], time: [{time}]',
575
                $sqlLog
576
            );
577
        } catch (PDOException $exception) {
578
            $this->logger->error('Error when execute query [{sql}]. Error message: {error}', [
579
                'error' => $exception->getMessage(),
580
                'sql' => $sql
581
            ]);
582
            throw new QueryException(
583
                $exception->getMessage() . ' [' . $sql . ']',
584
                (int) $exception->getCode(),
585
                $exception->getPrevious()
586
            );
587
        }
588
589
        return $result;
590
    }
591
592
    /**
593
     * Bind the parameters values
594
     * @param PDOStatement $statement
595
     * @param array<int, mixed> $values
596
     */
597
    protected function bindValues(PDOStatement $statement, array $values): void
598
    {
599
        foreach ($values as $key => $value) {
600
            $param = PDO::PARAM_STR;
601
            if (is_null($value)) {
602
                $param = PDO::PARAM_NULL;
603
            } elseif (is_int($value) || is_float($value)) {
604
                $param = PDO::PARAM_INT;
605
            } elseif (is_bool($value)) {
606
                $param = PDO::PARAM_BOOL;
607
            }
608
609
            $statement->bindValue($key + 1, $value, $param);
610
        }
611
    }
612
613
    /**
614
     * Set the PDO connection parameters to use
615
     * @return void
616
     */
617
    protected function setConnectionParams(): void
618
    {
619
        $port = $this->config->getPort();
620
        $database = $this->config->getDatabase();
621
        $hostname = $this->config->getHostname();
622
        $attr = [];
623
624
        $driverName = $this->config->getDriverName();
625
        switch ($driverName) {
626
            case 'mysql':
627
            case 'pgsql':
628
                $attr = [
629
                    'driver' => $driverName,
630
                    'dbname' => $database,
631
                    'host' => $hostname,
632
                ];
633
634
                if ($port > 0) {
635
                    $attr['port'] = $port;
636
                }
637
638
                if ($driverName === 'mysql') {
639
                    //Make MySQL using standard quoted identifier
640
                    $this->config->addCommand('SET SQL_MODE=ANSI_QUOTES');
641
                    $this->config->addCommand('SET CHARACTER SET "' . $this->config->getCharset() . '"');
642
643
                    $socket = $this->config->getSocket();
644
                    if (!empty($socket)) {
645
                        $attr['unix_socket'] = $socket;
646
                    }
647
                }
648
                break;
649
            case 'sqlsrv':
650
                //Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting
651
                $this->config->addCommand('SET QUOTED_IDENTIFIER ON');
652
653
                //Make ANSI_NULLS is ON for NULL value
654
                $this->config->addCommand('SET ANSI_NULLS ON');
655
656
                $attr = [
657
                    'driver' => 'sqlsrv',
658
                    'Server' => $hostname
659
                        . ($port > 0 ? ':' . $port : ''),
660
                    'Database' => $database
661
                ];
662
663
                $appName = $this->config->getAppname();
664
                if (!empty($appName)) {
665
                    $attr['APP'] = $appName;
666
                }
667
668
                $attributes = [
669
                    'ApplicationIntent',
670
                    'AttachDBFileName',
671
                    'Authentication',
672
                    'ColumnEncryption',
673
                    'ConnectionPooling',
674
                    'Encrypt',
675
                    'Failover_Partner',
676
                    'KeyStoreAuthentication',
677
                    'KeyStorePrincipalId',
678
                    'KeyStoreSecret',
679
                    'LoginTimeout',
680
                    'MultipleActiveResultSets',
681
                    'MultiSubnetFailover',
682
                    'Scrollable',
683
                    'TraceFile',
684
                    'TraceOn',
685
                    'TransactionIsolation',
686
                    'TransparentNetworkIPResolution',
687
                    'TrustServerCertificate',
688
                    'WSID',
689
                ];
690
691
                foreach ($attributes as $attribute) {
692
                    $str = preg_replace(
693
                        ['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'],
694
                        '$1_$2',
695
                        $attribute
696
                    );
697
698
                    if (is_string($str)) {
699
                        $keyname = strtolower($str);
700
701
                        if ($this->config->hasAttribute($keyname)) {
702
                            $attr[$attribute] = $this->config->getAttribute($keyname);
703
                        }
704
                    }
705
                }
706
                break;
707
            case 'oci':
708
            case 'oracle':
709
                $attr = [
710
                    'driver' => 'oci',
711
                    'dbname' => '//' . $hostname
712
                    . ($port > 0 ? ':' . $port : ':1521') . '/' . $database
713
                ];
714
715
                $attr['charset'] = $this->config->getCharset();
716
                break;
717
            case 'sqlite':
718
                $attr = [
719
                    'driver' => 'sqlite',
720
                    $database
721
                ];
722
                break;
723
        }
724
725
        $this->params = $attr;
726
    }
727
}
728