Passed
Push — develop ( bc0387...6879d5 )
by nguereza
03:17
created

Connection::execute()   A

Complexity

Conditions 3
Paths 12

Size

Total Lines 34
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 34
rs 9.552
cc 3
nc 12
nop 1
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;
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
     * @var Logger|null
125
     */
126
    protected ?Logger $logger = null;
127
128
    /**
129
     * Connection constructor.
130
     * @param array $config
131
     * @param Logger $logger
132
     * @throws ConnectionException
133
     */
134
    public function __construct(array $config = [], ?Logger $logger = null)
135
    {
136
        $this->logger = $logger ? $logger : new Logger(new NullLogger());
137
        
138
        $defaultConfig = [
139
            'driver' => 'mysql',
140
            'charset' => 'UTF8', //only for some drivers, 
141
            'appname' => '', //only for MSSQL, DBLIB, 
142
            'hostname' => 'localhost',
143
            'username' => '',
144
            'password' => '',
145
            'port' => null,
146
            'database' => '',
147
            'collation' => 'utf8_general_ci', //only for MySQL
148
            'socket' => '', //only for MySQL
149
            'options' => [],
150
            'commands' => [],
151
        ];
152
        
153
        $dbConfig = array_merge($defaultConfig, $config);
154
        
155
        if (is_string($dbConfig['driver'])) {
156
            $this->driverName = strtolower($dbConfig['driver']);
157
        }
158
159
        $options = $this->options;
160
        if (is_array($dbConfig['options'])) {
161
            $options = array_merge($options, $dbConfig['options']);
162
        }
163
164
        $commands = $this->commands;
165
        if (is_array($dbConfig['commands'])) {
166
            $commands = array_merge($commands, $dbConfig['commands']);
167
        }
168
169
        $port = null;
170
        $attr = [];
171
172
        if (is_int($dbConfig['port'])) {
173
            $port = $dbConfig['port'];
174
        }
175
        
176
        $driverName = $this->driverName;
177
        switch ($driverName) {
178
            case 'mysql':
179
            case 'pgsql':
180
                $attr = [
181
                    'driver' => $driverName,
182
                    'dbname' => $dbConfig['database'],
183
                    'host' => $dbConfig['hostname'],
184
                ];
185
                
186
                if ($port > 0) {
187
                    $attr['port'] = $port;
188
                }
189
                
190
                if ($driverName === 'mysql'){
191
                    //Make MySQL using standard quoted identifier
192
                    $commands[] = 'SET SQL_MODE=ANSI_QUOTES';
193
                    
194
                    if (!empty($dbConfig['socket'])) {
195
                        $attr['unix_socket'] = $dbConfig['socket'];
196
                        
197
                        unset($attr['host']);
198
                        unset($attr['port']);
199
                    } 
200
                }
201
                break;
202
            case 'sqlsrv':
203
                //Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting
204
                $commands[] = 'SET QUOTED_IDENTIFIER ON';
205
206
                //Make ANSI_NULLS is ON for NULL value
207
                $commands[] = 'SET ANSI_NULLS ON';
208
                
209
                $attr = [
210
                    'driver' => 'sqlsrv',
211
                    'Server' => $dbConfig['hostname'] 
212
                        . ($port > 0 ? ':' . $port : ''),
213
                    'Database' => $dbConfig['database']
214
                ];
215
216
                if (!empty($dbConfig['appname'])) {
217
                    $attr['APP'] = $dbConfig['appname'];
218
                }
219
220
                $attributes = [
221
                    'ApplicationIntent',
222
                    'AttachDBFileName',
223
                    'Authentication',
224
                    'ColumnEncryption',
225
                    'ConnectionPooling',
226
                    'Encrypt',
227
                    'Failover_Partner',
228
                    'KeyStoreAuthentication',
229
                    'KeyStorePrincipalId',
230
                    'KeyStoreSecret',
231
                    'LoginTimeout',
232
                    'MultipleActiveResultSets',
233
                    'MultiSubnetFailover',
234
                    'Scrollable',
235
                    'TraceFile',
236
                    'TraceOn',
237
                    'TransactionIsolation',
238
                    'TransparentNetworkIPResolution',
239
                    'TrustServerCertificate',
240
                    'WSID',
241
                ];
242
243
                foreach ($attributes as $attribute) {
244
                    $keyname = strtolower(preg_replace(
245
                        ['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'],
246
                        '$1_$2',
247
                        $attribute
248
                    ));
249
250
                    if (isset($dbConfig[$keyname])) {
251
                        $attr[$attribute] = $dbConfig[$keyname];
252
                    }
253
                }
254
                break;
255
            case 'oci':
256
            case 'oracle':
257
                $database = $dbConfig['database'];
258
                $attr = [
259
                    'driver' => 'oci',
260
                    'dbname' => '//' . $dbConfig['hostname']
261
                    . ($port > 0 ? ':' . $port : ':1521') . '/' . $database
262
                ];
263
                
264
                $attr['charset'] = $dbConfig['charset'];
265
                break;
266
            case 'sqlite':
267
                $attr = [
268
                    'driver' => 'sqlite',
269
                    $dbConfig['database']
270
                ];
271
                break;
272
        }
273
        
274
275
        if (empty($attr)) {
276
            throw new InvalidArgumentException('Invalid database options supplied');
277
        }
278
279
        $driver = $attr['driver'];
280
        if (!in_array($driver, PDO::getAvailableDrivers())) {
281
            throw new InvalidArgumentException(sprintf(
282
                'Invalid database driver [%s], must be one of [%s]',
283
                $driver,
284
                implode(', ', PDO::getAvailableDrivers())
285
            ));
286
        }
287
288
        unset($attr['driver']);
289
290
        $params = [];
291
        foreach ($attr as $key => $value) {
292
            $params[] = is_int($key) ? $value : $key . '=' . $value;
293
        }
294
295
        $dsn = $driver . ':' . implode(';', $params);
296
        if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv'])) {
297
            $commands[] = 'SET NAMES "' . $dbConfig['charset'] . '"' . (
298
                    $this->driverName === 'mysql' 
299
                            ? ' COLLATE "' . $dbConfig['collation'] . '"' 
300
                            : ''
301
                    );
302
        }
303
304
        $this->dsn = $dsn;
305
        $this->commands = $commands;
306
        $this->options = $options;
307
308
        try {
309
            $this->pdo = new PDO(
310
                $dsn,
311
                isset($config['username']) ? $config['username'] : '',
312
                isset($config['password']) ? $config['password'] : '',
313
                $options
314
            );
315
316
            foreach ($commands as $command) {
317
                $this->pdo->exec($command);
318
            }
319
        } catch (PDOException $exception) {
320
            $this->logger->emergency('Can not connect to database. Error message: {error}', [
321
                'exception' => $exception,
322
                'error' => $exception->getMessage()
323
            ]);
324
            throw new ConnectionException($exception->getMessage());
325
        }
326
    }
327
328
    /**
329
     * @param Logger $logger
330
     * @return self
331
     */
332
    public function setLogger(Logger $logger): self
333
    {
334
        $this->logger = $logger;
335
336
        return $this;
337
    }
338
339
    /**
340
     * Return the current driver instance
341
     * @return Driver
342
     */
343
    public function getDriver(): Driver
344
    {
345
        if ($this->driver === null) {
346
            $this->setDefaultDriver();
347
        }
348
        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...
349
    }
350
351
    /**
352
     * @param Driver $driver
353
     * @return self
354
     */
355
    public function setDriver(Driver $driver): self
356
    {
357
        $this->driver = $driver;
358
359
        return $this;
360
    }
361
362
    /**
363
     * Return the current Schema instance
364
     * @return Schema
365
     */
366
    public function getSchema(): Schema
367
    {
368
        if ($this->schema === null) {
369
            $this->schema = new Schema($this);
370
        }
371
        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...
372
    }
373
374
    /**
375
     * @param Schema $schema
376
     * @return self
377
     */
378
    public function setSchema(Schema $schema): self
379
    {
380
        $this->schema = $schema;
381
382
        return $this;
383
    }
384
385
    /**
386
     * Set PDO Connection options
387
     * @param array $options
388
     * @return self
389
     */
390
    public function options(array $options): self
391
    {
392
        foreach ($options as $name => $value) {
393
            $this->option($name, $value);
394
        }
395
396
        return $this;
397
    }
398
399
    /**
400
     * Set the PDO connection option
401
     * @param string $name
402
     * @param mixed $value
403
     * @return self
404
     */
405
    public function option(string $name, $value): self
406
    {
407
        $this->options[$name] = $value;
408
409
        return $this;
410
    }
411
412
    /**
413
     * Set connection to be persistent
414
     * @param bool $value
415
     * @return self
416
     */
417
    public function persistent(bool $value = true): self
418
    {
419
        $this->pdo->setAttribute(PDO::ATTR_PERSISTENT, $value);
0 ignored issues
show
Bug introduced by
The method setAttribute() 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

419
        $this->pdo->/** @scrutinizer ignore-call */ 
420
                    setAttribute(PDO::ATTR_PERSISTENT, $value);

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...
420
421
        return $this;
422
    }
423
424
    /**
425
     * Set the date format to use for the current driver
426
     * @param string $format
427
     * @return self
428
     */
429
    public function setDateFormat(string $format): self
430
    {
431
        $this->driverOptions['dateFormat'] = $format;
432
433
        return $this;
434
    }
435
436
    /**
437
     * Set the quote identifier to use for the current driver
438
     * @param string $identifier
439
     * @return self
440
     */
441
    public function setQuoteIdentifier(string $identifier): self
442
    {
443
        $this->driverOptions['identifier'] = $identifier;
444
445
        return $this;
446
    }
447
448
    /**
449
     * @return string
450
     */
451
    public function getDsn(): string
452
    {
453
        return $this->dsn;
454
    }
455
456
    /**
457
     * Return the name of the connection driver
458
     * @return string
459
     */
460
    public function getDriverName(): string
461
    {
462
        return $this->driverName;
463
    }
464
465
    /**
466
     * Return the instance of the PDO
467
     * @return PDO
468
     */
469
    public function getPDO(): PDO
470
    {
471
        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...
472
    }
473
474
    /**
475
     * CLose the connection
476
     */
477
    public function disconnect(): void
478
    {
479
        $this->pdo = null;
480
    }
481
482
    /**
483
     * Execute the SQL query and return the result
484
     * @param string $sql
485
     * @param array $params the query parameters
486
     * @return ResultSet
487
     * @throws QueryException
488
     */
489
    public function query(string $sql, array $params = []): ResultSet
490
    {
491
        $prepared = $this->prepare($sql, $params);
492
        $this->execute($prepared);
493
494
        return new ResultSet($prepared['statement']);
495
    }
496
497
    /**
498
     * Direct execute the SQL query
499
     * @param string $sql
500
     * @param array $params the query parameters
501
     * @return mixed
502
     * @throws QueryException
503
     */
504
    public function exec(string $sql, array $params = [])
505
    {
506
        return $this->execute($this->prepare($sql, $params));
507
    }
508
509
    /**
510
     *  Execute the SQL query and return the number
511
     * of affected rows
512
     * @param string $sql
513
     * @param array $params the query parameters
514
     * @return int
515
     * @throws QueryException
516
     */
517
    public function count(string $sql, array $params = []): int
518
    {
519
        $prepared = $this->prepare($sql, $params);
520
        $this->execute($prepared);
521
522
        $result = $prepared['statement']->rowCount();
523
        $prepared['statement']->closeCursor();
524
525
        return $result;
526
    }
527
528
    /**
529
     *  Execute the SQL query and return the first column result
530
     * @param string $sql
531
     * @param array $params the query parameters
532
     * @return mixed
533
     * @throws QueryException
534
     */
535
    public function column(string $sql, array $params = [])
536
    {
537
        $prepared = $this->prepare($sql, $params);
538
        $this->execute($prepared);
539
540
        $result = $prepared['statement']->fetchColumn();
541
        $prepared['statement']->closeCursor();
542
543
        return $result;
544
    }
545
546
    /**
547
     * @param callable $callback
548
     * @param mixed|null $that
549
     *
550
     * @return mixed
551
     *
552
     * @throws ConnectionException
553
     */
554
    public function transaction(
555
        callable $callback,
556
        $that = null
557
    ) {
558
        if ($that === null) {
559
            $that = $this;
560
        }
561
562
        if ($this->pdo->inTransaction()) {
563
            return $callback($that);
564
        }
565
566
        try {
567
            $this->pdo->beginTransaction();
568
            $result = $callback($that);
569
            $this->pdo->commit();
570
        } catch (PDOException $exception) {
571
            $this->pdo->rollBack();
572
            $this->logger->error('Database transaction error. Error message: {error}', [
0 ignored issues
show
Bug introduced by
The method error() 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

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

736
        $this->driver->/** @scrutinizer ignore-call */ 
737
                       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...
737
    }
738
}
739