Completed
Push — master ( a781c6...24e936 )
by Lars
06:09
created

Debug::displayError()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0045

Importance

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