Hi, I'm Jordan Dalton, VP of Engineering, Laravel Guru, Entrepreneur (1x acquired), Content Creator, Husband, Father, Musician.

PHP / Laravel / VueJs / TailwindCss

Advanced Laravel: Terminable Middlware

If you’re a Laravel developer, you’re likely familiar with the concept of middleware. These powerful tools allow you to manipulate incoming requests before they’re handled by your application, and outgoing responses before they are sent back to the client. But what if you need to perform some action at the end of a request cycle, after the response has been sent? That’s where terminable middleware comes in.

Terminable middleware can be useful for tasks such as logging, cache management, or any other cleanup tasks that need to be performed after the response has been sent. In this blog post, I’ll show you how to create a simple HTTP request/response logger. Let’s get started.

First, let’s create our middleware class via Artisan command:

php artisan make:middleware TerminatingMiddleware

This will place a file, TerminatingMiddleware.php, in our app/Http/Middleware folder. As you’ll see, the contents are that of a basic, middleware file.

<?php

namespace Illuminate\Session\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class TerminatingMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }
}

To make this a terminable middleware we’ll add the following method to the class, along with a logging point so we can log when this method is called.

/**
     * Handle tasks after the response has been sent to the browser.
     */
    public function terminate(Request $request, Response $response): void
    {
        \Log::info('terminate', $request->toArray());
    }

To make our application aware of this middleware we’ll need to add it to Laravel’s HTTP Kernel, particularly in the $middleware property which is where global middleware are defined (these are executed during every request to the application).

protected $middleware = [
    // \App\Http\Middleware\TrustHosts::class,
    \App\Http\Middleware\TrustProxies::class,
    \Illuminate\Http\Middleware\HandleCors::class,
    \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\TerminatingMiddleware::class
];

Now that Laravel knows to use our middleware let’s test it out by going to visiting our application’s homepage in the browser:

http://example.test/

We should now have a log entry.

[2023-04-12 09:58:55] local.INFO: terminate

So far so good. We’ll now begin building our tracking system by using Artisan to create a model along with a corresponding migration file.

php artisan make:model HttpRequest -m

Let’s design our migration to capture our desired datapoints:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('http_requests', function (Blueprint $table) {
            $table->id();
            $table->string('session_id')->index();
            $table->integer('user_id')->index()->nullable();
            $table->string('ip')->index();
            $table->boolean('ajax')->index();
            $table->string('url');
            $table->jsonb('payload');
            $table->integer('status_code')->index();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('http_requests');
    }
};

We’ll now move over to our model and make the appropriate definition so we can save data via this model.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class HttpRequest extends Model
{
    use HasFactory;

    protected $casts = [
        'payload' => 'collection' // very important
    ];

    protected $fillable = [
        'session_id',
        'user_id',
        'ip',
        'ajax',
        'url',
        'payload',
        'status_code'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Let’s now execute our migration.

php artisan migrate

Let’s go back to our middleware and update it so we can populate the database:

<?php

namespace App\Http\Middleware;

use App\Models\HttpRequest;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class TerminatingMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    /**
     * Handle tasks after the response has been sent to the browser.
     */
    public function terminate(Request $request, Response $response): void
    {
        $data = [
            'session_id'  => session()->getId(),
            'user_id'     => $request->user()->id ?? null,
            'ip'          => $request->ip(),
            'ajax'        => $request->ajax(),
            'url'         => $request->fullUrl(),
            'payload'     => $request->toArray(),
            'status_code' => $response->getStatusCode()
        ];

        \App\Models\HttpRequest::create($data);
    }
}

If we now re-visit our homepage, but this time we’ll add some query string information:

http://example.test?jordan=dalton

We should now have data in our database:

> App\Models\HttpRequest::first()
= App\Models\HttpRequest {#6918
    id: 1,
    session_id: "aHDgiFIU3JklAjF0AGOf3GGMNMbWyLMnwCtZ9kjH",
    user_id: null,
    ip: "127.0.0.1",
    ajax: 0,
    url: "http://terminable.test/?jordan=dalton",
    payload: "{"jordan": "dalton"}",
    status_code: 200,
    created_at: "2023-04-12 10:12:17",
    updated_at: "2023-04-12 10:12:17",
  }

Now that we have this working, if you like, you could take an even-more-advanced-approach by having this stored to the database via a queued job. The overall beauty of this solution is you can see the requests and responses throughout your system in realtime which also makes it easy to debug scenarios.

In closing, as you continue to develop your Laravel applications, be sure to keep terminable middleware in mind as a valuable tool for improving performance and functionality. With a little bit of planning and some creative thinking, you can use terminable middleware to take your applications to the next level.

Thanks for reading, and happy coding!

Source Code is available on Github.
Follow me on Twitter and YouTube.
Don't forget to share 😉