Passed
Push — develop ( e333c2...b3554d )
by nguereza
02:25
created

Connection::__wakeup()   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
{
0 ignored issues
show
Coding Style introduced by
Opening brace must not be followed by a blank line
Loading history...
66
67
    /**
68
     * The PDO instance
69
     * @var PDO
70
     */
71
    protected PDO $pdo;
72
73
    /**
74
     * The PDO data source name
75
     * @var string
76
     */
77
    protected string $dsn = '';
78
79
    /**
80
     * The list of execution query logs
81
     * @var array<int, array<string, mixed>>
82
     */
83
    protected array $logs = [];
84
85
    /**
86
     * The driver to use
87
     * @var Driver
88
     */
89
    protected Driver $driver;
90
91
    /**
92
     * The Schema instance to use
93
     * @var Schema
94
     */
95
    protected Schema $schema;
96
97
    /**
98
     * The connection configuration
99
     * @var Configuration
100
     */
101
    protected Configuration $config;
102
103
    /**
104
     * The connection parameters
105
     * @var array<int|string, mixed>
106
     */
107
    protected array $params = [];
108
109
    /**
110
     * The logger interface test
111
     * @var LoggerInterface
112
     */
113
    protected LoggerInterface $logger;
114
115
    /**
116
     * Connection constructor.
117
     * @param Configuration $config
118
     * @param LoggerInterface|null $logger
119
     * @throws ConnectionException
120
     */
121
    public function __construct(
122
        Configuration $config,
123
        ?LoggerInterface $logger = null
124
    ) {
125
        $this->config = $config;
126
127
        $this->logger = $logger ?? new Logger();
128
        $this->logger->setChannel(__CLASS__);
129
130
        $driverClass = $this->config->getDriverClassName();
131
        $this->driver = new $driverClass($this);
132
133
        $this->schema = new Schema($this);
134
135
        $this->connect();
136
    }
137
138
    /**
139
     * Connect to the database
140
     * @return void
141
     */
142
    public function connect(): void
143
    {
144
        $this->setConnectionParams();
145
146
        if ($this->config->isPersistent()) {
147
            $this->persistent(true);
148
        }
149
150
        $attr = $this->params;
151
152
        if (empty($attr)) {
153
            throw new InvalidArgumentException('Invalid database options supplied');
154
        }
155
156
        $driver = $attr['driver'];
157
        unset($attr['driver']);
158
159
        $params = [];
160
        foreach ($attr as $key => $value) {
161
            $params[] = is_int($key) ? $value : $key . '=' . $value;
162
        }
163
164
        $dsn = $driver . ':' . implode(';', $params);
165
        if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv'])) {
166
            $charset = $this->config->getCharset();
167
            $this->config->addCommand('SET NAMES "' . $charset . '"' . (
168
                    $this->config->getDriverName() === 'mysql'
169
                            ? ' COLLATE "' . $this->config->getCollation() . '"'
170
                            : ''
171
                    ));
172
        }
173
174
        $this->dsn = $dsn;
175
176
        $this->createPDO();
177
    }
178
179
    /**
180
     * Create PDO connection
181
     * @return void
182
     * @throws ConnectionException
183
     */
184
    protected function createPDO(): void
185
    {
186
        try {
187
            $this->pdo = new PDO(
188
                $this->dsn,
189
                $this->config->getUsername(),
190
                $this->config->getPassword(),
191
                $this->config->getOptions()
192
            );
193
194
            foreach ($this->config->getCommands() as $command) {
195
                $this->pdo->exec($command);
196
            }
197
        } catch (PDOException $exception) {
198
            $this->logger->emergency('Can not connect to database. Error message: {error}', [
199
                'exception' => $exception,
200
                'error' => $exception->getMessage()
201
            ]);
202
203
            throw new ConnectionException(
204
                'Can not connect to database',
205
                (int) $exception->getCode(),
206
                $exception->getPrevious()
207
            );
208
        }
209
    }
210
211
    /**
212
     *
213
     * @param LoggerInterface $logger
214
     * @return $this
215
     */
216
    public function setLogger(LoggerInterface $logger): self
217
    {
218
        $this->logger = $logger;
219
220
        return $this;
221
    }
222
223
    /**
224
     * Return the query execution logs
225
     * @return array<int, array<string, mixed>>
226
     */
227
    public function getLogs(): array
228
    {
229
        return $this->logs;
230
    }
231
232
    /**
233
     * Return the current connection parameters
234
     * @return array<int|string, mixed>
235
     */
236
    public function getParams(): array
237
    {
238
        return $this->params;
239
    }
240
241
    /**
242
     * Return the current connection configuration
243
     * @return Configuration
244
     */
245
    public function getConfig(): Configuration
246
    {
247
        return $this->config;
248
    }
249
250
    /**
251
     * Return the current driver instance
252
     * @return Driver
253
     */
254
    public function getDriver(): Driver
255
    {
256
        return $this->driver;
257
    }
258
259
    /**
260
     * Return the current Schema instance
261
     * @return Schema
262
     */
263
    public function getSchema(): Schema
264
    {
265
        return $this->schema;
266
    }
267
268
    /**
269
     * Set connection to be persistent
270
     * @param bool $value
271
     * @return self
272
     */
273
    public function persistent(bool $value = true): self
274
    {
275
        $this->config->setOption(PDO::ATTR_PERSISTENT, $value);
276
277
        return $this;
278
    }
279
280
    /**
281
     * @return string
282
     */
283
    public function getDsn(): string
284
    {
285
        return $this->dsn;
286
    }
287
288
    /**
289
     * Return the instance of the PDO
290
     * @return PDO
291
     */
292
    public function getPDO(): PDO
293
    {
294
        return $this->pdo;
295
    }
296
297
    /**
298
     * Execute the SQL query and return the result
299
     * @param string $sql
300
     * @param array<int, mixed> $params the query parameters
301
     * @return ResultSet
302
     * @throws QueryException
303
     */
304
    public function query(string $sql, array $params = []): ResultSet
305
    {
306
        $prepared = $this->prepare($sql, $params);
307
        $this->execute($prepared);
308
309
        return new ResultSet($prepared['statement']);
310
    }
311
312
    /**
313
     * Direct execute the SQL query
314
     * @param string $sql
315
     * @param array<int, mixed> $params the query parameters
316
     * @return bool
317
     * @throws QueryException
318
     */
319
    public function exec(string $sql, array $params = []): bool
320
    {
321
        return $this->execute($this->prepare($sql, $params));
322
    }
323
324
    /**
325
     *  Execute the SQL query and return the number
326
     * of affected rows
327
     * @param string $sql
328
     * @param array<int, mixed> $params the query parameters
329
     * @return int
330
     * @throws QueryException
331
     */
332
    public function count(string $sql, array $params = []): int
333
    {
334
        $prepared = $this->prepare($sql, $params);
335
        $this->execute($prepared);
336
337
        $result = $prepared['statement']->rowCount();
338
        $prepared['statement']->closeCursor();
339
340
        return $result;
341
    }
342
343
    /**
344
     *  Execute the SQL query and return the first column result
345
     * @param string $sql
346
     * @param array<int, mixed> $params the query parameters
347
     * @return mixed
348
     * @throws QueryException
349
     */
350
    public function column(string $sql, array $params = [])
351
    {
352
        $prepared = $this->prepare($sql, $params);
353
        $this->execute($prepared);
354
355
        $result = $prepared['statement']->fetchColumn();
356
        $prepared['statement']->closeCursor();
357
358
        return $result;
359
    }
360
361
    /**
362
     * @param callable $callback
363
     * @param mixed|null $that
364
     *
365
     * @return mixed
366
     *
367
     * @throws ConnectionException
368
     */
369
    public function transaction(
370
        callable $callback,
371
        $that = null
372
    ) {
373
        if ($that === null) {
374
            $that = $this;
375
        }
376
377
        if ($this->pdo->inTransaction()) {
378
            return $callback($that);
379
        }
380
381
        try {
382
            $this->pdo->beginTransaction();
383
            $result = $callback($that);
384
            $this->pdo->commit();
385
        } catch (PDOException $exception) {
386
            $this->pdo->rollBack();
387
            $this->logger->error('Database transaction error. Error message: {error}', [
388
                'exception' => $exception,
389
                'error' => $exception->getMessage()
390
            ]);
391
            throw new TransactionException(
392
                $exception->getMessage(),
393
                (int) $exception->getCode(),
394
                $exception->getPrevious()
395
            );
396
        }
397
398
        return $result;
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     */
404
    public function __sleep()
405
    {
406
        return [
407
            'dsn',
408
            'driver',
409
            'schema',
410
            'config',
411
            'params',
412
            'logger',
413
        ];
414
    }
415
416
    /**
417
     * {@inheritdoc}
418
     */
419
    public function __wakeup()
420
    {
421
        $this->createPDO();
422
    }
423
424
     /**
425
     * Change the query parameters placeholder with the value
426
     * @param string $query
427
     * @param array<int, mixed> $params
428
     * @return string
429
     */
430
    protected function replaceParameters(string $query, array $params): string
431
    {
432
        $driver = $this->driver;
433
434
        return (string) preg_replace_callback(
435
            '/\?/',
436
            function () use ($driver, &$params) {
437
                $param = array_shift($params);
438
439
                $value = is_object($param) ? get_class($param) : $param;
440
                if (is_int($value) || is_float($value)) {
441
                    return $value;
442
                }
443
                if ($value === null) {
444
                    return 'NULL';
445
                }
446
                if (is_bool($value)) {
447
                    return $value ? 'TRUE' : 'FALSE';
448
                }
449
                return $driver->quote($value);
450
            },
451
            $query
452
        );
453
    }
454
455
    /**
456
     * Prepare the query
457
     * @param string $query
458
     * @param array<mixed> $params
459
     * @return array<string, mixed>
460
     * @throws QueryException
461
     */
462
    protected function prepare(string $query, array $params): array
463
    {
464
        try {
465
            $statement = $this->pdo->prepare($query);
466
        } catch (PDOException $exception) {
467
            $this->logger->error('Error when prepare query [{query}]. Error message: {error}', [
468
                'exception' => $exception,
469
                'error' => $exception->getMessage(),
470
                'query' => $query
471
            ]);
472
            throw new QueryPrepareException(
473
                $exception->getMessage() . ' [' . $query . ']',
474
                (int) $exception->getCode(),
475
                $exception->getPrevious()
476
            );
477
        }
478
479
        return [
480
            'statement' => $statement,
481
            'query' => $query,
482
            'params' => $params
483
        ];
484
    }
485
486
    /**
487
     * Execute the prepared query
488
     * @param array<string, mixed> $prepared
489
     * @return bool the status of the execution
490
     * @throws QueryException
491
     */
492
    protected function execute(array $prepared): bool
493
    {
494
        $sql = $this->replaceParameters($prepared['query'], $prepared['params']);
495
        $sqlLog = [
496
            'query' => $prepared['query'],
497
            'parameters' => implode(', ', $prepared['params'])
498
        ];
499
500
        try {
501
            if ($prepared['params']) {
502
                $this->bindValues($prepared['statement'], $prepared['params']);
503
            }
504
            $start = microtime(true);
505
            $result = $prepared['statement']->execute();
506
            $sqlLog['time'] = number_format(microtime(true) - $start, 6);
507
508
            $this->logs[] = $sqlLog;
509
510
            $this->logger->info(
511
                'Execute Query: [{query}], parameters: [{parameters}], time: [{time}]',
512
                $sqlLog
513
            );
514
        } catch (PDOException $exception) {
515
            $this->logger->error('Error when execute query [{sql}]. Error message: {error}', [
516
                'exception' => $exception,
517
                'error' => $exception->getMessage(),
518
                'sql' => $sql
519
            ]);
520
            throw new QueryException(
521
                $exception->getMessage() . ' [' . $sql . ']',
522
                (int) $exception->getCode(),
523
                $exception->getPrevious()
524
            );
525
        }
526
527
        return $result;
528
    }
529
530
    /**
531
     * Bind the parameters values
532
     * @param PDOStatement $statement
533
     * @param array<int, mixed> $values
534
     */
535
    protected function bindValues(PDOStatement $statement, array $values): void
536
    {
537
        foreach ($values as $key => $value) {
538
            $param = PDO::PARAM_STR;
539
            if (is_null($value)) {
540
                $param = PDO::PARAM_NULL;
541
            } elseif (is_int($value) || is_float($value)) {
542
                $param = PDO::PARAM_INT;
543
            } elseif (is_bool($value)) {
544
                $param = PDO::PARAM_BOOL;
545
            }
546
547
            $statement->bindValue($key + 1, $value, $param);
548
        }
549
    }
550
551
    /**
552
     * Set the PDO connection parameters to use
553
     * @return void
554
     */
555
    protected function setConnectionParams(): void
556
    {
557
        $port = $this->config->getPort();
558
        $database = $this->config->getDatabase();
559
        $hostname = $this->config->getHostname();
560
        $attr = [];
561
562
        $driverName = $this->config->getDriverName();
563
        switch ($driverName) {
564
            case 'mysql':
565
            case 'pgsql':
566
                $attr = [
567
                    'driver' => $driverName,
568
                    'dbname' => $database,
569
                    'host' => $hostname,
570
                ];
571
572
                if ($port > 0) {
573
                    $attr['port'] = $port;
574
                }
575
576
                if ($driverName === 'mysql') {
577
                    //Make MySQL using standard quoted identifier
578
                    $this->config->addCommand('SET SQL_MODE=ANSI_QUOTES');
579
                    $this->config->addCommand('SET CHARACTER SET "' . $this->config->getCharset() . '"');
580
581
                    $socket = $this->config->getSocket();
582
                    if (!empty($socket)) {
583
                        $attr['unix_socket'] = $socket;
584
                    }
585
                }
586
                break;
587
            case 'sqlsrv':
588
                //Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting
589
                $this->config->addCommand('SET QUOTED_IDENTIFIER ON');
590
591
                //Make ANSI_NULLS is ON for NULL value
592
                $this->config->addCommand('SET ANSI_NULLS ON');
593
594
                $attr = [
595
                    'driver' => 'sqlsrv',
596
                    'Server' => $hostname
597
                        . ($port > 0 ? ':' . $port : ''),
598
                    'Database' => $database
599
                ];
600
601
                $appName = $this->config->getAppname();
602
                if (!empty($appName)) {
603
                    $attr['APP'] = $appName;
604
                }
605
606
                $attributes = [
607
                    'ApplicationIntent',
608
                    'AttachDBFileName',
609
                    'Authentication',
610
                    'ColumnEncryption',
611
                    'ConnectionPooling',
612
                    'Encrypt',
613
                    'Failover_Partner',
614
                    'KeyStoreAuthentication',
615
                    'KeyStorePrincipalId',
616
                    'KeyStoreSecret',
617
                    'LoginTimeout',
618
                    'MultipleActiveResultSets',
619
                    'MultiSubnetFailover',
620
                    'Scrollable',
621
                    'TraceFile',
622
                    'TraceOn',
623
                    'TransactionIsolation',
624
                    'TransparentNetworkIPResolution',
625
                    'TrustServerCertificate',
626
                    'WSID',
627
                ];
628
629
                foreach ($attributes as $attribute) {
630
                    $str = preg_replace(
631
                        ['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'],
632
                        '$1_$2',
633
                        $attribute
634
                    );
635
636
                    if (is_string($str)) {
637
                        $keyname = strtolower($str);
638
639
                        if ($this->config->hasAttribute($keyname)) {
640
                            $attr[$attribute] = $this->config->getAttribute($keyname);
641
                        }
642
                    }
643
                }
644
                break;
645
            case 'oci':
646
            case 'oracle':
647
                $attr = [
648
                    'driver' => 'oci',
649
                    'dbname' => '//' . $hostname
650
                    . ($port > 0 ? ':' . $port : ':1521') . '/' . $database
651
                ];
652
653
                $attr['charset'] = $this->config->getCharset();
654
                break;
655
            case 'sqlite':
656
                $attr = [
657
                    'driver' => 'sqlite',
658
                    $database
659
                ];
660
                break;
661
        }
662
663
        $this->params = $attr;
664
    }
665
}
666