Module Development

Complete guide to developing custom modules for LaraDashboard, from project setup to publishing and distribution. Learn hooks, Livewire integration, and best practices.

Module Development

This guide walks you through creating custom modules for LaraDashboard. Modules are self-contained packages that extend functionality without modifying core code.

Prerequisites

Before developing modules, ensure you understand:

Creating a New Module

Using Artisan Command

Generate a new module scaffold:

php artisan module:make YourModule

This creates the module structure at modules/YourModule/.

Module Structure

modules/YourModule/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── YourModuleController.php
│   │   ├── Middleware/
│   │   └── Requests/
│   ├── Livewire/
│   │   └── Admin/
│   │       └── Dashboard.php
│   ├── Models/
│   ├── Providers/
│   │   ├── YourModuleServiceProvider.php
│   │   └── RouteServiceProvider.php
│   └── Services/
├── config/
│   └── config.php
├── database/
│   ├── migrations/
│   ├── seeders/
│   └── factories/
├── resources/
│   ├── assets/
│   └── views/
├── routes/
│   ├── web.php
│   └── api.php
├── tests/
├── composer.json
├── module.json
└── README.md

Module Configuration

module.json

The module manifest file defines metadata:

{
    "name": "YourModule",
    "alias": "yourmodule",
    "description": "A custom module for LaraDashboard",
    "keywords": ["custom", "feature"],
    "priority": 0,
    "providers": [
        "Modules\\YourModule\\Providers\\YourModuleServiceProvider"
    ],
    "files": [],
    "requires": []
}

Extended module.json (Marketplace-Ready)

For modules distributed via the marketplace, include additional metadata:

{
    "name": "YourModule",
    "alias": "yourmodule",
    "title": "Your Module Title",
    "description": "A comprehensive description of your module",
    "keywords": ["custom", "feature", "extension"],
    "category": "utilities",
    "priority": 20,
    "icon": "lucide:box",
    "version": "1.0.0",
    "author": {
        "name": "Your Name",
        "email": "your@email.com"
    },
    "providers": [
        "Modules\\YourModule\\Providers\\YourModuleServiceProvider"
    ],
    "files": [],
    "requires": []
}
Field Description
title Display title for marketplace
category Module category (core, utilities, crm, etc.)
icon Iconify icon identifier
version Semantic version number
author Author information for attribution

composer.json

Define package information and autoloading:

{
    "name": "yourvendor/yourmodule",
    "description": "Your module description",
    "type": "laradashboard-module",
    "license": "MIT",
    "require": {
        "php": "^8.3"
    },
    "autoload": {
        "psr-4": {
            "Modules\\YourModule\\": ""
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Modules\\YourModule\\App\\Providers\\YourModuleServiceProvider"
            ]
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

config/config.php

Module configuration:

<?php

return [
    'name' => 'YourModule',

    /*
    |--------------------------------------------------------------------------
    | Feature Flags
    |--------------------------------------------------------------------------
    */
    'features' => [
        'api_enabled' => env('YOURMODULE_API_ENABLED', true),
        'webhooks' => env('YOURMODULE_WEBHOOKS', false),
    ],

    /*
    |--------------------------------------------------------------------------
    | Limits
    |--------------------------------------------------------------------------
    */
    'limits' => [
        'max_items' => env('YOURMODULE_MAX_ITEMS', 1000),
        'rate_limit' => env('YOURMODULE_RATE_LIMIT', 60),
    ],
];

Service Provider

Main Service Provider

<?php

namespace Modules\YourModule\App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
use Livewire\Livewire;

class YourModuleServiceProvider extends ServiceProvider
{
    protected string $moduleName = 'YourModule';
    protected string $moduleNameLower = 'yourmodule';

    /**
     * Boot the module services.
     */
    public function boot(): void
    {
        $this->registerCommands();
        $this->registerCommandSchedules();
        $this->registerTranslations();
        $this->registerConfig();
        $this->registerViews();
        $this->registerLivewireComponents();
        $this->loadMigrationsFrom(module_path($this->moduleName, 'database/migrations'));
    }

    /**
     * Register the module services.
     */
    public function register(): void
    {
        $this->app->register(RouteServiceProvider::class);
        $this->app->register(EventServiceProvider::class);

        // Register bindings
        $this->registerBindings();
    }

    /**
     * Register service bindings.
     */
    protected function registerBindings(): void
    {
        $this->app->singleton(YourModuleService::class, function ($app) {
            return new YourModuleService(
                $app->make(CacheService::class)
            );
        });
    }

    /**
     * Register Livewire components.
     */
    protected function registerLivewireComponents(): void
    {
        Livewire::component(
            'yourmodule::admin.dashboard',
            \Modules\YourModule\App\Livewire\Admin\Dashboard::class
        );

        Livewire::component(
            'yourmodule::admin.settings',
            \Modules\YourModule\App\Livewire\Admin\Settings::class
        );
    }

    /**
     * Register config.
     */
    protected function registerConfig(): void
    {
        $this->publishes([
            module_path($this->moduleName, 'config/config.php')
                => config_path($this->moduleNameLower . '.php'),
        ], 'config');

        $this->mergeConfigFrom(
            module_path($this->moduleName, 'config/config.php'),
            $this->moduleNameLower
        );
    }

    /**
     * Register views.
     */
    protected function registerViews(): void
    {
        $viewPath = resource_path('views/modules/' . $this->moduleNameLower);
        $sourcePath = module_path($this->moduleName, 'resources/views');

        $this->publishes([
            $sourcePath => $viewPath,
        ], ['views', $this->moduleNameLower . '-module-views']);

        $this->loadViewsFrom(array_merge([
            $sourcePath,
        ], $this->getPublishableViewPaths()), $this->moduleNameLower);

        // Register Blade components
        Blade::componentNamespace(
            'Modules\\YourModule\\View\\Components',
            $this->moduleNameLower
        );
    }

    /**
     * Register translations.
     */
    protected function registerTranslations(): void
    {
        $langPath = resource_path('lang/modules/' . $this->moduleNameLower);

        if (is_dir($langPath)) {
            $this->loadTranslationsFrom($langPath, $this->moduleNameLower);
            $this->loadJsonTranslationsFrom($langPath);
        } else {
            $this->loadTranslationsFrom(
                module_path($this->moduleName, 'resources/lang'),
                $this->moduleNameLower
            );
            $this->loadJsonTranslationsFrom(
                module_path($this->moduleName, 'resources/lang')
            );
        }
    }

    /**
     * Register commands.
     */
    protected function registerCommands(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                \Modules\YourModule\App\Console\SyncCommand::class,
            ]);
        }
    }

    /**
     * Register command schedules.
     */
    protected function registerCommandSchedules(): void
    {
        $this->app->booted(function () {
            $schedule = $this->app->make(\Illuminate\Console\Scheduling\Schedule::class);
            $schedule->command('yourmodule:sync')->daily();
        });
    }

    private function getPublishableViewPaths(): array
    {
        $paths = [];
        foreach (config('view.paths') as $path) {
            if (is_dir($path . '/modules/' . $this->moduleNameLower)) {
                $paths[] = $path . '/modules/' . $this->moduleNameLower;
            }
        }
        return $paths;
    }
}

Routes

Web Routes

<?php

// routes/web.php

use Illuminate\Support\Facades\Route;
use Modules\YourModule\App\Http\Controllers\YourModuleController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
*/

Route::middleware(['web', 'auth', 'verified'])
    ->prefix('admin/yourmodule')
    ->name('admin.yourmodule.')
    ->group(function () {
        // Dashboard
        Route::get('/', [YourModuleController::class, 'index'])
            ->name('index')
            ->middleware('can:yourmodule.view');

        // Items CRUD
        Route::resource('items', ItemController::class)
            ->middleware([
                'index' => 'can:yourmodule.view',
                'create' => 'can:yourmodule.create',
                'store' => 'can:yourmodule.create',
                'edit' => 'can:yourmodule.edit',
                'update' => 'can:yourmodule.edit',
                'destroy' => 'can:yourmodule.delete',
            ]);

        // Settings
        Route::get('settings', [SettingsController::class, 'index'])
            ->name('settings')
            ->middleware('can:yourmodule.settings');

        Route::post('settings', [SettingsController::class, 'update'])
            ->name('settings.update')
            ->middleware('can:yourmodule.settings');
    });

API Routes

<?php

// routes/api.php

use Illuminate\Support\Facades\Route;
use Modules\YourModule\App\Http\Controllers\Api\ItemController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/

Route::middleware(['api', 'auth:sanctum'])
    ->prefix('api/v1/yourmodule')
    ->name('api.yourmodule.')
    ->group(function () {
        Route::apiResource('items', ItemController::class);

        Route::post('items/{item}/duplicate', [ItemController::class, 'duplicate'])
            ->name('items.duplicate');
    });

Controllers

Web Controller

<?php

namespace Modules\YourModule\App\Http\Controllers;

use App\Http\Controllers\Controller;
use Modules\YourModule\App\Models\Item;
use Modules\YourModule\App\Services\YourModuleService;
use Modules\YourModule\App\Http\Requests\StoreItemRequest;
use Modules\YourModule\App\Http\Requests\UpdateItemRequest;

class ItemController extends Controller
{
    public function __construct(
        private YourModuleService $service
    ) {}

    public function index()
    {
        return view('yourmodule::admin.items.index');
    }

    public function create()
    {
        return view('yourmodule::admin.items.create');
    }

    public function store(StoreItemRequest $request)
    {
        $item = $this->service->create($request->validated());

        return redirect()
            ->route('admin.yourmodule.items.edit', $item)
            ->with('success', __('yourmodule::messages.item_created'));
    }

    public function edit(Item $item)
    {
        return view('yourmodule::admin.items.edit', compact('item'));
    }

    public function update(UpdateItemRequest $request, Item $item)
    {
        $this->service->update($item, $request->validated());

        return redirect()
            ->route('admin.yourmodule.items.index')
            ->with('success', __('yourmodule::messages.item_updated'));
    }

    public function destroy(Item $item)
    {
        $this->service->delete($item);

        return redirect()
            ->route('admin.yourmodule.items.index')
            ->with('success', __('yourmodule::messages.item_deleted'));
    }
}

API Controller

<?php

namespace Modules\YourModule\App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Modules\YourModule\App\Models\Item;
use Modules\YourModule\App\Services\YourModuleService;
use Modules\YourModule\App\Http\Resources\ItemResource;
use Modules\YourModule\App\Http\Requests\Api\StoreItemRequest;
use Modules\YourModule\App\Http\Requests\Api\UpdateItemRequest;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class ItemController extends Controller
{
    public function __construct(
        private YourModuleService $service
    ) {}

    public function index(): AnonymousResourceCollection
    {
        $items = Item::query()
            ->when(request('search'), fn($q, $s) => $q->search($s))
            ->when(request('status'), fn($q, $s) => $q->where('status', $s))
            ->latest()
            ->paginate(request('per_page', 15));

        return ItemResource::collection($items);
    }

    public function store(StoreItemRequest $request): ItemResource
    {
        $item = $this->service->create($request->validated());

        return new ItemResource($item);
    }

    public function show(Item $item): ItemResource
    {
        return new ItemResource($item->load('relations'));
    }

    public function update(UpdateItemRequest $request, Item $item): ItemResource
    {
        $item = $this->service->update($item, $request->validated());

        return new ItemResource($item);
    }

    public function destroy(Item $item): \Illuminate\Http\Response
    {
        $this->service->delete($item);

        return response()->noContent();
    }
}

Models

Creating a Model

<?php

namespace Modules\YourModule\App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\User;
use Modules\YourModule\Database\Factories\ItemFactory;

class Item extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'yourmodule_items';

    protected $fillable = [
        'user_id',
        'name',
        'description',
        'status',
        'settings',
        'published_at',
    ];

    protected function casts(): array
    {
        return [
            'settings' => 'array',
            'published_at' => 'datetime',
        ];
    }

    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    */

    public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function entries(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(Entry::class);
    }

    /*
    |--------------------------------------------------------------------------
    | Scopes
    |--------------------------------------------------------------------------
    */

    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }

    public function scopeSearch($query, string $search)
    {
        return $query->where(function ($q) use ($search) {
            $q->where('name', 'like', "%{$search}%")
              ->orWhere('description', 'like', "%{$search}%");
        });
    }

    /*
    |--------------------------------------------------------------------------
    | Factory
    |--------------------------------------------------------------------------
    */

    protected static function newFactory(): ItemFactory
    {
        return ItemFactory::new();
    }
}

Livewire Components

Dedicated Livewire Service Provider

For better organization, create a separate service provider for Livewire components:

<?php

declare(strict_types=1);

namespace Modules\YourModule\Providers;

use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use Modules\YourModule\Livewire\Admin\Dashboard;
use Modules\YourModule\Livewire\Admin\ItemList;
use Modules\YourModule\Livewire\Admin\ItemForm;

class YourModuleLivewireServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->registerLivewireComponents();
    }

    /**
     * Register Livewire components.
     */
    protected function registerLivewireComponents(): void
    {
        // Admin components
        Livewire::component('yourmodule::admin.dashboard', Dashboard::class);
        Livewire::component('yourmodule::admin.item-list', ItemList::class);
        Livewire::component('yourmodule::admin.item-form', ItemForm::class);

        // If you have frontend pages (like a theme module)
        // Livewire::component('yourmodule::pages.home', Home::class);
    }
}

Register in the main service provider:

public function register(): void
{
    $this->app->register(EventServiceProvider::class);
    $this->app->register(RouteServiceProvider::class);
    $this->app->register(YourModuleLivewireServiceProvider::class);
}

Creating Livewire Components

Generate a Livewire component for your module:

php artisan module:make-livewire Admin/Dashboard YourModule

Admin Dashboard Component

<?php

declare(strict_types=1);

namespace Modules\YourModule\Livewire\Admin;

use Livewire\Component;
use Livewire\WithPagination;
use Modules\YourModule\App\Models\Item;

class Dashboard extends Component
{
    use WithPagination;

    public string $search = '';
    public string $status = '';

    protected $queryString = [
        'search' => ['except' => ''],
        'status' => ['except' => ''],
    ];

    public function updatingSearch()
    {
        $this->resetPage();
    }

    public function deleteItem(int $id)
    {
        $item = Item::findOrFail($id);

        $this->authorize('yourmodule.delete');

        $item->delete();

        $this->dispatch('notify', [
            'type' => 'success',
            'message' => __('yourmodule::messages.item_deleted'),
        ]);
    }

    public function render()
    {
        $items = Item::query()
            ->when($this->search, fn($q) => $q->search($this->search))
            ->when($this->status, fn($q) => $q->where('status', $this->status))
            ->latest()
            ->paginate(10);

        return view('yourmodule::livewire.admin.dashboard', [
            'items' => $items,
        ])->layout('backend.layouts.app');
    }
}

Livewire View

{{-- resources/views/livewire/admin/dashboard.blade.php --}}

<div>
    <x-backend.page-header title="Your Module">
        <x-slot:actions>
            <a href="{{ route('admin.yourmodule.items.create') }}"
               class="btn btn-primary">
                <i class="bi bi-plus"></i> New Item
            </a>
        </x-slot:actions>
    </x-backend.page-header>

    <div class="card">
        <div class="card-header">
            <div class="row g-3">
                <div class="col-md-4">
                    <input type="text"
                           wire:model.live.debounce.300ms="search"
                           class="form-control"
                           placeholder="Search...">
                </div>
                <div class="col-md-3">
                    <select wire:model.live="status" class="form-select">
                        <option value="">All Statuses</option>
                        <option value="active">Active</option>
                        <option value="inactive">Inactive</option>
                    </select>
                </div>
            </div>
        </div>

        <div class="card-body">
            <table class="table">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Status</th>
                        <th>Created</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    @forelse($items as $item)
                        <tr wire:key="item-{{ $item->id }}">
                            <td>{{ $item->name }}</td>
                            <td>
                                <span class="badge bg-{{ $item->status === 'active' ? 'success' : 'secondary' }}">
                                    {{ ucfirst($item->status) }}
                                </span>
                            </td>
                            <td>{{ $item->created_at->format('M d, Y') }}</td>
                            <td>
                                <a href="{{ route('admin.yourmodule.items.edit', $item) }}"
                                   class="btn btn-sm btn-outline-primary">
                                    Edit
                                </a>
                                <button wire:click="deleteItem({{ $item->id }})"
                                        wire:confirm="Are you sure?"
                                        class="btn btn-sm btn-outline-danger">
                                    Delete
                                </button>
                            </td>
                        </tr>
                    @empty
                        <tr>
                            <td colspan="4" class="text-center text-muted py-4">
                                No items found.
                            </td>
                        </tr>
                    @endforelse
                </tbody>
            </table>

            {{ $items->links() }}
        </div>
    </div>
</div>

Database

Creating Migrations

php artisan module:make-migration create_yourmodule_items_table YourModule
<?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('yourmodule_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->text('description')->nullable();
            $table->string('status')->default('active');
            $table->json('settings')->nullable();
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
            $table->softDeletes();

            $table->index(['status', 'published_at']);
        });
    }

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

Creating Seeders

<?php

namespace Modules\YourModule\Database\Seeders;

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

class YourModuleDatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            PermissionsSeeder::class,
            DefaultDataSeeder::class,
        ]);
    }
}

Permissions via Hook (Recommended)

Register permissions via the hook system for automatic inclusion when roles are created:

<?php

declare(strict_types=1);

namespace Modules\YourModule\Services;

use App\Enums\Hooks\PermissionFilterHook;
use App\Support\Facades\Hook;

class YourModuleService
{
    public function bootstrap(): void
    {
        // Register permissions via hook
        Hook::addFilter(
            PermissionFilterHook::PERMISSION_GROUPS,
            [$this, 'addPermissions']
        );
    }

    /**
     * Add module permissions to the core permission groups.
     */
    public function addPermissions(array $groups): array
    {
        return array_merge($groups, [
            [
                'group_name' => 'yourmodule',
                'permissions' => [
                    'yourmodule.view',
                    'yourmodule.create',
                    'yourmodule.edit',
                    'yourmodule.delete',
                    'yourmodule.settings',
                ],
            ],
            // Add more permission groups as needed
            [
                'group_name' => 'yourmodule_items',
                'permissions' => [
                    'yourmodule_item.create',
                    'yourmodule_item.view',
                    'yourmodule_item.edit',
                    'yourmodule_item.delete',
                    'yourmodule_item.view_all',
                    'yourmodule_item.view_own',
                ],
            ],
        ]);
    }
}

Permissions Seeder (Alternative)

If you need to seed permissions manually:

<?php

namespace Modules\YourModule\Database\Seeders;

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

class PermissionsSeeder extends Seeder
{
    public function run(): void
    {
        $permissions = [
            'yourmodule.view',
            'yourmodule.create',
            'yourmodule.edit',
            'yourmodule.delete',
            'yourmodule.settings',
        ];

        foreach ($permissions as $permission) {
            Permission::firstOrCreate([
                'name' => $permission,
                'guard_name' => 'web',
            ]);
        }
    }
}

Menu Integration

Adding to Sidebar via Hooks

LaraDashboard uses an enum-based hook system for menu registration. Create a dedicated menu service:

<?php

declare(strict_types=1);

namespace Modules\YourModule\Services;

use App\Enums\Hooks\AdminFilterHook;
use App\Services\MenuService\AdminMenuItem;
use App\Support\Facades\Hook;

class YourModuleMenuService
{
    /**
     * Register the module menu.
     */
    public function register(): void
    {
        Hook::addFilter(
            AdminFilterHook::ADMIN_MENU_GROUPS_BEFORE_SORTING,
            [$this, 'addMenu']
        );
    }

    /**
     * Add menu items to the admin sidebar.
     *
     * @param array $menuGroups Existing menu groups
     * @return array Modified menu groups
     */
    public function addMenu(array $menuGroups): array
    {
        $menuGroups['yourmodule'] = AdminMenuItem::make('yourmodule', [
            'title' => __('yourmodule::menu.title'),
            'icon' => 'lucide:box',
            'route' => 'admin.yourmodule.index',
            'permission' => 'yourmodule.view',
            'order' => 50,
            'children' => [
                AdminMenuItem::make('items', [
                    'title' => __('yourmodule::menu.items'),
                    'route' => 'admin.yourmodule.items.index',
                    'permission' => 'yourmodule.view',
                ]),
                AdminMenuItem::make('settings', [
                    'title' => __('yourmodule::menu.settings'),
                    'route' => 'admin.yourmodule.settings',
                    'permission' => 'yourmodule.settings',
                ]),
            ],
        ]);

        return $menuGroups;
    }
}

Bootstrap Pattern

Create a bootstrap service to register all hooks:

<?php

declare(strict_types=1);

namespace Modules\YourModule\Services;

use App\Enums\Hooks\AdminFilterHook;
use App\Enums\Hooks\PermissionFilterHook;
use App\Support\Facades\Hook;

class YourModuleService
{
    public function __construct(
        private readonly YourModuleMenuService $menuService
    ) {}

    /**
     * Bootstrap the module - register all hooks and services.
     */
    public function bootstrap(): void
    {
        // Register admin menu
        Hook::addFilter(
            AdminFilterHook::ADMIN_MENU_GROUPS_BEFORE_SORTING,
            [$this->menuService, 'addMenu']
        );

        // Register permissions
        Hook::addFilter(
            PermissionFilterHook::PERMISSION_GROUPS,
            [$this, 'addPermissions']
        );
    }

    /**
     * Add module permissions to the core permission groups.
     */
    public function addPermissions(array $groups): array
    {
        return array_merge($groups, [
            [
                'group_name' => 'yourmodule',
                'permissions' => [
                    'yourmodule.view',
                    'yourmodule.create',
                    'yourmodule.edit',
                    'yourmodule.delete',
                    'yourmodule.settings',
                ],
            ],
        ]);
    }
}

Register in Service Provider

public function boot(): void
{
    // ... other boot code

    // Bootstrap module hooks
    $this->app->booted(function () {
        app(YourModuleService::class)->bootstrap();
    });
}

Creating Custom Module Hooks

Modules can define their own hooks using PHP enums, allowing other modules or user code to extend your module's functionality.

Filter Hook Enum

Create app/Enums/Hooks/YourModuleFilterHook.php:

<?php

declare(strict_types=1);

namespace Modules\YourModule\Enums\Hooks;

enum YourModuleFilterHook: string
{
    // Item hooks
    case ITEM_QUERY = 'yourmodule.filter.item_query';
    case ITEM_BEFORE_SAVE = 'yourmodule.filter.item_before_save';
    case ITEM_DISPLAY_COLUMNS = 'yourmodule.filter.item_display_columns';

    // Navigation hooks
    case NAV_ITEMS = 'yourmodule.filter.nav_items';
    case SIDEBAR_WIDGETS = 'yourmodule.filter.sidebar_widgets';

    // Layout hooks
    case HEAD_BEFORE = 'yourmodule.filter.head_before';
    case HEAD_AFTER = 'yourmodule.filter.head_after';
    case CONTENT_BEFORE = 'yourmodule.filter.content_before';
    case CONTENT_AFTER = 'yourmodule.filter.content_after';
}

Action Hook Enum

Create app/Enums/Hooks/YourModuleActionHook.php:

<?php

declare(strict_types=1);

namespace Modules\YourModule\Enums\Hooks;

enum YourModuleActionHook: string
{
    // Item lifecycle
    case ITEM_CREATED = 'yourmodule.action.item_created';
    case ITEM_UPDATED = 'yourmodule.action.item_updated';
    case ITEM_DELETED = 'yourmodule.action.item_deleted';

    // Status changes
    case ITEM_PUBLISHED = 'yourmodule.action.item_published';
    case ITEM_ARCHIVED = 'yourmodule.action.item_archived';
}

Using Your Hooks

In your module code, use the hooks:

use App\Support\Facades\Hook;
use Modules\YourModule\Enums\Hooks\YourModuleFilterHook;
use Modules\YourModule\Enums\Hooks\YourModuleActionHook;

// Apply a filter hook
$query = Item::query()->active();
$query = Hook::applyFilter(YourModuleFilterHook::ITEM_QUERY, $query);

// Fire an action hook
Hook::doAction(YourModuleActionHook::ITEM_CREATED, $item);

// In a view - apply filter for content
$extraContent = Hook::applyFilter(YourModuleFilterHook::CONTENT_AFTER, '');

Allowing Others to Extend

Other modules can hook into your module:

use App\Support\Facades\Hook;
use Modules\YourModule\Enums\Hooks\YourModuleFilterHook;
use Modules\YourModule\Enums\Hooks\YourModuleActionHook;

// Filter: Modify item query
Hook::addFilter(YourModuleFilterHook::ITEM_QUERY, function ($query) {
    return $query->where('is_featured', true);
});

// Action: React to item creation
Hook::addAction(YourModuleActionHook::ITEM_CREATED, function ($item) {
    Notification::send($admins, new ItemCreatedNotification($item));
});

Testing

Feature Tests

<?php

namespace Modules\YourModule\Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use Modules\YourModule\App\Models\Item;
use Illuminate\Foundation\Testing\RefreshDatabase;

class ItemTest extends TestCase
{
    use RefreshDatabase;

    protected User $admin;

    protected function setUp(): void
    {
        parent::setUp();

        $this->admin = User::factory()->create();
        $this->admin->givePermissionTo([
            'yourmodule.view',
            'yourmodule.create',
            'yourmodule.edit',
            'yourmodule.delete',
        ]);
    }

    public function test_can_list_items(): void
    {
        Item::factory()->count(5)->create();

        $this->actingAs($this->admin)
            ->get(route('admin.yourmodule.items.index'))
            ->assertStatus(200)
            ->assertSee('Items');
    }

    public function test_can_create_item(): void
    {
        $this->actingAs($this->admin)
            ->post(route('admin.yourmodule.items.store'), [
                'name' => 'Test Item',
                'description' => 'Test description',
            ])
            ->assertRedirect();

        $this->assertDatabaseHas('yourmodule_items', [
            'name' => 'Test Item',
        ]);
    }

    public function test_unauthorized_user_cannot_access(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)
            ->get(route('admin.yourmodule.items.index'))
            ->assertForbidden();
    }
}

Running Tests

# Run all module tests
php artisan test --filter=YourModule

# Run specific test
php artisan test modules/YourModule/tests/Feature/ItemTest.php

Frontend Theme Modules

Modules can serve as frontend themes, providing public-facing pages. The Starter26 module demonstrates this pattern.

Theme Module Structure

modules/YourTheme/
├── app/
│   ├── Livewire/
│   │   └── Pages/
│   │       ├── Home.php
│   │       ├── About.php
│   │       ├── Posts.php
│   │       └── SinglePost.php
│   ├── View/
│   │   └── Components/
│   │       ├── Navbar.php
│   │       └── Footer.php
│   └── Providers/
│       └── YourThemeLivewireServiceProvider.php
├── resources/
│   └── views/
│       ├── layouts/
│       │   └── app.blade.php
│       ├── livewire/
│       │   └── pages/
│       │       ├── home.blade.php
│       │       └── about.blade.php
│       └── components/
│           ├── navbar.blade.php
│           └── footer.blade.php
└── routes/
    └── web.php

Theme Routes

<?php

use Illuminate\Support\Facades\Route;
use Modules\YourTheme\Livewire\Pages\Home;
use Modules\YourTheme\Livewire\Pages\About;
use Modules\YourTheme\Livewire\Pages\Posts;
use Modules\YourTheme\Livewire\Pages\SinglePost;

// Optional route prefix from config
$prefix = config('yourtheme.route_prefix', '');

Route::prefix($prefix)->group(function () {
    Route::get('/', Home::class)->name('yourtheme.home');
    Route::get('/about', About::class)->name('yourtheme.about');
    Route::get('/posts', Posts::class)->name('yourtheme.posts');
    Route::get('/post/{slug}', SinglePost::class)->name('yourtheme.post');
});

Theme Page Component

<?php

declare(strict_types=1);

namespace Modules\YourTheme\Livewire\Pages;

use App\Models\Post;
use Illuminate\View\View;
use Livewire\Component;

class Home extends Component
{
    public $featuredPosts = [];
    public $recentPosts = [];

    public function mount(): void
    {
        $this->featuredPosts = Post::query()
            ->where('status', 'published')
            ->orderByDesc('created_at')
            ->limit(3)
            ->get();

        $this->recentPosts = Post::query()
            ->where('status', 'published')
            ->orderByDesc('created_at')
            ->limit(6)
            ->get();
    }

    public function render(): View
    {
        return view('yourtheme::livewire.pages.home')
            ->layout('yourtheme::layouts.app', [
                'title' => __('Home') . ' - ' . config('app.name'),
                'description' => __('Welcome to our website.'),
            ]);
    }
}

Theme Layout

{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title ?? config('app.name') }}</title>
    <meta name="description" content="{{ $description ?? '' }}">

    @vite(['resources/css/app.css', 'resources/js/app.js'])
    @livewireStyles
</head>
<body class="antialiased">
    <x-yourtheme::navbar />

    <main>
        {{ $slot }}
    </main>

    <x-yourtheme::footer />

    @livewireScripts
</body>
</html>

Disabling Admin-Only Mode

Theme modules typically need to disable admin-only mode:

use App\Enums\Hooks\AdminFilterHook;

public function boot(): void
{
    // ... other boot code

    // Allow public frontend access
    if (function_exists('ld_add_filter')) {
        ld_add_filter(AdminFilterHook::ADMIN_SITE_ONLY, function () {
            return false;
        });
    }
}

Packaging for Distribution

Prepare module.json

Ensure your module.json has all required metadata for marketplace distribution:

{
    "name": "YourModule",
    "alias": "yourmodule",
    "title": "Your Module Title",
    "description": "Your module description",
    "version": "1.0.0",
    "keywords": ["custom", "feature"],
    "category": "utilities",
    "icon": "lucide:box",
    "homepage": "https://yoursite.com/modules/yourmodule",
    "author": {
        "name": "Your Name",
        "email": "your@email.com"
    },
    "priority": 0,
    "providers": [
        "Modules\\YourModule\\App\\Providers\\YourModuleServiceProvider"
    ]
}

Build Process

LaraDashboard provides Artisan commands to compile and package modules for distribution:

Step 1: Compile the Module

Compile the module assets (JavaScript, CSS) and prepare it for packaging:

php artisan module:compile YourModule

This command:

  • Builds frontend assets (Vite/webpack)
  • Optimizes assets for production
  • Prepares the module for distribution

Step 2: Package the Module

Generate a distributable ZIP file:

php artisan module:package YourModule

This creates a versioned ZIP file in the modules directory:

modules/yourmodule-v1.0.0.zip

The version number is automatically read from your module.json file.

One-Step Build & Package

You can also compile and package in one command:

php artisan module:compile YourModule && php artisan module:package YourModule

Example: Packaging the CRM Module

# Compile CRM module assets
php artisan module:compile Crm

# Package for distribution
php artisan module:package Crm

# Output: modules/crm-v1.0.0.zip

Upload to Marketplace

Once packaged, upload the generated ZIP file to the LaraDashboard Module Marketplace:

  1. Go to the Module Marketplace
  2. Click "Submit Module" or just go to Submit Module
  3. Upload your yourmodule-v1.0.0.zip file
  4. Fill in additional marketplace metadata
  5. Submit for review

Files Automatically Excluded

The packaging command automatically excludes:

  • .git/ - Version control
  • node_modules/ - NPM dependencies
  • .env - Environment files
  • vendor/ - Composer dependencies (if separate)
  • tests/ - Test files (optional, configurable)
  • .DS_Store - macOS system files

Manual ZIP Creation (Alternative)

If you prefer manual packaging:

cd modules
zip -r YourModule.zip YourModule -x "*.git*" -x "*node_modules*" -x "*.env*" -x "*vendor/*" -x "*.DS_Store"

Best Practices

Coding Standards

  • Follow PSR-12 coding style
  • Use strict types: declare(strict_types=1);
  • Type hint all parameters and returns
  • Document complex logic

Naming Conventions

Type Convention Example
Module PascalCase YourModule
Tables snake_case with prefix yourmodule_items
Models PascalCase singular Item
Controllers PascalCase + Controller ItemController
Views kebab-case item-list.blade.php

Security

  • Always validate input via Form Requests
  • Check permissions on every action
  • Sanitize output in views
  • Use prepared statements (Eloquent handles this)

Performance

  • Use eager loading for relationships
  • Cache expensive queries
  • Index frequently queried columns
  • Use pagination for lists

Troubleshooting

Module Not Loading

  1. Check module.json syntax
  2. Verify service provider namespace
  3. Run composer dump-autoload
  4. Clear caches: php artisan optimize:clear

Views Not Found

php artisan view:clear
php artisan config:clear

Permissions Not Working

php artisan permission:cache-reset

Next Steps

/