Passed
Push — develop ( 64f9e9...eb6247 )
by nguereza
02:49
created

Connection::startTransaction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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