Large and scalable Laravel application with Domains
VΓctor FalcΓ³n
Posted on April 29, 2022
A few months ago, we talk about the repository pattern in Laravel and the reason I don't like it.
In this post, we are going to talk about how to structure our project if we are working on a large Laravel application.
I found that the default Laravel structure doesn't work with large projects while I was working in my side project, monse. With every commit I made, I felt that the project it's getting overwhelming, because it was impossible to see all the code related to a desire model at a glance.
π By the way, monse is a simple and automated personal finances
for normal people.If you want to stop over-expending, retire early and happier,
take a look now.
Table of content
The problem with the default folder structure
By default, a Laravel application structure is something like this:
βββ app
β βββ Console
β β βββ Commands
β βββ Contracts
β βββ Events
β βββ Exceptions
β β βββ Auth
β βββ Http
β β βββ Controllers
β β βββ Middleware
β β βββ Requests
β β βββ Resources
β βββ Jobs
β βββ Listeners
β βββ Mail
β β βββ Auth
β β βββ User
β βββ Models
β βββ Notifications
β βββ Policies
β βββ Providers
βββ database
β βββ factories
β βββ migrations
β βββ seeders
βββ config
βββ routes
βββ resources
βββ js
βββ sass
βββ views
βββ mail
βββ vendor
There is nothing wrong with this structure, but when we are working on a big application it's difficult to check, at a glance, all the models, controllers, and services involved in one request, for example.
Imagine that you have a model called Article
with a PostArticleController
, a GetArticleController
and also some events like ArticleCreated
, ArticleUpdated
and more.
You can create a subfolder inside any of this already created folders called Article to split different parts of your application, and you will have something like this:
βββ app
β βββ Console
β β βββ Commands
β βββ Contracts
β βββ Events
β β βββ Article
β β βββ ArticleCreated.php
β β βββ ArticleUpdated.php
β β βββ ArticleDeleted.php
β βββ Exceptions
β β βββ Auth
β β βββ Article
β β βββ ArticleWithoutValidDate.php
β βββ Http
β β βββ Controllers
β β β βββ Article
β β β βββ PostArticleController.php
β β β βββ GetArticleController.php
β β βββ Middleware
β β βββ Requests
β β β βββ Article
β β β βββ PostArticleRequest.php
β β β βββ GetArticleRequest.php
β β βββ Resources
β β β βββ Article
β β β βββ ArticleResource.php
β βββ Jobs
β βββ Listeners
β β βββ Article
β β βββ SendNotificationOnArticleCreated.php
β β βββ UpdateDashboardStatsOnArticleDeleted.php
β βββ Mail
β β βββ Auth
β β βββ User
β βββ Models
β β βββ Article.php
β βββ Notifications
β β βββ Article
β β βββ NewArticleNotification.php
β βββ Policies
β βββ Providers
βββ database
β βββ factories
β βββ migrations
β βββ seeders
βββ config
βββ routes
βββ resources
βββ js
βββ sass
βββ views
βββ mail
βββ vendor
As you can see, the folders are growing a lot, and we still have all our Article code spreader between all the application.
It's difficult to see, at a glance, all the code related with our domain model Article.
And this is only the beginning, in a big application we are going to have like 10 or more models with his controllers, events, listeners, and more. This is going to be a problem.
The solution
The idea it's to make domain folders or bounded contexts to store all the code related to one model of our application in one folder only.
The idea it's to achieve a folder structure like this:
src
βββ BankAccount
βΒ Β βββ Actions
βΒ Β βββ Http
βΒ Β βββ Infrastructure
βΒ Β βββ Policies
βΒ Β βββ BankAccount.php
βββ BankConnection
βΒ Β βββ Actions
βΒ Β βββ Console
βΒ Β βββ Events
βΒ Β βββ Http
βΒ Β βββ Infrastructure
βΒ Β βββ Jobs
βΒ Β βββ Listeners
βΒ Β βββ Mail
βΒ Β βββ Notifications
βΒ Β βββ Policies
βΒ Β βββ BankConnection.php
βββ StockOrder
βΒ Β βββ Actions
βΒ Β βββ Http
βΒ Β βββ Infrastructure
βΒ Β βββ Listeners
βΒ Β βββ Policies
βΒ Β βββ Support
βΒ Β βββ StockOrder.php
βββ UserStockPortfolio
βββ Actions
βββ Console
βββ Http
βββ Infrastructure
Β Β βββ Jobs
Β Β βββ UserStockPortfolio.php
This is the current folder structure of my Laravel project, get.monse.app.
As you can see, inside each folder we have the model and different classes related to it. We can see all the actions, HTTP controllers, jobs, listeners, policies, and more.
This way, when I'm working on StockOrders
, for example, I can see all the code related, and it's easier to add new controllers, routes, and more without affecting all the application.
How to implement this in Laravel
Implementing this in Laravel it's simple.
First, you need to create a new src
folder and manually add it to the composer file. I define the namespace as Monse\\
, because it's my project name, but you can use anything else.
// ...
"psr-4": {
"App\\": "app/",
"Monse\\": "src/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
},
// ...
Now we can add whatever we want to this folder and will be automatically load by composer, but there are some things that we need to take care.
We want each module to have his own routes, events, commands and all. To do this, we need to create different service providers and add them to our config/app.php
file.
1. Routes
For routes we are going to create a new service provider extending from Illuminate\Foundation\Support\Providers\RouteServiceProvider
. In this file we just need to implement the boot method like this:
// ...
public function boot(): void
{
$this->routes(function () {
Route::middleware(['api', 'auth:api'])->prefix('api')->group(function () {
Route::get('user', GetUserController::class);
// ...
});
});
}
// ...
2. Events
For events, we can extend from Illuminate\Foundation\Support\Providers\EventServiceProvider
and define the $listen
array. Something like this:
protected $listen = [
UserCreated::class => [
SendWelcomeMailOnUserCreated::class,
// ...
],
];
3. Commands
For commands, we will create a normal service provider and register them using the register
method:
// ...
private array $commands = [
CreateUserCommand::class,
];
public function register(): void
{
$this->commands($this->commands);
}
// ...
4. Factories
These are a bit tricky, but it's easy.
We need to put the factory UserFactory, for example, inside the factories/Monse/User
folder (because we define namespace of our folder as Monse in the first step).
Also, we need to define, inside the factory the model that is owning that factory:
public function modelName(): string
{
return User::class;
}
Everything else will be working as expected without anything special to do.
What do you think? Do you like this project structure?
Posted on April 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.