Stephen's Thoughts, and Other Nonsense...

<?php echo $cleverComments[array_rand($cleverComments)]; ?>

save

Using Laravel 5 Middleware for Parameter Persistence

This thought occured 2 years ago about Laravel, Laravel 5, Middleware, PHP, Persistence & Session.

I am lucky enough to get to work on a large Laravel 5 full time in my day job, with some awesome developers. I recently came up with what is, in my humble opinion, an elegant and simple solution for what we all initially thought was quite a complex problem. If you've used this trick before, or know how to improve on it, please let me know - I'd love to hear from you!

The problem

We are working on the latest iteration of our product, which is a re-write in Laravel 5 using PJAX for the UI interaction. The old version of the product used Laravel 3 and AngularJS. This means that we are using very different technologies for all parts of the stack, and as you can probably imagine, some things that were easy in the old version now have added complexity. (That said, the new stack also makes some things a lot easier!)

We needed to implement persistence in our record list filtering behaviour. The filter system is being powered using URL parameters that are appended to / updated when you change the filter options - however this does not persist when you leave the page and come back to it. We all agreed it was going to be tricky, given our design, and I was the unlucky developer whose queue it ended up at the top of.

Possible options

I thought for a while about possible solutions - maybe hijacking the events in PJAX and remembering the URL parameters in the browser, and manually injecting them, but I really don't like Javascript so I tried to find a solution that would approach the problem on the PHP side. I contemplated possible options of moving the filters out of URL parameters and persisting them in the session, but that gets messy: do they need to be POST'ed to the server? How do you share links with specific filter options? How should they be stored? Should the common options work across different lists?

No, I had to stick with URL parameters.

My solution

After a bit of thought, I remembered the middleware design that Laravel 5 uses. I realised that this is the perfect place to inject logic relating to a request before the request is actually processed.

The solution is elegant and simple:

<?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;

class ParameterPersistence
{
    public function handle(Request $request, Closure $next)
    {
        // Unique session key based on class name and route name
        $key = __CLASS__.'|'.$request->route()->getName();
        
        // Retrieve all request parameters
        $parameters = $request->all();

        // IF  no request parameters found
        // AND the session key exists (i.e. an previous URL has been saved)
        if (!count($parameters) && Session::has($key)) {
            // THEN redirect to the saved URL
            return redirect(Session::get($key));
        }

        // IF there are request parameters
        if (count($parameters)) {
            // THEN save them in the session
            Session::put($key, $request->fullUrl());
        }

        // Process and return the request
        return $next($request);
    }
}

The way it works is simple.

First, if you access the page initially without any parameters before a URL has been saved (http://example.com/users), then it will do nothing. The request will be processed and nothing will be stored in the session.

Next, if you access the page with URL parameters, for example by clicking on a sort order link (http://example.com/users?sort=name&order=asc), then it will save the requested URL in the session (under a unique key for that specific page) and then the request will be processed as per normal.

Finally, if you access the page without any URL parameters but after a URL has been saved (http://example.com/users), then it will redirect you to the last saved URL that contained parameters (http://example.com/users?sort=name&order=asc).

This process is completely transparent to the application itself, as it all happens in the middleware using redirects, before the application logic is initiated. This makes the redirects very fast, and removes any complexity around persistence in the filter handling logic.

Things to note

  1. It will only redirect when the request has no parameters (i.e. http://example.com/users) and has a previously saved URL. This means that any time a parameter is added or removed (without clearing all parameters), it will save the new request URL and render the page.
  2. It will only save requests that have parameters. If there are no parameters, and no saved URLs, it will do nothing. This saves data being needlessly stored and potential redirect loops.
  3. It should only be included on pages that need it - it will cause issues with other parameters that should not be automatically remembered. (Something like ?delete=true would be very bad!)
  4. If you are using a load balancer, or for some reason cannot use absolute URLs, you will need to replace $request->fullUrl() with something more appropriate. For example, this will give you a relative URL: $request->path().'?'.$request->getQueryString().
  5. We use named routes, so the session key uses the route name. If you don't use named routes, you will need to change the key generation to use whatever is unique about the request for that record list page.

I hope you found that useful! :-)

;