Passed
Push — develop ( 632372...fe1a17 )
by nguereza
02:36
created

Connection::connect()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 21
rs 9.7998
cc 3
nc 5
nop 0
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\Driver\MySQL;
54
use Platine\Database\Driver\Oracle;
55
use Platine\Database\Driver\PostgreSQL;
56
use Platine\Database\Driver\SQLite;
57
use Platine\Database\Driver\SQLServer;
58
use Platine\Database\Exception\ConnectionException;
59
use Platine\Database\Exception\QueryException;
60
use Platine\Logger\Logger;
61
use Platine\Logger\NullLogger;
62
63
/**
64
 * Class Connection
65
 * @package Platine\Database
66
 */
67
class Connection
68
{
69
70
    /**
71
     * The PDO instance
72
     * @var PDO|null
73
     */
74
    protected ?PDO $pdo = null;
75
76
    /**
77
     * The database driver name to use
78
     * @var string
79
     */
80
    protected string $driverName = '';
81
82
    /**
83
     * The PDO dsn
84
     * @var string
85
     */
86
    protected string $dsn = '';
87
88
    /**
89
     * The PDO connection options
90
     * @var array
91
     */
92
    protected array $options = [
93
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
94
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
95
        PDO::ATTR_STRINGIFY_FETCHES => false,
96
        PDO::ATTR_EMULATE_PREPARES => false,
97
    ];
98
99
    /**
100
     * The list of SQL command to execute after connection
101
     * @var array
102
     */
103
    protected array $commands = [];
104
105
    /**
106
     * The driver to use
107
     * @var Driver|null
108
     */
109
    protected ?Driver $driver = null;
110
111
    /**
112
     * The Schema instance to use
113
     * @var Schema|null
114
     */
115
    protected ?Schema $schema = null;
116
117
    /**
118
     * The driver options
119
     * @var array
120
     */
121
    protected array $driverOptions = [];
122
    
123
    /**
124
     * The connection configurations
125
     * @var array
126
     */
127
    protected array $config = [];
128
    
129
    /**
130
     * The connection parameters
131
     * @var array
132
     */
133
    protected array $params = [];
134
135
    /**
136
     * @var Logger|null
137
     */
138
    protected ?Logger $logger = null;
139
140
    /**
141
     * Connection constructor.
142
     * @param array $config
143
     * @param Logger $logger
144
     * @throws ConnectionException
145
     */
146
    public function __construct(array $config = [], ?Logger $logger = null)
147
    {
148
        $this->logger = $logger ? $logger : new Logger(new NullLogger());
149
        
150
        $defaultConfig = [
151
            'driver' => 'mysql',
152
            'charset' => 'UTF8', //only for some drivers, 
153
            'appname' => '', //only for MSSQL, DBLIB, 
154
            'hostname' => 'localhost',
155
            'username' => '',
156
            'password' => '',
157
            'port' => null,
158
            'database' => '',
159
            'auto_connect' => false,
160
            'collation' => 'utf8_general_ci', //only for MySQL
161
            'socket' => '', //only for MySQL
162
            'options' => [],
163
            'commands' => [],
164
        ];
165
        
166
        $dbConfig = array_merge($defaultConfig, $config);
167
        $this->config = $dbConfig;
168
        
169
        $this->driverName = strtolower((string) $dbConfig['driver']);
170
171
        $options = array_replace($this->options, (array) $dbConfig['options']);
172
        
173
        $commands = array_merge($this->commands, $dbConfig['commands']);
174
        
175
        $port = null;
176
        $attr = [];
177
178
        if (is_int($dbConfig['port'])) {
179
            $port = $dbConfig['port'];
180
        }
181
        
182
        $driverName = $this->driverName;
183
        switch ($driverName) {
184
            case 'mysql':
185
            case 'pgsql':
186
                $attr = [
187
                    'driver' => $driverName,
188
                    'dbname' => $dbConfig['database'],
189
                    'host' => $dbConfig['hostname'],
190
                ];
191
                
192
                if ($port > 0) {
193
                    $attr['port'] = $port;
194
                }
195
                
196
                if ($driverName === 'mysql'){
197
                    //Make MySQL using standard quoted identifier
198
                    $commands[] = 'SET SQL_MODE=ANSI_QUOTES';
199
                    
200
                    if (!empty($dbConfig['socket'])) {
201
                        $attr['unix_socket'] = $dbConfig['socket'];
202
                        
203
                        unset($attr['host']);
204
                        unset($attr['port']);
205
                    } 
206
                }
207
                break;
208
            case 'sqlsrv':
209
                //Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting
210
                $commands[] = 'SET QUOTED_IDENTIFIER ON';
211
212
                //Make ANSI_NULLS is ON for NULL value
213
                $commands[] = 'SET ANSI_NULLS ON';
214
                
215
                $attr = [
216
                    'driver' => 'sqlsrv',
217
                    'Server' => $dbConfig['hostname'] 
218
                        . ($port > 0 ? ':' . $port : ''),
219
                    'Database' => $dbConfig['database']
220
                ];
221
222
                if (!empty($dbConfig['appname'])) {
223
                    $attr['APP'] = $dbConfig['appname'];
224
                }
225
226
                $attributes = [
227
                    'ApplicationIntent',
228
                    'AttachDBFileName',
229
                    'Authentication',
230
                    'ColumnEncryption',
231
                    'ConnectionPooling',
232
                    'Encrypt',
233
                    'Failover_Partner',
234
                    'KeyStoreAuthentication',
235
                    'KeyStorePrincipalId',
236
                    'KeyStoreSecret',
237
                    'LoginTimeout',
238
                    'MultipleActiveResultSets',
239
                    'MultiSubnetFailover',
240
                    'Scrollable',
241
                    'TraceFile',
242
                    'TraceOn',
243
                    'TransactionIsolation',
244
                    'TransparentNetworkIPResolution',
245
                    'TrustServerCertificate',
246
                    'WSID',
247
                ];
248
249
                foreach ($attributes as $attribute) {
250
                    $keyname = strtolower(preg_replace(
251
                        ['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'],
252
                        '$1_$2',
253
                        $attribute
254
                    ));
255
256
                    if (isset($dbConfig[$keyname])) {
257
                        $attr[$attribute] = $dbConfig[$keyname];
258
                    }
259
                }
260
                break;
261
            case 'oci':
262
            case 'oracle':
263
                $database = $dbConfig['database'];
264
                $attr = [
265
                    'driver' => 'oci',
266
                    'dbname' => '//' . $dbConfig['hostname']
267
                    . ($port > 0 ? ':' . $port : ':1521') . '/' . $database
268
                ];
269
                
270
                $attr['charset'] = $dbConfig['charset'];
271
                break;
272
            case 'sqlite':
273
                $attr = [
274
                    'driver' => 'sqlite',
275
                    $dbConfig['database']
276
                ];
277
                break;
278
        }
279
        
280
281
        if (empty($attr)) {
282
            throw new InvalidArgumentException('Invalid database options supplied');
283
        }
284
285
        $this->params = $attr;
286
        
287
        $driver = $attr['driver'];
288
        if (!in_array($driver, PDO::getAvailableDrivers())) {
289
            throw new InvalidArgumentException(sprintf(
290
                'Invalid database driver [%s], must be one of [%s]',
291
                $driver,
292
                implode(', ', PDO::getAvailableDrivers())
293
            ));
294
        }
295
296
        unset($attr['driver']);
297
298
        $params = [];
299
        foreach ($attr as $key => $value) {
300
            $params[] = is_int($key) ? $value : $key . '=' . $value;
301
        }
302
303
        $dsn = $driver . ':' . implode(';', $params);
304
        if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv'])) {
305
            $commands[] = 'SET NAMES "' . $dbConfig['charset'] . '"' . (
306
                    $this->driverName === 'mysql' 
307
                            ? ' COLLATE "' . $dbConfig['collation'] . '"' 
308
                            : ''
309
                    );
310
        }
311
312
        $this->dsn = $dsn;
313
        $this->commands = $commands;
314
        $this->options = $options;
315
        
316
        if($dbConfig['auto_connect'] === true){
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after IF keyword; 0 found
Loading history...
317
            $this->connect();
318
        }
319
        
320
    }
0 ignored issues
show
Coding Style introduced by
Function closing brace must go on the next line following the body; found 1 blank lines before brace
Loading history...
321
    
322
    /**
323
     * Connect to the database
324
     * @return bool
325
     */
326
    public function connect():bool{
0 ignored issues
show
Coding Style introduced by
There must be a single space between the colon and type in a return type declaration
Loading history...
327
        try {
328
            $this->pdo = new PDO(
329
                $this->dsn,
330
                $this->config['username'],
331
                $this->config['password'],
332
                $this->options
333
            );
334
335
            foreach ($this->commands as $command) {
336
                $this->pdo->exec($command);
337
            }
338
        } catch (PDOException $exception) {
339
            $this->logger->emergency('Can not connect to database. Error message: {error}', [
0 ignored issues
show
Bug introduced by
The method emergency() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

339
            $this->logger->/** @scrutinizer ignore-call */ 
340
                           emergency('Can not connect to database. Error message: {error}', [

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
340
                'exception' => $exception,
341
                'error' => $exception->getMessage()
342
            ]);
343
            throw new ConnectionException($exception->getMessage());
344
        }
345
        
346
        return $this->pdo !== null;
347
    }
348
349
    /**
350
     * @param Logger $logger
351
     * @return self
352
     */
353
    public function setLogger(Logger $logger): self
354
    {
355
        $this->logger = $logger;
356
357
        return $this;
358
    }
359
360
    /**
361
     * Return the current driver instance
362
     * @return Driver
363
     */
364
    public function getDriver(): Driver
365
    {
366
        if ($this->driver === null) {
367
            $this->setDefaultDriver();
368
        }
369
        return $this->driver;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->driver could return the type null which is incompatible with the type-hinted return Platine\Database\Driver\Driver. Consider adding an additional type-check to rule them out.
Loading history...
370
    }
371
372
    /**
373
     * @param Driver $driver
374
     * @return self
375
     */
376
    public function setDriver(Driver $driver): self
377
    {
378
        $this->driver = $driver;
379
380
        return $this;
381
    }
382
383
    /**
384
     * Return the current Schema instance
385
     * @return Schema
386
     */
387
    public function getSchema(): Schema
388
    {
389
        if ($this->schema === null) {
390
            $this->schema = new Schema($this);
391
        }
392
        return $this->schema;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->schema could return the type null which is incompatible with the type-hinted return Platine\Database\Schema. Consider adding an additional type-check to rule them out.
Loading history...
393
    }
394
395
    /**
396
     * @param Schema $schema
397
     * @return self
398
     */
399
    public function setSchema(Schema $schema): self
400
    {
401
        $this->schema = $schema;
402
403
        return $this;
404
    }
405
406
    /**
407
     * Set PDO Connection options
408
     * @param array $options
409
     * @return self
410
     */
411
    public function options(array $options): self
412
    {
413
        foreach ($options as $name => $value) {
414
            $this->option($name, $value);
415
        }
416
417
        return $this;
418
    }
419
420
    /**
421
     * Set the PDO connection option
422
     * @param string $name
423
     * @param mixed $value
424
     * @return self
425
     */
426
    public function option(string $name, $value): self
427
    {
428
        $this->options[$name] = $value;
429
430
        return $this;
431
    }
432
433
    /**
434
     * Set connection to be persistent
435
     * @param bool $value
436
     * @return self
437
     */
438
    public function persistent(bool $value = true): self
439
    {
440
        $this->options[PDO::ATTR_PERSISTENT] = $value;
441
442
        return $this;
443
    }
444
445
    /**
446
     * Set the date format to use for the current driver
447
     * @param string $format
448
     * @return self
449
     */
450
    public function setDateFormat(string $format): self
451
    {
452
        $this->driverOptions['dateFormat'] = $format;
453
454
        return $this;
455
    }
456
457
    /**
458
     * Set the quote identifier to use for the current driver
459
     * @param string $identifier
460
     * @return self
461
     */
462
    public function setQuoteIdentifier(string $identifier): self
463
    {
464
        $this->driverOptions['identifier'] = $identifier;
465
466
        return $this;
467
    }
468
469
    /**
470
     * @return string
471
     */
472
    public function getDsn(): string
473
    {
474
        return $this->dsn;
475
    }
476
477
    /**
478
     * Return the name of the connection driver
479
     * @return string
480
     */
481
    public function getDriverName(): string
482
    {
483
        return $this->driverName;
484
    }
485
486
    /**
487
     * Return the instance of the PDO
488
     * @return PDO
489
     */
490
    public function getPDO(): PDO
491
    {
492
        if($this->pdo === null){
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after IF keyword; 0 found
Loading history...
493
            $this->connect();
494
        }
495
        return $this->pdo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->pdo could return the type null which is incompatible with the type-hinted return PDO. Consider adding an additional type-check to rule them out.
Loading history...
496
    }
497
498
    /**
499
     * CLose the connection
500
     */
501
    public function disconnect(): void
502
    {
503
        $this->pdo = null;
504
    }
505
506
    /**
507
     * Execute the SQL query and return the result
508
     * @param string $sql
509
     * @param array $params the query parameters
510
     * @return ResultSet
511
     * @throws QueryException
512
     */
513
    public function query(string $sql, array $params = []): ResultSet
514
    {
515
        $prepared = $this->prepare($sql, $params);
516
        $this->execute($prepared);
517
518
        return new ResultSet($prepared['statement']);
519
    }
520
521
    /**
522
     * Direct execute the SQL query
523
     * @param string $sql
524
     * @param array $params the query parameters
525
     * @return mixed
526
     * @throws QueryException
527
     */
528
    public function exec(string $sql, array $params = [])
529
    {
530
        return $this->execute($this->prepare($sql, $params));
531
    }
532
533
    /**
534
     *  Execute the SQL query and return the number
535
     * of affected rows
536
     * @param string $sql
537
     * @param array $params the query parameters
538
     * @return int
539
     * @throws QueryException
540
     */
541
    public function count(string $sql, array $params = []): int
542
    {
543
        $prepared = $this->prepare($sql, $params);
544
        $this->execute($prepared);
545
546
        $result = $prepared['statement']->rowCount();
547
        $prepared['statement']->closeCursor();
548
549
        return $result;
550
    }
551
552
    /**
553
     *  Execute the SQL query and return the first column result
554
     * @param string $sql
555
     * @param array $params the query parameters
556
     * @return mixed
557
     * @throws QueryException
558
     */
559
    public function column(string $sql, array $params = [])
560
    {
561
        $prepared = $this->prepare($sql, $params);
562
        $this->execute($prepared);
563
564
        $result = $prepared['statement']->fetchColumn();
565
        $prepared['statement']->closeCursor();
566
567
        return $result;
568
    }
569
570
    /**
571
     * @param callable $callback
572
     * @param mixed|null $that
573
     *
574
     * @return mixed
575
     *
576
     * @throws ConnectionException
577
     */
578
    public function transaction(
579
        callable $callback,
580
        $that = null
581
    ) {
582
        if ($that === null) {
583
            $that = $this;
584
        }
585
586
        if ($this->pdo->inTransaction()) {
0 ignored issues
show
Bug introduced by
The method inTransaction() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

586
        if ($this->pdo->/** @scrutinizer ignore-call */ inTransaction()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
587
            return $callback($that);
588
        }
589
590
        try {
591
            $this->pdo->beginTransaction();
592
            $result = $callback($that);
593
            $this->pdo->commit();
594
        } catch (PDOException $exception) {
595
            $this->pdo->rollBack();
596
            $this->logger->error('Database transaction error. Error message: {error}', [
597
                'exception' => $exception,
598
                'error' => $exception->getMessage()
599
            ]);
600
            throw new ConnectionException($exception->getMessage());
601
        }
602
603
        return $result;
604
    }
605
606
    /**
607
     * Change the query parameters placeholder with the value
608
     * @param string $query
609
     * @param array $params
610
     * @return string
611
     */
612
    protected function replaceParameters(string $query, array $params): string
613
    {
614
        $driver = $this->getDriver();
615
616
        return preg_replace_callback(
617
            '/\?/',
618
            function () use ($driver, &$params) {
619
                $param = array_shift($params);
620
                $param = is_object($param) ? get_class($param) : $param;
621
                if (is_int($param) || is_float($param)) {
622
                    return $param;
623
                }
624
                if ($param === null) {
625
                    return 'NULL';
626
                }
627
                if (is_bool($param)) {
628
                    return $param ? 'TRUE' : 'FALSE';
629
                }
630
                return $driver->quote($param);
631
            },
632
            $query
633
        );
634
    }
635
636
    /**
637
     * Prepare the query
638
     * @param string $query
639
     * @param array $params
640
     * @return array
641
     * @throws QueryException
642
     */
643
    protected function prepare(string $query, array $params): array
644
    {
645
        try {
646
            $statement = $this->pdo->prepare($query);
647
        } catch (PDOException $exception) {
648
            $sql = $this->replaceParameters($query, $params);
649
            $this->logger->error('Error when prepare query [{sql}]. Error message: {error}', [
650
                'exception' => $exception,
651
                'error' => $exception->getMessage(),
652
                'sql' => $sql
653
            ]);
654
            throw new QueryException(
655
                $exception->getMessage() . ' [' . $sql . ']',
656
                (int) $exception->getCode(),
657
                $exception->getPrevious()
658
            );
659
        }
660
661
        return [
662
            'statement' => $statement,
663
            'query' => $query,
664
            'params' => $params
665
        ];
666
    }
667
668
    /**
669
     * Execute the prepared query
670
     * @param array $prepared
671
     * @return bool the status of the execution
672
     * @throws QueryException
673
     */
674
    protected function execute(array $prepared): bool
675
    {
676
        $sql = $this->replaceParameters($prepared['query'], $prepared['params']);
677
        $sqlLog = [
678
            'query' => $prepared['query'],
679
            'parameters' => implode(', ', $prepared['params'])
680
        ];
681
682
        try {
683
            if ($prepared['params']) {
684
                $this->bindValues($prepared['statement'], $prepared['params']);
685
            }
686
            $start = microtime(true);
687
            $result = $prepared['statement']->execute();
688
            $sqlLog['time'] = number_format(microtime(true) - $start, 6);
689
690
            $this->logger->info(
691
                'Execute Query: [{query}], parameters: [{parameters}], time: [{time}]',
692
                $sqlLog
693
            );
694
        } catch (PDOException $exception) {
695
            $this->logger->error('Error when execute query [{sql}]. Error message: {error}', [
696
                'exception' => $exception,
697
                'error' => $exception->getMessage(),
698
                'sql' => $sql
699
            ]);
700
            throw new QueryException(
701
                $exception->getMessage() . ' [' . $sql . ']',
702
                (int) $exception->getCode(),
703
                $exception->getPrevious()
704
            );
705
        }
706
707
        return $result;
708
    }
709
710
    /**
711
     * Bind the parameters values
712
     * @param PDOStatement $statement
713
     * @param array $values
714
     */
715
    protected function bindValues(PDOStatement $statement, array $values): void
716
    {
717
        foreach ($values as $key => $value) {
718
            $param = PDO::PARAM_STR;
719
            if (is_null($value)) {
720
                $param = PDO::PARAM_NULL;
721
            } elseif (is_int($value) || is_float($value)) {
722
                $param = PDO::PARAM_INT;
723
            } elseif (is_bool($value)) {
724
                $param = PDO::PARAM_BOOL;
725
            }
726
727
            $statement->bindValue($key + 1, $value, $param);
728
        }
729
    }
730
731
    /**
732
     * Set the default driver instance using current driver name
733
     * @return void
734
     */
735
    protected function setDefaultDriver(): void
736
    {
737
        switch ($this->driverName) {
738
            case 'mysql':
739
                $this->driver = new MySQL($this);
740
                break;
741
            case 'pgsql':
742
                $this->driver = new PostgreSQL($this);
743
                break;
744
            case 'dblib':
745
            case 'mssql':
746
            case 'sqlsrv':
747
            case 'sybase':
748
                $this->driver = new SQLServer($this);
749
                break;
750
            case 'oci':
751
            case 'oracle':
752
                $this->driver = new Oracle($this);
753
                break;
754
            case 'sqlite':
755
                $this->driver = new SQLite($this);
756
                break;
757
            default:
758
                $this->driver = new Driver($this);
759
        }
760
        $this->driver->setOptions($this->driverOptions);
0 ignored issues
show
Bug introduced by
The method setOptions() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

760
        $this->driver->/** @scrutinizer ignore-call */ 
761
                       setOptions($this->driverOptions);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
761
    }
762
}
763