Skip to content

Instantly share code, notes, and snippets.

@psgganesh
Last active August 14, 2017 02:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save psgganesh/79dc2ce384185f84d2970f9de8364119 to your computer and use it in GitHub Desktop.
Save psgganesh/79dc2ce384185f84d2970f9de8364119 to your computer and use it in GitHub Desktop.
Hijack laravel exceptions

In-house incident handling

Just like how services like

Would do. Just one change, those would be added inside laravel project as a package where they would hijack the same from.

So anytime any error occurs, the dev. team will get a mail immediately so that we can manage such incidents.

If we want to know the GET or POST parameters of that request we can add this later in the tool, to mail it for us

Points to remember

  • Remember to also update the .env file with your mailgun credentials
  • Customize the SendEmails.php as per your needs, do not use or introduce $message variable as Laravel already uses the same variable in it's framework
  • If you do not want basic 404 exceptions to be mailed to your developer / ellipsonic email id, please update the protected $dontReport array present in app/Exceptions/Handler.php

How to include into your project

  • Copy paste the SendEmails.php inside App\Console\Commands - don't forget to add it as a command in it's Kernel.php
  • Add ErrorOccurredEvent.php inside App\Events
  • Add IssueEventListener.php inside App\Listeners
  • Add event and event listener inside the EventServiceProvider.php

PS:

  • Please make sure you test the app once all these are done, in-order to ensure the existing features are not broken
  • Mailgun sandbox account would not send mail if the project is in production *

Let's get into details, how does this work ?

Let's not forget the base truth, laravel is an excellent framework, build on top of ' symfony ' php framework. Now that being said, every time an error occurs, the \Symfony\Component\HttpKernel\Exception\NotFoundHttpException class handles the exception. Hence the following steps

  • We catch the exception using the same \Symfony\Component\HttpKernel\Exception\NotFoundHttpException class
  • We trigger the ErrorOccurredEvent which in turn triggers IssueEventListener class
  • This IssueEventListener will run the SendEmails console commmand and send the email to the developer about the exception

Advantage

No need to handle every exception on our code which we create, all default exceptions will be handedle here, this is the parent of the exception handler, which means, less work for us and whenever an exception occurs the mail will have complete details of

  • Which class
  • Which method
  • Until which line number

Just like papertrial. Decorate the email view the way you want it to look like.

Good news Laravel 5.5 will have more handlers in-built which will help us more as we improve this exception handling. Will update this gist / add a new gist once Laravel 5.5 is released.

<?php
namespace App\Events;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class ErrorOccurredEvent extends Event
{
use SerializesModels;
public $headers;
public $message;
public $code;
public $file;
public $line;
public $fullUrl;
public $trace;
public $parentClass;
public $exception;
public $statusCode;
/**
* Create a new event instance.
*
* @param Exception $exception
* @param Request $request
* @param int $statusCode
*/
public function __construct(Exception $exception, Request $request, $statusCode)
{
$this->message = $exception->getMessage();
$this->fullUrl = $request->fullUrl();
$this->headers = $request->header();
$this->parentClass = get_class($exception);
$this->exception = $exception->getTraceAsString();
$this->statusCode = $statusCode;
}
/**
* Get the channels the event should be broadcast on.
*
* @return array
*/
public function broadcastOn()
{
return [];
}
}
<?php
namespace App\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\ErrorOccurredEvent' => [
'App\Listeners\IssueEventListener',
],
];
/**
* Register any other events for your application.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function boot(DispatcherContract $events)
{
parent::boot($events);
//
}
}
<?php
namespace App\Exceptions;
use App\Events\ErrorOccurredEvent;
use Illuminate\Http\Response;
use Exception;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $e
* @return void
*/
public function report(Exception $e)
{
parent::report($e);
if ($e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException)
{
event( new ErrorOccurredEvent( $e, request(), 404 ) );
} else {
event(new ErrorOccurredEvent($e, request(), 500));
}
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
return parent::render($request, $e);
}
}
<?php
namespace App\Listeners;
use App\Events\ErrorOccurredEvent;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
class IssueEventListener
{
/**
* Create the event listener.
*
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ErrorOccurredEvent $event
*
*/
public function handle(ErrorOccurredEvent $event)
{
Log::error('Event occurred: '.$event->statusCode);
$data['statusCode'] = $event->statusCode;
$subject = $event->statusCode.' exception occurred in '.env('APP_NAME', null).' project';
Artisan::call('emails:send', [
'type' => 'exception',
'name' => 'developer',
'email' => 'psgganesh@gmail.com',
'--subject' => $subject,
'--caption' => $event->message,
'--fullUrl' => $event->fullUrl,
'--headers' => $event->headers['user-agent'][0],
'--parentClass' => $event->parentClass,
'--exceptionTrace' => $event->exception
]);
}
}
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
Commands\Inspire::class,
Commands\SendEmails::class
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')
// ->hourly();
}
}
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendEmails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'emails:send {type} {name} {email} {--subject=Greetings from application} {--token=NULL} {--caption=NULL} {--fullUrl=NULL} {--headers=NULL} {--parentClass=NULL} {--exceptionTrace=NULL}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send emails to a specified user';
/**
* Create a new command instance.
*
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$name = $this->argument('name');
$email = $this->argument('email');
$subject = $this->option('subject');
$token = $this->option('token');
$caption = $this->option('caption');
$fullUrl = $this->option('fullUrl');
$headers = $this->option('headers');
$parentClass = $this->option('parentClass');
$exceptionTrace = $this->option('exceptionTrace');
if(is_null($token)) {
Mail::send('emails.'.$this->argument('type').'', [
'name' => $name,
'email' => $email,
'subject' => $subject
], function ($mail) use ($name, $email, $subject) {
$mail->subject($subject);
$mail->to($email, $name);
});
} else {
switch($this->argument('type'))
{
case "exception":
Mail::send('emails.'.$this->argument('type').'', [
'name' => $name,
'email' => $email,
'subject' => $subject,
'caption' => $caption,
'fullUrl' => $fullUrl,
'headers' => $headers,
'parentClass' => $parentClass,
'exceptionTrace' => $exceptionTrace,
], function ($mail) use ($name, $email, $subject, $caption, $fullUrl, $headers, $parentClass, $exceptionTrace) {
$mail->subject($subject);
$mail->to($email, $name);
});
break;
default:
Mail::send('emails.'.$this->argument('type').'', [
'name' => $name,
'email' => $email,
'token' => $token,
'subject' => $subject
], function ($mail) use ($name, $email, $subject, $token) {
$mail->subject($subject);
$mail->to($email, $name);
});
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment