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:
- Laravel fundamentals (controllers, models, migrations)
- Livewire 3 basics
- LaraDashboard's architecture
- The module system
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:
- Go to the Module Marketplace
- Click "Submit Module" or just go to Submit Module
- Upload your
yourmodule-v1.0.0.zipfile - Fill in additional marketplace metadata
- Submit for review
Files Automatically Excluded
The packaging command automatically excludes:
.git/- Version controlnode_modules/- NPM dependencies.env- Environment filesvendor/- 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
- Check
module.jsonsyntax - Verify service provider namespace
- Run
composer dump-autoload - 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
- Admin Menu System - Adding sidebar menus from modules
- Settings System - Adding settings tabs and sections
- Settings API - Settings CRUD operations
- Hooks Reference - Extend core functionality
- API Development - Build module APIs
- Testing Guide - Write comprehensive tests