Completed
Push — master ( e2468c...a5594e )
by Henry
03:26
created

MySQL::getConnection()   B

Complexity

Conditions 6
Paths 15

Size

Total Lines 38
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

554
                return sprintf($query, /** @scrutinizer ignore-type */ $parameters);
Loading history...
555
            } else {
556
                return $query;
557
            }
558
        }
559
    }
560
561
    /**
562
     * Creates an associative array containing the query and time_start
563
     *
564
     * @param string $query The query you want to start logging.
565
     * @return false|array If App::$Config['environment']!='dev' this will return false. Otherwise an array containing 'query' and 'time_start' members.
566
     */
567
    protected static function startQueryLog($query)
568
    {
569
        if (App::$Config['environment']!='dev') {
570
            return false;
571
        }
572
573
        return [
574
            'query' => $query,
575
            'time_start' => sprintf('%f', microtime(true)),
576
        ];
577
    }
578
579
    /**
580
     * Uses the log array created by startQueryLog and sets 'time_finish' on it as well as 'time_duration_ms'
581
     *
582
     * If a PDO result is passed it will also set 'result_fields' and 'result_rows' on the passed in array.
583
     *
584
     * Probably gonna remove this entirely. Query logging should be done via services like New Relic.
585
     *
586
     * @param array|false $queryLog Passed by reference. The query log array created by startQueryLog
587
     * @param object|false $result The result from
588
     * @return void|false
589
     */
590
    protected static function finishQueryLog(&$queryLog, $result = false)
591
    {
592
        if ($queryLog == false) {
593
            return false;
594
        }
595
596
        // save finish time and number of affected rows
597
        $queryLog['time_finish'] = sprintf('%f', microtime(true));
598
        $queryLog['time_duration_ms'] = ($queryLog['time_finish'] - $queryLog['time_start']) * 1000;
599
600
        // save result information
601
        if ($result) {
602
            $queryLog['result_fields'] = $result->field_count;
603
            $queryLog['result_rows'] = $result->num_rows;
604
        }
605
606
        // build backtrace string
607
        // TODO: figure out a nice toString option that isn't too bulky
608
        //$queryLog['backtrace'] = debug_backtrace();
609
610
        // monolog here
611
    }
612
613
    /**
614
     * Gets the database config and sets it to static::$Config
615
     *
616
     * @uses static::$Config
617
     * @uses App::config
618
     *
619
     * @return array static::$Config
620
     */
621
    protected static function config()
622
    {
623
        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...
624
            static::$Config = App::config('db');
625
        }
626
627
        return static::$Config;
628
    }
629
630
    /**
631
     * Gets the label we should use in the current run time based on App::$Config['environment']
632
     *
633
     * @uses App::$Config
634
     * @uses static::$defaultProductionLabel
635
     * @uses static::$defaultDevLabel
636
     *
637
     * @return string The SQL config to use in the config based on the current environment.
638
     */
639
    protected static function getDefaultLabel()
640
    {
641
        if (App::$Config['environment'] == 'production') {
642
            return static::$defaultProductionLabel;
643
        } elseif (App::$Config['environment'] == 'dev') {
644
            return static::$defaultDevLabel;
645
        }
646
    }
647
}
648