Multiple Subdomain and TLD Routing in Laravel 5

A recent project I’ve been working on has a requirement to have two concurrent versions of an application running on different subdomains.

It looks something like:

  • sandbox.example.com – A sandbox version of the application
  • live.example.com – The live version

The local developer version has the same URLs, just with a .test or .local TLD.

Laravel 5 supports subdomain routing, but not really for this use case, and honestly, not all that well out of the box.

This is the example found in the documentation:

Route::domain('{account}.myapp.com')->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

Problem 1: This route is now hardcoded to the .com TLD.

You can’t develop locally any more because your local environment will be probably on a .test TLD or another one of the reserved testing TLDs.

You might think that Laravel could intelligently swap out the TLD when it’s being ran in development mode, but no such feature exists. The code example from the docs cannot be copy and pasted into a real project and work as expected.

That said, one possible work around for this is to put the TLD in an environment variable that can be swapped out at run time.

Route::domain('{account}.myapp.' . config('app.tld'))->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

Problem 2: This route now requires the {account} parameter.

Let’s name this route and try to use generate a link with the route helper.

Route::domain('{account}.myapp.' . config('app.tld'))->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    })->name('account.profile');
});
// Illuminate \ Routing \ Exceptions \ UrlGenerationException
// Missing required parameters for [Route: account.profile] [URI: user/{id}].
return route('account.profile', ['id' => 1234]);

The subdomain parameter is required but not provided, so UrlGenerationException is thrown. Also note that Laravel doesn’t tell you which parameter is missing which is not very helpful!

Obviously you can fix this by providing the subdomain in every call to route, but this has obvious problems:

  • All controller methods will need to accept the subdomain as a parameter and pass it along to the view.
  • All calls to the route helper will need to specify the subdomain

This perhaps makes sense if you are basing application logic on different subdomains, but in my case I’m not.

The solution

In your EventsServiceProvider, listen for the RouteMatched Event. When it fires, Laravel will have matched the route to use and will populate the current route with the parameters from the request.

You can inject these into the UrlGenerator‘s default values.

From this point on, the parameters you specified in the domain of your route will be filled with whatever matched them.

//EventsServiceProvider.php
protected $listen = [
   RouteMatched::class => [
       SetUrlDefaults::class,
   ],
];
//SetUrlDefaults.php
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;

class SetUrlDefaults
{
    private $urlGenerator;
    private $router;

    public function __construct(UrlGenerator $urlGenerator, Router $router)
    {
        $this->urlGenerator = $urlGenerator;
        $this->router       = $router;
    }

    public function handle(): void
    {
        $this->urlGenerator->defaults($this->router->current()->parameters);
    }
}

You could also use the URL and Router facades here if you really want. I prefer dependency injection.

Conclusion

Maybe all this seems obvious in hindsight, but figuring this out took me several hours and many missteps in the wrong direction.

It’s disappointing to me that examples in the official documentation have implementation issues that require either a good amount of experience of the framework or more advanced code splunking abilities.  I would put my solution and debugging above what I would expect from a junior or even mid tier developer.

Leave a Reply

Your e-mail address will not be published. Required fields are marked *