Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ignition Contracts Laravel Package

spatie/ignition-contracts

Contracts for spatie/ignition that define interfaces for building custom error pages. Provides shared abstractions so solutions can integrate with Ignition’s error reporting and rendering workflow.

View on GitHub
Deep Wiki
Context7

Getting Started

Check what you already have before installing anything. spatie/ignition-contracts is almost always a transitive dependency of Ignition. Running composer require spatie/ignition-contracts on a Laravel app that already has Ignition v2+ gets you a second, parallel set of interfaces in a different namespace — that's the single most common way to waste an afternoon.

composer show spatie/ignition-contracts        # is it even in the lockfile?
composer why spatie/ignition-contracts         # who pulled it in, at what constraint
grep -rl "interface Solution" vendor/spatie/ignition*/src | head

The last command tells you which namespace is actually live:

  • vendor/spatie/ignition-contracts/src/Solution.php → Spatie\IgnitionContracts\Solution
  • vendor/spatie/ignition/src/Contracts/Solution.php → Spatie\Ignition\Contracts\Solution

Write every use statement against whichever one grep found. Ignore all tutorial code you find online until you've confirmed it.

Where to look first. The contracts package itself is five files; read them all in one sitting. Then read Ignition's rendering of the solution block — that's the only consumer, and it defines the real behaviour (ordering, when run() is offered, whether links render). Finally look at config/ignition.php, specifically enable_runnable_solutions, which gates whether your RunnableSolution buttons appear at all.

First use case: one exception renders one solution. Don't build a provider repository on day one. Put the interface on the exception itself.

use Spatie\Ignition\Contracts\Solution;          // or Spatie\IgnitionContracts\Solution
use Spatie\Ignition\Contracts\ProvidesSolution;

class WebhookSecretMissing extends RuntimeException implements ProvidesSolution
{
    public function getSolution(): Solution
    {
        return new MissingWebhookSecretSolution();
    }
}
class MissingWebhookSecretSolution extends BaseSolution
{
    public function getSolutionTitle(): string
    {
        return 'The webhook signing secret is not set';
    }

    public function getSolutionDescription(): string
    {
        return 'Add WEBHOOK_SECRET to your .env and run `php artisan config:clear`.';
    }

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

Throw it in a route, load the page in local, and confirm the solution block appears above the stack trace. That's the whole loop; everything else is scale.

Implementation Patterns

Self-describing exceptions vs. providers. Two entry points, pick by ownership:

  • You own the exception class → implement ProvidesSolution (one solution) or HasSolutions (many) directly on it. Cheapest, most discoverable.
  • The exception comes from a vendor package → write a HasSolutionsForThrowable provider. Never subclass someone else's exception just to attach a fix.
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;

class StripeConfigSolutionProvider implements HasSolutionsForThrowable
{
    public function canSolve(\Throwable $throwable): bool
    {
        return $throwable instanceof \Stripe\Exception\AuthenticationException;
    }

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

Registration belongs in a service provider register(), guarded by class_exists. Ignition's repository is container-bound; resolve and register. Guard it so a stripped-down deployment (or a future Ignition version that drops the package) doesn't fatal.

public function register(): void
{
    if (! interface_exists(\Spatie\Ignition\Contracts\HasSolutionsForThrowable::class)) {
        return;
    }

    $this->app->afterResolving(
        SolutionProviderRepository::class,
        fn ($repo) => $repo->registerSolutionProvider(StripeConfigSolutionProvider::class)
    );
}

afterResolving (or boot()) beats register() alone here — you don't want to force the repository into every container build in production.

Keep canSolve() narrow. A bare instanceof check or a config-key-presence check is right. Do not return true for Throwable or for a base exception class — you'll inject your solution into every unrelated error page and dilute the signal that makes solutions useful.

RunnableSolution only for genuinely idempotent, local-safe actions. This is a command-execution surface reachable from a browser form. The parameters posted from the run button land in run(array $parameters) — treat them as hostile input, allowlist them, never interpolate into a shell string, and gate the whole thing:

public function run(array $parameters = []): void
{
    abort_unless(app()->environment('local'), 403);
    Artisan::call('config:clear');
}

Also confirm enable_runnable_solutions is on for the environment; the button silently won't render if it's off, which reads like a bug in your class.

Build a thin adapter at the boundary. The single highest-leverage pattern given this package's lifecycle: keep your own RemediationAdvice value object internal, and have exactly one mapper class turn it into Solution instances. When Ignition renames something or you move to the bundled namespace, you edit one file instead of grep-and-replace across the app.

final class AdviceMapper
{
    public static function toSolution(RemediationAdvice $advice): Solution { /* ... */ }
}

Group solutions by failure mode, not by exception class count. A SolutionProvider per domain (StripeConfigSolutionProvider, QueueConfigSolutionProvider) with an internal match on exception subtype reads better than forty single-exception providers and is far less to register.

Testing. Assert on the objects, not the HTML:

$provider = new StripeConfigSolutionProvider();
$this->assertTrue($provider->canSolve(new AuthenticationException('bad key')));
$this->assertCount(1, $provider->getSolutions(new AuthenticationException('bad key')));

Plus one integration smoke test per solution family that hits a route which throws the real exception and asserts assertSee('The webhook signing secret is not set'). That test is your only guard against the silent-failure mode described below. Note the test env must have debug enabled and Ignition active for it to pass; phpunit.xml defaults usually don't.

Gotchas and Tips

Wrong interface name fails silently. Solution, ProvidesSolution, HasSolutions, and HasSolutionsForThrowable look interchangeable and are not. Implement HasSolution when Ignition expects ProvidesSolution and nothing throws — the error page simply renders without a solution block. If your solution isn't showing, verify the interface name before you debug anything else.

Duplicate namespaces in one process. If both Spatie\IgnitionContracts\* and Spatie\Ignition\Contracts\* are autoloadable, instanceof checks can pass against one and fail against the other, and your provider registers but never fires. Symptom: solution classes exist, are unit-tested green, and never appear. Fix: pick one namespace, delete the direct spatie/ignition-contracts requirement if Ignition v2+ bundles the contracts.

getDocumentationLinks() returns objects, not strings. Returning ['https://...'] is a type error that surfaces only during rendering, so it can take out the whole error page rather than one block. Instantiate the DocumentationLink class your installed Ignition version actually ships — check the constructor in vendor/ rather than trusting a snippet.

Exceptions in solution code break the error page. getSolutionTitle() and friends run while Ignition is already handling a fatal. An undefined property or a null env value there produces a second, worse error. Keep solution methods pure string assembly — no container resolution, no DB, no HTTP.

Don't interpolate the exception message into the description. $throwable->getMessage() routinely contains connection strings, API keys, and file paths. Solutions are shown in local/staging but often get screenshotted into tickets and Slack.

Debugging a non-rendering solution. dd() inside canSolve() won't reach you — you're mid-error-render. Use ray(), write to storage/logs/laravel.log, or test directly:

app(Ignition::class)->getSolutionsForThrowable(new MyException());

That call outside the error path is the fastest way to see whether the problem is your provider or the renderer.

Providers run in registration order and all matching solutions are shown. Two providers that both claim the same exception produce two blocks. This looks like a rendering bug and is actually your canSolve() being too wide.

It's in maintenance mode — plan the namespace migration now. Newer Ignition vendored these contracts under NunoMaduro\Ignition\Contracts / Spatie\Ignition\Contracts. Write a grep for the legacy Spatie\IgnitionContracts namespace into your Ignition upgrade checklist, and keep the adapter layer from Implementation Patterns so that upgrade is a mechanical diff rather than an archaeology project.

Extension point worth knowing: you can bind your own SolutionProviderRepository implementation if you need different discovery rules than Ignition's default (e.g. a database-backed registry of known failure modes). The interface exists precisely so you don't have to fork the renderer — but for a single app, static providers are almost always enough.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package