Sub-domain “Profiles” in Laravel
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:
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.
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:
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 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.