Customizing Ignition with custom solutions

Posted by on 2nd Sep 2019

Customizing Ignition with custom solutions

Last Friday, my friend Freek Van der Herten and I announced two big projects that we've been working on for the last 8 months.

Flare - an error tracker built specifically for Laravel. You can find out all about it in our extensive blogpost and get on the early access list on the Flare homepage.

And Ignition a beautiful error page for your Laravel applications. It becomes the default error page in Laravel 6.

One feature that sets Ignition apart from other error pages is the fact that we do not solely focus on the error, but we try to deliver solutions for the user too. These solutions are simple textual helpers, but can also be runnable and perform custom actions.

These solutions can be provided in various different ways. Let's look at how it works.

An exception with a solution

This is the most straight forward approach. If you want to provide a solution as part of your own exceptions (either in your own application codebase or in a package) you can do this by implementing the ProvidesSolution interface that comes with out facade/ignition-contracts package.

Here's how a simple example looks like:

namespace App\Http\App;

use Exception;
use Facade\IgnitionContracts\Solution;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;

class MyException extends Exception implements ProvidesSolution
{
    /** @return  \Facade\IgnitionContracts\Solution */
    public function getSolution(): Solution
    {
        return BaseSolution::create('My solution title')
            ->setSolutionDescription('My solution description')
            ->setDocumentationLinks([
                'My docs' => 'https://flareapp.io/docs',
            ]);
    }
}

Now whenever you throw this exception two things can happen:

  1. If you are in your local environment and have debug mode enabled, Ignition is going to pick up the exception and display it along with the solution on your local Ignition error page.

  2. If your application is in production mode and you have a Flare subscription, the exception will be sent to Flare along with the solution and you can review it.

Alright. So custom exceptions can either provide basic textual solutions, or more complex runnable solutions.

Let's take a look at one of these.

An exception with a runnable solution

An exception with a runnable solution works the same as a generic solution. The only difference is that you create your own solution class and implement the logic that should be executed once a user "runs" your solution.

Let's see the exception code again:

class MyException extends Exception implements ProvidesSolution
{
     public function getSolution(): Solution
    {
        return new MyRunnableSolution();
    }
}

So we just create our own solution class that implements the Facade\IgnitionContracts\RunnableSolution interface.

This is the GenerateAppKeySolution that we have included in Ignition:

<?php

namespace Facade\Ignition\Solutions;

use Illuminate\Support\Facades\Artisan;
use Facade\IgnitionContracts\RunnableSolution;

class GenerateAppKeySolution implements RunnableSolution
{
    public function getSolutionTitle(): string
    {
        return 'Your app key is missing';
    }

    public function getDocumentationLinks(): array
    {
        return [
            'Laravel installation' => 'https://laravel.com/docs/master/installation#configuration',
        ];
    }

    public function getSolutionActionDescription(): string
    {
        return 'Generate your application encryption key using `php artisan key:generate`.';
    }

    public function getRunButtonText(): string
    {
        return 'Generate app key';
    }

    public function getSolutionDescription(): string
    {
        return '';
    }

    public function getRunParameters(): array
    {
        return [];
    }

    public function run(array $parameters = [])
    {
        Artisan::call('key:generate');
    }
}

So as you can see, runnable solutions can perform any task that you want and even send custom parameters along.

Solution providers

So adding solutions to your own exceptions is great - but sometimes it would be good to provide solutions for exceptions we do not have any control over. Like basic PHP exceptions or exceptions coming from other packages.

For this, we have added solution providers. Solution providers can be registered by yourself or other packages.

Under the hood, when an exception gets thrown, Ignition loops through all registered solution providers and checks if each provider could provide one or multiple solutions for the given exception.

Here is how such a solution provider looks like. This is again taken from Ignition itself - it's the MissingAppKeySolutionProvider:

use Throwable;
use RuntimeException;
use Facade\Ignition\Solutions\GenerateAppKeySolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;

class MissingAppKeySolutionProvider implements HasSolutionsForThrowable
{
    public function canSolve(Throwable $throwable): bool
    {
        if (! $throwable instanceof RuntimeException) {
            return false;
        }

        return $throwable->getMessage() === 'No application encryption key has been specified.';
    }

    public function getSolutions(Throwable $throwable): array
    {
        return [new GenerateAppKeySolution()];
    }
}

In the canSolve method we receive the exception that our specific solution provider hopefully can solve. In order to provide a solution for a missing app key exception, we need to see if the given exception is a RuntimeException and has the message 'No application encryption key has been specified.'.

If that's the case, Ignition will add all provided solutions in the getSolutions method to the Ignition page (or send them along to Flare if the exception happens in production).

In case there are multiple solutions possible for a given exception, Ignition is going to display a paginated view of the solution cards:

Next, you must register your solution provider. This can typically be done in a service provider:

namespace App\Providers;

use App\Solutions\GenerateAppKeySolution;
use Facade\IgnitionContracts\SolutionProviderRepository;
use Illuminate\Support\ServiceProvider;

class YourServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return  void
     */
    public function register(SolutionProviderRepository $solutionProviderRepository)
    {
        $solutionProviderRepository->registerSolutionProvider(GenerateAppKeySolution::class);

        // alternatively you can register multiple solution providers at once
        $solutionProviderRepository->registerSolutionProviders([
            MySolution::class,
            AnotherSolution::class,
        ]);
    }
}

With solution providers, Ignition will just become better and smarter over time. Not only are these solutions possible for exceptions that life outside of your own codebase, but these solutions are also being sent to Flare so you know exactly which solutions can be applied to your production errors.

If you want to learn more about Ignition, take a look at the official documentation and our GitHub repository if you want to improve Ignition with a custom solution provider.

To benefit from these solutions in your production environment, go and sign up for early-access on Flare - our error reporter for Laravel.