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!

Selecting carefully with Laravel joins

Laravel comes packaged with an ORM called Eloquent, which is one of the better PHP tools for dealing with your database layer using objects. It provides all of the features you’d expect from a modern relational mapper, and cleanly uses some PHP magic to make the process pleasant.

But it has its quirks, and today i’m setting my sights on a not-uncommon operation – the join.

The tables

Let’s say we have two tables: users and posts. users has an id and a name. posts has an id, name, content and user_id (a foreign key to the users table). Each table has the following data

users 
id | name 
1  | jp 
2  | cosmo

posts 
id | name         | content      | user_id 
18 | jp's post    | hello there! | 1 
22 | cosmo's post | hi there!    | 2

Here are their definitions in Eloquent

class User extends Eloquent {
  protected $fillable = ['name'];
  public function posts()
  {
    return $this->hasMany('Post');
  }
}

class Post extends Eloquent {
  protected $fillable = ['name', 'content', 'user_id'];
}

With this definition, we can retrieve the first user easily, and the results are what we’d expect

$user = User::first();
print $user->id;   //prints 1
print $user->name; //prints "jp"

Where it gets tricky

Retrieving a User object directly is nice, but many times when dealing with the database you’ll be issuing joins to retrieve your data. A join is fairly simple in Eloquent

$user = 
  User
    ::join('posts', 'posts.user_id', '=', 'users.id')
    ->where('posts.name', 'LIKE', '%jp%')
    ->first(); 

The join takes a table name, and then the pieces of the join clause. In this case we’re joining on the relationship between the posts.user_id and the users.id. A little bit clunky, but usable.

What isn’t usable, are the results.

print $user->id;   //prints 18
print $user->name; //prints "jp's post"

The object looks the same, but we’re getting what seems to be invalid results. When we ran it before, we printed an id of 1 and a name of “jp” – now we’re seeing an id of 18, which isn’t even available in the users table at all. And the name “jp’s post” is the from the posts table. What’s going wrong?

Under the hood

By default when Eloquent issues a query, it selects all columns

User::all(); //SELECT * FROM `users`

When you perform a join, it continues issuing the same select call, additionally adding the join and where clauses. So our join from earlier results in the following query

SELECT * 
FROM users 
INNER JOIN posts ON users.id = posts.user_id 
WHERE posts.name LIKE '%jp%' 

The result is a row with two id and name columns. Raw sample output from a database tool would look something like this

id | name | id | name      | content      | user_id
1  | jp   | 18 | jp's post | hello there! | 1

This means that when Eloquent parses the results into the User object, it’s contending with two versions of the same column name. Most likely it’s using a hash internally, and the second key overrides the first. The result is a User object with the id and name of the related Post.

Fixing the issue

The solution turns out to be very simple. You have to be specific with your select clause.


$user = 
  User
    ::join('posts', 'posts.user_id', '=', 'users.id')
    ->where('posts.name', 'LIKE', '%jp%')
    ->select('users.*')
    ->first(); 

Now our select is specifically selecting columns from the users table only, and our results are consistent with our expectations again

print $user->id;   //prints 1
print $user->name; //prints "jp"

How Rails does it

One reason this tripped me up in the first place, is coming to Laravel from Ruby on Rails. In Ruby on Rails, this problem is taken care of for you.

User.joins(:posts).where(name: '%jp%').first
SELECT users.* 
FROM users 
INNER JOIN posts ON users.id = posts.user_id
WHERE name LIKE '%jp%'

I assumed a similar behavior in Laravel without ever checking the results. Personally I think the Rails behavior is the more sensible default, since it keeps your class mapping directly to your table (unless you explicitly tell it otherwise using #select).

Why not use #with?

For people used to using eloquent, it may be suggested to instead use the #with method. Using #with does technically fix the select issue, but the use case it solves and queries it generates aren’t the same.

#with allows you to work around the N + 1 problem and also can give some form of filtering. To achieve a similar affect to the join clause using #with

User::with(['posts' => function ($query)
{
  $query->where('name', 'LIKE', '%jp%');
}])->first();

Using #with you might expect a similar result to the join, but the queries generated work differently.

SELECT * FROM users
SELECT * FROM `posts` WHERE `posts`.`user_id` IN (1) AND `name` LIKE '%jp%'

#with generates two queries, so if you’re trying to perform an operation on the data with one call, it’s not the way to go. It first fully retrieves the users table information, then uses that to construct a query on the posts table. It’s less efficient and may not always be accurate.

Also – it’s up for debate, but I find that adding a where clause to a #with method is more complicated to read.

Conclusion

The way joins work in Eloquent has bitten me a few times, and it teaches you to be careful about making assumptions when moving around between frameworks and abstraction layers. Laravel feels right at home when you’re used to using other systems like Rails, but truly understanding the framework you’re using and implications its tools have is important to making sure mistakes like this don’t slip through.