Passed
Push — develop ( 2d5306...ec7ed8 )
by Anton
08:18
created

Db::connect()   A

Complexity

Conditions 3
Paths 9

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3.0261

Importance

Changes 0
Metric Value
eloc 14
nc 9
nop 0
dl 0
loc 21
ccs 12
cts 14
cp 0.8571
c 0
b 0
f 0
cc 3
crap 3.0261
rs 9.7998
1
<?php
2
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link      https://github.com/bluzphp/framework
7
 */
8
9
declare(strict_types=1);
10
11
namespace Bluz\Db;
12
13
use Bluz\Common\Exception\ConfigurationException;
14
use Bluz\Common\Options;
15
use Bluz\Db\Exception\DbException;
16
use Bluz\Db\Query;
17
use Bluz\Proxy\Logger;
18
19
/**
20
 * PDO wrapper
21
 *
22
 * @package  Bluz\Db
23
 * @author   Anton Shevchuk
24
 * @link     https://github.com/bluzphp/framework/wiki/Db
25
 */
26
class Db
27
{
28
    use Options;
29
30
    /**
31
     * PDO connection settings
32
     *
33
     * @var  array
34
     * @link http://php.net/manual/en/pdo.construct.php
35
     */
36
    protected $connect = [
37
        'type' => 'mysql',
38
        'host' => 'localhost',
39
        'name' => '',
40
        'user' => 'root',
41
        'pass' => '',
42
        'options' => []
43
    ];
44
45
    /**
46
     * PDO connection flags
47
     *
48
     * @var  array
49
     * @link http://php.net/manual/en/pdo.setattribute.php
50
     */
51
    protected $attributes = [
52
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
53
    ];
54
55
    /**
56
     * @var \PDO PDO instance
57
     */
58
    protected $handler;
59
60
    /**
61
     * Setup connection
62
     *
63
     * Just save connection settings
64
     * <code>
65
     *     $db->setConnect([
66
     *         'type' => 'mysql',
67
     *         'host' => 'localhost',
68
     *         'name' => 'db name',
69
     *         'user' => 'root',
70
     *         'pass' => ''
71
     *     ]);
72
     * </code>
73
     *
74
     * @param  array $connect options
75
     *
76
     * @throws ConfigurationException
77
     * @return void
78
     */
79 52
    public function setConnect(array $connect): void
80
    {
81 52
        $this->connect = array_merge($this->connect, $connect);
82 52
        $this->checkConnect();
83 52
    }
84
85
    /**
86
     * Check connection options
87
     *
88
     * @return void
89
     * @throws ConfigurationException
90
     */
91 52
    private function checkConnect(): void
92
    {
93 52
        if (empty($this->connect['type']) ||
94 52
            empty($this->connect['host']) ||
95 52
            empty($this->connect['name']) ||
96 52
            empty($this->connect['user'])
97
        ) {
98 1
            throw new ConfigurationException(
99 1
                'Database adapter is not configured.
100
                Please check `db` configuration section: required type, host, db name and user'
101
            );
102
        }
103 52
    }
104
105
    /**
106
     * Setup attributes for PDO connect
107
     *
108
     * @param  array $attributes
109
     *
110
     * @return void
111
     */
112
    public function setAttributes(array $attributes): void
113
    {
114
        $this->attributes = $attributes;
115
    }
116
117
    /**
118
     * Connect to Db
119
     *
120
     * @return bool
121
     * @throws DbException
122
     */
123 44
    public function connect(): bool
124
    {
125
        try {
126 44
            $this->checkConnect();
127 44
            $this->log('Connect to ' . $this->connect['host']);
128 44
            $this->handler = new \PDO(
129 44
                $this->connect['type'] . ':host=' . $this->connect['host'] . ';dbname=' . $this->connect['name'],
130 44
                $this->connect['user'],
131 44
                $this->connect['pass'],
132 44
                $this->connect['options']
133
            );
134
135 44
            foreach ($this->attributes as $attribute => $value) {
136 44
                $this->handler->setAttribute($attribute, $value);
137
            }
138
139 44
            $this->ok();
140
        } catch (\Exception $e) {
141
            throw new DbException("Attempt connection to database is failed: {$e->getMessage()}");
142
        }
143 44
        return true;
144
    }
145
146
    /**
147
     * Disconnect PDO and clean default adapter
148
     *
149
     * @return void
150
     */
151 22
    public function disconnect(): void
152
    {
153 22
        if ($this->handler) {
154 16
            $this->handler = null;
155
        }
156 22
    }
157
158
    /**
159
     * Return PDO handler
160
     *
161
     * @return \PDO
162
     * @throws DbException
163
     */
164 44
    public function handler(): \PDO
165
    {
166 44
        if (null === $this->handler) {
167 43
            $this->connect();
168
        }
169 44
        return $this->handler;
170
    }
171
172
    /**
173
     * Prepare SQL query and return PDO Statement
174
     *
175
     * @param  string $sql    SQL query with placeholders
176
     * @param  array  $params params for query placeholders
177
     *
178
     * @todo Switch to PDO::activeQueryString() when it will be possible
179
     * @link https://wiki.php.net/rfc/debugging_pdo_prepared_statement_emulation
180
     *
181
     * @return \PDOStatement
182
     * @throws DbException
183
     */
184 28
    protected function prepare(string $sql, array $params = []): \PDOStatement
185
    {
186 28
        $stmt = $this->handler()->prepare($sql);
187 28
        $stmt->execute($params);
188
189 28
        $this->log($sql, $params);
190
191 28
        return $stmt;
192
    }
193
194
    /**
195
     * Quotes a string for use in a query
196
     *
197
     * Example of usage
198
     * <code>
199
     *     $db->quote($_GET['id'])
200
     * </code>
201
     *
202
     * @param  string $value
203
     * @param  int    $type
204
     *
205
     * @return string
206
     * @throws DbException
207
     */
208 1
    public function quote(string $value, int $type = \PDO::PARAM_STR): string
209
    {
210 1
        return $this->handler()->quote($value, $type);
211
    }
212
213
    /**
214
     * Quote a string so it can be safely used as a table or column name
215
     *
216
     * @param  string $identifier
217
     *
218
     * @return string
219
     */
220 20
    public function quoteIdentifier(string $identifier): string
221
    {
222
        // switch statement for DB type
223 20
        switch ($this->connect['type']) {
224 20
            case 'mysql':
225 20
                return '`' . str_replace('`', '``', $identifier) . '`';
226
            case 'postgresql':
227
            case 'sqlite':
228
            default:
229
                return '"' . str_replace('"', '\\' . '"', $identifier) . '"';
230
        }
231
    }
232
233
    /**
234
     * Execute SQL query
235
     *
236
     * Example of usage
237
     * <code>
238
     *     $db->query("SET NAMES 'utf8'");
239
     * </code>
240
     *
241
     * @param  string $sql    SQL query with placeholders
242
     *                        "UPDATE users SET name = :name WHERE id = :id"
243
     * @param  array  $params params for query placeholders (optional)
244
     *                        array (':name' => 'John', ':id' => '123')
245
     * @param  array  $types  Types of params (optional)
246
     *                        array (':name' => \PDO::PARAM_STR, ':id' => \PDO::PARAM_INT)
247
     *
248
     * @return integer the number of rows
249
     * @throws DbException
250
     */
251 25
    public function query($sql, array $params = [], array $types = []): int
252
    {
253 25
        $stmt = $this->handler()->prepare($sql);
254 25
        foreach ($params as $key => &$param) {
255 14
            $stmt->bindParam(
256 14
                (is_int($key) ? $key + 1 : ':' . $key),
257 14
                $param,
258 14
                $types[$key] ?? \PDO::PARAM_STR
259
            );
260
        }
261 25
        $this->log($sql, $params);
262 25
        $stmt->execute($params);
263 25
        $this->ok();
264 25
        return $stmt->rowCount();
265
    }
266
267
    /**
268
     * Create new query select builder
269
     *
270
     * @param string[] $select The selection expressions
271
     *
272
     * @return Query\Select
273
     */
274 1
    public function select(...$select): Query\Select
275
    {
276 1
        $query = new Query\Select();
277 1
        $query->select(...$select);
278 1
        return $query;
279
    }
280
281
    /**
282
     * Create new query insert builder
283
     *
284
     * @param  string $table
285
     *
286
     * @return Query\Insert
287
     */
288 1
    public function insert(string $table): Query\Insert
289
    {
290 1
        $query = new Query\Insert();
291 1
        $query->insert($table);
292 1
        return $query;
293
    }
294
295
    /**
296
     * Create new query update builder
297
     *
298
     * @param  string $table
299
     *
300
     * @return Query\Update
301
     */
302 1
    public function update(string $table): Query\Update
303
    {
304 1
        $query = new Query\Update();
305 1
        $query->update($table);
306 1
        return $query;
307
    }
308
309
    /**
310
     * Create new query update builder
311
     *
312
     * @param  string $table
313
     *
314
     * @return Query\Delete
315
     */
316 9
    public function delete(string $table): Query\Delete
317
    {
318 9
        $query = new Query\Delete();
319 9
        $query->delete($table);
320 9
        return $query;
321
    }
322
323
    /**
324
     * Return first field from first element from the result set
325
     *
326
     * Example of usage
327
     * <code>
328
     *     $db->fetchOne("SELECT COUNT(*) FROM users");
329
     * </code>
330
     *
331
     * @param  string $sql     SQL query with placeholders
332
     *                         "SELECT * FROM users WHERE name = :name AND pass = :pass"
333
     * @param  array  $params  params for query placeholders (optional)
334
     *                         array (':name' => 'John', ':pass' => '123456')
335
     *
336
     * @return string|false
337
     * @throws DbException
338
     */
339 5
    public function fetchOne(string $sql, array $params = [])
340
    {
341 5
        $stmt = $this->prepare($sql, $params);
342 5
        $result = $stmt->fetch(\PDO::FETCH_COLUMN);
343
344 5
        $this->ok();
345 5
        return $result;
346
    }
347
348
    /**
349
     * Returns an array containing first row from the result set
350
     *
351
     * Example of usage
352
     * <code>
353
     *     $db->fetchRow("SELECT name, email FROM users WHERE id = ". $db->quote($id));
354
     *     $db->fetchRow("SELECT name, email FROM users WHERE id = ?", [$id]);
355
     *     $db->fetchRow("SELECT name, email FROM users WHERE id = :id", [':id'=>$id]);
356
     * </code>
357
     *
358
     * @param  string $sql     SQL query with placeholders
359
     *                         "SELECT * FROM users WHERE name = :name AND pass = :pass"
360
     * @param  array  $params  params for query placeholders (optional)
361
     *                         array (':name' => 'John', ':pass' => '123456')
362
     *
363
     * @return array|false     array ('name' => 'John', 'email' => '[email protected]')
364
     * @throws DbException
365
     */
366 1
    public function fetchRow(string $sql, array $params = [])
367
    {
368 1
        $stmt = $this->prepare($sql, $params);
369 1
        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
370
371 1
        $this->ok();
372 1
        return $result;
373
    }
374
375
    /**
376
     * Returns an array containing all of the result set rows
377
     *
378
     * Example of usage
379
     * <code>
380
     *     $db->fetchAll("SELECT * FROM users WHERE ip = ?", ['192.168.1.1']);
381
     * </code>
382
     *
383
     * @param  string $sql    SQL query with placeholders
384
     *                        "SELECT * FROM users WHERE ip = :ip"
385
     * @param  array  $params params for query placeholders (optional)
386
     *                        array (':ip' => '127.0.0.1')
387
     *
388
     * @return array[]|false
389
     * @throws DbException
390
     */
391 3
    public function fetchAll(string $sql, array $params = [])
392
    {
393 3
        $stmt = $this->prepare($sql, $params);
394 3
        $result = $stmt->fetchAll(\PDO::FETCH_ASSOC);
395
396 3
        $this->ok();
397 3
        return $result;
398
    }
399
400
    /**
401
     * Returns an array containing one column from the result set rows
402
     *
403
     * @param  string $sql    SQL query with placeholders
404
     *                        "SELECT id FROM users WHERE ip = :ip"
405
     * @param  array  $params params for query placeholders (optional)
406
     *                        array (':ip' => '127.0.0.1')
407
     *
408
     * @return array|false
409
     * @throws DbException
410
     */
411 1
    public function fetchColumn(string $sql, array $params = [])
412
    {
413 1
        $stmt = $this->prepare($sql, $params);
414 1
        $result = $stmt->fetchAll(\PDO::FETCH_COLUMN);
415
416 1
        $this->ok();
417 1
        return $result;
418
    }
419
420
    /**
421
     * Returns an array containing all of the result set rows
422
     *
423
     * Group by first column
424
     *
425
     * <code>
426
     *     $db->fetchGroup("SELECT ip, COUNT(id) FROM users GROUP BY ip", []);
427
     * </code>
428
     *
429
     * @param  string $sql    SQL query with placeholders
430
     *                        "SELECT ip, id FROM users"
431
     * @param  array  $params params for query placeholders (optional)
432
     * @param  mixed  $object
433
     *
434
     * @return array|false
435
     * @throws DbException
436
     */
437 1
    public function fetchGroup(string $sql, array $params = [], $object = null)
438
    {
439 1
        $stmt = $this->prepare($sql, $params);
440
441 1
        if ($object) {
442
            $result = $stmt->fetchAll(\PDO::FETCH_CLASS | \PDO::FETCH_GROUP, $object);
443
        } else {
444 1
            $result = $stmt->fetchAll(\PDO::FETCH_ASSOC | \PDO::FETCH_GROUP);
445
        }
446
447 1
        $this->ok();
448 1
        return $result;
449
    }
450
451
    /**
452
     * Returns an array containing all of the result set rows
453
     *
454
     * Group by first column
455
     *
456
     * @param  string $sql    SQL query with placeholders
457
     *                        "SELECT ip, id FROM users"
458
     * @param  array  $params params for query placeholders (optional)
459
     *
460
     * @return array|false
461
     * @throws DbException
462
     */
463 1
    public function fetchColumnGroup(string $sql, array $params = [])
464
    {
465 1
        $stmt = $this->prepare($sql, $params);
466 1
        $result = $stmt->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP);
467
468 1
        $this->ok();
469 1
        return $result;
470
    }
471
472
    /**
473
     * Returns an array containing all of the result set rows
474
     *
475
     * Group by first unique column
476
     *
477
     * @param  string $sql    SQL query with placeholders
478
     *                        "SELECT email, name, sex FROM users"
479
     * @param  array  $params params for query placeholders (optional)
480
     *
481
     * @return array|false
482
     * @throws DbException
483
     */
484 2
    public function fetchUniqueGroup(string $sql, array $params = [])
485
    {
486 2
        $stmt = $this->prepare($sql, $params);
487 2
        $result = $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_ASSOC | \PDO::FETCH_GROUP);
488
489 2
        $this->ok();
490 2
        return $result;
491
    }
492
493
    /**
494
     * Returns a key-value array
495
     *
496
     * @param  string $sql    SQL query with placeholders
497
     *                        "SELECT id, username FROM users WHERE ip = :ip"
498
     * @param  array  $params params for query placeholders (optional)
499
     *                        array (':ip' => '127.0.0.1')
500
     *
501
     * @return array|false
502
     * @throws DbException
503
     */
504 1
    public function fetchPairs(string $sql, array $params = [])
505
    {
506 1
        $stmt = $this->prepare($sql, $params);
507 1
        $result = $stmt->fetchAll(\PDO::FETCH_KEY_PAIR);
508
509 1
        $this->ok();
510 1
        return $result;
511
    }
512
513
    /**
514
     * Returns an object containing first row from the result set
515
     *
516
     * Example of usage
517
     * <code>
518
     *     // Fetch object to stdClass
519
     *     $stdClass = $db->fetchObject('SELECT * FROM some_table WHERE id = ?', [$id]);
520
     *     // Fetch object to new Some object
521
     *     $someClass = $db->fetchObject('SELECT * FROM some_table WHERE id = ?', [$id], 'Some');
522
     *     // Fetch object to exists instance of Some object
523
     *     $someClass = $db->fetchObject('SELECT * FROM some_table WHERE id = ?', [$id], $someClass);
524
     * </code>
525
     *
526
     * @param  string $sql    SQL query with placeholders
527
     *                        "SELECT * FROM users WHERE name = :name AND pass = :pass"
528
     * @param  array  $params params for query placeholders (optional)
529
     *                        array (':name' => 'John', ':pass' => '123456')
530
     * @param  mixed  $object
531
     *
532
     * @return mixed
533
     * @throws DbException
534
     */
535 3
    public function fetchObject(string $sql, array $params = [], $object = 'stdClass')
536
    {
537 3
        $stmt = $this->prepare($sql, $params);
538
539 3
        if (is_string($object)) {
540
            // some class name
541 2
            $result = $stmt->fetchObject($object);
542
        } else {
543
            // some instance
544 1
            $stmt->setFetchMode(\PDO::FETCH_INTO, $object);
545 1
            $result = $stmt->fetch(\PDO::FETCH_INTO);
546
        }
547
548 3
        $stmt->closeCursor();
549 3
        $this->ok();
550 3
        return $result;
551
    }
552
553
    /**
554
     * Returns an array of objects containing the result set
555
     *
556
     * @param  string $sql    SQL query with placeholders
557
     *                        "SELECT * FROM users WHERE name = :name AND pass = :pass"
558
     * @param  array  $params params for query placeholders (optional)
559
     *                        array (':name' => 'John', ':pass' => '123456')
560
     * @param  mixed  $object Class name or instance
561
     *
562
     * @return array|false
563
     * @throws DbException
564
     */
565 14
    public function fetchObjects(string $sql, array $params = [], $object = null)
566
    {
567 14
        $stmt = $this->prepare($sql, $params);
568
569 14
        if (\is_string($object)) {
570
            // fetch to some class by name
571 13
            $result = $stmt->fetchAll(\PDO::FETCH_CLASS, $object);
572
        } else {
573
            // fetch to StdClass
574 1
            $result = $stmt->fetchAll(\PDO::FETCH_OBJ);
575
        }
576
577 14
        $stmt->closeCursor();
578 14
        $this->ok();
579 14
        return $result;
580
    }
581
582
    /**
583
     * Returns an array of linked objects containing the result set
584
     *
585
     * @param  string $sql    SQL query with placeholders
586
     *                        "SELECT '__users', u.*, '__users_profile', up.*
587
     *                        FROM users u
588
     *                        LEFT JOIN users_profile up ON up.userId = u.id
589
     *                        WHERE u.name = :name"
590
     * @param  array  $params params for query placeholders (optional)
591
     *                        array (':name' => 'John')
592
     *
593
     * @return array|false
594
     * @throws DbException
595
     */
596
    public function fetchRelations(string $sql, array $params = [])
597
    {
598
        $stmt = $this->prepare($sql, $params);
599
600
        $result = $stmt->fetchAll(\PDO::FETCH_ASSOC);
601
602
        // prepare results
603
        $result = Relations::fetch($result);
604
605
        $stmt->closeCursor();
606
        $this->ok();
607
        return $result;
608
    }
609
610
    /**
611
     * Transaction wrapper
612
     *
613
     * Example of usage
614
     * <code>
615
     *     $db->transaction(function() use ($db) {
616
     *         $db->query("INSERT INTO `table` ...");
617
     *         $db->query("UPDATE `table` ...");
618
     *         $db->query("DELETE FROM `table` ...");
619
     *     })
620
     * </code>
621
     *
622
     * @param  callable $process callable structure - closure function or class with __invoke() method
623
     *
624
     * @return mixed|bool
625
     * @throws DbException
626
     */
627 6
    public function transaction(callable $process)
628
    {
629
        try {
630 6
            $this->handler()->beginTransaction();
631 6
            $result = $process();
632 5
            $this->handler()->commit();
633 5
            return $result ?? true;
634 1
        } catch (\PDOException $e) {
635 1
            $this->handler()->rollBack();
636 1
            Logger::error($e->getMessage());
637 1
            return false;
638
        }
639
    }
640
641
    /**
642
     * Setup timer
643
     *
644
     * @return void
645
     */
646 44
    protected function ok(): void
647
    {
648 44
        Logger::info('<<<');
649 44
    }
650
651
    /**
652
     * Log queries by Application
653
     *
654
     * @param  string $sql SQL query for logs
655
     * @param  array  $context
656
     *
657
     * @return void
658
     */
659 44
    protected function log(string $sql, array $context = []): void
660
    {
661 44
        $sql = str_replace('%', '%%', $sql);
662 44
        $sql = preg_replace('/\?/', '"%s"', $sql, \count($context));
663
664
        // replace mask by data
665 44
        $log = vsprintf($sql, $context);
666
667 44
        Logger::info($log);
668 44
    }
669
}
670