2

I created a middleware class named PathParser which runs on every request. The purpose is to handle requests for "vanity URL paths" that we allowed users to create in our pre-Laravel vanilla PHP app. For example: A user created a URL path such as: http://example.com/i-love-this-place

What PathParser does is to check for 404 responses, and then look to see if the URL path matched one of our old vanity paths. Like this:

class PathParser
{   
    public function handle($request, Closure $next, $guard = null)
    {
        $next_response = $next($request);       
        $status_code = $next_response->getStatusCode();

        if ($status_code === 404) {
            $script_url = $request->server("SCRIPT_URL");

            $vanity_controller = new VanityController();
            $found_vanity_id = Search::findVanityPath($script_url);

            if (!empty($found_vanity_id)) {
                $next_response = response()->make($vanity_controller->one($found_vanity_id));
            }
        }

        return $next_response;
    }
}

Assume the following:

  1. A user has never created a URL path that would conflict with any route

  2. I must support a number of existing (pre-Laravel) vanity URL paths that are out in the wild -- posted to social media and so on.

In Kernel.php, I have the following:

protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\PathParser::class,
        //\Illuminate\Session\Middleware\StartSession::class,
        //\Illuminate\View\Middleware\ShareErrorsFromSession::class,
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],    
    ];

In the $middleware array I tried added StartSession and ShareErrorsFromSession (uncommenting the 2 lines above), and this works partially. But with two major problems:

  • Auth::user is null, even for requests made to vanity paths by logged in users
  • $errors no longer gets populated on form submission (such as on the registration and login pages) when a user submits incorrect/invalid information

Is there a way both to check the route on all requests and get ahold of the authenticated user, also preserving $errors?

I have a feeling I don't understand the request lifecycle well enough to succeed. But maybe there is a way?

If it's not possible to do what I require, then use of 302 redirection to a standardized prefixed path (such as http://example.com/vanity/i-love-this-place) is a fine solution. But I'm hoping there is another means.

udog
  • 1,490
  • 2
  • 17
  • 30
  • 1
    Is there a reason you can't add your `PathParser` to the end of your `web` middleware group? – patricus Dec 24 '16 at 00:27
  • When I put PathParser in the web middleware array, any request for a "vanity path" goes straight to Laravel's built-in 404 page, and bypasses the PathParser entirely. In fact, the framework doesn't even seem to instantiate PathParser -- I can put bad syntax in it and save it and there is no exception, just straight to 404. But when I hit a proper route, that syntax exception shows up. And maybe this gives us a hint -- I mean, perhaps there is a way to bypass Laravel's automatic 404 handling (which could lead to a solution)? – udog Dec 24 '16 at 04:52
  • Ah, yeah, that makes sense. Route matching is performed before the route middleware is applied, so the `NotFoundHttpException` exception is thrown before your middleware will execute. – patricus Dec 24 '16 at 05:32

1 Answers1

1

A couple suggestions:

If you don't need auth/sessions/etc, then you can just handle the Symfony\Component\HttpKernel\Exception\NotFoundHttpException exception inside your application's exception handler.

In app/Exceptions/Handler.php, modify the render() method to look something like:

public function render($request, Exception $e)
{
    if ($e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
        // your code that returns a Response
    }

    return parent::render($request, Exception $e);
}

If you do need auth/sessions/etc, I would suggest creating a "catchall" route at the end of your routes file. For example, as the very last line in your routes/web.php file, put:

Route::any('{catchall}', 'VanityController@handle')->where('catchall', '(.*)');

Then, inside your VanityController, have a handle method that looks like:

public function handle(Request $request, $url)
{
    // logic to search and render your vanity page for $url

    // if no vanity page was found:
    throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException();
}
patricus
  • 59,488
  • 15
  • 143
  • 145
  • This solution is thorough and worked perfectly as soon as I implemented it -- I used the catchall route but it's also nice to know that I could handle HTTP exceptions selectively. – udog Dec 26 '16 at 18:27