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?
Top comments (1)
Hi, do you have GitHub repo for this?