Class PrettyErrorHandler

Avatar du membre
Mammouth du PHP | 875 Messages

02 juin 2019, 22:39

Bonjour à tous amis développeurs !

J'ai travaillé à mes heures perdues sur une petite classe que je souhaite vous partager. Peut être qu'elle n'intéressera personne, mais peut être que certains d'entre vous apprécieront de l'utiliser.

J'ai appelé cette classe PrettyErrorHandler car elle permet de remplacer l'affichage des erreurs php par un affichage amélioré.

Pour ceux d'entre vous qui développent encore from scratch, avec cette classe, non seulement vous aurez comme à votre habitude le message d'erreur original de php, mais vous aurez également les éléments utiles de la stack trace présentés de manière lisible ainsi que le bout de code où se situe l'erreur.

Une image vaut mille mots, voici donc un lien pour vous faire une idée.
Image

Si vous êtes toujours intéressés, j'ai maintenant quelques informations à ajouter.

Travaillant aujourd'hui essentiellement avec Symfony, je n'ai pas vraiment eu l'occasion de la tester. Il y a donc très probablement encore des bugs. Si certains d'entre vous s'y essayez, vous pouvez me laisser ici vos retour, j'y jetterai un oeil soyez en certain et si mon emploi du temps me le permet j'essaierai de les corriger.

Dans le même ordre d'idées, le beautifier php que j'ai moi même développé semble actuellement bien fonctionner avec des fichiers full php. Mais je n'ai pas eu l'occasion de le tester sur des fichiers mélangeant php, html, css ou encore javascript et il doit donc y avoir des problèmes de colorations syntaxique dans ces cas.

Je vais maintenant vous montrer comment l'utiliser.
/* A moins que vous ayez un autoloader auquel cas je pense que vous n'aurez aucun problème à gérer
   son chargement, on inclut la class */
require_once 'PrettyErrorHandler.class.php';

/* L'initialisation par défaut. Dans ce cas toutes les erreurs seront affichées. */
PrettyErrorHandler::init();

/* Vous pouvez toutefois définir le niveau d'erreur en premier paramètre avec les constantes php
   prévues à cet effet */
PrettyErrorHandler::init(E_ALL);

/* Si vous ne souhaitez pas voir la stack trace passez le deuxième paramètre à false.
   Dans ce cas seul le message d'erreur sera affiché */
PrettyErrorHandler::init(E_ALL, false);

/* Si vous voulez la stack trace sans voir le bout de code en erreur passez le troisème paramètre
   à false */
PrettyErrorHandler::init(E_ALL, true, false);

/* Mais si vous voulez avoir le bout de code de chaque stack trace, passer le quatrième paramètre
   à false */
PrettyErrorHandler::init(E_ALL, true, true, false);

Si vous avez des commentaires, je suis tout à votre écoute.

Je vous laisse ci-dessous le code de la class.
<?php

class PrettyErrorHandler
{
  private static $showDebugPrintBacktrace = false;
  private static $showDebugBacktrace = false;
  private static $showDefaultError = false;
  private static $isCssPrinted = false;
  private static $showBacktrace = true;
  private static $showCode = true;
  private static $showCodeOnErrorOnly = false;
  private static $doBeautifyPhp = true;

  public static function init($level = E_ALL, $showBacktrace = true, $showCode = true, $showCodeOnErrorOnly = true)
  {
    ini_set('display_errors', 'On');
    error_reporting($level & ~E_ERROR);

    set_error_handler(array('PrettyErrorHandler', 'errorHandler'));
    register_shutdown_function(array('PrettyErrorHandler', 'fatalErrorHandler'));

    if (function_exists('xdebug_disable'))
      xdebug_disable();

    self::$showBacktrace = $showBacktrace;
    self::$showCode = $showCode;
    self::$showCodeOnErrorOnly = $showCodeOnErrorOnly;
  }

  private static function printKey($key)
  {
    return !is_numeric($key) ? "'$key' => " : '';
  }

  private static function prepareArgs($args)
  {
    $output = array();

    foreach ($args as $key => $value) {
      if (is_array($value))
        $output[] = '<i>array</i>('.self::printKey($key).self::prepareArgs($value).')';
      elseif (is_object($value))
        $output[] = self::printKey($key).'<i>object</i>('.get_class($value).')';
      else
        $output[] = self::printKey($key)."'$value'";
    }

    return implode(', ', $output);
  }

  private static function prepareTrace($trace, $file, $line)
  {
    $trace = (object)$trace;
    $output = array();

    if (isset($trace->class) && $trace->class == 'PrettyErrorHandler' && (!isset($trace->line) || $trace->line != $line )) {
      return '';
    }

    if (isset($trace->function)) {
      if (!isset($trace->class) || (isset($trace->class) && $trace->class != 'PrettyErrorHandler'))
        $output[] = sprintf((isset($trace->class) ? '<span class="eh-class">'.$trace->class.'</span>->' : '')
          .'<span class="eh-function">%s</span><span class="eh-args">(%s)</span>',
          $trace->function, self::prepareArgs($trace->args));
    }

    if (isset($trace->file)) {
      $path = explode('/', $trace->file);
      $filename = array_pop($path);

      if (!isset($trace->class) || (isset($trace->class) && $trace->class != 'PrettyErrorHandler'))
        $output[] = sprintf('in %s/<strong>%s</strong> (line <span class="eh-strong">%s</span>)',
          implode('/', $path), $filename, $trace->line);

      if (self::$showCode && (!self::$showCodeOnErrorOnly || (self::$showCodeOnErrorOnly && $file == $trace->file && $line == $trace->line)))
        $output[] = self::printCode($trace->file, $trace->line);
    }

    return implode('<br/>', $output);
  }

  public static function errorHandler($severity, $message, $file, $line)
  {
    if (!(error_reporting() & $severity) && $severity != E_ERROR) {
      /* do not show errors that are not included in error_reporting */
      return;
    }

    $debugPrintBacktraceClass = self::$showDebugPrintBacktrace ? '' : 'eh-hidden';
    $debugBacktraceClass = self::$showDebugBacktrace ? '' : 'eh-hidden';

    $levels = array(
      E_ERROR => 'Fatal Error',
      E_WARNING => 'Warning',
      E_PARSE => 'Parse Error',
      E_NOTICE => 'Notice',
      E_CORE_ERROR => 'Core Fatal Error',
      E_CORE_WARNING => 'Core Warning',
      E_COMPILE_ERROR => 'Compile Error',
      E_COMPILE_WARNING => 'Compile Warning',
      E_USER_ERROR => 'User Fatal Error',
      E_USER_WARNING => 'User Warning',
      E_USER_NOTICE => 'User Notice',
      E_STRICT => 'Strict',
      E_RECOVERABLE_ERROR => '',
      E_DEPRECATED => 'Deprecated',
      E_USER_DEPRECATED => 'User Deprecated',
      E_ALL => '',
    );

    /** DEBUG PRINT BACKTRACE */
    ob_start();
    debug_print_backtrace();
    $printbacktrace = explode("\n", ob_get_contents());
    array_pop($printbacktrace);
    $printbacktrace = '<li>'.implode('</li><li>', $printbacktrace).'</li>';
    ob_end_clean();

    /** BEAUTIFIED DEBUG BACKTRACE */
    $backtraceClass = self::$showBacktrace ? '' : 'eh-hidden';
    if ($severity != E_ERROR) {
      $backtrace = array_map(function ($trace) use ($file, $line) {
        return self::prepareTrace($trace, $file, $line);
      }, debug_backtrace());

      if (isset($backtrace[0]) && !$backtrace[0])
        array_shift($backtrace);

      $backtrace = count($backtrace) > 0
        ? '<li>'.implode('</li><li>', $backtrace).'</li>'
        : (self::$showCode ? '<li>'.self::printCode($file, $line).'</li>' : '');
    }
    else {
      $backtrace = self::$showCode ? '<li>'.self::printCode($file, $line).'</li>' : '';
    }

    /** PRINT_R DEBUG BACKTRACE */
    $printrbacktrace = debug_backtrace();
    ob_start();
    print_r($printrbacktrace);
    $printrbacktrace = '<pre>'.ob_get_contents().'</pre>';
    ob_end_clean();

    if (!self::$isCssPrinted) {
      echo <<<EOD
<style type="text/css">
  .eh-error-font {
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
    font-size: 13px;
  }
  .eh-error {
    background-color: #b0413e;
    color: white;
    padding: 20px;
  }
  .eh-backtrace {
    background-color: #f9f9f9;
    border: 1px solid #dddddd;
    border-top: 0;
  }
  .eh-backtrace ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
  }
  .eh-backtrace ul li {
    padding: 10px 20px;
    border-bottom: 1px solid #dddddd;
  }
  .eh-backtrace ul li:last-child {
    border-bottom: 0;
  }
  .eh-backtrace pre {
    margin: 10px 0;
    /*color: whitesmoke;*/
    overflow: auto;
    padding: 10px;
    border: 1px solid #eee;
    border-radius: .25rem;
    background-color: white;
  }
  .eh-hidden {
    display: none;
  }
  .eh-strong {
    font-weight: 500;
  }
  .eh-function {
    color: #b0413e;
    font-weight: 600;
  }
  .eh-class {
    color: #b0413e;
    font-weight: 400;
  }
  .eh-args {
    color: #666666;
  }
  .eh-eoe {
    border: 0;
    margin-bottom: 10px;
  }
</style>
EOD;
      self::printCodeViewerCss();
      self::$isCssPrinted = true;
    }
    echo <<<EOD
<div class="eh-error-font eh-error"><b>{$levels[$severity]}</b>: $message in <b>$file</b> on line <b>$line</b></div>
<div class="eh-error-font eh-backtrace $debugPrintBacktraceClass">
  <ul>$printbacktrace</ul>
</div>
<div class="eh-error-font eh-backtrace $backtraceClass">
  <ul>$backtrace</ul>
</div>
<div class="eh-error-font eh-backtrace $debugBacktraceClass">
  <ul><li>$printrbacktrace</li></ul>
</div>
<hr class="eh-eoe"/>
EOD;

    return !self::$showDefaultError;
  }

  public static function fatalErrorHandler()
  {
    $error = error_get_last();
    if ($error['type'] === E_ERROR)
      self::errorHandler(E_ERROR, $error['message'], $error['file'], $error['line']);
  }

  private static function printCodeViewerCss()
  {
    echo <<<EOD
<style type="text/css">
  /* code display */
  .eh-backtrace pre.eh-code-view {
    position: relative;
    border: 1px solid #eeeeee;
    background-color: #232323;
    border-radius: 0.25rem;
    padding: 10px;
    margin: 10px 0;
    overflow: auto;
  }
  .eh-backtrace pre.eh-code-view div, .eh-backtrace pre.eh-code-view span {
    font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
    font-size: 12px;
    color: #e6e1dc;
    color: #b9cce0;
  }
  .eh-backtrace pre .eh-code {
    margin-left: 45px;
  }
  .eh-backtrace pre .eh-line-numbers {
    position: absolute;
    top: 0;
    left: 0;
    padding: 10px 5px 10px 15px;
    background-color: #333333;
    font-weight: 300;
    color: #b6b1ac !important;
    min-width: 30px;
    text-align: right;
  }
  .eh-backtrace pre .eh-code .eh-current {
    width: 100%;
    display: inline-block;
    background-color: #2F2F2F;
  }
  .eh-backtrace pre .eh-line-numbers .eh-current {
    color: #fff;
  }
  .eh-code .eh-integer {
    color: #6ebcff !important;
  }
  .eh-code .eh-variable .eh-integer {
    color: #bf87ce !important;
  }
  .eh-code .eh-variable {
    color: #bf87ce !important;
  }
  .eh-code .eh-variable .eh-variable {
    color: #be69d5 !important;
  }
  .eh-code .eh-keyword {
    color: #ff840b !important;
  }
  .eh-code .eh-ponctuation {
    color: #ffa60b !important;
  }
  .eh-code .eh-string, 
  .eh-code .eh-string .eh-keyword,
  .eh-code .eh-string .eh-comment,
  .eh-code .eh-string .eh-property {
    color: #63a24f !important;
  }
  .eh-code .eh-string .eh-ponctuation {
    color: #51893f !important;
    color: #7ccb63 !important;
  }
  .eh-code .eh-string .eh-integer {
    color: #7ccb63 !important;
  }
  .eh-code .eh-string .eh-variable {
    color: #63a24f !important;
  }
  .eh-code .eh-string.eh-quote .eh-variable {
    color: #bf87ce !important;
  }
  .eh-code .eh-method {
    color: #ffc773 !important;
    font-weight: 300;
  }
  .eh-code .eh-property {
    color: #e74fb0 !important;
    color: #bf87ce !important;
  }
  .eh-code .eh-arobase {
    color: #777 !important;
  }
  .eh-code .eh-constant {
    color: #bf87ce !important;
  }
  .eh-code .eh-comment {
    color: #999 !important;
  }
  .eh-code .eh-comment .eh-variable,
  .eh-code .eh-comment .eh-integer,
  .eh-code .eh-comment .eh-ponctuation,
  .eh-code .eh-comment .eh-keyword,
  .eh-code .eh-comment .eh-method,
  .eh-code .eh-comment .eh-string .eh-property,
  .eh-code .eh-comment .eh-string,
  .eh-code .eh-comment .eh-string.eh-quote .eh-variable
  {
    color: #aaa !important;
  }
  
  .eh-code .eh-heredoc {
    color: #b9cce0 !important;
    color: #7cc963 !important;
    width: 100%;
    display: inline-block;
    background-color: #282c27;
  }
</style>
EOD;
  }

  public static function printCodeView($filepath)
  {
    echo '<div class="eh-backtrace">';
    self::printCodeViewerCss();
    echo self::printCode($filepath);
    echo '</div>';
  }

  private static function printCode($file, $line = 0)
  {
    list($nums, $lines) = self::getFileContent($file, $line);
    return <<<EOD
<pre class="eh-code-view"><!--
  --><div class="eh-code">$lines</div><!--
  --><div class="eh-line-numbers">$nums</div><!--
--></pre>
EOD;
  }

  private static function getFileContent($file, $line)
  {
    $lines = explode("\n", file_get_contents($file));

    $offset  = $line ? 5 : count($lines);
    $nums    = array();
    $extract = array();
    for($i = $line - $offset - 1; $i < $line + $offset; $i++)
      if(isset($lines[$i])) {
        if($i + 1 != $line) {
          $nums[]    = $i + 1;
          $extract[] = $lines[$i] ? self::beautifyPhp($lines[$i]) : '&nbsp;';
        }
        else {
          $nums[]    = '<span class="eh-current">'.($i + 1).'</span>';
          $extract[] = '<span class="eh-current">'.($lines[$i] ? self::beautifyPhp($lines[$i]) : '&nbsp;').'</span>';
        }
      }

    return array(implode("\n", $nums), implode("\n", $extract));
  }

  private static $heredoc = null;
  private static function beautifyPhp($php)
  {
    if (!self::$doBeautifyPhp)
      return htmlentities($php);

    /* heredoc end */
    if (self::$heredoc && !preg_match('`'.self::$heredoc.'`', $php)) {
      return '<span class="eh-heredoc">'.htmlentities($php).'</span>';
    }
    elseif (self::$heredoc) self::$heredoc = null;

    /* escape semicolon */
    $php = str_replace(';', '[semicolon]', $php);
    $php = htmlentities($php);

    $hasMethodDeclaration = false;
    if(preg_match('`((public|protected|private|)(\s+)function)\s+.*\(`', $php))
      $hasMethodDeclaration = true;

    /* DO NOT FORGET HERE the $php string has html special chars escaped so chars like <, >, &, ... are transformed */
    $keywords = '&lt;\?php|\?&gt;|namespace|use|class|extends|'
      .'require_once|require|include_once|include|const|'
      .'(public|protected|private)(\s+)(static\s+|)(function|)|public|private|protected|var|function|new|'
      .'return(\s+)(parent|self|array|true|false|null)|exit|throw|'
      .'return|parent|self|catch|try|array|echo self|echo|endif|else(\s+)if|if|else|elseif|endforeach|foreach|endfor|for|as|'
      .'continue|endswitch|switch|break|'
      .'null|true|false|\\\r\\\n|\\\r|\\\n|\\\t';
    $php = preg_replace(
      array(
        '`(?!\$)(?!_)(?!@)(^|[^;])('.$keywords.')(\W| |$)`',
        '`(,)`',
        '`(\$\$\w+)`',
        '`(\$\w+)`',
        '`(\'(.*?)\')`',
        '`(&quot;(.*?)&quot;)`',
        '`(/\*(.*?)\*/)`',
        '`(//.*|/\*.*|\*/)`',
        '`(^\W)(\*.*)`',
        '`(&gt;|::)(\w+)(\s*\()`',
        '`(&gt;)(\w+)`',
        '`(\d+)`',
        '`(@\w+)`',
      ),
      array(
        '$1<span class="eh-keyword">$2</span>$10',
        '<span class="eh-ponctuation">$1</span>',
        '<span class="eh-variable">$1</span>',
        '<span class="eh-variable">$1</span>',
        '<span class="eh-string">$1</span>',
        '<span class="eh-string eh-quote">$1</span>',
        '<span class="eh-comment">$1</span>',
        '<span class="eh-comment">$1</span>',
        '$1<span class="eh-comment">$2</span>',
        '$1<span class="eh-method">$2</span>$3',
        '$1<span class="eh-property">$2</span>',
        '<span class="eh-integer">$1</span>',
        '<span class="eh-arobase">$1</span>',
      ),
      $php);

    /* objects methods */
    if ($hasMethodDeclaration)
      $php = preg_replace('`(\w*)(\s*\()`', '<span class="eh-method">$1</span>$2', $php);

    /* heredoc start */
    if (preg_match('`&lt;&lt;&lt;(\w+)`', $php, $matches))
      self::$heredoc = $matches[1];

    /* restore semicolon */
    $php = str_replace('[semicolon]', '<span class="eh-ponctuation">;</span>', $php);

    /* trying to capture constant */
    $php = preg_replace(
      '`([(\[\.;])(\s*)(\w+)(\s*)([)\]\.;])`',
      '$1$2<span class="eh-constant">$3</span>$4$5',
      $php);

    return $php;
  }
}
J'édite souvent mon message après avoir répondu pour le corriger où y apporter des informations complémentaires alors n'hésitez pas à y jeter un nouveau coup d'oeil ^^