Passed
Push — develop ( d2d625...405251 )
by nguereza
03:36
created

Connection::connect()   B

Complexity

Conditions 9
Paths 86

Size

Total Lines 55
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 35
nc 86
nop 0
dl 0
loc 55
rs 8.0555
c 0
b 0
f 0

How to fix   Long Method   

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:

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