Passed
Push — develop ( d74067...306891 )
by nguereza
02:18
created

Connection::createDriver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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