Connection::setEmulate()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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