Completed
Push — master ( 264bad...d292ba )
by Anton
12s
created

Db::fetchUniqueGroup()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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