In this article, we'll cover how to register subdomains for any user, team, company, whatever the model may be.
If you prefer to watch, this course covers putting everything in this article into a real app, with some additional features.
Let's look at the basics of capturing subdomains within your Laravel routes before we make this practical.
To experiment with subdomains locally, your development setup will need to support subdomains. I'm using Laravel Herd, so we'll be working with a project called subdomains
, which is accessible in the browser at subdomains.test
. Laravel Valet will also work with subdomains by default.
If you're using any other solution, you may need to tweak nginx or whatever server you use before this works.
To capture subdomain routes in Laravel, create a route group and specify the domain.
Route::domain('{subdomain}.subdomains.test')->group(function () {
// all your grouped subdomain routes go here
});
This new route will respond to any subdomain. All of your subdomain routes will live within the group. Here's an example.
Route::domain('{subdomain}.subdomains.test')->group(function () {
Route::get('/', function (string $subdomain) {
dd($subdomain);
});
});
This new route, representing the home of the subdomain, gets a $subdomain
passed into. Any routes you register within this group will get the same.
Accessing the /
route will dump whatever you've passed as the subdomain. If I access alex.subdomains.test
, it'll dump alex
. Pretty simple so far!
Right now, we're hardcoding the primary domain of our app within the route. This isn't ideal when we push to production because our domain will change. No worries, access the domain you're working on from config and replace it.
Route::domain('{subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', function (string $subdomain) {
dd($subdomain);
});
});
There are a bunch of ways to do this, but this solution replaces https://
and http://
from the URL set inside config/app.php
, so we're left with a clean URL to add to the end of our subdomain group.
Let's make this more useful by adding a subdomain
column to the users table and fetch that user by their subdomain.
php artisan make:migration add_subdomain_to_users_table
Then, in your migration's up
method, add the subdomain
column to make sure it's unique.
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('subdomain', 20)->unique();
});
}
Run your migrations, create a user in the database, and ensure a subdomain is set. If you need to build the entire registration flow, this course covers everything.
Once the user is in your database, update the route to look like this.
Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', function (User $user) {
dd($user);
});
});
This route now uses route model binding to fetch the user by the subdomain
column. Any routes defined inside of this group can now fetch the User
instance and use it within the route. Running the above route will dump the matched user, and really importantly will 404 if the user can't be found.
Congratulations, you're now successfully fetching users (or any model) dynamically with a subdomain in your app!
It's unlikely you'll be using route closures, so let's talk about the organisation of controllers.
I like to keep all controllers related to subdomain matching in a separate directory. For example, we might have a HomeController
for the actual homepage of our application, but each user's subdomain will also have a HomeController
.
Let's create a subdomain, HomeController
, and tidy it away.
php artisan make:controller Subdomain\\HomeController
Keeping it simple, we'll still just dump the user out.
class HomeController extends Controller
{
public function __invoke(User $user)
{
dump($user);
}
}
And then fix up our routes so they're tidier.
Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', HomeController::class);
});
Use the same pattern here to keep any controllers related to the subdomain for your application in one place.
We're already using route model binding to resolve the user, but what if we need to continue route model binding to fetch things that belong to that user?
Using an example of an Article
for each user, create a model and migration.
php artisan make:model Article -m
Fill out the schema definition and run your migrations.
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('title');
$table->string('slug')->unique();
$table->timestamps();
});
Finally, the relationships between users and articles should be set up.
In the User
model, relate articles.
public function articles()
{
return $this->hasMany(Article::class);
}
And in the Article
model, relate it back to a user.
public function user()
{
return $this->belongsTo(User::class);
}
Create a fake article in the database (or use a seeder) so we can experiment with accessing articles on the subdomain.
Create a new route and controller to handle the article accessed on the subdomain (remember to organise this within the Subdomains
directory).
Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->group(function () {
Route::get('/', HomeController::class);
Route::get('/{article:slug}', ArticleController::class);
});
And here's the ArticleController
.
class ArticleController extends Controller
{
public function __invoke(User $user, Article $article)
{
dd($article);
}
}
If we create an article in the database with the slug an-article
and visit alex.subdomains.test/an-article
, we should see the article dumped out.
Even better, the article we pass in is automatically scoped to the user. This means we can't pass in an article under Alex's subdomain that doesn't belong to him.
You'll likely need to internally link to different pages within a subdomain. So, how do we generate routes for this?
First, let's create some views for the two routes we have and pass down a list of articles that belong to the subdomain user so we can click and read them.
Here's the new subdomain HomeController
.
class HomeController extends Controller
{
public function __invoke(User $user)
{
return view('subdomain.home', [
'user' => $user,
'articles' => $user->articles()->latest()->get(),
]);
}
}
And here's the home.blade.php
view (once again, separated into a directory).
<div>
<h1>Articles by {{ $user->name }}</h1>
@foreach($articles as $article)
<div>
<a href="">{{ $article->title }}</a>
</div>
@endforeach
</div>
Pretty simple, but we now need to link. The key here is setting up specific names for each layer of our routes. Here's what our subdomain route looks like after the change.
Route::domain('{user:subdomain}.' . str()->replace(['https://', 'http://'], '', config('app.url')))
->name('subdomain.')
->group(function () {
Route::get('/', HomeController::class);
Route::get('/{article:slug}', ArticleController::class)
->name('article');
});
We've given our overall domain the name of subdomain.
(notice the extra dot), and then each route inside a standard name.
This means we're able to access the route name like this.
@foreach($articles as $article)
<div>
<a href="{{ route('subdomain.article', ['user' => $user, 'article' => $article]) }}">{{ $article->title }}</a>
</div>
@endforeach
Now that's added, we can link through to any subdomain route for any user.
We've covered the absolute essentials of working with subdomains in Laravel, and you're now free to add any additional routes to your subdomain group, access any of the data on that model, and link within your app to subdomains (including internally).
Happy subdomaining!