January 8, 2023 · 2 minutes

A Custom Middleware Practical Use Case

The Problem

Say you have a requirement to keep a tally of views on a Post model. The model has a views column, so it should be pretty easy in Laravel to increment it:

class PostController extends Controller
{
public function show(Post $post): View
{
$post->increment('views');
 
return view('posts.show', compact('post'));
}
}

No problem right? However, as the code stands right now, each time the user returns to the page, the count will increment even if it is the same user returning in the same session. Adding additional logic to handle this new requirement will quickly start to clutter up your controller. Additionally, what if you have other models that need this functionality?

Middleware to the Rescue!

Begin by creating a new middleware:

php artisan make:middleware RecordViews

Here is the full middleware code that addresses the issue. Note the custom $model parameter as well:

namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Http\Request;
 
class RecordViews
{
public function handle(Request $request, Closure $next, string $model)
{
$resource = $request->route()->parameter($model);
 
if ($this->hasNotViewedResource($model, $resource->id, $request)) {
$resource->increment('views');
$this->markResourceViewed($model, $resource->id, $request);
}
 
return $next($request);
}
 
private function hasNotViewedResource(string $model, int $resource_id, Request $request): bool
{
return ! in_array($resource_id, (array) $request->session()->get($model.'.views', []));
}
 
private function markResourceViewed(string $model, int $resource_id, Request $request): void
{
$request->session()->push($model.'.views', $resource_id);
}
}

Remember to add the middleware to the Kernel in app/Http/Kernel.php

protected $routeMiddleware = [
// ...
'views.record' => \App\Http\Middleware\RecordViews::class,
];

Now you can apply the middleware to any route you need it and your controller stays nice and clean! Again, note the custom parameter passed into the middleware (denoted with the colon):

use App\Http\Controllers\PostController;
use App\Http\Controllers\ShowController;
use Illuminate\Support\Facades\Route;
 
Route::get('/posts/{post}', [PostController::class, 'show'])
->middleware('views.record:post')
->name('posts.show');
 
Route::get('/shows/{show}', [ShowController::class, 'show'])
->middleware('views.record:show')
->name('shows.show');

That's it for now! Thanks for reading!

© 2024 Curran Jensen.

Twitter · GitHub

Proudly built with Laravel and hosted on Digital Ocean.

Syntax highlighting kindly provided by torchlight.dev