Route model binding using middleware
Our team is currently working on a Spark app. Spark makes is real easy to add an API that can be consumed by the users of the app. The generation of API tokens and authentication middleware comes out of the box. It all works really great.
In our API the a team owner can fetch information on every member on the team and himself. The url to fetch info of a user looks something like this: /users/<id-of-user>
. Nothing too special. But we also want to make fetching a user's own information as easy as possible. Sure, the user could look op his own userid and then call the aforementioned url, but using something like /users/me
is much nicer. In this way the user doesn't have to look op his own id. Let's make that possible.
In our app we use these functions to get the the current user and team:
/**
* @return \App\Models\User|null
*/
function currentUser()
{
return request()->user();
}
/**
* @return \App\Models\Team|null
*/
function currentTeam()
{
if (!request()->user()) {
return;
}
return request()->user()->currentTeam();
}
The route look something to get the user data looks something like this:
Route::post('/users/{userId}', 'UserController@show');
My first stab to get /users/me
working was to leverage route model binding. In the RouteServiceProvider
I put this code:
$router->bind('userId', function ($userId) {
if ($userId === "me") {
return currentUser();
}
$user = currentTeam()->users->where('id', $userId)->first();
abort_unless($user, 404, "There's no user on your team with id `{$id}`");
return $user;
});
Unfortunately this does not work. When Laravel is binding route parameters the authentication has not started up yet. At this point currentUser
and currentTeam
will always return null
.
Middleware comes to the rescue. Route-middleware is processed at a moment when authentication has started up. To make /users/me
work this middleware can be used:
namespace App\Http\Middleware;
use Closure;
class BindRouteParameters
{
public function handle($request, Closure $next)
{
if ($request->route()->hasParameter('userId')) {
$id = $request->route()->parameter('userId');
$user = $this->getUser($id);
abort_unless($user, 404, "There's no user on your team with id `{$id}`");
$request->route()->setParameter('userId', $user);
}
return $next($request);
}
public function getUser(string $id)
{
if ($id === 'me') {
return currentUser();
}
return currentTeam()->users->where('id', $id)->first();
}
}
There are two things you must do to use this middleware. First: it's route middleware so you such register it as such at the http-kernel.
// app/Http/Kernel.php
...
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
...
'bindRouteParameters' => \App\Http\Middleware\BindRouteParameters::class,
]
Second: you must apply the middleware to certain routes. In a default Spark app you'll find all api-routes in a file at app/Http/api.php
. That file starts with this line:
Route::group(['prefix' => 'api', 'middleware' => ['auth:api']], function () {
...
Just add the bindRouteParameters
middleware to the group:
Route::group(['prefix' => 'api', 'middleware' => ['auth:api', 'bindRouteParameters']], function () {
...
I'm currently using the above solution in my app. You could make a solution that's more generic by checking if the parameters ends with orMe
. Here's an example how that might work:
namespace App\Http\Middleware;
use Closure;
class BindCurrentUserRouteParameter
{
public function handle($request, Closure $next)
{
collect($request->route()->parameters())
->each(function ($value, $parameterName) use ($request) {
if (!ends_with($parameterName, 'orMe')) {
return;
}
if ($value === 'me') {
$request->route()->setParameter($parameterName, currentUser());
}
});
return $next($request);
}
If you have any questions about this approach or have any ideas how to make it better, let me know in the comments below.
What are your thoughts on "Route model binding using middleware"?