Clean Subdomains in Laravel

Subdomain

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!

  • Elias Gonz├ílez

    nice post, thank you for sharing your knowledge

  • Yury Boichuk

    1. How to get subdomain variable in controller?
    2. How to automatically add dynamic subdomain variable to all route() (in domain group) without rewrite it all?