Completed
Push — master ( 195702...f3dab2 )
by Lars
02:13 queued 15s
created

Debug::setLoggerClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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