Passed
Push — master ( 157485...b7615a )
by Nikolaos
09:23
created

AbstractConnection   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 688
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 161
dl 0
loc 688
rs 8.5599
c 0
b 0
f 0
wmc 48

34 Methods

Rating   Name   Duplication   Size   Complexity  
A exec() 0 8 1
A rollBack() 0 9 1
A lastInsertId() 0 9 1
A beginTransaction() 0 8 1
A __call() 0 12 2
A getDriverName() 0 5 1
A getQuoteNames() 0 26 4
A perform() 0 15 2
A setProfiler() 0 3 1
A fetchObject() 0 9 1
A errorInfo() 0 5 1
A getProfiler() 0 3 1
A fetchObjects() 0 9 1
A quote() 0 18 3
A fetchValue() 0 5 1
A prepare() 0 9 1
A fetchAssoc() 0 9 2
A fetchData() 0 18 2
A fetchAffected() 0 5 1
A fetchColumn() 0 10 1
A query() 0 15 1
A fetchGroup() 0 10 1
A inTransaction() 0 7 1
A setAttribute() 0 5 1
A getAdapter() 0 5 1
A fetchOne() 0 7 1
A fetchAll() 0 7 1
A isConnected() 0 3 1
A errorCode() 0 5 1
A getAttribute() 0 5 1
A performBind() 0 23 6
A commit() 0 8 1
A fetchPairs() 0 7 1
A getAvailableDrivers() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractConnection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractConnection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by AtlasPHP
12
 *
13
 * @link    https://github.com/atlasphp/Atlas.Pdo
14
 * @license https://github.com/atlasphp/Atlas.Pdo/blob/1.x/LICENSE.md
15
 */
16
17
declare(strict_types=1);
18
19
namespace Phalcon\DataMapper\Pdo\Connection;
20
21
use BadMethodCallException;
22
use PDO;
23
use PDOStatement;
24
use Phalcon\DataMapper\Pdo\Exception\CannotBindValue;
25
use Phalcon\DataMapper\Pdo\Profiler\ProfilerInterface;
26
27
use function array_merge;
28
use function call_user_func_array;
29
use function current;
30
use function func_get_args;
31
use function get_class;
32
use function implode;
33
use function is_array;
34
use function is_bool;
35
use function is_int;
36
use function method_exists;
37
38
/**
39
 * Provides array quoting, profiling, a new `perform()` method, new `fetch*()`
40
 * methods
41
 *
42
 * @property PDO               $pdo
43
 * @property ProfilerInterface $profiler
44
 */
45
abstract class AbstractConnection implements ConnectionInterface
46
{
47
    /**
48
     * @var PDO
49
     */
50
    protected $pdo;
51
52
    /**
53
     * @var ProfilerInterface
54
     */
55
    protected $profiler;
56
57
    /**
58
     * Proxies to PDO methods created for specific drivers; in particular,
59
     * `sqlite` and `pgsql`.
60
     *
61
     * @param string $name
62
     * @param array  $arguments
63
     *
64
     * @return mixed
65
     * @throws BadMethodCallException
66
     */
67
    public function __call($name, array $arguments)
68
    {
69
        $this->connect();
70
71
        if (!method_exists($this->pdo, $name)) {
72
            $class   = get_class($this);
73
            $message = "Class '" . $class
74
                . "' does not have a method '" . $name . "'";
75
            throw new BadMethodCallException($message);
76
        }
77
78
        return call_user_func_array([$this->pdo, $name], $arguments);
79
    }
80
81
    /**
82
     * Begins a transaction. If the profiler is enabled, the operation will
83
     * be recorded.
84
     *
85
     * @return bool
86
     */
87
    public function beginTransaction(): bool
88
    {
89
        $this->connect();
90
        $this->profiler->start(__FUNCTION__);
91
        $result = $this->pdo->beginTransaction();
92
        $this->profiler->finish();
93
94
        return $result;
95
    }
96
97
    /**
98
     * Commits the existing transaction. If the profiler is enabled, the
99
     * operation will be recorded.
100
     *
101
     * @return bool
102
     */
103
    public function commit(): bool
104
    {
105
        $this->connect();
106
        $this->profiler->start(__FUNCTION__);
107
        $result = $this->pdo->commit();
108
        $this->profiler->finish();
109
110
        return $result;
111
    }
112
113
    /**
114
     * Connects to the database.
115
     */
116
    abstract public function connect(): void;
117
118
    /**
119
     * Disconnects from the database.
120
     */
121
    abstract public function disconnect(): void;
122
123
    /**
124
     * Gets the most recent error code.
125
     *
126
     * @return string|null
127
     */
128
    public function errorCode(): ?string
129
    {
130
        $this->connect();
131
132
        return $this->pdo->errorCode();
133
    }
134
135
    /**
136
     * Gets the most recent error info.
137
     *
138
     * @return array
139
     */
140
    public function errorInfo(): array
141
    {
142
        $this->connect();
143
144
        return $this->pdo->errorInfo();
145
    }
146
147
    /**
148
     * Executes an SQL statement and returns the number of affected rows. If
149
     * the profiler is enabled, the operation will be recorded.
150
     *
151
     * @param string $statement
152
     *
153
     * @return int
154
     */
155
    public function exec(string $statement): int
156
    {
157
        $this->connect();
158
        $this->profiler->start(__FUNCTION__);
159
        $affectedRows = $this->pdo->exec($statement);
160
        $this->profiler->finish($statement);
161
162
        return $affectedRows;
163
    }
164
165
    /**
166
     * Performs a statement and returns the number of affected rows.
167
     *
168
     * @param string $statement
169
     * @param array  $values
170
     *
171
     * @return int
172
     * @throws CannotBindValue
173
     */
174
    public function fetchAffected(string $statement, array $values = []): int
175
    {
176
        $sth = $this->perform($statement, $values);
177
178
        return $sth->rowCount();
179
    }
180
181
    /**
182
     * Fetches a sequential array of rows from the database; the rows are
183
     * returned as associative arrays.
184
     *
185
     * @param string $statement
186
     * @param array  $values
187
     *
188
     * @return array
189
     * @throws CannotBindValue
190
     */
191
    public function fetchAll(string $statement, array $values = []): array
192
    {
193
        return $this->fetchData(
194
            "fetchAll",
195
            [PDO::FETCH_ASSOC],
196
            $statement,
197
            $values
198
        );
199
    }
200
201
    /**
202
     * Fetches an associative array of rows from the database; the rows are
203
     * returned as associative arrays, and the array of rows is keyed on the
204
     * first column of each row.
205
     *
206
     * If multiple rows have the same first column value, the last row with
207
     * that value will overwrite earlier rows. This method is more resource
208
     * intensive and should be avoided if possible.
209
     *
210
     * @param string $statement
211
     * @param array  $values
212
     *
213
     * @return array
214
     * @throws CannotBindValue
215
     */
216
    public function fetchAssoc(string $statement, array $values = []): array
217
    {
218
        $sth  = $this->perform($statement, $values);
219
        $data = [];
220
        while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
221
            $data[current($row)] = $row;
222
        }
223
224
        return $data;
225
    }
226
227
    /**
228
     * Fetches a column of rows as a sequential array (default first one).
229
     *
230
     * @param string $statement
231
     * @param array  $values
232
     * @param int    $column
233
     *
234
     * @return array
235
     * @throws CannotBindValue
236
     */
237
    public function fetchColumn(
238
        string $statement,
239
        array $values = [],
240
        int $column = 0
241
    ): array {
242
        return $this->fetchData(
243
            "fetchAll",
244
            [PDO::FETCH_COLUMN, $column],
245
            $statement,
246
            $values
247
        );
248
    }
249
250
    /**
251
     * Fetches multiple from the database as an associative array. The first
252
     * column will be the index key. The default flags are
253
     * PDO::FETCH_ASSOC | PDO::FETCH_GROUP
254
     *
255
     * @param string $statement
256
     * @param array  $values
257
     * @param int    $flags
258
     *
259
     * @return array
260
     * @throws CannotBindValue
261
     */
262
    public function fetchGroup(
263
        string $statement,
264
        array $values = [],
265
        int $flags = PDO::FETCH_ASSOC
266
    ): array {
267
        return $this->fetchData(
268
            "fetchAll",
269
            [PDO::FETCH_GROUP | $flags],
270
            $statement,
271
            $values
272
        );
273
    }
274
275
    /**
276
     * Fetches one row from the database as an object where the column values
277
     * are mapped to object properties.
278
     *
279
     * Since PDO injects property values before invoking the constructor, any
280
     * initializations for defaults that you potentially have in your object's
281
     * constructor, will override the values that have been injected by
282
     * `fetchObject`. The default object returned is `\stdClass`
283
     *
284
     * @param string $statement
285
     * @param array  $values
286
     * @param string $class
287
     * @param array  $arguments
288
     *
289
     * @return object
290
     * @throws CannotBindValue
291
     */
292
    public function fetchObject(
293
        string $statement,
294
        array $values = [],
295
        string $class = 'stdClass',
296
        array $arguments = []
297
    ): object {
298
        $sth = $this->perform($statement, $values);
299
300
        return $sth->fetchObject($class, $arguments);
301
    }
302
303
    /**
304
     * Fetches a sequential array of rows from the database; the rows are
305
     * returned as objects where the column values are mapped to object
306
     * properties.
307
     *
308
     * Since PDO injects property values before invoking the constructor, any
309
     * initializations for defaults that you potentially have in your object's
310
     * constructor, will override the values that have been injected by
311
     * `fetchObject`. The default object returned is `\stdClass`
312
     *
313
     * @param string $statement
314
     * @param array  $values
315
     * @param string $class
316
     * @param array  $arguments
317
     *
318
     * @return array
319
     * @throws CannotBindValue
320
     */
321
    public function fetchObjects(
322
        string $statement,
323
        array $values = [],
324
        string $class = 'stdClass',
325
        array $arguments = []
326
    ): array {
327
        $sth = $this->perform($statement, $values);
328
329
        return $sth->fetchAll(PDO::FETCH_CLASS, $class, $arguments);
330
    }
331
332
    /**
333
     * Fetches one row from the database as an associative array.
334
     *
335
     * @param string $statement
336
     * @param array  $values
337
     *
338
     * @return array
339
     * @throws CannotBindValue
340
     */
341
    public function fetchOne(string $statement, array $values = []): array
342
    {
343
        return $this->fetchData(
344
            "fetch",
345
            [PDO::FETCH_ASSOC],
346
            $statement,
347
            $values
348
        );
349
    }
350
351
    /**
352
     * Fetches an associative array of rows as key-value pairs (first column is
353
     * the key, second column is the value).
354
     *
355
     * @param string $statement
356
     * @param array  $values
357
     *
358
     * @return array
359
     * @throws CannotBindValue
360
     */
361
    public function fetchPairs(string $statement, array $values = []): array
362
    {
363
        return $this->fetchData(
364
            "fetchAll",
365
            [PDO::FETCH_KEY_PAIR],
366
            $statement,
367
            $values
368
        );
369
    }
370
371
    /**
372
     * Fetches the very first value (i.e., first column of the first row).
373
     *
374
     * @param string $statement
375
     * @param array  $values
376
     *
377
     * @return mixed
378
     * @throws CannotBindValue
379
     */
380
    public function fetchValue(string $statement, array $values = [])
381
    {
382
        $sth = $this->perform($statement, $values);
383
384
        return $sth->fetchColumn(0);
385
    }
386
387
    /**
388
     * Return the inner PDO (if any)
389
     *
390
     * @return PDO
391
     */
392
    public function getAdapter(): PDO
393
    {
394
        $this->connect();
395
396
        return $this->pdo;
397
    }
398
399
    /**
400
     * Retrieve a database connection attribute
401
     *
402
     * @param int $attribute
403
     *
404
     * @return mixed
405
     */
406
    public function getAttribute($attribute)
407
    {
408
        $this->connect();
409
410
        return $this->pdo->getAttribute($attribute);
411
    }
412
413
    /**
414
     * Return an array of available PDO drivers (empty array if none available)
415
     *
416
     * @return array
417
     */
418
    public static function getAvailableDrivers(): array
419
    {
420
        return PDO::getAvailableDrivers();
421
    }
422
423
    /**
424
     * Return the driver name
425
     *
426
     * @return string
427
     */
428
    public function getDriverName(): string
429
    {
430
        $this->connect();
431
432
        return $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
433
    }
434
435
    /**
436
     * Returns the Profiler instance.
437
     *
438
     * @return ProfilerInterface
439
     */
440
    public function getProfiler(): ProfilerInterface
441
    {
442
        return $this->profiler;
443
    }
444
445
    /**
446
     * Gets the quote parameters based on the driver
447
     *
448
     * @param string $driver
449
     *
450
     * @return array
451
     */
452
    public function getQuoteNames($driver = ""): array
453
    {
454
        $driver = "" === $driver ? $this->getDriverName() : $driver;
455
        switch ($driver) {
456
            case 'mysql':
457
                return [
458
                    "prefix"  => '`',
459
                    "suffix"  => '`',
460
                    "find"    => '`',
461
                    "replace" => '``',
462
                ];
463
464
            case 'sqlsrv':
465
                return [
466
                    "prefix"  => '[',
467
                    "suffix"  => ']',
468
                    "find"    => ']',
469
                    "replace" => '][',
470
                ];
471
472
            default:
473
                return [
474
                    "prefix"  => '"',
475
                    "suffix"  => '"',
476
                    "find"    => '"',
477
                    "replace" => '""',
478
                ];
479
        }
480
    }
481
482
    /**
483
     * Is a transaction currently active? If the profiler is enabled, the
484
     * operation will be recorded. If the profiler is enabled, the operation
485
     * will be recorded.
486
     *
487
     * @return bool
488
     */
489
    public function inTransaction(): bool
490
    {
491
        $this->connect();
492
        $this->profiler->start(__FUNCTION__);
493
        $result = $this->pdo->inTransaction();
494
        $this->profiler->finish();
495
        return $result;
496
    }
497
498
    /**
499
     * Is the PDO connection active?
500
     *
501
     * @return bool
502
     */
503
    public function isConnected(): bool
504
    {
505
        return (bool) $this->pdo;
506
    }
507
508
    /**
509
     * Returns the last inserted autoincrement sequence value. If the profiler
510
     * is enabled, the operation will be recorded.
511
     *
512
     * @param string $name
513
     *
514
     * @return string
515
     */
516
    public function lastInsertId(string $name = null): string
517
    {
518
        $this->connect();
519
520
        $this->profiler->start(__FUNCTION__);
521
        $result = $this->pdo->lastInsertId($name);
522
        $this->profiler->finish();
523
524
        return $result;
525
    }
526
527
    /**
528
     * Performs a query with bound values and returns the resulting
529
     * PDOStatement; array values will be passed through `quote()` and their
530
     * respective placeholders will be replaced in the query string. If the
531
     * profiler is enabled, the operation will be recorded.
532
     *
533
     * @param string $statement
534
     * @param array  $values
535
     *
536
     * @return PDOStatement
537
     */
538
    public function perform(string $statement, array $values = []): PDOStatement
539
    {
540
        $this->connect();
541
542
        $this->profiler->start(__FUNCTION__);
543
544
        $sth = $this->prepare($statement);
545
        foreach ($values as $name => $value) {
546
            $this->performBind($sth, $name, $value);
547
        }
548
        $sth->execute();
549
550
        $this->profiler->finish($statement, $values);
551
552
        return $sth;
553
    }
554
555
    /**
556
     * Prepares an SQL statement for execution.
557
     *
558
     * @param string $statement
559
     * @param array  $options
560
     *
561
     * @return PDOStatement|false
562
     */
563
    public function prepare(string $statement, array $options = [])
564
    {
565
        $this->connect();
566
567
        $this->profiler->start(__FUNCTION__);
568
        $sth = $this->pdo->prepare($statement, $options);
569
        $this->profiler->finish($sth->queryString);
570
571
        return $sth;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sth also could return the type boolean which is incompatible with the documented return type PDOStatement|false.
Loading history...
572
    }
573
574
    /**
575
     * Queries the database and returns a PDOStatement. If the profiler is
576
     * enabled, the operation will be recorded.
577
     *
578
     * @param string $statement
579
     * @param mixed  ...$fetch
580
     *
581
     * @return PDOStatement|false
582
     */
583
    public function query(string $statement)
584
    {
585
        $this->connect();
586
587
        $this->profiler->start(__FUNCTION__);
588
        $sth = call_user_func_array(
589
            [
590
                $this->pdo,
591
                "query",
592
            ],
593
            func_get_args()
594
        );
595
        $this->profiler->finish($sth->queryString);
596
597
        return $sth;
598
    }
599
600
    /**
601
     * Quotes a value for use in an SQL statement. This differs from
602
     * `PDO::quote()` in that it will convert an array into a string of
603
     * comma-separated quoted values. The default type is `PDO::PARAM_STR`
604
     *
605
     * @param mixed $value
606
     * @param int   $type
607
     *
608
     * @return string The quoted value.
609
     */
610
    public function quote($value, int $type = PDO::PARAM_STR): string
611
    {
612
        $this->connect();
613
614
        $quotes = $this->getQuoteNames();
615
        if (!is_array($value)) {
616
            $value = (string) $value;
617
618
            return $quotes["prefix"] . $value . $quotes["suffix"];
619
        }
620
621
        // quote array values, not keys, then combine with commas
622
        foreach ($value as $key => $element) {
623
            $element     = (string) $element;
624
            $value[$key] = $quotes["prefix"] . $element . $quotes["suffix"];
625
        }
626
627
        return implode(', ', $value);
628
    }
629
630
    /**
631
     * Rolls back the current transaction, and restores autocommit mode. If the
632
     * profiler is enabled, the operation will be recorded.
633
     *
634
     * @return bool
635
     */
636
    public function rollBack(): bool
637
    {
638
        $this->connect();
639
640
        $this->profiler->start(__FUNCTION__);
641
        $result = $this->pdo->rollBack();
642
        $this->profiler->finish();
643
644
        return $result;
645
    }
646
647
    /**
648
     * Set a database connection attribute
649
     *
650
     * @param int   $attribute
651
     * @param mixed $value
652
     *
653
     * @return bool
654
     */
655
    public function setAttribute(int $attribute, $value): bool
656
    {
657
        $this->connect();
658
659
        return $this->pdo->setAttribute($attribute, $value);
660
    }
661
662
    /**
663
     * Sets the Profiler instance.
664
     *
665
     * @param ProfilerInterface $profiler
666
     */
667
    public function setProfiler(ProfilerInterface $profiler)
668
    {
669
        $this->profiler = $profiler;
670
    }
671
672
    /**
673
     * Bind a value using the proper PDO::PARAM_* type.
674
     *
675
     * @param PDOStatement $statement
676
     * @param mixed        $name
677
     * @param mixed        $arguments
678
     */
679
    protected function performBind(PDOStatement $statement, $name, $arguments): void
680
    {
681
        if (is_int($name)) {
682
            $name++;
683
        }
684
685
        if (is_array($arguments)) {
686
            $type = $arguments[1] ?? PDO::PARAM_STR;
687
            if ($type === PDO::PARAM_BOOL && is_bool($arguments[0])) {
688
                $arguments[0] = $arguments[0] ? '1' : '0';
689
            }
690
691
            $parameters = array_merge([$name], $arguments);
692
        } else {
693
            $parameters = [$name, $arguments];
694
        }
695
696
        call_user_func_array(
697
            [
698
                $statement,
699
                "bindValue",
700
            ],
701
            $parameters
702
        );
703
    }
704
705
    /**
706
     * Helper method to get data from PDO based on the method passed
707
     *
708
     * @param string $method
709
     * @param array  $arguments
710
     * @param string $statement
711
     * @param array  $values
712
     *
713
     * @return array
714
     */
715
    protected function fetchData(
716
        string $method,
717
        array $arguments,
718
        string $statement,
719
        array $values = []
720
    ) {
721
        $sth    = $this->perform($statement, $values);
722
        $result = call_user_func_array([$sth, $method], $arguments);
723
724
        /**
725
         * If this returns boolean or anything other than an array, return
726
         * an empty array back
727
         */
728
        if (!is_array($result)) {
729
            $result = [];
730
        }
731
732
        return $result;
733
    }
734
}
735