When building a website or application there are a variety of reasons you might want a subdomain ranging from SEO to full separation of application responsibilities. Craigslist uses subdomains to separate it's different regions within states. Other websites use a subdomain to differentiate their blog from their main marketing content.
In my case, i've developed several web applications that have had siloed account logins (commonly referred to as Multitenancy). The way I approached that requirement was to subdomain the site, and use that subdomain to differentiate which tenant you were trying to access.
In Laravel this is pretty trivial to setup, but there are some snags associated with it and some nicer ways i've found to handle it. In this post we'll see what Laravel offers by default, how we can clean it up using middleware, and talk a little bit about how to integrate it into your overall app.
How Laravel handles subdomains
Adding a subdomain in Laravel is trivial, and in our case we want to treat the subdomain portion as a wildcard that is handed to our code (see the overall docs for more on Laravel Routing).
//routes.php
Route::group(['domain' => '{subdomain}.mysite.com'], function () {
Route::get('/', 'SubdomainedController@index');
});
//SubdomainedController.php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
class SubdomainedController extends Controller {
public function index($subdomain)
{
dd($subdomain); //die and dump the subdomain
}
}
From that example you can see the subdomain can be specified as a simple option called domain
to the Route#group
method. Here we're saying we want the subdomain
in {subdomain}.mysite.com
to be a variable parameter (allowing us to use a wildcard subdomain which can be controlled from a configuration file or database entry).
The other thing we see is the actual controller definition, where the #index
method is being supplied $subdomain
as a parameter. We can then use that parameter to determine how to handle the current user and url. From here we could create a common method or service that we call within our controllers and handles the subdomain logic for us...
But what if every controller in our app is under that subdomain? What if we have 50 controllers and each controller has some slice of the typical REST interface? Now we have to remember to not only call this common helper in every single method in our entire application, but we also have to clutter our method signatures by having every single one include $subdomain
as a parameter.
It works, but it kinda sucks. We can improve it.
Cleaning up our method signatures with middleware
We can use a combination of middleware and request modification to both utilize our subdomain and keep our controller interfaces clean. Here's what it looks like:
//routes.php
Route::group([
'domain' => '{subdomain}.mysite.com'
'middleware' => ['subdomain']
], function () { ... }
//Subdomain.php
namespace App\Http\Middleware;
class Subdomain
{
public function handle($request, $next)
{
$route = $request->route();
$subdomain = $route->parameter('subdomain');
$route->forgetParameter('subdomain');
//store parameter for later use
return $next($request);
}
}
//SubdomainedController.php
class SubdomainedController extends Controller
{
public function index()
{
dd(func_get_args()); //die and dump any arguments - we no longer have the subdomain!
}
}
To make this work, we start by grabbing a representation of the current route being acted on using $request->route()
. This instance gives us access to information about the route including the current url, what HTTP verbs it responds to, and the parameters that have been supplied (if any). We utilize the #parameter
method to retrieve the subdomain
part of the url.
Once we have a handle on the subdomain, the key to this middleware is the call to #forgetParameter
. By calling this with our parameter name 'subdomain'
as the argument, it's no longer handed to our controller methods. We can get access to the parameter without cluttering responsibilities in our code. After we get the parameter we can then store it for later use (in the session, in the database, in a global app service).
A note about the #forgetParameter
method: It took a little digging and I wouldn't suggest it everywhere, but I think this is a case where it makes the app cleaner and is more inline with our expectations. In general I think of the Principle of least surprise in these cases. By which I mean modifying the request so that it no longer matches our expectations could be considered "surprising". But the alternative responsibility that you place on every single controller is a much bigger problem in my opinion (and a big violation of the Single Responsibility Principle).
Testing your subdomain locally using php artisan serve
or any local server
If you're using php artisan serve
to run your local server and you want to test using subdomains you have to make a small modification. Add the --host
option and set it to 0.0.0.0
.
php artisan serve --host=0.0.0.0
This allows your server to be available from any interface of your machine. It will be accessible by other computers on your network, tools like browserstack in local mode, and most importantly it can be configured with a local subdomain using your hosts
file.
127.0.0.1 dev.mylaravelproject.com
Now you can go to dev.mylaravelproject.com:8000
and it will point to your local development server. Laravel will be able to pull the subdomain using your route group, and you're good to go.
Making the subdomain less hard-coded
You may have noticed the example I showed for local development (dev.mylaravelproject.com) and the route I setup weren't identical. One option is to make them identical, but then if you are switching between testing your actual server and dev server you would have to continually update your hosts file. You could manually change the entry, but that's error prone (wouldn't want to update production with a route pointing to "mylaravelproject.com" instead of your actual url). So I think you have two options:
1. Make the entire route configurable
2. Keep the route as an entry in your environment file
Making the entire route configurable
There's nothing that stops us from having more parameterized pieces of your subdomain as in this example:
//routes.php
Route::group([
'domain' => '{subdomain}.{domain}.{tld}'
'middleware' => ['subdomain']
], function () { ... }
We now have three parameters, one representing the subdomain
, one the domain
and one the tld
(.com, .net, etc). In our Subdomain
class we could pull all the pieces out, and similarly call #forgetParameter
on each of them. This works, but I think it might be overkill and a little deceiving. If we aren't actually going to use the domain
and tld
for anything, why parameterize them - just for convenience? And if the URL ends up more elaborate at some point, this pattern may need to be adjusted again. Workable, but not ideal.
Making the route an entry in your environment file
The other approach is to make the entire string configurable at the .env
level. I prefer this approach, because it means I can keep it parameterized for the subdomain but make it as different as I want otherwise. I just change the env variable depending on where i'm deploying to.
//.env
DOMAIN_URL={subdomain}.mylaravelproject.com
//routes.php
Route::group([
'domain' => env('DOMAIN_URL'), //Use the environment value instead
'middleware' => ['subdomain']
], function () { ... }
Now we can still focus on the {subdomain}
parameter alone, and have the configurability we need to move between environments easily.
Closing
We've learned how to use subdomains in Laravel, what it does to our code, how to make the experience cleaner, and how to keep it configurable. It took a few extra steps, but adding in these settings and middleware will make your code more easily maintainable as you move forward. Now that we have a subdomain in our site: what can we do with it, how do we track it, and what does it mean for our users? We'll explore that in a later post. Let me know if you have any questions or input in the meantime by leaving a comment!