<?php
class ErrorHandler {
/**
* Error number constant used when an error is silenced by use of the error
* control operator '@'.
*
* @var int
*/
const E_SILENCED_ERROR = 0;
/**
* Chain before
* @var string
*/
const CHAIN_BEFORE = 'before';
/**
* Chain after
* @var string
*/
const CHAIN_AFTER = 'after';
/**
* Don't chain
* @var bool
*/
const CHAIN_NONE = false;
/**
* @var string|bool
*/
private $_chainPreviousHandler = self::CHAIN_AFTER;
/**
* Gets if the previous handler will be called. Values are: before, after, false
* @return string|bool
*/
public function getChainPreviousHandler() {
return $this->_chainPreviousHandler;
}
/**
* Sets if the previous handler will be called. Values are: before, after, false
* @param string|bool $chainPreviousHandler
*/
public function setChainPreviousHandler($chainPreviousHandler) {
$this->_chainPreviousHandler = $chainPreviousHandler;
}
/**
* @var bool
*/
private $_usePhpDefaultBehaviour = true;
/**
* Gets if the default PHP behaviour for errors will be allowed at the end
* of the error handling.
* @return bool
*/
public function isUsePhpDefaultBehaviour() {
return $this->_usePhpDefaultBehaviour;
}
/**
* Sets if the default PHP behaviour for errors will be allowed at the end
* of the error handling.
* @param bool $usePhpDefaultBehaviour
*/
public function setUsePhpDefaultBehaviour($usePhpDefaultBehaviour) {
$this->_usePhpDefaultBehaviour = $usePhpDefaultBehaviour;
}
/**
* @var bool
*/
private $_convertErrorsToExceptions = true;
/**
* Gets if this handler will convert recoverable errors to exceptions
* @return bool
*/
public function isConvertErrorsToExceptions() {
return $this->_convertErrorsToExceptions;
}
/**
* Sets if this handler will convert recoverable errors to exceptions
* @param bool $convertErrorsToExceptions
*/
protected function setConvertErrorsToExceptions($convertErrorsToExceptions) {
$this->_convertErrorsToExceptions = $convertErrorsToExceptions;
}
/**
* Array of default errors converted to exceptions.
* @var array
*/
private $_errorsToExceptions = array(
self::E_SILENCED_ERROR, // Silenced errors
E_USER_ERROR,
E_RECOVERABLE_ERROR);
/**
* Gets an array with all the error numbers to be converted to exceptions
* @return array
*/
public function getErrorsToExceptions() {
return $this->_errorsToExceptions;
}
/**
* Sets an array with all the error numbers to be converted to exceptions
* @param array $errorsToExceptions
*/
public function setErrorsToExceptions(array $errorsToExceptions) {
$this->_errorsToExceptions = $errorsToExceptions;
}
/**
* Adds errors to the array of errors that will throw an exception
* @param array $errorsToExceptions
*/
public function addErrorsToExceptions(array $errorsToExceptions) {
foreach($errorsToExceptions as $pos => $errorNumber) {
if (!in_array($errorNumber, $this->_errorsToExceptions)) {
$this->_errorsToExceptions[] = $errorNumber;
}
}
}
/**
* @var bool
*/
private $_registered = false;
/**
* Gets if this class was registered to be the default error handler for
* php errors.
* @return bool
*/
public function isRegistered() {
return $this->_registered;
}
/**
* Sets if this class was registered to be the default error handler for
* php errors.
* @param bool $registered
*/
protected function setRegistered($registered) {
$this->_registered = $registered;
}
/**
* @var mixed
*/
private $_previousHandler = null;
/**
* Gets the previously set global error handler
* @return mixed
*/
public function getPreviousErrorHandler() {
return $this->_previousHandler;
}
/**
* Sets the previously set global error handler
* @param mixed $previousHandler
*/
protected function setPreviousErrorHandler($previousHandler) {
$this->_previousHandler = $previousHandler;
}
/**
* @var array
*/
private $_errorHandlers = array();
/**
* Gets stack of error handlers
* @return array
*/
public function getErrorHandlers() {
return $this->_errorHandlers;
}
/**
* Adds a handler to the stack of error handlers
* @param callable $errorHandler
*/
public function addErrorHandler(callable $errorHandler) {
// Avoid duplicated error handlers
$this->removeErrorHandler($errorHandler);
$this->_errorHandlers[]= $errorHandler;
}
/**
* Remove the error handler comparing the one provided with the ones stored.
* It does not affect the previous error handler.
* @param callable $errorHandler
* @return boolean True if an error handler was found and removed or false
* if not.
*/
public function removeErrorHandler($errorHandler) {
$type = gettype($errorHandler);
$found = false;
foreach($this->_errorHandlers as $pos => $existingErrorHandler) {
if ($type != gettype($existingErrorHandler)) {
continue;
}
$remove = false;
if ($type == 'string'
&& $errorHandler == $existingErrorHandler) {
$found = true;
}
else if ($type == 'array'
&& $errorHandler[0] == $existingErrorHandler[0]
&& $errorHandler[1] == $existingErrorHandler[1]) {
$found = true;
}
else if (is_object($errorHandler)
&& $errorHandler == $existingErrorHandler) {
$found = true;
}
if ($remove) {
unset($this->_errorHandlers[$pos]);
$found = true;
break;
}
}
return $found;
}
/**
* Finds the name of an error constant.
*
* @param int $error
* @return string | false
*/
public function getErrorName($error) {
$constants = get_defined_constants();
$name = array_search($error, $constants, true);
return $name;
}
/**
* Calls every error handler passing in the arguments.
*
* If an error handler throws an exception the rest of error handlers will
* not be called.
*
* If this class is set as the default error handler the previous handler
* will be called after all handlers have been called.
*
* Error handlers must conform to standard error handler signature for
* set_error_handler function but with an additional optional parameter
* whose value will be this same instance.
*
* This method follows the following order:
* 1- Execute previous error handler (only uf previous error handler is
* chained before)
* 2- Execute error handlers
* 3- Execute previous error handler (only uf previous error handler is
* chained after)
* 4- Throw exceptions for errors (optional)
* 5- Call PHP's default error handler if configured to do so.
*
* Error handlers should not raise more errors but they can throw exceptions
* as they will not be handled in this method so they can be handled
* upstream.
*
* @param int $errorNumber
* @param string $errorMessage
* @param string $file
* @param int $line
* @param array $context
* @throws \ErrorException When error to exception conversion is enabled
* @throws \Exception When any error handler throws it
* @return bool
*/
public function handleError($errorNumber, $errorMessage, $file, $line, $context) {
$errorParams = array(
$errorNumber,
$errorMessage,
$file,
$line,
$context,
$this
);
if ($this->_previousHandler
&& $this->_chainPreviousHandler == self::CHAIN_BEFORE) {
call_user_func_array($this->_previousHandler, $errorParams);
}
foreach($this->_errorHandlers as $errorHandler) {
call_user_func_array($errorHandler, $errorParams);
}
if ($this->_previousHandler
&& $this->_chainPreviousHandler == self::CHAIN_AFTER) {
call_user_func_array($this->_previousHandler, $errorParams);
}
// The last thing we do is throwing ourselves an exception in the
// error handler as this will halt execution and we need to call
// all error handlers first.
if ($this->_convertErrorsToExceptions) {
$this->throwErrorException($errorNumber, $errorMessage, $file, $line, $context);
}
if ($this->_usePhpDefaultBehaviour) {
return false;
}
else {
return true;
}
}
/**
* Throws an ErrorException with the given information.
*
* This method does not honors error_reporting setting and will throw an
* exception for every configured error number as it makes no sense to
* convert a certain error to an exception and then allow it to be
* "silenced".
*
* @param int $errorNumber
* @param string $errorMessage
* @param string $file
* @param int $line
* @param array $context
* @throws \ErrorException Always
*/
protected function throwErrorException($errorNumber, $errorMessage, $file, $line, array $context) {
if (in_array($errorNumber, $this->_errorsToExceptions)) {
throw new \ErrorException(
$errorMessage, $errorNumber, 0, $file, $line);
}
}
/**
* Registers this instance to handle PHP's errors (recoverable ones only)
* @throws \Exception If the error handler is registered twice for some
* reason.
* @return boolean True if this handler was not registered, false if it
* was.
*/
public function registerErrorHandler() {
if ($this->_registered) {
return false;
}
$previousHandler = set_error_handler(array($this, 'handleError'));
if (is_array($previousHandler)
&& !empty($previousHandler)
&& $previousHandler[0] === $this) {
throw new \Exception("Error handler registered twice!");
}
$this->_previousHandler = $previousHandler;
$this->_registered = true;
return true;
}
/**
* Unregisters this instance if it was set to handle PHP's errors
* @return boolean True if this handler was registered, false if it doesn't.
*/
public function unregisterErrorHandler() {
if (!$this->_registered) {
return false;
}
// If one or more error handlers were set after we registered our own
// this will pop the last one set and leave our own one in the stack
// :(
restore_error_handler();
$this->_registered = false;
$this->_previousHandler = null;
return true;
}
}