Large and scalable Laravel application with Domains

victoor

VΓ­ctor FalcΓ³n

Posted on April 29, 2022

Large and scalable Laravel application with Domains

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

  1. The problem with the default folder structure
  2. The solution
  3. How to implement this in Laravel
    1. Routes
    2. Events
    3. Commands
    4. Factories

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/"
    },

    // ...
Enter fullscreen mode Exit fullscreen mode

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);

                // ...
            });
        });
    }

    // ...
Enter fullscreen mode Exit fullscreen mode

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,
            // ...
        ],
    ];
Enter fullscreen mode Exit fullscreen mode

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);
    }

    // ...
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

Everything else will be working as expected without anything special to do.


What do you think? Do you like this project structure?

πŸ’– πŸ’ͺ πŸ™… 🚩
victoor
VΓ­ctor FalcΓ³n

Posted on April 29, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related