Overview

Namespaces

  • GAubry
    • ErrorHandler
    • Helpers

Classes

  • ErrorHandler
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: 
  3: namespace GAubry\ErrorHandler;
  4: 
  5: use GAubry\Helpers\Helpers;
  6: 
  7: /**
  8:  * Simple error and exception handler.
  9:  *   – wraps the error to an ErrorException instance according to error reporting level
 10:  *   – when running the PHP CLI, reports errors/exceptions to STDERR (even fatal error)
 11:  *     and uses exception code as exit status
 12:  *   – allows to deactivate '@' operator
 13:  *   – catches fatal error
 14:  *   – accepts callback to be executed at the end of the internal shutdown function
 15:  *   – accepts callback to display an apology when errors are hidden
 16:  *   – allows to ignore errors on some paths, useful with old libraries and deprecated code…
 17:  *
 18:  * Copyright (c) 2012 Geoffroy Aubry <geoffroy.aubry@free.fr>
 19:  * Licensed under the GNU Lesser General Public License v3 (LGPL version 3).
 20:  *
 21:  * @copyright 2012 Geoffroy Aubry <geoffroy.aubry@free.fr>
 22:  * @license http://www.gnu.org/licenses/lgpl.html
 23:  */
 24: class ErrorHandler
 25: {
 26: 
 27:     /**
 28:      * Error codes.
 29:      * @var array
 30:      * @see internalErrorHandler()
 31:      */
 32:     public static $aErrorTypes = array(
 33:         E_ERROR             => 'ERROR',
 34:         E_WARNING           => 'WARNING',
 35:         E_PARSE             => 'PARSING ERROR',
 36:         E_NOTICE            => 'NOTICE',
 37:         E_CORE_ERROR        => 'CORE ERROR',
 38:         E_CORE_WARNING      => 'CORE WARNING',
 39:         E_COMPILE_ERROR     => 'COMPILE ERROR',
 40:         E_COMPILE_WARNING   => 'COMPILE WARNING',
 41:         E_USER_ERROR        => 'USER ERROR',
 42:         E_USER_WARNING      => 'USER WARNING',
 43:         E_USER_NOTICE       => 'USER NOTICE',
 44:         E_STRICT            => 'STRICT NOTICE',
 45:         E_RECOVERABLE_ERROR => 'RECOVERABLE ERROR'
 46:     );
 47: 
 48:     /**
 49:      * CLI ?
 50:      * @var bool
 51:      */
 52:     private $bIsRunningFromCLI;
 53: 
 54:     /**
 55:      * Errors will be ignored on these paths.
 56:      * Useful with old libraries and deprecated code.
 57:      *
 58:      * @var array
 59:      * @see addExcludedPath()
 60:      */
 61:     private $aExcludedPaths;
 62: 
 63:     /**
 64:      * Callback to display an apology when errors are hidden.
 65:      * @var callback
 66:      */
 67:     private $callbackGenericDisplay;
 68: 
 69:     /**
 70:      * Callback to be executed at the end of the internal shutdown function
 71:      * @var callback
 72:      */
 73:     private $callbackAdditionalShutdownFct;
 74: 
 75:     /**
 76:      * Default config.
 77:      *   – 'display_errors'        => (bool) Determines whether errors should be printed to the screen
 78:      *                                as part of the output or if they should be hidden from the user.
 79:      *   – 'error_log_path'        => (string) Name of the file where script errors should be logged.
 80:      *   – 'error_reporting_level' => (int) Error reporting level.
 81:      *   – 'auth_error_suppr_op'   => (bool) Allows to deactivate '@' operator.
 82:      *   – 'default_error_code'    => (int) Default error code for errors converted into exceptions
 83:      *                                or for exceptions without code.
 84:      *   – 'error_div_class'       => (string) CSS class for <DIV> tags surrounding errors displayed
 85:      *                                in HTML context (non-CLI).
 86:      * @var array
 87:      */
 88:     private static $aDefaultConfig = array(
 89:         'display_errors'        => true,
 90:         'error_log_path'        => '',
 91:         'error_reporting_level' => -1,
 92:         'auth_error_suppr_op'   => false,
 93:         'default_error_code'    => 1,
 94:         'error_div_class'       => 'error'
 95:     );
 96: 
 97:     /**
 98:      * Configuration.
 99:      * @var array
100:      * @see self::$aDefaultConfig
101:      */
102:     private $aConfig;
103: 
104:     /**
105:      * Constructor.
106:      *
107:      * @param array $aConfig see self::$aDefaultConfig
108:      */
109:     public function __construct (array $aConfig = array())
110:     {
111:         $this->aConfig = Helpers::arrayMergeRecursiveDistinct(self::$aDefaultConfig, $aConfig);
112:         $this->aExcludedPaths = array();
113:         $this->bIsRunningFromCLI = defined('STDIN');    // or (PHP_SAPI === 'cli')
114:         $this->callbackGenericDisplay = array($this, 'displayDefaultApologies');
115:         $this->callbackAdditionalShutdownFct = '';
116: 
117:         error_reporting($this->aConfig['error_reporting_level']);
118:         if ($this->aConfig['display_errors'] && $this->bIsRunningFromCLI) {
119:             ini_set('display_errors', 'stderr');
120:         } else {
121:             ini_set('display_errors', $this->aConfig['display_errors']);
122:         }
123:         ini_set('log_errors', true);
124:         ini_set('html_errors', false);
125:         ini_set('display_startup_errors', true);
126:         if (! empty($this->aConfig['error_log_path'])) {
127:             ini_set('error_log', $this->aConfig['error_log_path']);
128:         }
129:         ini_set('ignore_repeated_errors', true);
130: 
131:         // Make sure we have a timezone for date functions. It is not safe to rely on the system's timezone settings.
132:         // Please use the date.timezone setting, the TZ environment variable
133:         // or the date_default_timezone_set() function.
134:         if (ini_get('date.timezone') == '') {
135:             date_default_timezone_set('Europe/Paris');
136:         }
137: 
138:         set_error_handler(array($this, 'internalErrorHandler'));
139:         set_exception_handler(array($this, 'internalExceptionHandler'));
140:         register_shutdown_function(array($this, 'internalShutdownFunction'));
141:     }
142: 
143:     /**
144:      * Allows to ignore errors on some paths, useful with old libraries and deprecated code…
145:      * Trailing slash is optional.
146:      *
147:      * @param string $sPath
148:      * @param bool   $bEnforce By default all paths are normalized with realpath().
149:      *   Set TRUE to avoid normalization.
150:      *   Useful, for example, with some external PHP modules:
151:      *   Couchbase PHP module say error was raised in "[CouchbaseNative]/" filename…
152:      * @see internalErrorHandler()
153:      */
154: 
155:     public function addExcludedPath ($sPath, $bEnforce=false)
156:     {
157:         if (substr($sPath, -1) !== '/') {
158:             $sPath .= '/';
159:         }
160:         if (! $bEnforce) {
161:             $sPath = realpath($sPath);
162:         }
163:         if (! in_array($sPath, $this->aExcludedPaths)) {
164:             $this->aExcludedPaths[] = $sPath;
165:         }
166:     }
167: 
168:     /**
169:      * Set callback to display an apology when errors are hidden.
170:      * Current \Exception will be provided in parameter.
171:      *
172:      * @param callback $cbGenericDisplay
173:      */
174:     public function setCallbackGenericDisplay ($cbGenericDisplay)
175:     {
176:         $this->callbackGenericDisplay = $cbGenericDisplay;
177:     }
178: 
179:     /**
180:      * Set callback to be executed at the end of the internal shutdown function.
181:      *
182:      * @param callback $cbAddShutdownFct
183:      */
184:     public function setCallbackAdditionalShutdownFct ($cbAddShutdownFct)
185:     {
186:         $this->callbackAdditionalShutdownFct = $cbAddShutdownFct;
187:     }
188: 
189:     /**
190:      * Customized error handler function: throws an Exception with the message error if @ operator not used
191:      * and error source is not in excluded paths.
192:      *
193:      * @param int    $iErrNo   level of the error raised.
194:      * @param string $sErrStr  the error message.
195:      * @param string $sErrFile the filename that the error was raised in.
196:      * @param int    $iErrLine the line number the error was raised at.
197:      * @throws \ErrorException if $iErrNo is present in $iErrorReporting
198:      * @return boolean true, then the normal error handler does not continue.
199:      * @see addExcludedPath()
200:      */
201:     public function internalErrorHandler ($iErrNo, $sErrStr, $sErrFile, $iErrLine)
202:     {
203:         // Si l'erreur provient d'un répertoire exclu de ce handler, alors l'ignorer.
204:         foreach ($this->aExcludedPaths as $sExcludedPath) {
205:             if (stripos($sErrFile, $sExcludedPath) === 0) {
206:                 return true;
207:             }
208:         }
209: 
210:         // Gestion de l'éventuel @ (error suppression operator) :
211:         if ($this->aConfig['error_reporting_level'] !== 0
212:             && error_reporting() === 0 && $this->aConfig['auth_error_suppr_op']
213:         ) {
214:             $iErrorReporting = 0;
215:         } else {
216:             $iErrorReporting = $this->aConfig['error_reporting_level'];
217:         }
218: 
219:         // Le seuil de transformation en exception est-il atteint ?
220:         if (($iErrorReporting & $iErrNo) !== 0) {
221:             $msg = "[from error handler] " . self::$aErrorTypes[$iErrNo]
222:                  . " -- $sErrStr, in file: '$sErrFile', line $iErrLine";
223:             throw new \ErrorException($msg, $this->aConfig['default_error_code'], $iErrNo, $sErrFile, $iErrLine);
224:         }
225:         return true;
226:     }
227: 
228:     /**
229:      * Exception handler.
230:      * @SuppressWarnings(ExitExpression)
231:      *
232:      * @param \Exception $oException
233:      */
234:     public function internalExceptionHandler (\Exception $oException)
235:     {
236:         if (! $this->aConfig['display_errors'] && ini_get('error_log') !== '' && ! $this->bIsRunningFromCLI) {
237:             call_user_func($this->callbackGenericDisplay, $oException);
238:         }
239:         $this->log($oException);
240:         if ($oException->getCode() != 0) {
241:             $iErrorCode = $oException->getCode();
242:         } else {
243:             $iErrorCode = $this->aConfig['default_error_code'];
244:         }
245:         exit($iErrorCode);
246:     }
247: 
248:     /**
249:      * Default callback to display an apology when errors are hidden.
250:      */
251:     public function displayDefaultApologies ()
252:     {
253:         echo '<div class="exception-handler-message">We are sorry, an internal error occurred.<br />'
254:              . 'We apologize for any inconvenience this may cause</div>';
255:     }
256: 
257:     /**
258:      * Registered shutdown function.
259:      */
260:     public function internalShutdownFunction ()
261:     {
262:         $aError = error_get_last();
263:         if (! $this->aConfig['display_errors'] && is_array($aError) && $aError['type'] === E_ERROR) {
264:             $oException = new \ErrorException(
265:                 $aError['message'],
266:                 $this->aConfig['default_error_code'],
267:                 $aError['type'],
268:                 $aError['file'],
269:                 $aError['line']
270:             );
271:             call_user_func($this->callbackGenericDisplay, $oException);
272:         }
273:         if (! empty($this->callbackAdditionalShutdownFct)) {
274:             call_user_func($this->callbackAdditionalShutdownFct);
275:             // @codeCoverageIgnoreStart
276:         }
277:     }
278:     // @codeCoverageIgnoreEnd
279: 
280:     /**
281:      * According to context, logs specified error into STDERR, STDOUT or via error_log().
282:      *
283:      * @param mixed $mError Error to log. Can be string, array or object.
284:      */
285:     public function log ($mError)
286:     {
287:         if (is_array($mError) || (is_object($mError) && ! ($mError instanceof \Exception))) {
288:             $mError = print_r($mError, true);
289:         }
290: 
291:         if ($this->aConfig['display_errors']) {
292:             if ($this->bIsRunningFromCLI) {
293:                 file_put_contents('php://stderr', $mError . "\n", E_USER_ERROR);
294:             } else {
295:                 echo '<div class="' . $this->aConfig['error_div_class'] . '">' . $mError . '</div>';
296:             }
297:         }
298: 
299:         if (! empty($this->aConfig['error_log_path'])) {
300:             error_log($mError);
301:         }
302:     }
303: }
304: 
ErrorHandler API documentation generated by ApiGen 2.8.0