DEV Community

david duymelinck
david duymelinck

Posted on

Laravel modular folder structure

I am not the first with the idea, but I wanted to make it myself as an exercise.
The idea behind a modular folder structure is creating package-like folders that have almost no ties with other parts of the applications. For example they could share the database, but they don't share tables.
This way it is easy to reuse them or split them from the application to put on a more powerful server if needed.

Discovering the module parts step by step

Most people that know Laravel know the AppServiceProvider.
I'm named it ModuleServiceProvider and only have the boot method.
Because I'm going to use modules in the app directory I removed the Http/Controllers and Models directories. For this post I'm going to use a User module to demonstrate the folder structure.

  • app
    • Providers
      • ModuleServiceProvider
    • User

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{

    public function boot(): void
    {
       $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(base_path('app')));
        $files->setMaxDepth(2);

        foreach ($files as $file) {
           // code in the sections below
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

I am using the iterator classes because then I have more configuration options than using glob.
One of those options is setmaxDepth.
I want a flatter folder structure in the modules, so all directories that are significant are a direct children of the module directory.

Routes

if($file->isFile() && 
   str_contains($file->getPathname(), 'routes') && 
   $file->getFilename() == 'web.php') {
                Route::middleware('web')->group($file->getRealPath());
            }
Enter fullscreen mode Exit fullscreen mode

I want the routes in the modules to have the middleware without the need the add it explicitly.
You can add a similar if for the api.php file, or any other group of routes you want.

The controller are no problem because they are configured by the routes.

use App\User\UserController;
use Illuminate\Support\Facades\Route;

Route::get('/login', new UserController()->login(...));
Enter fullscreen mode Exit fullscreen mode
  • app
    • Providers
      • ModuleServiceProvider
    • User
      • routes
        • web.php
      • UserController.php

Views

if ($file->isDir() && str_contains($file->getPathname(), 'views')) {
                $namespace = strtolower(str_replace(['/app/app/', '/views'], '', $file->getPath()));
                $this->loadViewsFrom($file->getRealPath(), $namespace);
            }
Enter fullscreen mode Exit fullscreen mode

To make it more clear that the views in the modules are used, I added the name of the module in lowercase as namespace.

namespace App\User;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class UserController extends Controller
{
    public function login(Request $request) {
        return view('user::login');
    }
}
Enter fullscreen mode Exit fullscreen mode
  • app
    • Providers
      • ModuleServiceProvider
    • User
      • routes
        • web.php
      • views
        • login.blade.php
      • UserController.php

Migrations

Remember before I mentioned modules don't share tables. So it is logical to move the migrations to the module folders.

if($file->isDir() && str_contains($file->getPathname(), 'migrations')) {
                $this->loadMigrationsFrom($file->getRealPath());
            }
Enter fullscreen mode Exit fullscreen mode
  • app
    • Providers
      • ModuleServiceProvider
    • User
      • migrations
        • create_users_table.php
      • routes
        • web.php
      • views
        • login.blade.php
      • UserController.php

Localisation

The last thing I wanted in the modules are the language files.

if($file->isDir() && str_contains($file->getPathname(), 'lang')) {
                $this->loadTranslationsFrom($file->getRealPath());
            }
Enter fullscreen mode Exit fullscreen mode
  • app
    • Providers
      • ModuleServiceProvider
    • User
      • lang
        • en
          • user.php
      • migrations
        • create_users_table.php
      • routes
        • web.php
      • views
        • login.blade.php
      • UserController.php

Because Laravel has groups to provide context for the localisation strings, I choose to match the language filename with the module name. This gives translation keys like {{ __('user.label.email') }}.

The full ModulesServiceProvider

use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{

    public function boot(): void
    {
        $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(base_path('app')));
        $files->setMaxDepth(2);

        foreach ($files as $file) {
            if ($file->isDir() && str_contains($file->getPathname(), 'views')) {
                $namespace = strtolower(str_replace(['/app/app/', '/views'], '', $file->getPath()));
                $this->loadViewsFrom($file->getRealPath(), $namespace);
            }

            if($file->isDir() && str_contains($file->getPathname(), 'migrations')) {
                $this->loadMigrationsFrom($file->getRealPath());
            }

            if($file->isDir() && str_contains($file->getPathname(), 'lang')) {
                $this->loadTranslationsFrom($file->getRealPath());
            }

            if($file->isFile()  && str_contains($file->getPathname(), 'routes') && $file->getFilename() == 'web.php') {
                Route::middleware('web')->group($file->getRealPath());
            }
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

I left out the assets because the current way of adding them to the templates is with @vite and there you add the full paths for the assets.

For the classes you can structure the folders as you want.

Conclusion

After I was satisfied with the result, I looked if someone did it already. And I found Laravel-modules. They create almost a full Laravel application folder structure in each module directory. While it will be very familiar for most of the Laravel developers, I think find it a bit too much.
The benefit of that package is that they have ported almost all artisan methods to work with modules.

One place where I think there is going to be a dependency is in the views with the layout. At the moment I don't have a solution.
You can adopt a standard name for the layout file, and in case that the module becomes a service you could create an empty file.

Another thing that could be an issue is modules in modules. That is a good way to split modules with a lot of code.

But for now I like the result.

Top comments (0)