Sub-domain “Profiles” in Laravel

Warrick Bayman
6 min readJul 5, 2018

2019/03/03: I recently updated this post, and there’s quite a number of changes I’ve made. The old post was fairly outdated and needed some attention.

I work on a lot of apps that load up an “organisation” or “client” profile based on a sub-domain slug. It’s quite common in multi-tenant applications. This is something we do on MotorPress (https://motorpress.co.za) and it works really well, so I’ll use that as an example.

Each publisher on MotorPress gets their profile. They can publish content to the main MotorPress feed, or exclusively to their own branded, public profile feed. We provide each publisher with their own sub-domain. So we end up with something like this:

https://motorpress.co.za/
https://vw.motorpress.co.za/
https://audi.motorpress.co.za/
https://toyota.motorpress.co.za/

The bit before the “motorpress.co.za” domain is a unique “slug” that each publisher is assigned. If a visitor finds their way to each of those profile pages they’ll see a slightly different version of the MotorPress home page. Each branded exclusively for that organisation.

It turns out that Laravel provides a toolset that makes this sort of thing really easy to achieve. Here’s an example of how we do it.

The database

We’ll need to store the organisation slugs somewhere. You’ll want to include a slug column in your organisations (or whatever your organisational units are called) table. An updated up method in the migration file might look something like this:

A simple `CreateOrganisationsTable` migration

In the example, the slug must be unique and a maximum of 40 characters long. You’ll probably have some other details in there as well.

Generating slugs automatically

I have a SlugService which we use to generate unique slugs for each organisation. There’s plenty of packages out there that do this as well, so you can decide if you want to do this yourself or use one of those. A good one to look at it https://github.com/spatie/laravel-sluggable from Freek Van der Herten.

The `SlugService` class

You can make a call to the uniqueSlug method to generate a new slug for any model. Pass in the model instance, the source for the slug and the name of the column where the slug will be stored. So it can be used like this:

(new SlugService())->uniqueSlug($organisation, 'title', 'slug');

And since I want to do this automatically, I can use the creating model event which will fire each time a new Organisation instance is created. I’ll add this to the boot method of the Organisation class:

static::creating(function (Organisation $organisation) {
$organisation->slug = $organisation->slug ?:
(new SlugService())->uniqueSlug($organisation);
});

By using $organisation->slug = $organisation->slug ?: ... I can pass in an different slug if I need to, otherwise the uniqueSlug class in the SlugService will generate a new slug for me based on the organisation title.

Dealing with the request

Laravel allows me to hook into the request lifecycle really easily through the use of middleware. In this case, I want to create a middleware class that will sort out the organisation profile before the request hits the router. So I have a middleware class named SetOrganisationProfile. Artisan has a make:middleware command that makes this real easy:

php ./artisan make:middleware SetCompanyProfile

The handle method in any middleware is generally where most of the action happens. However, since I don’t like having very long methods I’ll often move a lot of this logic into something like a ProfileService which I reply on when I need profile information. This is a fairly opinionated approach and you may have a completely different solution, but this gives me a service that I can use elsewhere if needed. I sometimes like to create facades that make access to those services even easier. I’ll get to that a little later.

Here’s how my app/Services/ProfileService.php :

This isn’t actually that complicated, really. The middleware, when we get to it, will make a call to the getProfileOrganisation method and pass in the Request object. The getProfileOrganisation method will in turn make a call to the getProfileSlugFromRequest method to get the actual slug from the URL, and then pass the slug to the getProfileOrganisationFromSlug method to get the associated Organisation model. I’ve also added an ignoreSlugs array to the class with a specified list of slugs that should be ignored. For example, I sometimes have an images slug.

You may notice the extractDomainFromApplicationUrl protected method. This service assumes that I’ve got https://motorpress.co.za set as the APP_URL in my .env file. If you have a www in the domain name, then you’ll need to update this method accordingly. A super quick way to do that is just change +3 to a +7 (“://www.” is 7 characters long).

return substr(
config('app.url'),
strrpos(config('app.url'), '://www') +7);

That will return motorpress.co.za. Anything before that string, in my case anyway, is a slug. That’s where the getHostSlug method comes in which will assume that if the actual host is longer than the domain we extracted, then there must be a slug prefix. We then make sure that the prefix doesn’t exist in the ignoreSlugs array, and return the result.

Okay, now onto that Middleware.

The middleware

Since the service is doing the heavy lifting as far as the request is concerned, I can keep my middleware class nice and light:

The `SetOrganisationProfile` middleware

So when a request is made, the handle method gets called and a Request object gets passed in. A new instance of the service is created and the request gets passed to the getOrganisationProfile method of the ProfileService class and hopefully I get an Organisation instance back. If the organisation doesn’t exist, I’ll get null.

If we don’t get a Organisation instance back but there definitely is a slug in the URL, then, I’ll usually bail out here with a 404 response. This is where the hasSlug method on the service to check comes in. I don’t do anything special here and Laravel’s abort helper does exactly what I need.

If there is a Organisation model then I can put the instance onto the Session, and also share it with any views that might need access to it. There’s two protected methods for that: updateSession and shareWithViews respectively.

Lastly, I just need to ensure that the middleware is registered by adding it to the list of middleware in app/Http/Kernel.php. I usually add it to the web middleware group:

'web' => [
\App\Http\Middleware\SetOrganisationProfile::class
],

I usually don’t use this any API stuff, but it’s something you need then you won’t be able to use the session to store the profile since API’s are inherently stateless. In that case, you can just make a call to the getOrganisationProfile method on the ProfileService as you need to.

Facades

So this is just a little update that makes accessing those services a little easier if you want to go this route. I like using facades in Laravel as they feel more expressive and they look neater. But again, this is opinionated and I suppose there’s other ways to do this.

Since facades are not essential to the running of my app, I think of them as support classes. They’re really handy, but not a requirement. Therefore they go in an app/Support/Facades directory. First I’ll create a Profile facade class:

The `Profile` facade

The facade needs to be registered, so in AppServiceProvider:

public function register()
{
$this->app->bind('profile', function ($app) {
return new ProfileService();
});
}

Now I don’t need to new up an instance of ProfileService myself and I don’t need to inject it into a constructor. I can simple do:

use App\Support\Facades\Profile;...$profile = Profile::getProfileOrganisation();

That’s it. There’s been a few other ideas pop up around this topic lately. This is just something I’ve been using for some time.

Where to from here

I suppose there’s a lot you could do with this idea now. For example, you could update the getProfileOrganisation method to return the Organisation instance that’s on the session if it exists instead of pulling the instance from the DB again (if you’re using the facade method). You could create a facade for the SlugService as well, which is something I’ll do sometimes.

--

--

Warrick Bayman

Programmer, musician, cyclist (well... I own a bike), husband and father.