Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer. You can expect quick tips, links to interesting tutorials, opinions and packages. Want to learn the cool stuff? Then sign up now!

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.

Freek Van der Herten is a partner and developer at Spatie, an Antwerp based company that specializes in creating web apps with Laravel. After hours he writes about modern PHP and Laravel on this blog. When not coding he’s probably rehearsing with his kraut rock band.
  • Jonathan Vercoutere

    Freek, why not just make use of the procedural nature of PHP and declare two routes like this:

    Route::get(‘/users/me’, ‘UserController@getMe’);
    Route::get(‘/users/{id}’, ‘UserController@getSomeoneElse’);

    That way you can name both routes for ease of use and easily use a regex constraint on the {id} route.
    The only downside would be that you need to always define the “me” route before the {id} route (I guess this could be considered an anti-pattern. Well written comments are your friend here), but personally I feel the benefit of simplicity makes up for this.

    Let me know what you think!

    • Hi Jonathan,

      if you only have one route where “me” should handled, that just defining that route, like you did in your example, seems like the best option. It’s simple and readable. Nice!

      But image you have 10, 20 or more endpoints where you want to handle the “me”-case, then you’d have to define a lot of extra routes. In that case using the middleware from my blogpost feels like a better option imho.

      • Jonathan Vercoutere

        If I understand you correctly, I would still probably prefer the following approach which is still very readable and to the point:

        Route::get(‘/users/{meEndpoint}, ‘UserController@getMe’)
        ->where(‘meEndpoint’, ‘(me|i|myself|self|ego)’);
        Route::get(‘/users/{id}’, ‘UserController@getSomeoneElse’)
        ->where(‘id’, ‘[0-9]+’);

        It might be unjustified, but I have this weird itch that keeps telling me this sort of logic shouldn’t be in my middleware. Imagine we want to apply some extra middleware to the ‘meEndpoint’ route, that would needlessly complicate things imo. I like to try and keep actual routing logic in the appropriate files.

        I haven’t tested this to back up my claim, but I wouldn’t be surprised if this solution would also be just a tad more performant.

        Please let me know if I misunderstood what you’re trying to do or if you have any reservations towards this approach.

        • Putting route parameter binding in middleware feels fine to me. I like the fact that it just works on all routes.

          Your approach is valid too. There’s nothing wrong with validating the content of the parameter in the routes file.

  • Pingback: May 2016 Newsletter - Nomad PHP()