Skip to content

Module System

Architecture decision

The rationale for using a modular monolith and domain-focused modules is described in ADR 0001: Modular monolith architecture.

Overview

Wedissimo uses a modular monolith architecture where features are organized into self-contained modules in the /modules/ directory. Each module is a Laravel package with its own structure but shares the application's dependencies.

Module Structure

modules/YourModule/
├── Config/
│   └── config.php              # Module configuration
├── Database/
│   ├── Migrations/             # Module-specific migrations
│   └── Seeders/                # Module-specific seeders
├── Http/
│   ├── Controllers/            # Module controllers
│   ├── Requests/               # Form validation
│   └── Resources/              # API resources
├── Models/                     # Module models
├── Notifications/              # Module notifications
├── Providers/
│   └── YourModuleServiceProvider.php
├── Routes/
│   └── api.php                 # Module routes
├── Services/                   # Business logic
└── Tests/
    ├── Feature/                # Integration tests
    └── Unit/                   # Unit tests

Module Loading

Modules are auto-loaded via Modules\common\Providers\ModuleServiceProvider:

  1. Reads enabled modules from config/platform.common.modules.enabled
  2. Registers each module's ServiceProvider
  3. ServiceProviders load routes, configs, and migrations

Configuration

php
// config/platform.common.modules
return [
    'enabled' => [
        'MagicLink',
        'YourModule',
    ],
    'classes' => [
        'MagicLink' => MagicLinkServiceProvider::class,
        'YourModule' => YourModuleServiceProvider::class,
    ],
];

Creating a New Module

The easiest way to create a new module is using the built-in Artisan command:

bash
docker exec wedissimo-api php artisan make:module YourModule

This command automatically:

  • Creates the complete module directory structure
  • Generates a ServiceProvider with Livewire support
  • Creates API and web route files with proper middleware
  • Sets up permission seeder with plural naming conventions
  • Creates navigation partial for admin sidebar
  • Generates OpenAPI documentation stub
  • Registers the module in config/platform.common.modules

Generated Structure:

modules/YourModule/
├── Config/
│   ├── config.php              # Module configuration
│   └── scout.php               # Optional Scout/Typesense config
├── Database/
│   ├── Migrations/             # Module-specific migrations
│   └── Seeders/
│       └── YourModulePermissionSeeder.php
├── Http/
│   ├── Controllers/            # Module controllers
│   ├── Requests/               # Form validation
│   └── Resources/              # API resources
├── Livewire/                   # Livewire components
├── Models/                     # Module models
├── Notifications/              # Module notifications
├── Providers/
│   └── YourModuleServiceProvider.php
├── Resources/
│   └── views/
│       ├── livewire/           # Livewire views
│       └── partials/
│           └── nav.blade.php   # Admin navigation items
├── Routes/
│   ├── api.php                 # API routes
│   └── web.php                 # Web routes with session support
├── Services/                   # Business logic
├── Tests/
│   ├── Feature/                # Integration tests
│   └── Unit/                   # Unit tests
└── openapi.yaml               # OpenAPI documentation

Important Notes:

  • Web Routes: Automatically wrapped in web middleware for session/cookie support
  • Permissions: Uses plural naming (e.g., view vendors, create vendors)
  • Navigation: Auto-discovered and injected into admin sidebar
  • Livewire: Components are registered and views have proper namespace

After Generation: The module is immediately ready to use. You can start adding:

  • Models in modules/YourModule/Models/
  • Controllers in modules/YourModule/Http/Controllers/
  • Livewire components in modules/YourModule/Livewire/
  • Migrations using: php artisan make:migration create_your_table --path=modules/YourModule/Database/Migrations

Module Navigation & Permissions

Auto-Discovered Navigation

Modules can contribute navigation items to the admin sidebar using the @push directive. Navigation is automatically loaded when the main nav renders.

Create navigation partial (modules/YourModule/Resources/views/partials/nav.blade.php):

blade
@push('module-navigation')
    @can('view your_resources')
        <flux:sidebar.item
            icon="cube"
            href="{{ route('admin.your-resources.index') }}"
            :current="request()->routeIs('admin.your-resources.index')">
            {{ __('Your Resources') }}
        </flux:sidebar.item>
    @endcan
@endpush

Register in ServiceProvider:

php
protected function registerNavigation(): void
{
    View::composer('layouts.nav', function () {
        if (view()->exists("{$this->moduleNameLower}::partials.nav")) {
            view("{$this->moduleNameLower}::partials.nav")->render();
        }
    });
}

The navigation item will automatically appear in the admin sidebar when:

  1. The user has the required permission (view your_resources)
  2. The view composer renders the navigation partial

Module Permissions

Modules should define their own permissions using a dedicated seeder.

Create permission seeder (modules/YourModule/Database/Seeders/YourModulePermissionSeeder.php):

php
<?php

namespace Modules\YourModule\Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;

class YourModulePermissionSeeder extends Seeder
{
    public function run(): void
    {
        $permissions = [
            'manage your_resources',
            'view your_resources',
            'create your_resources',
            'update your_resources',
            'delete your_resources',
        ];

        foreach ($permissions as $permission) {
            Permission::updateOrCreate(['name' => $permission]);
        }
    }
}

Important: Use plural naming for permissions (e.g., view vendors, not view vendor). This follows Laravel's convention.

Auto-discovery: The core PermissionSeeder automatically discovers and calls module permission seeders:

php
// database/seeders/PermissionSeeder.php
protected function callModulePermissionSeeders(): void
{
    $moduleConfig = Config::get('platform.common.modules');

    foreach ($moduleConfig['enabled'] as $module) {
        $seederClass = "Modules\\{$module}\\Database\\Seeders\\{$module}PermissionSeeder";
        if (class_exists($seederClass)) {
            $this->call($seederClass);
        }
    }
}

Run the seeder:

bash
docker exec wedissimo-api php artisan db:seed --class=PermissionSeeder

Gate Authorization

The super_admin role has access to all permissions via Gate::before() in AuthServiceProvider:

php
Gate::before(function ($user) {
    return $user->hasRole(Config::get('constants.roles.super_admin')) ? true : null;
});

This means super_admin will pass all @can() checks and permission middleware without needing explicit permission assignments.

Existing Modules

See the Domains documentation for detailed information about each implemented module:

  • Vendors - Vendor profiles, onboarding, search, and management
  • Listings - Service listings and marketplace functionality
  • Payments - Payment processing and transactions (planned)
  • Communications - Messaging and notifications (planned)

For authentication-related modules (MagicLink, Profile, UserInvitation), see the Authentication guide.

Module Best Practices

1. Self-Contained

Each module should be as independent as possible:

php
// GOOD - Module handles its own logic
$service = app(YourModuleService::class);
$result = $service->process($data);

// BAD - Tight coupling to other modules
$otherModuleService = app(OtherModuleService::class);

2. Service Layer

Use services for business logic:

php
namespace Modules\YourModule\Services;

class YourModuleService
{
    public function process(array $data): Model
    {
        // Business logic here
        return YourModel::create($data);
    }
}

3. API Resources

Always use resources for API responses:

php
namespace Modules\YourModule\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class YourResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'created_at' => $this->created_at,
        ];
    }
}

4. Form Requests

Use form request classes for validation:

php
namespace Modules\YourModule\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreYourModelRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:your_models',
        ];
    }
}

5. Testing

Test at the module level:

php
// modules/YourModule/Tests/Feature/YourFeatureTest.php

use Tests\TestCase;

test('can create your model', function () {
    $user = createUser();

    $response = $this->actingAs($user)
        ->postJson('/api/v1/your-resource', [
            'name' => 'Test Name',
        ]);

    $response->assertStatus(201)
        ->assertJsonStructure(['data' => ['id', 'name']]);
});

Module Communication

1. Events & Listeners

php
// Module A dispatches event
event(new YourModuleEvent($data));

// Module B listens
class YourModuleListener
{
    public function handle(YourModuleEvent $event): void
    {
        // Handle event
    }
}

2. Service Contracts

php
// Define interface
interface YourModuleServiceInterface
{
    public function process(array $data): Model;
}

// Bind in service provider
$this->app->bind(
    YourModuleServiceInterface::class,
    YourModuleService::class
);

3. Eloquent Relationships

php
// Cross-module relationships are fine
class ModuleAModel extends Model
{
    public function moduleBRelation()
    {
        return $this->hasMany(ModuleBModel::class);
    }
}

Database Migrations

Creating Module Migrations

bash
docker exec wedissimo-api php artisan make:migration create_your_table --path=modules/YourModule/Database/Migrations

Migration Pattern

php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('your_table', function (Blueprint $table) {
            $table->uuid('id')->primary();
            $table->string('name');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('your_table');
    }
};

Module Configuration

Accessing Module Config

php
// Get module config
$value = config('yourmodule.key');

// Set at runtime
config(['yourmodule.key' => 'value']);

Publishing Module Config

php
// In ServiceProvider
$this->publishes([
    __DIR__ . '/../Config/config.php' => config_path('yourmodule.php'),
], 'config');

Testing Modules

Running Module Tests

bash
# All module tests
docker exec wedissimo-api vendor/bin/pest modules/YourModule/Tests/

# Specific test file
docker exec wedissimo-api vendor/bin/pest modules/YourModule/Tests/Feature/YourFeatureTest.php

# With filter
docker exec wedissimo-api vendor/bin/pest modules/YourModule/Tests/ --filter="your test"

Module Test Setup

php
// modules/YourModule/Tests/Pest.php

uses(Tests\TestCase::class)
    ->beforeEach(function () {
        // Module-specific setup
    })
    ->in('Feature', 'Unit');

Troubleshooting

Module Not Loading

  1. Check config/platform.common.modules:

    • Module listed in enabled
    • ServiceProvider in classes
  2. Clear caches:

    bash
    docker exec wedissimo-api php artisan config:clear
    docker exec wedissimo-api php artisan cache:clear
  3. Verify ServiceProvider namespace matches directory structure

Routes Not Found

  1. Check route file location: modules/YourModule/Routes/api.php or web.php
  2. Verify loadRoutesFrom() in ServiceProvider
  3. Check route middleware and prefixes
  4. Run php artisan route:list --name=your.route to verify registration

Authenticated Users Redirected to Login

If authenticated users are being redirected to /admin/login when accessing module routes:

Cause: Module web routes are missing the web middleware wrapper.

Solution: Wrap all module web routes in Route::middleware('web')->group():

php
// modules/YourModule/Routes/web.php

// WRONG - Missing web middleware
Route::prefix('/admin')->middleware(['auth', 'verified'])->group(function () {
    Route::get('/your-resource', YourComponent::class);
});

// CORRECT - Wrapped in web middleware
Route::middleware('web')->group(function () {
    Route::prefix('/admin')->middleware(['auth', 'verified'])->group(function () {
        Route::get('/your-resource', YourComponent::class);
    });
});

Why this happens: Module routes loaded via loadRoutesFrom() don't automatically inherit the web middleware group. Without it, session-based authentication cannot function, causing the auth middleware to fail and redirect users to login.

Debugging steps:

  1. Check if route has web middleware: php artisan route:list --name=your.route

  2. Clear route cache: php artisan route:clear

  3. Test with tinker:

    php
    $route = Route::getRoutes()->getByName('your.route');
    dd($route->middleware()); // Should include 'web'

Migrations Not Running

  1. Verify path: modules/YourModule/Database/Migrations/
  2. Check registerMigrations() in ServiceProvider
  3. Run php artisan migrate:status to see pending migrations

Next Steps

Wedissimo API Documentation