Completed
Push — master ( 578b2d...d7dae2 )
by Lars
13:43 queued 10s
created

Debug::mailToAdmin()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 8.125

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 5
cts 10
cp 0.5
rs 9.4888
c 0
b 0
f 0
cc 5
nc 5
nop 3
crap 8.125
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use voku\db\exceptions\QueryException;
8
9
/**
10
 * Debug: This class can handle debug and error-logging for SQL-queries for the "Simple-MySQLi"-classes.
11
 */
12
class Debug
13
{
14
    /**
15
     * @var array
16
     */
17
    private $_errors = [];
18
19
    /**
20
     * @var bool
21
     */
22
    private $exit_on_error = true;
23
24
    /**
25
     * echo the error if "checkForDev()" returns true
26
     *
27
     * @var bool
28
     */
29
    private $echo_on_error = true;
30
31
    /**
32
     * @var string
33
     */
34
    private $css_mysql_box_border = '3px solid red';
35
36
    /**
37
     * @var string
38
     */
39
    private $css_mysql_box_bg = '#FFCCCC';
40
41
    /**
42
     * @var string
43
     */
44
    private $logger_class_name;
45
46
    /**
47
     * @var DB
48
     */
49
    private $_db;
50
51
    /**
52
     * @var string
53
     *
54
     * 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'
55
     */
56
    private $logger_level;
57
58
    /**
59
     * define what a slow query is in ms
60
     *
61
     * @var float
62
     */
63
    private $slowQueryTimeWarning = 0.005;
64
65
    /**
66
     * define what a slow query is in ms
67
     *
68
     * @var float
69
     */
70
    private $slowQueryTimeError = 0.1;
71
72
    /**
73
     * define what a max query repeat is
74
     *
75
     * @var int
76
     */
77
    private $maxQueryRepeatWarning = 20;
78
79
    /**
80
     * define what a max query repeat is
81
     *
82
     * @var int
83
     */
84
    private $maxQueryRepeatError = 50;
85
86
87
    /**
88
     * Debug constructor.
89
     *
90
     * @param DB $db
91
     */
92 24
    public function __construct(DB $db)
93
    {
94 24
        $this->_db = $db;
95 24
    }
96
97
    /**
98
     * Check is the current user is a developer.
99
     *
100
     * INFO:
101
     * By default we will return "true" if the remote-ip-address is localhost or
102
     * if the script is called via CLI. But you can also overwrite this method or
103
     * you can implement a global "checkForDev()"-function.
104
     *
105
     * @return bool
106
     */
107 12
    public function checkForDev(): bool
108
    {
109
        // init
110 12
        $return = false;
111
112 12
        if (\function_exists('checkForDev')) {
113
            $return = checkForDev();
114
        } else {
115
116
            // for testing with dev-address
117 12
            $noDev = isset($_GET['noDev']) ? (int) $_GET['noDev'] : 0;
118 12
            $remoteIpAddress = $_SERVER['REMOTE_ADDR'] ?? false;
119
120
            if (
121 12
                $noDev !== 1
122
                &&
123
                (
124 12
                    $remoteIpAddress === '127.0.0.1'
125
                    ||
126 12
                    $remoteIpAddress === '::1'
127
                    ||
128 12
                    \PHP_SAPI === 'cli'
129
                )
130
            ) {
131 12
                $return = true;
132
            }
133
        }
134
135 12
        return $return;
136
    }
137
138
    /**
139
     * Clear the errors in "$this->_errors".
140
     *
141
     * @return bool
142
     */
143 18
    public function clearErrors(): bool
144
    {
145 18
        $this->_errors = [];
146
147 18
        return true;
148
    }
149
150
    /**
151
     * Display SQL-Errors or throw Exceptions (for dev).
152
     *
153
     * @param string    $error                          <p>The error message.</p>
154
     * @param bool|null $force_exception_after_error    <p>
155
     *                                                  If you use default "null" here, then the behavior depends
156
     *                                                  on "$this->exit_on_error (default: true)".
157
     *                                                  </p>
158
     *
159
     * @throws QueryException
160
     */
161 69
    public function displayError($error, $force_exception_after_error = null)
162
    {
163 69
        $fileInfo = $this->getFileAndLineFromSql();
164
165 69
        $log = '[' . \date('Y-m-d H:i:s') . ']: SQL-Error: ' . $error . ' | Trace: ' . $fileInfo['path'] . '<br>';
166
167 69
        $this->logger(['error', $log]);
168
169 69
        $this->_errors[] = $log;
170
171
        if (
172 69
            $this->echo_on_error
173
            &&
174 69
            $this->checkForDev() === true
175
        ) {
176 12
            $box_border = $this->css_mysql_box_border;
177 12
            $box_bg = $this->css_mysql_box_bg;
178
179 12
            if (\PHP_SAPI === 'cli') {
180 12
                echo "\n";
181 12
                echo 'Error: ' . $error . "\n";
182 12
                echo 'Trace: ' . $fileInfo['path'] . "\n";
183 12
                echo "\n";
184
            } else {
185
                echo '
186
                <div class="OBJ-mysql-box" style="border: ' . $box_border . '; background: ' . $box_bg . '; padding: 10px; margin: 10px;">
187
                  <b style="font-size: 14px;">MYSQL Error:</b>
188
                  <code style="display: block;">
189
                    Error:' . $error . '
190
                    <br><br>
191
                    Trace: ' . $fileInfo['path'] . '
192
                  </code>
193
                </div>
194
                ';
195
            }
196
        }
197
198
        if (
199 69
            $force_exception_after_error === true
200
            ||
201
            (
202 69
                $force_exception_after_error === null
203
                &&
204 69
                $this->exit_on_error === true
205
            )
206
        ) {
207
            throw new QueryException($error);
208
        }
209 69
    }
210
211
    /**
212
     * Get errors from "$this->_errors".
213
     *
214
     * @return array
215
     */
216 15
    public function getErrors(): array
217
    {
218 15
        return $this->_errors;
219
    }
220
221
    /**
222
     * Try to get the file & line from the current sql-query.
223
     *
224
     * @return array will return array['path']
225
     */
226 69
    private function getFileAndLineFromSql(): array {
227
        // init
228 69
        $return = [];
229 69
        $path = '';
230 69
        $referrer = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
231
232 69
        foreach ($referrer as $key => $ref) {
233
234
            if (
235 69
                isset($ref['class'])
236
                &&
237
                (
238 69
                    $ref['class'] === DB::class
239
                    ||
240 69
                    $ref['class'] === self::class
241
                )
242
            ) {
243 69
                continue;
244
            }
245
246 69
            $path .= ($referrer[$key]['class'] ?? $referrer[$key]['file']) . '::' . ($referrer[$key]['function'] ?? '') . ':' . ($referrer[$key - 1]['line'] ?? '') . ' <- ';
247
        }
248
249 69
        $return['path'] = $path;
250
251 69
        return $return;
252
    }
253
254
    /**
255
     * @return string
256
     */
257
    public function getLoggerClassName(): string
258
    {
259
        return $this->logger_class_name;
260
    }
261
262
    /**
263
     * @return string
264
     */
265
    public function getLoggerLevel(): string
266
    {
267
        return $this->logger_level;
268
    }
269
270
    /**
271
     * @return bool
272
     */
273
    public function isEchoOnError(): bool
274
    {
275
        return $this->echo_on_error;
276
    }
277
278
    /**
279
     * @return bool
280
     */
281
    public function isExitOnError(): bool
282
    {
283
        return $this->exit_on_error;
284
    }
285
286
    /**
287
     * Log the current query via "$this->logger".
288
     *
289
     * @param string     $sql sql-query
290
     * @param float|int  $duration
291
     * @param false|int|string|null $results field_count | insert_id | affected_rows
292
     * @param bool       $sql_error
293
     *
294
     * @return false|mixed
295
     *                     <p>Will return false, if no logging was used.</p>
296
     */
297 140
    public function logQuery($sql, $duration, $results, bool $sql_error = false)
298
    {
299 140
        $logLevelUse = \strtolower($this->logger_level);
300
301
        if (
302 140
            $sql_error === false
303
            &&
304 140
            ($logLevelUse !== 'trace' && $logLevelUse !== 'debug')
305
        ) {
306 135
            return false;
307
        }
308
309
        // set log-level
310 41
        $logLevel = $logLevelUse;
311 41
        if ($sql_error === true) {
312 38
            $logLevel = 'error';
313
        }
314
315
        //
316
        // logging
317
        //
318
319 41
        $traceStringExtra = '';
320 41
        if ($logLevelUse === 'trace') {
321
            $tmpLink = $this->_db->getLink();
322
            if ($tmpLink && $tmpLink instanceof \mysqli) {
323
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
324
                $traceStringExtra = @\mysqli_info($tmpLink);
325
                if ($traceStringExtra) {
326
                    $traceStringExtra = ' | info => ' . $traceStringExtra;
327
                }
328
            }
329
330
            $traceStringExtra = ' | results => ' . \print_r($results, true) . $traceStringExtra;
331
        }
332
333 41
        static $SLOW_QUERY_WARNING = null;
334 41
        static $QUERY_LOG_FILE_INFO = [];
335
336 41
        $queryStatus = '';
337 41
        if ($duration >= $this->slowQueryTimeWarning) {
338
            $queryStatus = ' WARN (DURATION) ';
339
        }
340 41
        if ($duration >= $this->slowQueryTimeError) {
341
            $queryStatus = ' ERROR (DURATION) ';
342
        }
343
344 41
        $fileInfo = $this->getFileAndLineFromSql();
345 41
        $cacheKey = \md5($fileInfo['path']);
346 41
        if (empty($QUERY_LOG_FILE_INFO[$cacheKey])) {
347 41
            $QUERY_LOG_FILE_INFO[$cacheKey] = 0;
348
        }
349 41
        ++$QUERY_LOG_FILE_INFO[$cacheKey];
350
351 41
        if ($QUERY_LOG_FILE_INFO[$cacheKey] >= $this->maxQueryRepeatWarning) {
352
            $queryStatus = ' WARN (REPEAT) ';
353
        }
354 41
        if ($QUERY_LOG_FILE_INFO[$cacheKey] >= $this->maxQueryRepeatError) {
355
            $queryStatus = ' ERROR (REPEAT) ';
356
        }
357
358 41
        $queryLog = '[' . \date('Y-m-d H:i:s') . ']: ' . $queryStatus . ' Duration: SQL::::DURATION-START' . \round($duration, 5) . 'SQL::::DURATION-END | Repeat: ' . $QUERY_LOG_FILE_INFO[$cacheKey] . ' | Host: ' . $this->_db->getConfig()['hostname'] . ' | Trace: ' . $fileInfo['path'] . ' | SQL: SQL::::QUERY-START ' . \str_replace("\n", '', $sql) . ' SQL::::QUERY-END' . $traceStringExtra . "\n";
359
360 41
        return $this->logger([$logLevel, $queryLog, 'sql',]);
361
    }
362
363
    /**
364
     * Wrapper-Function for a "Logger"-Class.
365
     *
366
     * INFO:
367
     * The "Logger"-ClassName is set by "$this->logger_class_name",<br />
368
     * the "Logger"-Method is the [0] element from the "$log"-parameter,<br />
369
     * the text you want to log is the [1] element and<br />
370
     * the type you want to log is the next [2] element.
371
     *
372
     * @param string[] $log [method, text, type]<br />e.g.: array('error', 'this is a error', 'sql')
373
     *
374
     * @return false|mixed
375
     *                     <p>Will return false, if no logging was used.</p>
376
     */
377 72
    public function logger(array $log)
378
    {
379
        // init
380 72
        $logMethod = '';
381 72
        $logText = '';
382 72
        $logType = 'sql';
383 72
        $logClass = $this->logger_class_name;
384
385 72
        if (isset($log[0])) {
386 72
            $logMethod = $log[0];
387
        }
388
389 72
        if (isset($log[1])) {
390 72
            $logText = $log[1];
391
        }
392
393 72
        if (isset($log[2])) {
394 41
            $logType = $log[2];
395
        }
396
397
        if (
398 72
            $logClass
399
            &&
400 72
            $logMethod
401
            &&
402 72
            \class_exists($logClass)
403
            &&
404 72
            \method_exists($logClass, $logMethod)
405
        ) {
406
            if (\method_exists($logClass, 'getInstance')) {
407
                return $logClass::getInstance()->{$logMethod}($logText, ['log_type' => $logType]);
408
            }
409
410
            return $logClass::$logMethod($logText, $logType);
411
        }
412
413 72
        return false;
414
    }
415
416
    /**
417
     * Send a error mail to the admin / dev.
418
     *
419
     * @param string $subject
420
     * @param string $htmlBody
421
     * @param int    $priority
422
     */
423 41
    public function mailToAdmin($subject, $htmlBody, $priority = 3)
424
    {
425 41
        if (\function_exists('mailToAdmin')) {
426
            mailToAdmin($subject, $htmlBody, $priority);
427
        } else {
428 41
            if ($priority === 3) {
429 41
                $this->logger(['debug', $subject . ' | ' . $htmlBody]);
430
            } elseif ($priority > 3) {
431
                $this->logger(['error', $subject . ' | ' . $htmlBody]);
432
            } elseif ($priority < 3) {
433
                $this->logger(['info', $subject . ' | ' . $htmlBody]);
434
            }
435
        }
436 41
    }
437
438
    /**
439
     * @param bool $echo_on_error
440
     */
441 24
    public function setEchoOnError($echo_on_error)
442
    {
443 24
        $this->echo_on_error = (bool) $echo_on_error;
444 24
    }
445
446
    /**
447
     * @param bool $exit_on_error
448
     */
449 24
    public function setExitOnError($exit_on_error)
450
    {
451 24
        $this->exit_on_error = (bool) $exit_on_error;
452 24
    }
453
454
    /**
455
     * @param string $logger_class_name
456
     */
457 24
    public function setLoggerClassName($logger_class_name)
458
    {
459 24
        $this->logger_class_name = (string) $logger_class_name;
460 24
    }
461
462
    /**
463
     * @param string $logger_level
464
     */
465 24
    public function setLoggerLevel($logger_level)
466
    {
467 24
        $this->logger_level = (string) $logger_level;
468 24
    }
469
}
470