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

Laravel Terminator Laravel Package

imanghafoori/laravel-terminator

Laravel Terminator adds the missing terminate method to controllers, so heavy work like logging, notifications, or cache cleanup runs after the response is sent. Keep pages fast and users happy with a simple, middleware-based trait, no queues required.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require imanghafoori/laravel-terminator

Laravel's package auto-discovery registers the service provider. On Laravel 11+/12 you may still need to confirm the package's middleware is attached to the web and api groups in bootstrap/app.php, because that middleware is what turns the terminator's exception into an actual HTTP response:

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \Imanghafoori\LaravelTerminator\Middleware\TerminatorMiddleware::class,
    ]);
})

Where to look first

The package is tiny (~a handful of files). Read these three, in this order, before using it:

  1. The Terminator facade — this is the API surface. Skim the full method list; it is the fastest way to learn what responses you can short-circuit with.
  2. The exception class it throws — you need to know its type so you never accidentally swallow it.
  3. The middleware that catches it — this tells you exactly where in the request lifecycle termination is legal, which drives every gotcha below.

First use case

The canonical use: a controller that delegates to private helper methods and needs to bail out from several levels deep without threading return values back up every call.

use Imanghafoori\LaravelTerminator\Facades\Terminator;

class PostController extends Controller
{
    public function store(StorePostRequest $request)
    {
        $this->guardQuota();          // may terminate
        $this->guardDuplicate($request); // may terminate

        $post = Post::create($request->validated());

        Terminator::redirect(route('posts.show', $post));
    }

    private function guardQuota(): void
    {
        if (auth()->user()->posts()->count() >= 10) {
            Terminator::back()->with('error', 'Quota reached.');
        }
    }
}

Notice there is no return anywhere. guardQuota() communicates "stop everything and go back" by unwinding the stack to the middleware, which converts it to the redirect response. That is the entire value proposition — and also the entire risk.

Implementation Patterns

Terminate with a ready-made response. If you already build a Response/RedirectResponse/JsonResponse, hand it to terminate() and let the package pass it through untouched. This is the safest form because you control the payload exactly and don't rely on the facade's sugar methods.

Terminator::terminate(response()->json(['error' => 'nope'], 422));

Guard clauses in an abstract base controller. The pattern this package was built for: one shared helper used by every action, where returning a value is awkward.

abstract class Controller
{
    protected function ensureOwner($model): void
    {
        abort_unless($model->user_id === auth()->id(), 403); // still preferable when possible
    }

    protected function ensureOwnerOrBack($model): void
    {
        if ($model->user_id !== auth()->id()) {
            Terminator::back()->with('error', 'Not yours.');
        }
    }
}

Where a plain abort() or $this->authorize() gets you what you want, prefer it — it's native Laravel, testable, and doesn't need middleware. Reach for Terminator only when you specifically need a redirect + flash or a custom response body from a nested call.

Split web and API paths. In an API controller, terminate with JSON; in a web controller, terminate with a redirect and flashed session data. Don't let a single service class decide — push the terminate call up into the HTTP layer so the service stays reusable in jobs and commands (see Gotchas).

Compose with FormRequest validation. For flows where a form request validates but a business rule fails afterwards (quota, state machine, rate limit), the terminator gives you the same "redirect back with errors" UX as validation without throwing ValidationException yourself.

if ($invoice->isPaid()) {
    Terminator::back()->withErrors(['invoice' => 'Already paid.']);
}

Use it to flatten deeply nested if trees in a controller that orchestrates several steps. The alternative — every private method returning null|Response and the caller checking if ($r) return $r; — is the boilerplate this package removes. If you write more than two such checks in one method, that's the moment to switch.

Keep the response construction in one place. Wrap the facade in a small app-level service (HttpTerminator::quotaExceeded()), so the rest of your codebase depends on your helper, not directly on a 254-star package with a last release in early 2023. This costs ten minutes and makes removal trivial.

Testing pattern. Feature tests through $this->get()/$this->post() behave normally because the middleware is in the stack — assert on assertRedirect() and assertSessionHas('error'). Direct unit tests that call controller methods on a raw instance will throw the terminator exception, which is a feature if you assert it: $this->expectException(...).

Gotchas and Tips

It's an exception, not a return. Any catch (\Throwable $e) or catch (\Exception $e) in the call stack between your Terminator::call() and the middleware will swallow the termination — the request keeps running and your redirect is silently lost. Audit try/catch blocks in services and in DB::transaction() closures. This is the #1 way to waste an afternoon.

It does not work in queue jobs or Artisan commands. The console kernel has no such middleware, so the terminator exception bubbles to the top as an unhandled error. Treat Terminator as strictly an HTTP-layer tool; never import it into a service class you also call from a job.

Middleware ordering matters for session flashes. ->with('error', ...) and ->withErrors(...) need the session started. If you terminate very early — inside a middleware that runs before StartSession — the flash message has nowhere to go. Put the terminator's handling middleware inside the web/api groups, not in the global stack.

Laravel version drift. Last release: February 2023. Before committing, verify it installs cleanly on your Laravel version (composer require will tell you immediately). If auto-discovery misbehaves on Laravel 11+, either register the provider manually or drop the middleware class name in bootstrap/app.php and confirm the namespace from vendor/imanghafoori/laravel-terminator/composer.json.

Naming collision. Laravel middleware itself has a terminate() method that runs after the response is sent. Don't let a reader confuse Terminator::terminate() (abort the request now) with $middleware->terminate() (post-response cleanup). If your team is large, wrap the facade behind your own method name like HttpTerminator::bail().

Debugging hidden control flow. When a redirect appears from nowhere, add a temporary \Log::debug('terminated', ['trace' => ...]) at the top of the facade call, or put a dd() inside the middleware's catch block — the stack trace there points straight at the terminating call site. Better: log the backtrace in non-production permanently, at debug level, so weird redirects are diagnosable months later.

Refactoring away from it is the intended end state. The package's own pitch is "an opportunity to refactor your controllers" — it's scaffolding. If a controller still needs Terminator after you've extracted an action class and moved the branch into a policy or a custom exception handler, it probably belongs in app/Exceptions/Handler.php as a renderable exception instead. Renderable exceptions are a first-class framework feature with no third-party dependency, and they achieve the same "throw from anywhere, respond in one place" outcome.

Prefer abort() / ValidationException when they fit. abort(403) and throw ValidationException::withMessages([...]) are both caught by Laravel's handler and rendered correctly. Only reach for the terminator when you need a specific response shape (a redirect to a named route with two flash keys, say) that would otherwise require threading a return value upward.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib