Passed
Push — develop ( 33032a...666b96 )
by Henry
02:03
created

MySQL   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 608
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 152
dl 0
loc 608
rs 4.5599
c 6
b 1
f 0
wmc 58

22 Methods

Rating   Name   Duplication   Size   Complexity  
A setConnection() 0 12 4
A getDefaultLabel() 0 6 3
A escape() 0 15 5
A oneRecordCached() 0 20 2
A allValues() 0 11 2
A table() 0 11 3
B getConnection() 0 41 7
A foundRows() 0 3 1
A prepareQuery() 0 3 1
A clearCachedRecord() 0 3 1
A finishQueryLog() 0 14 3
A preprocessQuery() 0 9 4
A oneValue() 0 10 2
A config() 0 7 2
A handleException() 0 28 4
A insertID() 0 3 1
A query() 0 27 4
A allRecords() 0 11 2
A startQueryLog() 0 9 2
A affectedRows() 0 3 1
A oneRecord() 0 10 1
A nonQuery() 0 19 3

How to fix   Complexity   

Complex Class

Complex classes like MySQL 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 MySQL, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\IO\Database;
12
13
use Exception;
14
use PDO as PDO;
15
use Divergence\App as App;
16
17
/**
18
 * MySQL.
19
 *
20
 * @package Divergence
21
 * @author  Henry Paradiz <[email protected]>
22
 *
23
 */
24
class MySQL
25
{
26
    /**
27
     * Timezones in TZ format
28
     *
29
     * @var string $Timezone
30
     */
31
    public static $TimeZone;
32
33
    /**
34
     * Character encoding to use
35
     *
36
     * @var string $encoding
37
     */
38
    public static $encoding = 'UTF-8';
39
40
    /**
41
     * Character set to use
42
     *
43
     * @var string $charset
44
     */
45
    public static $charset = 'utf8';
46
47
    /**
48
     * Default config label to use in production
49
     *
50
     * @var string $defaultProductionLabel
51
     */
52
    public static $defaultProductionLabel = 'mysql';
53
54
    /**
55
     * Default config label to use in development
56
     *
57
     * @var string $defaultDevLabel
58
     */
59
    public static $defaultDevLabel = 'dev-mysql';
60
61
    /**
62
     * Current connection label
63
     *
64
     * @var string|null $currentConnection
65
     */
66
    public static $currentConnection = null;
67
68
    /**
69
     * Internal reference list of connections
70
     *
71
     * @var array $Connections
72
     */
73
    protected static $Connections = [];
74
75
    /**
76
     * In-memory record cache
77
     *
78
     * @var array $_record_cache
79
     */
80
    protected static $_record_cache = [];
81
82
    /**
83
     * An internal reference to the last PDO statement returned from a query.
84
     *
85
     * @var \PDOStatement|false|null $LastStatement
86
     */
87
    protected static $LastStatement;
88
89
    /**
90
     * In-memory cache of the data in the global database config
91
     *
92
     * @var array $Config
93
     */
94
    protected static $Config;
95
96
97
    /**
98
     * Sets the connection that should be returned by getConnection when $label is null
99
     *
100
     * @param string $label
101
     * @return void
102
     */
103
    public static function setConnection(string $label=null)
104
    {
105
        if ($label === null && static::$currentConnection === null) {
106
            static::$currentConnection = static::getDefaultLabel();
107
            return;
108
        }
109
110
        $config = static::config();
111
        if (isset($config[$label])) {
112
            static::$currentConnection = $label;
113
        } else {
114
            throw new Exception('The provided label does not exist in the config.');
115
        }
116
    }
117
118
    /**
119
     * Attempts to make, store, and return a PDO connection.
120
     * - By default will use the label provided by static::getDefaultLabel()
121
     * - The label corresponds to a config in /config/db.php
122
     * - Also sets timezone on the connection based on static::$Timezone
123
     * - Sets static::$Connections[$label] with the connection after connecting.
124
     * - If static::$Connections[$label] already exists it will return that.
125
     *
126
     * @param string|null $label A specific connection.
127
     * @return PDO A PDO connection
128
     *
129
     * @throws Exception
130
     *
131
     * @uses static::$Connections
132
     * @uses static::getDefaultLabel()
133
     * @uses static::$Timezone
134
     * @uses PDO
135
     */
136
    public static function getConnection($label=null)
137
    {
138
        if ($label === null) {
139
            if (static::$currentConnection === null) {
140
                static::setConnection();
141
            }
142
            $label = static::$currentConnection;
143
        }
144
145
        if (!isset(static::$Connections[$label])) {
146
            static::config();
147
148
            $config = array_merge([
149
                'host' => 'localhost',
150
                'port' => 3306,
151
            ], static::$Config[$label]);
152
153
            if (isset($config['socket'])) {
154
                // socket connection
155
                $DSN = 'mysql:unix_socket=' . $config['socket'] . ';dbname=' . $config['database'];
156
            } else {
157
                // tcp connection
158
                $DSN = 'mysql:host=' . $config['host'] . ';port=' . $config['port'] .';dbname=' . $config['database'];
159
            }
160
161
            try {
162
                // try to initiate connection
163
                static::$Connections[$label] = new PDO($DSN, $config['username'], $config['password']);
164
            } catch (\PDOException $e) {
165
                throw $e;
166
                //throw new Exception('PDO failed to connect on config "'.$label.'" '.$DSN);
167
            }
168
169
            // set timezone
170
            if (!empty(static::$TimeZone)) {
171
                $q = static::$Connections[$label]->prepare('SET time_zone=?');
172
                $q->execute([static::$TimeZone]);
173
            }
174
        }
175
176
        return static::$Connections[$label];
177
    }
178
179
    /**
180
     * Recursive escape for strings or arrays of strings.
181
     *
182
     * @param mixed $data If string will do a simple escape. If array will iterate over array members recursively and escape any found strings.
183
     * @return mixed Same as $data input but with all found strings escaped in place.
184
     */
185
    public static function escape($data)
186
    {
187
        if (is_string($data)) {
188
            $data = static::getConnection()->quote($data);
189
            $data = substr($data, 1, strlen($data)-2);
190
            return $data;
191
        } elseif (is_array($data)) {
192
            foreach ($data as $key=>$string) {
193
                if (is_string($string)) {
194
                    $data[$key] = static::escape($string);
195
                }
196
            }
197
            return $data;
198
        }
199
        return $data;
200
    }
201
202
    /**
203
     * Returns affected rows from the last query.
204
     *
205
     * @return int Affected row count.
206
     */
207
    public static function affectedRows()
208
    {
209
        return static::$LastStatement->rowCount();
210
    }
211
212
    /**
213
     * Runs SELECT FOUND_ROWS() and returns the result.
214
     * @see https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_found-rows
215
     *
216
     * @return string|int|false An integer as a string.
217
     */
218
    public static function foundRows()
219
    {
220
        return static::oneValue('SELECT FOUND_ROWS()');
221
    }
222
223
    /**
224
     * Returns the insert id from the last insert.
225
     * @see http://php.net/manual/en/pdo.lastinsertid.php
226
     * @return string An integer as a string usually.
227
     */
228
    public static function insertID()
229
    {
230
        return static::getConnection()->lastInsertId();
231
    }
232
233
    /**
234
     * Formats a query with vsprintf if you pass an array and sprintf if you pass a string.
235
     *
236
     *  This is a public pass through for the private method preprocessQuery.
237
     *
238
     * @param string $query A database query.
239
     * @param array|string $parameters Parameter(s) for vsprintf (array) or sprintf (string)
240
     * @return string A formatted query.
241
     *
242
     * @uses static::preprocessQuery
243
     */
244
    public static function prepareQuery($query, $parameters = [])
245
    {
246
        return static::preprocessQuery($query, $parameters);
247
    }
248
249
    /**
250
     * Run a query that returns no data (like update or insert)
251
     *
252
     * This method will still set static::$LastStatement
253
     *
254
     * @param string $query A MySQL query
255
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
256
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
257
     * @return void
258
     */
259
    public static function nonQuery($query, $parameters = [], $errorHandler = null)
260
    {
261
        $query = static::preprocessQuery($query, $parameters);
262
263
        // start query log
264
        $queryLog = static::startQueryLog($query);
265
266
        // execute query
267
        try {
268
            static::$LastStatement = static::getConnection()->query($query);
269
        } catch(\Exception $e) {
270
            $ErrorInfo = $e->errorInfo;
271
            if ($ErrorInfo[0] != '00000') {
272
                static::handleException($e, $query, $queryLog, $errorHandler);
273
            }
274
        }
275
276
        // finish query log
277
        static::finishQueryLog($queryLog);
278
    }
279
280
    /**
281
     * Run a query and returns a PDO statement
282
     *
283
     * @param string $query A MySQL query
284
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
285
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
286
     * @throws Exception
287
     * @return \PDOStatement
288
     */
289
    public static function query($query, $parameters = [], $errorHandler = null)
290
    {
291
        $query = static::preprocessQuery($query, $parameters);
292
293
        // start query log
294
        $queryLog = static::startQueryLog($query);
295
296
        // execute query
297
        try {
298
            static::$LastStatement = $Statement = static::getConnection()->query($query);
299
            // finish query log
300
            static::finishQueryLog($queryLog);
301
302
            return $Statement;
303
        } catch(\Exception $e) {
304
            $ErrorInfo = $e->errorInfo;
305
            if ($ErrorInfo[0] != '00000') {
306
                // handledException should return a PDOStatement from a successful query so let's pass this up
307
                $handledException = static::handleException($e, $query, $queryLog, $errorHandler);
308
                if (is_a($handledException,\PDOStatement::class)) {
309
                    static::$LastStatement = $handledException;
310
                    // start query log
311
                    $queryLog = static::startQueryLog($query);
0 ignored issues
show
Unused Code introduced by
The assignment to $queryLog is dead and can be removed.
Loading history...
312
313
                    return $handledException;
314
                } else {
315
                    throw $e;
316
                }
317
            }
318
           
319
        }
320
    }
321
322
    /*
323
     *  Uses $tableKey instead of primaryKey (usually ID) as the PHP array index
324
     *      Only do this with unique indexed fields. This is a helper method for that exact situation.
325
     */
326
    /**
327
     * Runs a query and returns all results as an associative array with $tableKey as the index instead of auto assignment in order of appearance by PHP.
328
     *
329
     * @param string $tableKey A column to use as an index for the returned array.
330
     * @param string $query A MySQL query
331
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
332
     * @param string $nullKey Optional fallback column to use as an index if the $tableKey param isn't found in a returned record.
333
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
334
     * @return array Result from query or an empty array if nothing found.
335
     */
336
    public static function table($tableKey, $query, $parameters = [], $nullKey = '', $errorHandler = null)
337
    {
338
        // execute query
339
        $result = static::query($query, $parameters, $errorHandler);
340
341
        $records = [];
342
        while ($record = $result->fetch(PDO::FETCH_ASSOC)) {
343
            $records[$record[$tableKey] ? $record[$tableKey] : $nullKey] = $record;
344
        }
345
346
        return $records;
347
    }
348
349
    /**
350
     * Runs a query and returns all results as an associative array.
351
     *
352
     * @param string $query A MySQL query
353
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
354
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
355
     * @return array Result from query or an empty array if nothing found.
356
     */
357
    public static function allRecords($query, $parameters = [], $errorHandler = null)
358
    {
359
        // execute query
360
        $result = static::query($query, $parameters, $errorHandler);
361
362
        $records = [];
363
        while ($record = $result->fetch(PDO::FETCH_ASSOC)) {
364
            $records[] = $record;
365
        }
366
367
        return $records;
368
    }
369
370
371
    /**
372
     * Gets you some column from every record.
373
     *
374
     * @param string $valueKey The name of the column you want.
375
     * @param string $query A MySQL query
376
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
377
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
378
     * @return array The column provided in $valueKey from each found record combined as an array. Will be an empty array if no records are found.
379
     */
380
    public static function allValues($valueKey, $query, $parameters = [], $errorHandler = null)
381
    {
382
        // execute query
383
        $result = static::query($query, $parameters, $errorHandler);
384
385
        $records = [];
386
        while ($record = $result->fetch(PDO::FETCH_ASSOC)) {
387
            $records[] = $record[$valueKey];
388
        }
389
390
        return $records;
391
    }
392
393
    /**
394
     * Unsets static::$_record_cache[$cacheKey]
395
     *
396
     * @param string $cacheKey
397
     * @return void
398
     *
399
     * @uses static::$_record_cache
400
     */
401
    public static function clearCachedRecord($cacheKey)
402
    {
403
        unset(static::$_record_cache[$cacheKey]);
404
    }
405
406
    /**
407
     * Returns the first database record from a query with caching
408
     *
409
     * It is recommended that you LIMIT 1 any records you want out of this to avoid having the database doing any work.
410
     *
411
     * @param string $cacheKey A key for the cache to use for this query. If the key is found in the existing cache will return that instead of running the query.
412
     * @param string $query A MySQL query
413
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
414
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
415
     * @return array Result from query or an empty array if nothing found.
416
     *
417
     * @uses static::$_record_cache
418
     */
419
    public static function oneRecordCached($cacheKey, $query, $parameters = [], $errorHandler = null)
420
    {
421
422
        // check for cached record
423
        if (array_key_exists($cacheKey, static::$_record_cache)) {
424
            // return cache hit
425
            return static::$_record_cache[$cacheKey];
426
        }
427
428
        // preprocess and execute query
429
        $result = static::query($query, $parameters, $errorHandler);
430
431
        // get record
432
        $record = $result->fetch(PDO::FETCH_ASSOC);
433
434
        // save record to cache
435
        static::$_record_cache[$cacheKey] = $record;
436
437
        // return record
438
        return $record;
439
    }
440
441
442
    /**
443
     * Returns the first database record from a query.
444
     *
445
     * It is recommended that you LIMIT 1 any records you want out of this to avoid having the database doing any work.
446
     *
447
     * @param string $query A MySQL query
448
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
449
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
450
     * @return array Result from query or an empty array if nothing found.
451
     */
452
    public static function oneRecord($query, $parameters = [], $errorHandler = null)
453
    {
454
        // preprocess and execute query
455
        $result = static::query($query, $parameters, $errorHandler);
456
457
        // get record
458
        $record = $result->fetch(PDO::FETCH_ASSOC);
459
460
        // return record
461
        return $record;
462
    }
463
464
    /**
465
     * Returns the first value of the first database record from a query.
466
     *
467
     * @param string $query A MySQL query
468
     * @param array|string $parameters Optional parameters for vsprintf (array) or sprintf (string) to use for formatting the query.
469
     * @param callable $errorHandler A callback that will run in the event of an error instead of static::handleError
470
     * @return string|false First field from the first record from a query or false if nothing found.
471
     */
472
    public static function oneValue($query, $parameters = [], $errorHandler = null)
473
    {
474
        // get the first record
475
        $record = static::oneRecord($query, $parameters, $errorHandler);
476
477
        if ($record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
478
            // return first value of the record
479
            return array_shift($record);
480
        } else {
481
            return false;
482
        }
483
    }
484
485
    /**
486
     * Handles any errors that are thrown by PDO
487
     *
488
     * If App::$App->Config['environment'] is 'dev' this method will attempt to hook into whoops and provide it with information about this query.
489
     *
490
     * @throws \RuntimeException Database error!
491
     *
492
     * @param Exception $e
493
     * @param string $query The query which caused the error.
494
     * @param boolean|array $queryLog An array created by startQueryLog containing logging information about this query.
495
     * @param callable $errorHandler An array handler to use instead of this one. If you pass this in it will run first and return directly.
496
     * @return void|mixed If $errorHandler is set to a callable it will try to run it and return anything that it returns. Otherwise void
497
     */
498
    public static function handleException(Exception $e, $query = '', $queryLog = false, $errorHandler = null)
499
    {
500
        if (is_callable($errorHandler, false, $callable)) {
501
            return call_user_func($errorHandler, $e, $query, $queryLog);
1 ignored issue
show
Bug introduced by
It seems like $errorHandler can also be of type null; however, parameter $callback of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

501
            return call_user_func(/** @scrutinizer ignore-type */ $errorHandler, $e, $query, $queryLog);
Loading history...
502
        }
503
504
        // save queryLog
505
        if ($queryLog) {
506
            $error = static::getConnection()->errorInfo();
507
            $queryLog['error'] = $error[2];
508
            static::finishQueryLog($queryLog);
509
        }
510
511
        // get error message
512
        $error = static::getConnection()->errorInfo();
513
        $message = $error[2];
514
515
        if (App::$App->Config['environment']=='dev') {
516
            $Handler = \Divergence\App::$App->whoops->popHandler();
517
518
            $Handler->addDataTable("Query Information", [
0 ignored issues
show
Bug introduced by
The method addDataTable() does not exist on Whoops\Handler\HandlerInterface. It seems like you code against a sub-type of Whoops\Handler\HandlerInterface such as Whoops\Handler\PrettyPageHandler. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

518
            $Handler->/** @scrutinizer ignore-call */ 
519
                      addDataTable("Query Information", [
Loading history...
519
                'Query'     	=>	$query,
520
                'Error'		=>	$message,
521
                'ErrorCode'	=>	static::getConnection()->errorCode(),
522
            ]);
523
            \Divergence\App::$App->whoops->pushHandler($Handler);
524
        }
525
        throw new \RuntimeException(sprintf("Database error: [%s]", static::getConnection()->errorCode()).$message);
526
    }
527
528
    /**
529
     * Formats a query with vsprintf if you pass an array and sprintf if you pass a string.
530
     *
531
     * @param string $query A database query.
532
     * @param array|string $parameters Parameter(s) for vsprintf (array) or sprintf (string)
533
     * @return string A formatted query.
534
     */
535
    protected static function preprocessQuery($query, $parameters = [])
536
    {
537
        if (is_array($parameters) && count($parameters)) {
538
            return vsprintf($query, $parameters);
539
        } else {
540
            if (isset($parameters)) {
541
                return sprintf($query, $parameters);
1 ignored issue
show
Bug introduced by
It seems like $parameters can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

541
                return sprintf($query, /** @scrutinizer ignore-type */ $parameters);
Loading history...
542
            } else {
543
                return $query;
544
            }
545
        }
546
    }
547
548
    /**
549
     * Creates an associative array containing the query and time_start
550
     *
551
     * @param string $query The query you want to start logging.
552
     * @return false|array If App::$App->Config['environment']!='dev' this will return false. Otherwise an array containing 'query' and 'time_start' members.
553
     */
554
    protected static function startQueryLog($query)
555
    {
556
        if (App::$App->Config['environment']!='dev') {
557
            return false;
558
        }
559
560
        return [
561
            'query' => $query,
562
            'time_start' => sprintf('%f', microtime(true)),
563
        ];
564
    }
565
566
    /**
567
     * Uses the log array created by startQueryLog and sets 'time_finish' on it as well as 'time_duration_ms'
568
     *
569
     * If a PDO result is passed it will also set 'result_fields' and 'result_rows' on the passed in array.
570
     *
571
     * Probably gonna remove this entirely. Query logging should be done via services like New Relic.
572
     *
573
     * @param array|false $queryLog Passed by reference. The query log array created by startQueryLog
574
     * @param object|false $result The result from
575
     * @return void|false
576
     */
577
    protected static function finishQueryLog(&$queryLog, $result = false)
578
    {
579
        if ($queryLog == false) {
580
            return false;
581
        }
582
583
        // save finish time and number of affected rows
584
        $queryLog['time_finish'] = sprintf('%f', microtime(true));
585
        $queryLog['time_duration_ms'] = ($queryLog['time_finish'] - $queryLog['time_start']) * 1000;
586
587
        // save result information
588
        if ($result) {
589
            $queryLog['result_fields'] = $result->field_count;
590
            $queryLog['result_rows'] = $result->num_rows;
591
        }
592
593
        // build backtrace string
594
        // TODO: figure out a nice toString option that isn't too bulky
595
        //$queryLog['backtrace'] = debug_backtrace();
596
597
        // monolog here
598
    }
599
600
    /**
601
     * Gets the database config and sets it to static::$Config
602
     *
603
     * @uses static::$Config
604
     * @uses App::config
605
     *
606
     * @return array static::$Config
607
     */
608
    protected static function config()
609
    {
610
        if (!static::$Config) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::Config of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
611
            static::$Config = App::$App->config('db');
612
        }
613
614
        return static::$Config;
615
    }
616
617
    /**
618
     * Gets the label we should use in the current run time based on App::$App->Config['environment']
619
     *
620
     * @uses App::$App->Config
621
     * @uses static::$defaultProductionLabel
622
     * @uses static::$defaultDevLabel
623
     *
624
     * @return string The SQL config to use in the config based on the current environment.
625
     */
626
    protected static function getDefaultLabel()
627
    {
628
        if (App::$App->Config['environment'] == 'production') {
629
            return static::$defaultProductionLabel;
630
        } elseif (App::$App->Config['environment'] == 'dev') {
631
            return static::$defaultDevLabel;
632
        }
633
    }
634
}
635