Completed
Branch feature/pre-split (42159e)
by Anton
05:36
created

PDODriver   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 521
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 0
loc 521
rs 8.439
c 0
b 0
f 0
wmc 47
lcom 1
cbo 9

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
A getName() 0 4 1
A getSource() 0 9 2
A getType() 0 4 1
A getTimezone() 0 4 1
A setProfiling() 0 6 1
A isProfiling() 0 4 1
A connect() 0 15 2
A disconnect() 0 6 1
A isConnected() 0 4 1
A withPDO() 0 7 1
A getPDO() 0 8 2
A identifier() 0 4 2
A quote() 0 8 2
A query() 0 10 1
B statement() 0 52 7
A prepare() 0 11 1
A lastInsertID() 0 6 2
B flattenParameters() 0 54 9
A __debugInfo() 0 10 1
A createPDO() 0 9 1
A clarifyException() 0 5 1
A normalizeTimestamp() 0 7 1
A bindParameters() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like PDODriver 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 PDODriver, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Database\Entities;
10
11
use PDO;
12
use Psr\Log\LoggerAwareInterface;
13
use Spiral\Core\Component;
14
use Spiral\Core\Exceptions\ScopeException;
15
use Spiral\Database\Exceptions\DriverException;
16
use Spiral\Database\Exceptions\QueryException;
17
use Spiral\Database\Helpers\QueryInterpolator;
18
use Spiral\Database\Injections\Parameter;
19
use Spiral\Database\Injections\ParameterInterface;
20
use Spiral\Debug\Traits\BenchmarkTrait;
21
use Spiral\Debug\Traits\LoggerTrait;
22
23
/**
24
 * Basic implementation of DBAL Driver, basically decorates PDO. Extends component to provide access
25
 *  to functionality like shared loggers and benchmarking.
26
 */
27
abstract class PDODriver extends Component implements LoggerAwareInterface
28
{
29
    use LoggerTrait, BenchmarkTrait;
30
31
    /**
32
     * One of DatabaseInterface types, must be set on implementation.
33
     */
34
    const TYPE = null;
35
36
    /**
37
     * DateTime format to be used to perform automatic conversion of DateTime objects.
38
     *
39
     * @var string
40
     */
41
    const DATETIME = 'Y-m-d H:i:s';
42
43
    /**
44
     * Driver name.
45
     *
46
     * @var string
47
     */
48
    private $name = '';
49
50
    /**
51
     * @var PDO|null
52
     */
53
    private $pdo = null;
54
55
    /**
56
     * Connection configuration described in DBAL config file. Any driver can be used as data source
57
     * for multiple databases as table prefix and quotation defined on Database instance level.
58
     *
59
     * @var array
60
     */
61
    protected $options = [
62
        'profiling'  => false,
63
64
        //All datetime objects will be converted relative to this timezone
65
        'timezone'   => 'UTC',
66
67
        //DSN
68
        'connection' => '',
69
        'username'   => '',
70
        'password'   => '',
71
        'options'    => [],
72
    ];
73
74
    /**
75
     * PDO connection options set.
76
     *
77
     * @var array
78
     */
79
    protected $pdoOptions = [
80
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
81
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
82
        PDO::ATTR_STRINGIFY_FETCHES => true,
83
    ];
84
85
    /**
86
     * @param string $name
87
     * @param array  $options
88
     *
89
     * @throws ScopeException
90
     */
91
    public function __construct(string $name, array $options)
92
    {
93
        $this->name = $name;
94
95
        $this->options = $options + $this->options;
96
97
        if (!empty($options['options'])) {
98
            //PDO connection options has to be stored under key "options" of config
99
            $this->pdoOptions = $options['options'] + $this->pdoOptions;
100
        }
101
    }
102
103
    /**
104
     * Source name, can include database name or database file.
105
     *
106
     * @return string
107
     */
108
    public function getName(): string
109
    {
110
        return $this->name;
111
    }
112
113
    /**
114
     * Get driver source database or file name.
115
     *
116
     * @return string
117
     *
118
     * @throws DriverException
119
     */
120
    public function getSource(): string
121
    {
122
        if (preg_match('/(?:dbname|database)=([^;]+)/i', $this->options['connection'],
123
            $matches)) {
124
            return $matches[1];
125
        }
126
127
        throw new DriverException('Unable to locate source name.');
128
    }
129
130
    /**
131
     * Database type driver linked to.
132
     *
133
     * @return string
134
     */
135
    public function getType(): string
136
    {
137
        return static::TYPE;
138
    }
139
140
    /**
141
     * Connection specific timezone, at this moment locked to UTC.
142
     *
143
     * @return \DateTimeZone
144
     */
145
    public function getTimezone(): \DateTimeZone
146
    {
147
        return new \DateTimeZone($this->options['timezone']);
148
    }
149
150
    /**
151
     * Enabled profiling will raise set of log messages and benchmarks associated with PDO queries.
152
     *
153
     * @param bool $enabled Enable or disable driver profiling.
154
     *
155
     * @return self
156
     */
157
    public function setProfiling(bool $enabled = true): PDODriver
158
    {
159
        $this->options['profiling'] = $enabled;
160
161
        return $this;
162
    }
163
164
    /**
165
     * Check if profiling mode is enabled.
166
     *
167
     * @return bool
168
     */
169
    public function isProfiling(): bool
170
    {
171
        return $this->options['profiling'];
172
    }
173
174
    /**
175
     * Force driver to connect.
176
     *
177
     * @return PDO
178
     *
179
     * @throws DriverException
180
     */
181
    public function connect(): PDO
182
    {
183
        if ($this->isConnected()) {
184
            return $this->pdo;
185
        }
186
187
        $benchmark = $this->benchmark('connect', $this->options['connection']);
188
        try {
189
            $this->pdo = $this->createPDO();
190
        } finally {
191
            $this->benchmark($benchmark);
192
        }
193
194
        return $this->pdo;
195
    }
196
197
    /**
198
     * Disconnect driver.
199
     *
200
     * @return self
201
     */
202
    public function disconnect(): PDODriver
203
    {
204
        $this->pdo = null;
205
206
        return $this;
207
    }
208
209
    /**
210
     * Check if driver already connected.
211
     *
212
     * @return bool
213
     */
214
    public function isConnected(): bool
215
    {
216
        return !empty($this->pdo);
217
    }
218
219
    /**
220
     * Change PDO instance associated with driver. Returns new copy of driver.
221
     *
222
     * @param PDO $pdo
223
     *
224
     * @return self
225
     */
226
    public function withPDO(PDO $pdo): PDODriver
227
    {
228
        $driver = clone $this;
229
        $driver->pdo = $pdo;
230
231
        return $driver;
232
    }
233
234
    /**
235
     * Get associated PDO connection. Will automatically connect if such connection does not exists.
236
     *
237
     * @return PDO
238
     */
239
    public function getPDO(): PDO
240
    {
241
        if (!$this->isConnected()) {
242
            $this->connect();
243
        }
244
245
        return $this->pdo;
246
    }
247
248
    /**
249
     * Driver specific database/table identifier quotation.
250
     *
251
     * @param string $identifier
252
     *
253
     * @return string
254
     */
255
    public function identifier(string $identifier): string
256
    {
257
        return $identifier == '*' ? '*' : '"' . str_replace('"', '""', $identifier) . '"';
258
    }
259
260
    /**
261
     * Quote value using PDO.
262
     *
263
     * @param mixed $value
264
     * @param int   $type Parameter type.
265
     *
266
     * @return string
267
     */
268
    public function quote($value, int $type = PDO::PARAM_STR): string
269
    {
270
        if ($value instanceof \DateTimeInterface) {
271
            $value = $this->normalizeTimestamp($value);
272
        }
273
274
        return $this->getPDO()->quote($value, $type);
275
    }
276
277
    /**
278
     * Wraps PDO query method with custom representation class.
279
     *
280
     * @param string $statement
281
     * @param array  $parameters
282
     *
283
     * @return PDOResult
284
     */
285
    public function query(string $statement, array $parameters = []): PDOResult
286
    {
287
        //Forcing specific return class
288
        $result = $this->statement($statement, $parameters, PDOResult::class, [$parameters]);
289
290
        /**
291
         * @var PDOResult $result
292
         */
293
        return $result;
294
    }
295
296
    /**
297
     * Create instance of PDOStatement using provided SQL query and set of parameters and execute
298
     * it.
299
     *
300
     * @param string $query
301
     * @param array  $parameters Parameters to be binded into query.
302
     * @param string $class      Class to be used to represent results.
303
     * @param array  $args       Class construction arguments (by default filtered parameters)
304
     *
305
     * @return \PDOStatement
306
     *
307
     * @throws QueryException
308
     */
309
    public function statement(
310
        string $query,
311
        array $parameters = [],
312
        $class = PDOResult::class,
313
        array $args = []
314
    ): \PDOStatement {
315
316
        try {
317
            //Filtered and normalized parameters
318
            $parameters = $this->flattenParameters($parameters);
319
320
            if ($this->isProfiling()) {
321
                $queryString = QueryInterpolator::interpolate($query, $parameters);
322
                $benchmark = $this->benchmark($this->name, $queryString);
323
            }
324
325
            //PDOStatement instance (prepared)
326
            $pdoStatement = $this->prepare($query, $class, !empty($args) ? $args : [$parameters]);
327
328
            //Mounting all input parameters
329
            $pdoStatement = $this->bindParameters($pdoStatement, $parameters);
330
331
            try {
332
                $pdoStatement->execute();
333
            } finally {
334
                if (!empty($benchmark)) {
335
                    $this->benchmark($benchmark);
336
                }
337
            }
338
339
            //Only exists if profiling on
340
            if (!empty($queryString)) {
341
                //This is place you can use to handle ALL sql messages passed thought the driver
342
                $this->logger()->info($queryString, compact('query', 'parameters'));
343
            }
344
        } catch (\PDOException $e) {
345
            if (empty($queryString)) {
346
                $queryString = QueryInterpolator::interpolate($query, $parameters);
347
            }
348
349
            //Logging error even when no profiling is enabled
350
            $this->logger()->error($queryString, compact('query', 'parameters'));
351
352
            //Logging error even when no profiling is enabled
353
            $this->logger()->alert($e->getMessage());
354
355
            //Converting exception into query or integrity exception
356
            throw $this->clarifyException($e, $queryString);
357
        }
358
359
        return $pdoStatement;
360
    }
361
362
    /**
363
     * Get prepared PDO statement.
364
     *
365
     * @param string $statement Query statement.
366
     * @param string $class     Class to represent PDO statement.
367
     * @param array  $args      Class construction arguments (by default paramaters)
368
     *
369
     * @return \PDOStatement
370
     */
371
    public function prepare(
372
        string $statement,
373
        $class = \PDOStatement::class,
374
        array $args = []
375
    ): \PDOStatement {
376
        $pdo = $this->getPDO();
377
378
        $pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [$class, $args]);
379
380
        return $pdo->prepare($statement);
381
    }
382
383
    /**
384
     * Get id of last inserted row, this method must be called after insert query. Attention,
385
     * such functionality may not work in some DBMS property (Postgres).
386
     *
387
     * @param string|null $sequence Name of the sequence object from which the ID should be
388
     *                              returned.
389
     *
390
     * @return mixed
391
     */
392
    public function lastInsertID(string $sequence = null)
393
    {
394
        $pdo = $this->getPDO();
395
396
        return $sequence ? (int)$pdo->lastInsertId($sequence) : (int)$pdo->lastInsertId();
397
    }
398
399
    /**
400
     * Prepare set of query builder/user parameters to be send to PDO. Must convert DateTime
401
     * instances into valid database timestamps and resolve values of ParameterInterface.
402
     *
403
     * Every value has to wrapped with parameter interface.
404
     *
405
     * @param array $parameters
406
     *
407
     * @return ParameterInterface[]
408
     *
409
     * @throws DriverException
410
     */
411
    public function flattenParameters(array $parameters): array
412
    {
413
        $flatten = [];
414
        foreach ($parameters as $key => $parameter) {
415
            if (!$parameter instanceof ParameterInterface) {
416
                //Let's wrap value
417
                $parameter = new Parameter($parameter, Parameter::DETECT_TYPE);
418
            }
419
420
            if ($parameter->isArray()) {
421
                if (!is_numeric($key)) {
422
                    throw new DriverException("Array parameters can not be named");
423
                }
424
425
                //Flattening arrays
426
                $nestedParameters = $parameter->flatten();
427
428
                /**
429
                 * @var ParameterInterface $parameter []
430
                 */
431
                foreach ($nestedParameters as &$nestedParameter) {
432
                    if ($nestedParameter->getValue() instanceof \DateTime) {
433
434
                        //Original parameter must not be altered
435
                        $nestedParameter = $nestedParameter->withValue(
436
                            $this->normalizeTimestamp($nestedParameter->getValue())
437
                        );
438
                    }
439
440
                    unset($nestedParameter);
441
                }
442
443
                //Quick and dirty
444
                $flatten = array_merge($flatten, $nestedParameters);
445
446
            } else {
447
                if ($parameter->getValue() instanceof \DateTime) {
448
                    //Original parameter must not be altered
449
                    $parameter = $parameter->withValue(
450
                        $this->normalizeTimestamp($parameter->getValue())
451
                    );
452
                }
453
454
                if (is_numeric($key)) {
455
                    //Numeric keys can be shifted
456
                    $flatten[] = $parameter;
457
                } else {
458
                    $flatten[$key] = $parameter;
459
                }
460
            }
461
        }
462
463
        return $flatten;
464
    }
465
466
    /**
467
     * @return array
468
     */
469
    public function __debugInfo()
470
    {
471
        return [
472
            'connection' => $this->options['connection'],
473
            'connected'  => $this->isConnected(),
474
            'profiling'  => $this->isProfiling(),
475
            'source'     => $this->getSource(),
476
            'options'    => $this->pdoOptions,
477
        ];
478
    }
479
480
    /**
481
     * Create instance of configured PDO class.
482
     *
483
     * @return PDO
484
     */
485
    protected function createPDO(): PDO
486
    {
487
        return new PDO(
488
            $this->options['connection'],
489
            $this->options['username'],
490
            $this->options['password'],
491
            $this->pdoOptions
492
        );
493
    }
494
495
    /**
496
     * Convert PDO exception into query or integrity exception.
497
     *
498
     * @param \PDOException $exception
499
     * @param string        $query
500
     *
501
     * @return QueryException
502
     */
503
    protected function clarifyException(\PDOException $exception, string $query): QueryException
504
    {
505
        //@todo more exceptions to be thrown
506
        return new QueryException($exception, $query);
507
    }
508
509
    /**
510
     * Convert DateTime object into local database representation. Driver will automatically force
511
     * needed timezone.
512
     *
513
     * @param \DateTimeInterface $value
514
     *
515
     * @return string
516
     */
517
    protected function normalizeTimestamp(\DateTimeInterface $value): string
518
    {
519
        $datetime = new \DateTime('now', $this->getTimezone());
520
        $datetime->setTimestamp($value->getTimestamp());
521
522
        return $datetime->format(static::DATETIME);
523
    }
524
525
    /**
526
     * Bind parameters into statement.
527
     *
528
     * @param \PDOStatement        $statement
529
     * @param ParameterInterface[] $parameters Named hash of ParameterInterface.
530
     *
531
     * @return \PDOStatement
532
     */
533
    private function bindParameters(\PDOStatement $statement, array $parameters): \PDOStatement
534
    {
535
        foreach ($parameters as $index => $parameter) {
536
            if (is_numeric($index)) {
537
                //Numeric, @see http://php.net/manual/en/pdostatement.bindparam.php
538
                $statement->bindValue($index + 1, $parameter->getValue(), $parameter->getType());
539
            } else {
540
                //Named
541
                $statement->bindValue($index, $parameter->getValue(), $parameter->getType());
542
            }
543
        }
544
545
        return $statement;
546
    }
547
}
548