CoreErrorLogger::echoPageLeader()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 0
dl 0
loc 19
ccs 12
cts 12
cp 1
crap 1
rs 9.7666
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Plaisio\ErrorLogger;
5
6
use Plaisio\Debug\VarDumper;
7
use Plaisio\Helper\Html;
8
9
/**
10
 * An abstract error logger that writes the error log in HTML format to a stream and any errors and exception during
11
 * the error logging itself are suppressed.
12
 */
13
abstract class CoreErrorLogger implements ErrorLogger
14
{
15
  //--------------------------------------------------------------------------------------------------------------------
16
  protected static array $errorNames = [E_COMPILE_ERROR     => 'PHP Compile Error',
17
                                        E_COMPILE_WARNING   => 'PHP Compile Warning',
18
                                        E_CORE_ERROR        => 'PHP Core Error',
19
                                        E_CORE_WARNING      => 'PHP Core Warning',
20
                                        E_DEPRECATED        => 'PHP Deprecated Warning',
21
                                        E_ERROR             => 'PHP Fatal Error',
22
                                        E_NOTICE            => 'PHP Notice',
23
                                        E_PARSE             => 'PHP Parse Error',
24
                                        E_RECOVERABLE_ERROR => 'PHP Recoverable Error',
25
                                        E_STRICT            => 'PHP Strict Warning',
26
                                        E_USER_DEPRECATED   => 'PHP User Deprecated Warning',
27
                                        E_USER_ERROR        => 'PHP User Error',
28
                                        E_USER_NOTICE       => 'PHP User Notice',
29
                                        E_USER_WARNING      => 'PHP User Warning',
30
                                        E_WARNING           => 'PHP Warning'];
31
32
  /**
33
   * The output handle.
34
   *
35
   * @var resource
36
   */
37
  protected $handle;
38
39
  /**
40
   * The variables to be dumped in the log.
41
   *
42
   * @var array|null
43
   */
44
  private ?array $dump = null;
45
46
  /**
47
   * The number of source lines shown.
48
   *
49
   * @var int
50
   */
51
  private int $numberOfSourceLines = 24;
52
53
  /**
54
   * If true scalar references to values must be traced.
55
   *
56
   * @var bool
57
   */
58
  private bool $scalarReferences;
59
60
  //--------------------------------------------------------------------------------------------------------------------
61
  /**
62
   * Main function for dumping.
63
   *
64
   * @param mixed $dump             The variables for dumping.
65
   * @param bool  $scalarReferences If true scalar references to values must be traced.
66
   *
67
   * @api
68
   * @since 1.0.0
69
   */
70
  public function dumpVars(mixed $dump, bool $scalarReferences = false): void
71 2
  {
72
    $this->dump             = $dump;
73 2
    $this->scalarReferences = $scalarReferences;
74 2
  }
75 2
76
  //--------------------------------------------------------------------------------------------------------------------
77
  /**
78
   * Logs an error.
79
   *
80
   * @param \Throwable $throwable The error to be logged.
81
   *
82
   * @return void
83
   *
84
   * @api
85
   * @since 1.0.0
86
   */
87
  public function logError(\Throwable $throwable): void
88 15
  {
89
    try
90
    {
91
      $this->openStream();
92 15
      $this->echoPageLeader();
93
      $this->echoErrorLog($throwable);
94 15
      $this->echoVarDump();
95
      $this->echoPageTrailer();
96 15
      $this->closeStream();
97
    }
98 15
    catch (\Throwable $throwable)
99
    {
100 15
      // Nothing to do.
101
    }
102 15
  }
103
104
  //--------------------------------------------------------------------------------------------------------------------
105
  /**
106
   * Closes the stream were the error log is written to.
107
   *
108 15
   * @return void
109
   */
110
  abstract protected function closeStream(): void;
111
112
  //--------------------------------------------------------------------------------------------------------------------
113
  /**
114
   * Echos the log of an error.
115
   *
116
   * @param \Throwable|null $throwable  The error.
117
   * @param bool            $isPrevious If true the exception is a previous exception.
118
   */
119
  protected function echoErrorLog(?\Throwable $throwable, bool $isPrevious = false): void
120
  {
121
    // Return immediately if there is not throwable.
122
    if ($throwable===null) return;
123
124
    if (!$isPrevious)
125 15
    {
126
      fwrite($this->handle, Html::htmlNested(['tag'  => 'h1',
127
                                              'text' => get_class($throwable)]));
128 15
    }
129
    else
130 15
    {
131
      fwrite($this->handle, Html::htmlNested(['tag'  => 'h2',
132 15
                                              'text' => 'Previous Exception: '.get_class($throwable)]));
133 15
    }
134
135
    if (isset(self::$errorNames[$throwable->getCode()]))
136
    {
137
      fwrite($this->handle, Html::htmlNested(['tag'  => 'p',
138
                                              'attr' => ['class' => 'code'],
139
                                              'text' => self::$errorNames[$throwable->getCode()]]));
140
    }
141 15
142
    $message = str_replace("\n", '<br/>', Html::txt2Html($throwable->getMessage()));
143 10
    fwrite($this->handle, Html::htmlNested(['tag'  => 'p',
144
                                            'attr' => ['class' => 'message'],
145 10
                                            'html' => $message]));
146
147
    fwrite($this->handle, Html::htmlNested(['tag'  => 'p',
148 15
                                            'attr' => ['class' => 'file'],
149 15
                                            'text' => $throwable->getFile().'('.$throwable->getLine().')']));
150
151 15
    $this->echoFileSnippet($throwable->getFile(), $throwable->getLine());
152
153 15
    $this->echoTraceStack($throwable);
154
155 15
    $this->echoErrorLog($throwable->getPrevious(), true);
156
  }
157 15
158
  //--------------------------------------------------------------------------------------------------------------------
159 15
  /**
160
   * Echos the XHTML document leader, i.e. the start html tag, the head element, and start body tag.
161 15
   */
162 15
  protected function echoPageLeader(): void
163
  {
164
    fwrite($this->handle, '<!DOCTYPE html>');
165
    fwrite($this->handle, '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">');
166
    fwrite($this->handle, '<head>');
167
    fwrite($this->handle, Html::htmlNested(['tag' => 'meta', 'attr' => ['charset' => Html::$encoding]]));
168 15
    fwrite($this->handle, '<title>Exception</title>');
169
170 15
    fwrite($this->handle, '<style>');
171 15
    fwrite($this->handle, file_get_contents(__DIR__.'/../assets/css/reset.css'));
172 15
    fwrite($this->handle, file_get_contents(__DIR__.'/../assets/css/error.css'));
173 15
    fwrite($this->handle, file_get_contents(__DIR__.'/../assets/css/dracula.css'));
174 15
    fwrite($this->handle, '</style>');
175
176 15
    fwrite($this->handle, '<script>');
177 15
    fwrite($this->handle, file_get_contents(__DIR__.'/../assets/js/highlight.pack.js'));
178 15
    fwrite($this->handle, '</script>');
179 15
    fwrite($this->handle, '<script>hljs.initHighlightingOnLoad();</script>');
180 15
    fwrite($this->handle, '</head><body>');
181
  }
182 15
183 15
  //--------------------------------------------------------------------------------------------------------------------
184 15
  /**
185 15
   * Echos the XHTML document trailer.
186 15
   */
187 15
  protected function echoPageTrailer(): void
188
  {
189
    fwrite($this->handle, '</body></html>');
190
  }
191
192
  //--------------------------------------------------------------------------------------------------------------------
193 15
  /**
194
   * Opens the stream to where the error log must be written.
195 15
   *
196 15
   * @return void
197
   */
198
  abstract protected function openStream(): void;
199
200
  //--------------------------------------------------------------------------------------------------------------------
201
  /**
202
   * Converts the arguments of a callable to a string.
203
   *
204
   * @param array $args The arguments.
205
   *
206
   * @return string
207
   */
208
  private function argumentsToString(array $args): string
209
  {
210
    $isAssoc = ($args!==array_values($args));
211
212
    $count = 0;
213
    $out   = [];
214 15
    foreach ($args as $key => $value)
215
    {
216 15
      $count++;
217
      if ($count>=7)
218 15
      {
219 15
        $out[$key] = '...';
220 15
        break;
221
      }
222 15
223 15
      if (is_object($value))
224
      {
225 15
        $out[$key] = Html::htmlNested(['tag'  => 'span',
226 15
                                       'attr' => ['class' => 'class'],
227
                                       'text' => get_class($value)]);
228
      }
229 15
      elseif (is_bool($value))
230
      {
231 15
        $out[$key] = Html::htmlNested(['tag'  => 'span',
232
                                       'attr' => ['class' => 'keyword'],
233 15
                                       'text' => ($value ? 'true' : 'false')]);
234
      }
235 15
      elseif (is_string($value))
236
      {
237 15
        if (mb_strlen($value)>32)
238
        {
239 15
          $out[$key] = Html::htmlNested(['tag'  => 'span',
240
                                         'attr' => ['class' => 'string',
241 15
                                                    'title' => mb_substr($value, 0, 512)],
242
                                         'text' => mb_substr($value, 0, 32).'...']);
243 15
        }
244
        else
245 15
        {
246 15
          $out[$key] = Html::htmlNested(['tag'  => 'span',
247 15
                                         'attr' => ['class' => 'string'],
248 15
                                         'text' => $value]);
249
        }
250
      }
251
      elseif (is_array($value))
252 15
      {
253
        $out[$key] = '['.$this->argumentsToString($value).']';
254 15
      }
255
      elseif ($value===null)
256
      {
257 15
        $out[$key] = '<span class="keyword">null</span>';
258
      }
259 15
      elseif (is_resource($value))
260
      {
261 15
        $out[$key] = Html::htmlNested(['tag'  => 'span',
262
                                       'attr' => ['class' => 'keyword'],
263 15
                                       'text' => get_resource_type($value)]);
264
      }
265 10
      elseif (is_numeric($value))
266
      {
267 1
        $out[$key] = Html::htmlNested(['tag'  => 'span',
268
                                       'attr' => ['class' => 'number'],
269 1
                                       'text' => $value]);
270
      }
271 10
      else
272
      {
273 10
        $out[$key] = '<span class="unknown">???</span>';
274
      }
275 10
276
      if (is_string($key))
277
      {
278
        $tmp = Html::htmlNested(['tag'  => 'span',
279
                                 'attr' => ['class' => 'string'],
280
                                 'text' => $key]);
281
        $tmp .= ' => ';
282 15
        $tmp .= (strpos($key, 'password')===false) ? $out[$key] : str_repeat('*', 12);
283
284 15
        $out[$key] = $tmp;
285
      }
286 15
      elseif ($isAssoc)
287 15
      {
288 15
        $tmp = Html::htmlNested(['tag'  => 'span',
289
                                 'attr' => ['class' => 'number'],
290 15
                                 'text' => $key]);
291
        $tmp .= ' => ';
292 15
        $tmp .= $out[$key];
293
294
        $out[$key] = $tmp;
295
      }
296
    }
297
298
    return implode(', ', $out);
299
  }
300
301
  //--------------------------------------------------------------------------------------------------------------------
302
  /**
303
   * Echos the name of a callable in a trace stack item.
304 15
   *
305
   * @param array $item The trace stack item.
306
   */
307
  private function echoCallable(array $item): void
308
  {
309
    if (isset($item['class']))
310
    {
311
      fwrite($this->handle, Html::htmlNested(['tag'  => 'span',
312
                                              'attr' => ['class' => 'class'],
313 15
                                              'text' => $item['class']]));
314
      fwrite($this->handle, '::');
315 15
      fwrite($this->handle, Html::htmlNested(['tag'  => 'span',
316
                                              'attr' => ['class' => 'function'],
317 15
                                              'text' => $item['function']]));
318
    }
319 15
    else
320 15
    {
321 15
      fwrite($this->handle, Html::htmlNested(['tag'  => 'span',
322
                                              'attr' => ['class' => 'function'],
323 15
                                              'text' => $item['function']]));
324
    }
325
326
    fwrite($this->handle, '(');
327 15
    fwrite($this->handle, $this->argumentsToString($item['args'] ?? []));
328
    fwrite($this->handle, ')');
329 15
  }
330
331
  //--------------------------------------------------------------------------------------------------------------------
332 15
  /**
333 15
   * Echos a snippet of a source file around a source line.
334 15
   *
335 15
   * @param string $filename The name of the file.
336
   * @param int    $line     The source line number.
337
   */
338
  private function echoFileSnippet(string $filename, int $line): void
339
  {
340
    $lines = explode("\n", file_get_contents($filename));
341
    $first = max(1, $line - $this->numberOfSourceLines / 2);
342
    $last  = min(sizeof($lines), $line + $this->numberOfSourceLines / 2);
343
344 15
    fwrite($this->handle, '<div class="source">');
345
346 15
    // div with lines numbers.
347 15
    fwrite($this->handle, '<div class="lines">');
348 15
    fwrite($this->handle, str_replace('/>', '>', Html::htmlNested(['tag'  => 'ol',
349
                                                                   'attr' => ['start' => $first]])));
350 15
    for ($i = $first; $i<=$last; $i++)
351
    {
352
      fwrite($this->handle, '<li></li>');
353 15
    }
354 15
    fwrite($this->handle, '</ol>');
355 15
    fwrite($this->handle, '</div>');
356 15
357
    // The code as plain text (without markup and tags).
358 15
    fwrite($this->handle, '<pre><code class="php">');
359
    for ($i = $first; $i<=$last; $i++)
360 15
    {
361 15
      fwrite($this->handle, Html::txt2Html($lines[$i - 1]));
362
      fwrite($this->handle, "\n");
363
    }
364 15
    fwrite($this->handle, '</code></pre>');
365 15
366
    // div for markup.
367 15
    fwrite($this->handle, '<div class="markup">');
368 15
    fwrite($this->handle, str_replace('/>', '>', Html::htmlNested(['tag'  => 'ol',
369
                                                                   'attr' => ['start' => $first]])));
370 15
    for ($i = $first; $i<=$last; $i++)
371
    {
372
      fwrite($this->handle, Html::htmlNested(['tag'  => 'li',
373 15
                                              'attr' => ['class' => ($i==$line) ? 'error' : null],
374 15
                                              'html' => null]));
375 15
    }
376 15
    fwrite($this->handle, '</ol>');
377
    fwrite($this->handle, '</div>');
378 15
379 15
    fwrite($this->handle, '</div>');
380
  }
381
382 15
  //--------------------------------------------------------------------------------------------------------------------
383 15
  /**
384
   * Echos an item of a trace stack.
385 15
   *
386 15
   * @param int   $number The item number.
387
   * @param array $item   The item of the trace stack.
388
   */
389
  private function echoTraceItem(int $number, array $item): void
390
  {
391
    fwrite($this->handle, '<p class="file">');
392
393
    fwrite($this->handle, Html::htmlNested(['tag'  => 'span',
394
                                            'attr' => ['class' => 'level'],
395 15
                                            'text' => $number]));
396
397 15
    if (isset($item['file']))
398
    {
399 15
      fwrite($this->handle, Html::htmlNested(['tag'  => 'span',
400
                                              'attr' => ['class' => 'file'],
401 15
                                              'text' => $item['file'].'('.$item['line'].'):']));
402
    }
403 15
404
    $this->echoCallable($item);
405 15
406
    if (isset($item['file']))
407 15
    {
408
      $this->echoFileSnippet($item['file'], $item['line']);
409
    }
410 15
411
    fwrite($this->handle, '</p>');
412 15
  }
413
414 15
  //--------------------------------------------------------------------------------------------------------------------
415
  /**
416
   * Echos the trace stock of a throwable.
417 15
   *
418 15
   * @param \Throwable $throwable The throwable.
419
   */
420
  private function echoTraceStack(\Throwable $throwable): void
421
  {
422
    $trace = $throwable->getTrace();
423
424
    // Return immediately if the trace is empty.
425
    if (empty($trace)) return;
426 15
427
    fwrite($this->handle, '<div class="trace">');
428 15
    fwrite($this->handle, '<h2>Stack Trace</h2>');
429
430
    $level = count($trace);
431 15
    foreach ($trace as $item)
432
    {
433 15
      $this->echoTraceItem(--$level, $item);
434 15
    }
435
436 15
    fwrite($this->handle, '</div>');
437 15
  }
438
439 15
  //--------------------------------------------------------------------------------------------------------------------
440
  /**
441
   * Echos variables.
442 15
   */
443 15
  private function echoVarDump(): void
444
  {
445
    // Return immediately if there are no variables to dump.
446
    if ($this->dump===null) return;
447
448
    fwrite($this->handle, Html::htmlNested(['tag'  => 'h2',
449 15
                                            'text' => 'VarDump']));
450
451
    $varDumper = new VarDumper(new HtmlVarWriter($this->handle));
452 15
    $varDumper->dump('', $this->dump, $this->scalarReferences);
453
  }
454 2
455
  //--------------------------------------------------------------------------------------------------------------------
456
}
457 2
458
//----------------------------------------------------------------------------------------------------------------------
459