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: