Dynamic custom domain routing in Laravel

When I first came across Laravel, one of the built-in features that impressed me the most was its support for subdomain routing. You can define a route “{subdomain}.myapp.com” and the value of the subdomain is passed to the route handler as a parameter. You can then filter your users or do some other logic to present the correct content to the user based on the subdomain.

In a recent project, I implemented user subdomains and it worked very nicely. I wanted to go one step further however, and allow users to add their own custom domain names. In other words, their content should be accessible via userdomain.com rather than (or as well as) subdomain.myapp.com.

In this post I’ll explain how I achieved that in Laravel 5.1. Note that in order to get the domain to work fully the user would need to create a CNAME record in the DNS settings for their domain to point it to the application. Typically it’s best practice to give the user detailed information on how to do this with various domain registrars and implement some validation mechanism to check that they have set it up correctly. I won’t cover that here.

The first thing I tried was to remove the “.myapp.com” from the route definition. So rather than this:

Route::group(['domain' => '{subdomain}.myapp.com'], function() { });

… I tried this:

Route::group(['domain' => '{domain}'], function() { });

… which unfortunately did not do the trick. I searched the Web for a solution, and ran into a number of articles, StackOverflow responses and other sources with a few suggestions on how to overcome this. The solutions were to skip using Laravel’s domain routing altogether, and a few ugly hacks where you would store the extension separately to the main part of the domain — along the lines of:

Route::group(['domain' => '{domain}.{ext}'], function() { });

Yuck. Not to mention that this would work for a domain like userdomain.com but not www.userdomain.com (at least not without adding an additional route handler to cater for that scenario). I figured there had to be a more elegant solution.

When I looked at that hack, I wondered why that worked and using the full domain as a parameter did not. The only difference was that the period character was not included in the parameter. I figured the router was blocking any non-alphanumeric characters from the parameter (such as a period) and looked to implement a workaround.

In my project’s app/Providers/RouteServiceProvider.php file, I changed the boot method to the following:

public function boot()
{
\Route::pattern('domain', '[a-z0-9.\-]+');
parent::boot();
}

This would allow all lowercase letters, numbers, hyphens and the period character only for the domain parameter in a route. I tried my full domain parameter route group once more:

Route::group(['domain' => '{domain}'], function() { });

… and it worked! The full domain name is passed to the route handler as a parameter named domain, and I can figure out what user content I should display from there.