core/ErrorHandler.php
<?php
namespace CodeJetter\core;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ChromePHPHandler;
use Monolog\Handler\HipChatHandler;
use Monolog\Handler\MongoDBHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
/**
* Class ErrorHandler.
*
* A class to handle errors, exceptions and also to log errors, notices, etc.
*
* How to use it:
* (new ErrorHandler())->run();
*
* This will set the custom error handler to log errors including fatal and notices using a logger (currently Monolog)
*
* In order to log errors manually use it like this:
* (new ErrorHandler())->logError('this is a test');
*/
class ErrorHandler
{
private $configs;
private $logger;
/**
* ErrorHandler constructor.
*
* @param array $configs
*/
public function __construct(array $configs = [])
{
// overwrite the default configs if $configs is passed
$defaultConfigs = Registry::getConfigClass()->get('errorHandler');
$this->setConfigs(array_merge($defaultConfigs, $configs));
$this->initLogger();
}
/**
* @return array
*/
public function getConfigs()
{
return $this->configs;
}
/**
* @param array $configs
*/
public function setConfigs(array $configs)
{
$this->configs = $configs;
}
/**
* @param Logger $logger
* @param $handlerKey
* @param array $handlerConfig
*
* @throws \Exception
*
* @return Logger
*/
private function pushMonologHandler(Logger $logger, $handlerKey, $handlerConfig = [])
{
$activeHandlers = [];
switch ($handlerKey) {
case 'hipchat':
if (empty($handlerConfig['token'])) {
throw new \Exception('Token cannot be empty for Hipchat handler');
}
if (empty($handlerConfig['room'])) {
throw new \Exception('Room cannot be empty for Hipchat handler');
}
$handler = new HipChatHandler($handlerConfig['token'], $handlerConfig['room']);
$handler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context%\n"));
$activeHandlers[] = $handler;
break;
case 'mongo':
if (empty($handlerConfig['server']) || empty($handlerConfig['db'])
|| empty($handlerConfig['collection'])) {
throw new \Exception('MongoDB details are not provided');
}
$activeHandlers[] = new MongoDBHandler(
new \MongoClient($handlerConfig['server']),
$handlerConfig['db'],
$handlerConfig['collection']
);
// Status 'new' means that the error has not been addressed yet
$logger->pushProcessor(function ($record) {
$record['status'] = 'new';
return $record;
});
break;
case 'chrome':
$activeHandlers[] = new ChromePHPHandler();
break;
case 'file':
$path = Registry::getConfigClass()->get('URI').$handlerConfig['path'];
if (empty($path)) {
throw new \Exception('File path is empty');
}
$activeHandlers[] = new StreamHandler($path);
break;
}
if (!empty($activeHandlers)) {
foreach ($activeHandlers as $activeHandler) {
$logger->pushHandler($activeHandler);
}
}
return $logger;
}
/**
* @throws \Exception
*/
private function initLogger()
{
// get options
$configs = $this->getConfigs();
$channelName = isset($configs['monolog']['channel']) ? $configs['monolog']['channel'] : '';
// init the logger
$logger = new Logger($channelName);
/*
* Start pushing handlers
*/
if (empty($configs['monolog']['handlers'])) {
throw new \Exception('Monolog must have at least one handler');
}
foreach ($configs['monolog']['handlers'] as $handlerKey => $handlerConfig) {
if ($handlerConfig['active'] === true) {
$logger = $this->pushMonologHandler($logger, $handlerKey, $handlerConfig);
}
}
/*
* Finish pushing handlers
*/
$this->setLogger($logger);
}
/**
* @return LoggerInterface
*/
private function getLogger()
{
return $this->logger;
}
/**
* @param LoggerInterface $logger
*/
private function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @return bool
*/
public function run()
{
if ($this->getConfigs()['inOperation'] !== true) {
return false;
}
$this->registerShutdownFunction();
$this->setErrorHandler();
$this->setExceptionHandler();
return $this;
}
/**
* This is used to handle fatal errors since set_error_handler cannot do that.
*/
private function registerShutdownFunction()
{
register_shutdown_function(function () {
$error = error_get_last();
if (isset($error['type']) && $error['type'] === E_ERROR) {
if ($this->canLog($error['message'], $error['file']) === true) {
$this->logError($error['message'], [
'module' => $this->extractComponentName($error['file']),
'number' => $error['type'],
'line' => $error['line'],
'file' => $error['file'],
]);
}
}
});
}
private function setErrorHandler()
{
set_error_handler(function ($errorNo, $errorMessage, $errorFile, $errorLine) {
if ($this->canLog($errorMessage, $errorFile) === true) {
$this->logError($errorMessage, [
'component' => $this->extractComponentName($errorFile),
'number' => $errorNo,
'line' => $errorLine,
'file' => $errorFile,
]);
}
if ($this->getConfigs()['bypassInternalErrorHandler'] === true) {
// Do NOT execute PHP internal error handler
return true;
} else {
// Execute PHP internal error handler as well
return false;
}
});
}
private function canLog($errorMessage, $errorFile)
{
$configs = $this->getConfigs();
if ($configs['inOperation'] !== true) {
return false;
}
if ($configs['respectErrorReporting'] === true && error_reporting() == 0) {
return false;
}
if ($configs['respectDisplayErrors'] === true && empty(ini_get('display_errors'))) {
return false;
}
if ($this->isInBlacklist($errorMessage, $errorFile) !== false) {
return false;
}
return true;
}
private function setExceptionHandler()
{
set_exception_handler(function (\Exception $exception) {
if ($this->canLog($exception->getMessage(), $exception->getFile()) === true) {
$this->logError($exception->getMessage(), [
'component' => $this->extractComponentName($exception->getFile()),
'number' => $exception->getCode(),
'line' => $exception->getLine(),
'file' => $exception->getFile(),
]);
}
});
}
/**
* @param $errorFile
*
* @return string
*/
private function extractComponentName($errorFile)
{
// TODO implement body
}
/**
* @param $errorMessage
* @param $errorFile
*
* @return bool
*/
private function isInBlacklist($errorMessage, $errorFile)
{
$configs = $this->getConfigs();
if ($configs['blacklist']['inOperation'] !== true) {
return false;
}
/*
* Start checking against strings
*/
if (!empty($configs['blacklist']['strings'])) {
foreach ($configs['blacklist']['strings'] as $string) {
if (strpos($errorMessage, $string) !== false) {
return true;
}
}
}
/*
* Finish checking against strings
*/
/*
* Start checking against regular expressions
*/
if (!empty($configs['blacklist']['regex'])) {
foreach ($configs['blacklist']['regex'] as $regex) {
if (preg_match($regex, $errorMessage)) {
return true;
}
}
}
/**
* Finish checking against regular expressions.
*/
$component = $this->extractComponentName($errorFile);
if (!empty($component) && in_array($component, $configs['blacklist']['components'])) {
return true;
}
// did not find in the blacklist, return false
return false;
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logDebug($message, array $context = [])
{
$this->logMessage('debug', $message, $context);
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logInfo($message, array $context = [])
{
$this->logMessage('info', $message, $context);
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logNotice($message, array $context = [])
{
$this->logMessage('notice', $message, $context);
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logWarning($message, array $context = [])
{
$this->logMessage('warning', $message, $context);
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logError($message, array $context = [])
{
$this->logMessage('error', $message, $context);
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logCritical($message, array $context = [])
{
$this->logMessage('critical', $message, $context);
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logAlert($message, array $context = [])
{
$this->logMessage('alert', $message, $context);
}
/**
* @param $message
* @param array $context
*
* @throws \Exception
*/
public function logEmergency($message, array $context = [])
{
$this->logMessage('emergency', $message, $context);
}
/**
* @param $level
* @param $message
* @param array $context
*
* @throws \Exception
*
* @return bool
*/
private function logMessage($level, $message, $context = [])
{
if ($this->getConfigs()['inOperation'] !== true) {
return false;
}
switch ($level) {
case 'debug':
$this->getLogger()->debug($message, $context);
break;
case 'info':
$this->getLogger()->info($message, $context);
break;
case 'notice':
$this->getLogger()->notice($message, $context);
break;
case 'warning':
$this->getLogger()->warning($message, $context);
break;
case 'error':
$this->getLogger()->error($message, $context);
break;
case 'critical':
$this->getLogger()->critical($message, $context);
break;
case 'alert':
$this->getLogger()->alert($message, $context);
break;
case 'emergency':
$this->getLogger()->emergency($message, $context);
break;
default:
throw new \Exception("'{$level}' is invalid log level");
}
}
}