Passed
Push — develop ( afb3e4...0e9461 )
by nguereza
02:29
created

Connection::getDsn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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