Passed
Push — develop ( a3aa5d...84fa8b )
by nguereza
06:38
created

Connection::getParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * Platine Database
5
 *
6
 * Platine Database is the abstraction layer using PDO with support of query and schema builder
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Database
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file Connection.php
33
 *
34
 *  The Database Connection class
35
 *
36
 *  @package    Platine\Database
37
 *  @author Platine Developers Team
38
 *  @copyright  Copyright (c) 2020
39
 *  @license    http://opensource.org/licenses/MIT  MIT License
40
 *  @link   http://www.iacademy.cf
41
 *  @version 1.0.0
42
 *  @filesource
43
 */
44
declare(strict_types=1);
45
46
namespace Platine\Database;
47
48
use InvalidArgumentException;
49
use PDO;
50
use PDOException;
51
use PDOStatement;
52
use Platine\Database\Driver\Driver;
53
use Platine\Database\Exception\ConnectionException;
54
use Platine\Database\Exception\QueryException;
55
use Platine\Logger\Logger;
56
use Platine\Logger\NullHandler;
57
58
/**
59
 * Class Connection
60
 * @package Platine\Database
61
 */
62
class Connection
63
{
64
65
    /**
66
     * The PDO instance
67
     * @var PDO
68
     */
69
    protected PDO $pdo;
70
71
    /**
72
     * The PDO data source name
73
     * @var string
74
     */
75
    protected string $dsn = '';
76
77
    /**
78
     * The list of execution query logs
79
     * @var array<int, array<string, mixed>>
80
     */
81
    protected array $logs = [];
82
83
    /**
84
     * The driver to use
85
     * @var Driver
86
     */
87
    protected Driver $driver;
88
89
    /**
90
     * The Schema instance to use
91
     * @var Schema
92
     */
93
    protected Schema $schema;
94
95
    /**
96
     * The connection configuration
97
     * @var ConfigurationInterface
98
     */
99
    protected ConfigurationInterface $config;
100
101
    /**
102
     * The connection parameters
103
     * @var array<int|string, mixed>
104
     */
105
    protected array $params = [];
106
107
    /**
108
     * @var Logger
109
     */
110
    protected Logger $logger;
111
112
    /**
113
     * Connection constructor.
114
     * @param ConfigurationInterface $config
115
     * @param Logger $logger
116
     * @throws ConnectionException
117
     */
118
    public function __construct(
119
        ConfigurationInterface $config,
120
        ?Logger $logger = null
121
    ) {
122
        $this->config = $config;
123
124
        $this->logger = $logger ? $logger : new Logger(new NullHandler());
125
        $this->logger->setChannel(__CLASS__);
126
127
        $driverClass = $this->config->getDriverClassName();
128
        $this->driver = new $driverClass($this);
129
130
        $this->schema = new Schema($this);
131
132
        $this->connect();
133
    }
134
135
        /**
136
     * Connect to the database
137
     * @return void
138
     */
139
    public function connect(): void
140
    {
141
        $this->setConnectionParams();
142
143
        if ($this->config->isPersistent()) {
144
            $this->persistent(true);
145
        }
146
147
        $attr = $this->params;
148
149
        if (empty($attr)) {
150
            throw new InvalidArgumentException('Invalid database options supplied');
151
        }
152
153
        $driver = $attr['driver'];
154
        unset($attr['driver']);
155
156
        $params = [];
157
        foreach ($attr as $key => $value) {
158
            $params[] = is_int($key) ? $value : $key . '=' . $value;
159
        }
160
161
        $dsn = $driver . ':' . implode(';', $params);
162
        if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv'])) {
163
            $charset = $this->config->getCharset();
164
            $this->config->addCommand('SET NAMES "' . $charset . '"' . (
165
                    $this->config->getDriverName() === 'mysql'
166
                            ? ' COLLATE "' . $this->config->getCollation() . '"'
167
                            : ''
168
                    ));
169
        }
170
171
        $this->dsn = $dsn;
172
173
        try {
174
            $this->pdo = new PDO(
175
                $this->dsn,
176
                $this->config->getUsername(),
177
                $this->config->getPassword(),
178
                $this->config->getOptions()
179
            );
180
181
            foreach ($this->config->getCommands() as $command) {
182
                $this->pdo->exec($command);
183
            }
184
        } catch (PDOException $exception) {
185
            $this->logger->emergency('Can not connect to database. Error message: {error}', [
186
                'exception' => $exception,
187
                'error' => $exception->getMessage()
188
            ]);
189
            throw new ConnectionException($exception->getMessage());
190
        }
191
    }
192
193
    /**
194
     * Return the query execution logs
195
     * @return array<int, array<string, mixed>>
196
     */
197
    public function getLogs(): array
198
    {
199
        return $this->logs;
200
    }
201
202
    /**
203
     * Return the current connection parameters
204
     * @return array<int|string, mixed>
205
     */
206
    public function getParams(): array
207
    {
208
        return $this->params;
209
    }
210
211
    /**
212
     * Return the current connection configuration
213
     * @return ConfigurationInterface
214
     */
215
    public function getConfig(): ConfigurationInterface
216
    {
217
        return $this->config;
218
    }
219
220
    /**
221
     * Return the current driver instance
222
     * @return Driver
223
     */
224
    public function getDriver(): Driver
225
    {
226
        return $this->driver;
227
    }
228
229
    /**
230
     * Return the current Schema instance
231
     * @return Schema
232
     */
233
    public function getSchema(): Schema
234
    {
235
        return $this->schema;
236
    }
237
238
    /**
239
     * Set connection to be persistent
240
     * @param bool $value
241
     * @return self
242
     */
243
    public function persistent(bool $value = true): self
244
    {
245
        $this->config->setOption(PDO::ATTR_PERSISTENT, $value);
246
247
        return $this;
248
    }
249
250
    /**
251
     * @return string
252
     */
253
    public function getDsn(): string
254
    {
255
        return $this->dsn;
256
    }
257
258
    /**
259
     * Return the instance of the PDO
260
     * @return PDO
261
     */
262
    public function getPDO(): PDO
263
    {
264
        return $this->pdo;
265
    }
266
267
    /**
268
     * Execute the SQL query and return the result
269
     * @param string $sql
270
     * @param array<int, mixed> $params the query parameters
271
     * @return ResultSet
272
     * @throws QueryException
273
     */
274
    public function query(string $sql, array $params = []): ResultSet
275
    {
276
        $prepared = $this->prepare($sql, $params);
277
        $this->execute($prepared);
278
279
        return new ResultSet($prepared['statement']);
280
    }
281
282
    /**
283
     * Direct execute the SQL query
284
     * @param string $sql
285
     * @param array<int, mixed> $params the query parameters
286
     * @return mixed
287
     * @throws QueryException
288
     */
289
    public function exec(string $sql, array $params = [])
290
    {
291
        return $this->execute($this->prepare($sql, $params));
292
    }
293
294
    /**
295
     *  Execute the SQL query and return the number
296
     * of affected rows
297
     * @param string $sql
298
     * @param array<int, mixed> $params the query parameters
299
     * @return int
300
     * @throws QueryException
301
     */
302
    public function count(string $sql, array $params = []): int
303
    {
304
        $prepared = $this->prepare($sql, $params);
305
        $this->execute($prepared);
306
307
        $result = $prepared['statement']->rowCount();
308
        $prepared['statement']->closeCursor();
309
310
        return $result;
311
    }
312
313
    /**
314
     *  Execute the SQL query and return the first column result
315
     * @param string $sql
316
     * @param array<int, mixed> $params the query parameters
317
     * @return mixed
318
     * @throws QueryException
319
     */
320
    public function column(string $sql, array $params = [])
321
    {
322
        $prepared = $this->prepare($sql, $params);
323
        $this->execute($prepared);
324
325
        $result = $prepared['statement']->fetchColumn();
326
        $prepared['statement']->closeCursor();
327
328
        return $result;
329
    }
330
331
    /**
332
     * @param callable $callback
333
     * @param mixed|null $that
334
     *
335
     * @return mixed
336
     *
337
     * @throws ConnectionException
338
     */
339
    public function transaction(
340
        callable $callback,
341
        $that = null
342
    ) {
343
        if ($that === null) {
344
            $that = $this;
345
        }
346
347
        if ($this->pdo->inTransaction()) {
348
            return $callback($that);
349
        }
350
351
        try {
352
            $this->pdo->beginTransaction();
353
            $result = $callback($that);
354
            $this->pdo->commit();
355
        } catch (PDOException $exception) {
356
            $this->pdo->rollBack();
357
            $this->logger->error('Database transaction error. Error message: {error}', [
358
                'exception' => $exception,
359
                'error' => $exception->getMessage()
360
            ]);
361
            throw new ConnectionException($exception->getMessage());
362
        }
363
364
        return $result;
365
    }
366
367
    /**
368
     * Change the query parameters placeholder with the value
369
     * @param string $query
370
     * @param array<int, mixed> $params
371
     * @return string
372
     */
373
    protected function replaceParameters(string $query, array $params): string
374
    {
375
        $driver = $this->driver;
376
377
        return (string) preg_replace_callback(
378
            '/\?/',
379
            function () use ($driver, &$params) {
380
                $param = array_shift($params);
381
382
                $value = is_object($param) ? get_class($param) : $param;
383
                if (is_int($value) || is_float($value)) {
384
                    return $value;
385
                }
386
                if ($value === null) {
387
                    return 'NULL';
388
                }
389
                if (is_bool($value)) {
390
                    return $value ? 'TRUE' : 'FALSE';
391
                }
392
                return $driver->quote($value);
393
            },
394
            $query
395
        );
396
    }
397
398
    /**
399
     * Prepare the query
400
     * @param string $query
401
     * @param array<mixed> $params
402
     * @return array<string, mixed>
403
     * @throws QueryException
404
     */
405
    protected function prepare(string $query, array $params): array
406
    {
407
        try {
408
            $statement = $this->pdo->prepare($query);
409
        } catch (PDOException $exception) {
410
            $sql = $this->replaceParameters($query, $params);
411
            $this->logger->error('Error when prepare query [{sql}]. Error message: {error}', [
412
                'exception' => $exception,
413
                'error' => $exception->getMessage(),
414
                'sql' => $sql
415
            ]);
416
            throw new QueryException(
417
                $exception->getMessage() . ' [' . $sql . ']',
418
                (int) $exception->getCode(),
419
                $exception->getPrevious()
420
            );
421
        }
422
423
        return [
424
            'statement' => $statement,
425
            'query' => $query,
426
            'params' => $params
427
        ];
428
    }
429
430
    /**
431
     * Execute the prepared query
432
     * @param array<string, mixed> $prepared
433
     * @return bool the status of the execution
434
     * @throws QueryException
435
     */
436
    protected function execute(array $prepared): bool
437
    {
438
        $sql = $this->replaceParameters($prepared['query'], $prepared['params']);
439
        $sqlLog = [
440
            'query' => $prepared['query'],
441
            'parameters' => implode(', ', $prepared['params'])
442
        ];
443
444
        try {
445
            if ($prepared['params']) {
446
                $this->bindValues($prepared['statement'], $prepared['params']);
447
            }
448
            $start = microtime(true);
449
            $result = $prepared['statement']->execute();
450
            $sqlLog['time'] = number_format(microtime(true) - $start, 6);
451
452
            $this->logs[] = $sqlLog;
453
454
            $this->logger->info(
455
                'Execute Query: [{query}], parameters: [{parameters}], time: [{time}]',
456
                $sqlLog
457
            );
458
        } catch (PDOException $exception) {
459
            $this->logger->error('Error when execute query [{sql}]. Error message: {error}', [
460
                'exception' => $exception,
461
                'error' => $exception->getMessage(),
462
                'sql' => $sql
463
            ]);
464
            throw new QueryException(
465
                $exception->getMessage() . ' [' . $sql . ']',
466
                (int) $exception->getCode(),
467
                $exception->getPrevious()
468
            );
469
        }
470
471
        return $result;
472
    }
473
474
    /**
475
     * Bind the parameters values
476
     * @param PDOStatement $statement
477
     * @param array<int, mixed> $values
478
     */
479
    protected function bindValues(PDOStatement $statement, array $values): void
480
    {
481
        foreach ($values as $key => $value) {
482
            $param = PDO::PARAM_STR;
483
            if (is_null($value)) {
484
                $param = PDO::PARAM_NULL;
485
            } elseif (is_int($value) || is_float($value)) {
486
                $param = PDO::PARAM_INT;
487
            } elseif (is_bool($value)) {
488
                $param = PDO::PARAM_BOOL;
489
            }
490
491
            $statement->bindValue($key + 1, $value, $param);
492
        }
493
    }
494
495
    /**
496
     * Set the PDO connection parameters to use
497
     * @return void
498
     */
499
    protected function setConnectionParams(): void
500
    {
501
        $port = $this->config->getPort();
502
        $database = $this->config->getDatabase();
503
        $hostname = $this->config->getHostname();
504
        $attr = [];
505
506
        $driverName = $this->config->getDriverName();
507
        switch ($driverName) {
508
            case 'mysql':
509
            case 'pgsql':
510
                $attr = [
511
                    'driver' => $driverName,
512
                    'dbname' => $database,
513
                    'host' => $hostname,
514
                ];
515
516
                if ($port > 0) {
517
                    $attr['port'] = $port;
518
                }
519
520
                if ($driverName === 'mysql') {
521
                    //Make MySQL using standard quoted identifier
522
                    $this->config->addCommand('SET SQL_MODE=ANSI_QUOTES');
523
                    $this->config->addCommand('SET CHARACTER SET "' . $this->config->getCharset() . '"');
524
525
                    $socket = $this->config->getSocket();
526
                    if (!empty($socket)) {
527
                        $attr['unix_socket'] = $socket;
528
529
                        unset($attr['host']);
530
                        unset($attr['port']);
531
                    }
532
                }
533
                break;
534
            case 'sqlsrv':
535
                //Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting
536
                $this->config->addCommand('SET QUOTED_IDENTIFIER ON');
537
538
                //Make ANSI_NULLS is ON for NULL value
539
                $this->config->addCommand('SET ANSI_NULLS ON');
540
541
                $attr = [
542
                    'driver' => 'sqlsrv',
543
                    'Server' => $hostname
544
                        . ($port > 0 ? ':' . $port : ''),
545
                    'Database' => $database
546
                ];
547
548
                $appName = $this->config->getAppname();
549
                if (!empty($appName)) {
550
                    $attr['APP'] = $appName;
551
                }
552
553
                $attributes = [
554
                    'ApplicationIntent',
555
                    'AttachDBFileName',
556
                    'Authentication',
557
                    'ColumnEncryption',
558
                    'ConnectionPooling',
559
                    'Encrypt',
560
                    'Failover_Partner',
561
                    'KeyStoreAuthentication',
562
                    'KeyStorePrincipalId',
563
                    'KeyStoreSecret',
564
                    'LoginTimeout',
565
                    'MultipleActiveResultSets',
566
                    'MultiSubnetFailover',
567
                    'Scrollable',
568
                    'TraceFile',
569
                    'TraceOn',
570
                    'TransactionIsolation',
571
                    'TransparentNetworkIPResolution',
572
                    'TrustServerCertificate',
573
                    'WSID',
574
                ];
575
576
                foreach ($attributes as $attribute) {
577
                    $str = preg_replace(
578
                        ['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'],
579
                        '$1_$2',
580
                        $attribute
581
                    );
582
583
                    if (is_string($str)) {
584
                        $keyname = strtolower($str);
585
586
                        if ($this->config->hasAttribute($keyname)) {
587
                            $attr[$attribute] = $this->config->getAttribute($keyname);
588
                        }
589
                    }
590
                }
591
                break;
592
            case 'oci':
593
            case 'oracle':
594
                $attr = [
595
                    'driver' => 'oci',
596
                    'dbname' => '//' . $hostname
597
                    . ($port > 0 ? ':' . $port : ':1521') . '/' . $database
598
                ];
599
600
                $attr['charset'] = $this->config->getCharset();
601
                break;
602
            case 'sqlite':
603
                $attr = [
604
                    'driver' => 'sqlite',
605
                    $database
606
                ];
607
                break;
608
        }
609
610
        $this->params = $attr;
611
    }
612
}
613