Upgrade make:module command with full ServiceProvider structure

Enhanced make:module command:
- Added --model flag for creating model + migration + Filament resource
- Added --api flag for including API routes
- Added --no-filament flag to skip Filament resource
- Creates full ServiceProvider structure with auto-registration
- Creates Config, Permissions, Routes, Views, Tests
- Module migrations stored in module folder
- Filament resources stored in module folder
- Auto-registers ServiceProvider in bootstrap/providers.php

Also added:
- ModuleAuditable trait for audit trail support
- Updated Modules README to document new command options
This commit is contained in:
2026-03-11 08:12:54 +02:00
parent ac16dbc153
commit 2b153f1759
3 changed files with 647 additions and 275 deletions

View File

@@ -8,82 +8,322 @@
class MakeModuleCommand extends Command
{
protected $signature = 'make:module {name : The name of the module}';
protected $signature = 'make:module
{name : The name of the module (PascalCase)}
{--model= : Create a model with the given name}
{--api : Include API routes}
{--no-filament : Skip Filament resource generation}';
protected $description = 'Create a new module with standard structure';
protected $description = 'Create a new module with full structure (ServiceProvider, Config, Routes, Views, Permissions)';
protected string $studlyName;
protected string $kebabName;
protected string $snakeName;
protected string $modulePath;
protected ?string $modelName;
public function handle(): int
{
$name = $this->argument('name');
$studlyName = Str::studly($name);
$kebabName = Str::kebab($name);
$this->studlyName = Str::studly($name);
$this->kebabName = Str::kebab($name);
$this->snakeName = Str::snake($name);
$this->modelName = $this->option('model') ? Str::studly($this->option('model')) : null;
$modulePath = app_path("Modules/{$studlyName}");
$this->modulePath = app_path("Modules/{$this->studlyName}");
if (File::exists($modulePath)) {
$this->error("Module {$studlyName} already exists!");
if (File::exists($this->modulePath)) {
$this->error("Module {$this->studlyName} already exists!");
return self::FAILURE;
}
$this->info("Creating module: {$studlyName}");
$this->info("Creating module: {$this->studlyName}");
$this->newLine();
$this->createDirectories($modulePath);
$this->createModel($modulePath, $studlyName);
$this->createMigration($studlyName);
$this->createController($modulePath, $studlyName);
$this->createRoutes($modulePath, $studlyName, $kebabName);
$this->createViews($modulePath, $studlyName, $kebabName);
$this->createFilamentResource($studlyName);
$this->createTests($studlyName);
$this->createDirectoryStructure();
$this->createServiceProvider();
$this->createConfig();
$this->createPermissions();
$this->createController();
$this->createRoutes();
$this->createViews();
if ($this->modelName) {
$this->createModel();
$this->createMigration();
if (!$this->option('no-filament')) {
$this->createFilamentResource();
}
}
$this->createTests();
$this->registerServiceProvider();
$this->info("\n✅ Module {$studlyName} created successfully!");
$this->info("\nNext steps:");
$this->info("1. Run migrations: php artisan migrate");
$this->info("2. Register routes in routes/web.php:");
$this->info(" require __DIR__.'/../app/Modules/{$studlyName}/routes.php';");
$this->info("3. Access at: /{$kebabName}");
$this->newLine();
$this->info("✅ Module {$this->studlyName} created successfully!");
$this->newLine();
$this->warn("Next steps:");
$this->line(" 1. Run migrations: <info>php artisan migrate</info>");
$this->line(" 2. Seed permissions: <info>php artisan db:seed --class=RolePermissionSeeder</info>");
$this->line(" 3. Clear caches: <info>php artisan optimize:clear</info>");
$this->newLine();
$this->line(" Access at:");
$this->line(" - Frontend: <info>/{$this->kebabName}</info>");
if ($this->modelName && !$this->option('no-filament')) {
$this->line(" - Admin: <info>/admin → {$this->studlyName}</info>");
}
if ($this->option('api')) {
$this->line(" - API: <info>/api/{$this->kebabName}</info>");
}
return self::SUCCESS;
}
protected function createDirectories(string $path): void
protected function createDirectoryStructure(): void
{
File::makeDirectory($path, 0755, true);
File::makeDirectory("{$path}/Models", 0755, true);
File::makeDirectory("{$path}/Controllers", 0755, true);
File::makeDirectory("{$path}/Views", 0755, true);
$directories = [
'',
'/Config',
'/Database/Migrations',
'/Database/Seeders',
'/Filament/Resources',
'/Http/Controllers',
'/Http/Middleware',
'/Http/Requests',
'/Models',
'/Policies',
'/Services',
'/Routes',
'/Resources/views',
];
foreach ($directories as $dir) {
File::makeDirectory("{$this->modulePath}{$dir}", 0755, true);
}
$this->info("✓ Created directory structure");
}
protected function createModel(string $path, string $name): void
protected function createServiceProvider(): void
{
$stub = <<<PHP
<?php
namespace App\Modules\\{$name}\Models;
namespace App\Modules\\{$this->studlyName};
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Route;
class {$name} extends Model
class {$this->studlyName}ServiceProvider extends ServiceProvider
{
use HasFactory;
public function register(): void
{
\$this->mergeConfigFrom(
__DIR__ . '/Config/{$this->snakeName}.php',
'{$this->snakeName}'
);
}
public function boot(): void
{
\$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
\$this->loadViewsFrom(__DIR__ . '/Resources/views', '{$this->kebabName}');
\$this->registerRoutes();
\$this->registerPermissions();
}
protected function registerRoutes(): void
{
Route::middleware(['web', 'auth'])
->prefix('{$this->kebabName}')
->name('{$this->kebabName}.')
->group(__DIR__ . '/Routes/web.php');
if (file_exists(__DIR__ . '/Routes/api.php')) {
Route::middleware(['api', 'auth:sanctum'])
->prefix('api/{$this->kebabName}')
->name('api.{$this->kebabName}.')
->group(__DIR__ . '/Routes/api.php');
}
}
protected function registerPermissions(): void
{
// Permissions are registered via RolePermissionSeeder
// See: Permissions.php in this module
}
}
PHP;
File::put("{$this->modulePath}/{$this->studlyName}ServiceProvider.php", $stub);
$this->info("✓ Created ServiceProvider");
}
protected function createConfig(): void
{
$stub = <<<PHP
<?php
return [
'name' => '{$this->studlyName}',
'slug' => '{$this->kebabName}',
'version' => '1.0.0',
'audit' => [
'enabled' => true,
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
'include' => [],
'exclude' => [],
],
];
PHP;
File::put("{$this->modulePath}/Config/{$this->snakeName}.php", $stub);
$this->info("✓ Created Config");
}
protected function createPermissions(): void
{
$stub = <<<PHP
<?php
return [
'{$this->snakeName}.view' => 'View {$this->studlyName}',
'{$this->snakeName}.create' => 'Create {$this->studlyName} records',
'{$this->snakeName}.edit' => 'Edit {$this->studlyName} records',
'{$this->snakeName}.delete' => 'Delete {$this->studlyName} records',
];
PHP;
File::put("{$this->modulePath}/Permissions.php", $stub);
$this->info("✓ Created Permissions");
}
protected function createController(): void
{
$stub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class {$this->studlyName}Controller extends Controller
{
public function index()
{
\$this->authorize('{$this->snakeName}.view');
return view('{$this->kebabName}::index');
}
}
PHP;
File::put("{$this->modulePath}/Http/Controllers/{$this->studlyName}Controller.php", $stub);
$this->info("✓ Created Controller");
}
protected function createRoutes(): void
{
// Web routes
$webStub = <<<PHP
<?php
use App\Modules\\{$this->studlyName}\Http\Controllers\\{$this->studlyName}Controller;
use Illuminate\Support\Facades\Route;
Route::get('/', [{$this->studlyName}Controller::class, 'index'])->name('index');
PHP;
File::put("{$this->modulePath}/Routes/web.php", $webStub);
// API routes (if requested)
if ($this->option('api')) {
$apiStub = <<<PHP
<?php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
// API routes here
});
PHP;
File::put("{$this->modulePath}/Routes/api.php", $apiStub);
}
$this->info("✓ Created Routes" . ($this->option('api') ? ' (web + api)' : ''));
}
protected function createViews(): void
{
$stub = <<<BLADE
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('{$this->studlyName}') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
{{ __('{$this->studlyName} module is working!') }}
</div>
</div>
</div>
</div>
</x-app-layout>
BLADE;
File::put("{$this->modulePath}/Resources/views/index.blade.php", $stub);
$this->info("✓ Created Views");
}
protected function createModel(): void
{
$tableName = Str::snake(Str::plural($this->modelName));
$stub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Models;
use App\Traits\ModuleAuditable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use OwenIt\Auditing\Contracts\Auditable;
class {$this->modelName} extends Model implements Auditable
{
use HasFactory, ModuleAuditable;
protected \$table = '{$tableName}';
protected \$fillable = [
'name',
'description',
];
protected \$casts = [
//
];
}
PHP;
File::put("{$path}/Models/{$name}.php", $stub);
$this->info("✓ Created Model: {$name}");
File::put("{$this->modulePath}/Models/{$this->modelName}.php", $stub);
$this->info("✓ Created Model: {$this->modelName}");
}
protected function createMigration(string $name): void
protected function createMigration(): void
{
$table = Str::snake(Str::plural($name));
$tableName = Str::snake(Str::plural($this->modelName));
$timestamp = date('Y_m_d_His');
$migrationName = "create_{$table}_table";
$migrationName = "create_{$tableName}_table";
$stub = <<<PHP
<?php
@@ -96,7 +336,7 @@ protected function createMigration(string $name): void
{
public function up(): void
{
Schema::create('{$table}', function (Blueprint \$table) {
Schema::create('{$tableName}', function (Blueprint \$table) {
\$table->id();
\$table->string('name');
\$table->text('description')->nullable();
@@ -106,231 +346,252 @@ public function up(): void
public function down(): void
{
Schema::dropIfExists('{$table}');
Schema::dropIfExists('{$tableName}');
}
};
PHP;
$migrationPath = database_path("migrations/{$timestamp}_{$migrationName}.php");
$migrationPath = "{$this->modulePath}/Database/Migrations/{$timestamp}_{$migrationName}.php";
File::put($migrationPath, $stub);
$this->info("✓ Created Migration: {$migrationName}");
}
protected function createController(string $path, string $name): void
protected function createFilamentResource(): void
{
$plural = Str::plural($name);
$variable = Str::camel($name);
$pluralVariable = Str::camel($plural);
$modelClass = "App\\Modules\\{$this->studlyName}\\Models\\{$this->modelName}";
$tableName = Str::snake(Str::plural($this->modelName));
$stub = <<<PHP
// Create the resource
$resourceStub = <<<PHP
<?php
namespace App\Modules\\{$name}\Controllers;
namespace App\Modules\\{$this->studlyName}\Filament\Resources;
use App\Http\Controllers\Controller;
use App\Modules\\{$name}\Models\\{$name};
use Illuminate\Http\Request;
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
use App\Modules\\{$this->studlyName}\Models\\{$this->modelName};
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class {$name}Controller extends Controller
class {$this->modelName}Resource extends Resource
{
public function index()
protected static ?string \$model = {$this->modelName}::class;
protected static ?string \$navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string \$navigationGroup = '{$this->studlyName}';
protected static ?int \$navigationSort = 1;
public static function canAccess(): bool
{
\${$pluralVariable} = {$name}::latest()->paginate(15);
return view('{$name}::index', compact('{$pluralVariable}'));
return auth()->user()?->can('{$this->snakeName}.view') ?? false;
}
public function create()
public static function form(Form \$form): Form
{
return view('{$name}::create');
return \$form
->schema([
Forms\Components\Section::make('{$this->modelName} Details')
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\Textarea::make('description')
->rows(3),
]),
]);
}
public function store(Request \$request)
public static function table(Table \$table): Table
{
\$validated = \$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
]);
{$name}::create(\$validated);
return redirect()->route('{$variable}.index')
->with('success', '{$name} created successfully.');
return \$table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('description')
->limit(50),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public function show({$name} \${$variable})
public static function getRelations(): array
{
return view('{$name}::show', compact('{$variable}'));
return [
//
];
}
public function edit({$name} \${$variable})
public static function getPages(): array
{
return view('{$name}::edit', compact('{$variable}'));
}
public function update(Request \$request, {$name} \${$variable})
{
\$validated = \$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
]);
\${$variable}->update(\$validated);
return redirect()->route('{$variable}.index')
->with('success', '{$name} updated successfully.');
}
public function destroy({$name} \${$variable})
{
\${$variable}->delete();
return redirect()->route('{$variable}.index')
->with('success', '{$name} deleted successfully.');
return [
'index' => Pages\List{$this->modelName}s::route('/'),
'create' => Pages\Create{$this->modelName}::route('/create'),
'edit' => Pages\Edit{$this->modelName}::route('/{record}/edit'),
];
}
}
PHP;
File::put("{$path}/Controllers/{$name}Controller.php", $stub);
$this->info("✓ Created Controller: {$name}Controller");
}
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource.php", $resourceStub);
protected function createRoutes(string $path, string $name, string $kebabName): void
{
$variable = Str::camel($name);
$stub = <<<PHP
// Create Pages directory
File::makeDirectory("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages", 0755, true);
// Create List page
$listStub = <<<PHP
<?php
use Illuminate\Support\Facades\Route;
use App\Modules\\{$name}\Controllers\\{$name}Controller;
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
Route::middleware(['web', 'auth'])->group(function () {
Route::resource('{$kebabName}', {$name}Controller::class)->names('{$variable}');
});
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class List{$this->modelName}s extends ListRecords
{
protected static string \$resource = {$this->modelName}Resource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
PHP;
File::put("{$path}/routes.php", $stub);
$this->info("✓ Created Routes");
}
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/List{$this->modelName}s.php", $listStub);
protected function createViews(string $path, string $name, string $kebabName): void
{
$plural = Str::plural($name);
$variable = Str::camel($name);
$pluralVariable = Str::camel($plural);
$indexView = <<<BLADE
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('{$plural}') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<div class="flex justify-between mb-4">
<h3 class="text-lg font-semibold">{$plural} List</h3>
<a href="{{ route('{$variable}.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Create New
</a>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse(\${$pluralVariable} as \${$variable})
<tr>
<td class="px-6 py-4">{{ \${$variable}->name }}</td>
<td class="px-6 py-4">{{ \${$variable}->description }}</td>
<td class="px-6 py-4">
<a href="{{ route('{$variable}.edit', \${$variable}) }}" class="text-blue-600 hover:text-blue-900">Edit</a>
<form action="{{ route('{$variable}.destroy', \${$variable}) }}" method="POST" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900 ml-2">Delete</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="px-6 py-4 text-center">No {$pluralVariable} found.</td>
</tr>
@endforelse
</tbody>
</table>
<div class="mt-4">
{{ \${$pluralVariable}->links() }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
BLADE;
File::put("{$path}/Views/index.blade.php", $indexView);
$this->info("✓ Created Views");
}
protected function createFilamentResource(string $name): void
{
$this->call('make:filament-resource', [
'name' => $name,
'--generate' => true,
]);
}
protected function createTests(string $name): void
{
$variable = Str::camel($name);
$stub = <<<PHP
// Create Create page
$createStub = <<<PHP
<?php
use App\Modules\\{$name}\Models\\{$name};
use App\Models\User;
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
it('can list {$variable}s', function () {
\$user = User::factory()->create();
{$name}::factory()->count(3)->create();
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
use Filament\Resources\Pages\CreateRecord;
\$response = \$this->actingAs(\$user)->get(route('{$variable}.index'));
\$response->assertStatus(200);
});
it('can create {$variable}', function () {
\$user = User::factory()->create();
\$response = \$this->actingAs(\$user)->post(route('{$variable}.store'), [
'name' => 'Test {$name}',
'description' => 'Test description',
]);
\$response->assertRedirect(route('{$variable}.index'));
\$this->assertDatabaseHas('{$variable}s', ['name' => 'Test {$name}']);
});
class Create{$this->modelName} extends CreateRecord
{
protected static string \$resource = {$this->modelName}Resource::class;
}
PHP;
$testPath = base_path("tests/Modules/{$name}");
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/Create{$this->modelName}.php", $createStub);
// Create Edit page
$editStub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class Edit{$this->modelName} extends EditRecord
{
protected static string \$resource = {$this->modelName}Resource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
PHP;
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/Edit{$this->modelName}.php", $editStub);
$this->info("✓ Created Filament Resource: {$this->modelName}Resource");
}
protected function createTests(): void
{
$testPath = base_path("tests/Feature/Modules/{$this->studlyName}");
File::makeDirectory($testPath, 0755, true);
File::put("{$testPath}/{$name}Test.php", $stub);
$stub = <<<PHP
<?php
namespace Tests\Feature\Modules\\{$this->studlyName};
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class {$this->studlyName}Test extends TestCase
{
use RefreshDatabase;
public function test_user_can_view_{$this->snakeName}_index(): void
{
\$user = User::factory()->create();
\$user->givePermissionTo('{$this->snakeName}.view');
\$response = \$this->actingAs(\$user)
->get('/{$this->kebabName}');
\$response->assertStatus(200);
}
public function test_unauthorized_user_cannot_view_{$this->snakeName}(): void
{
\$user = User::factory()->create();
\$response = \$this->actingAs(\$user)
->get('/{$this->kebabName}');
\$response->assertStatus(403);
}
}
PHP;
File::put("{$testPath}/{$this->studlyName}Test.php", $stub);
$this->info("✓ Created Tests");
}
protected function registerServiceProvider(): void
{
$providersPath = base_path('bootstrap/providers.php');
if (File::exists($providersPath)) {
$content = File::get($providersPath);
$providerClass = "App\\Modules\\{$this->studlyName}\\{$this->studlyName}ServiceProvider::class";
if (!str_contains($content, $providerClass)) {
// Add the provider before the closing bracket
$content = preg_replace(
'/(\];)/',
" {$providerClass},\n$1",
$content
);
File::put($providersPath, $content);
$this->info("✓ Registered ServiceProvider in bootstrap/providers.php");
}
} else {
$this->warn("⚠ Could not auto-register ServiceProvider. Add manually to config/app.php:");
$this->line(" App\\Modules\\{$this->studlyName}\\{$this->studlyName}ServiceProvider::class");
}
}
}

View File

@@ -1,94 +1,128 @@
# Module System
> **🤖 AI Agents**: For EXACT file templates, see [CLAUDE.md](../../../CLAUDE.md)
This Laravel application uses a modular architecture to organize features into self-contained modules.
## Creating a New Module
Use the artisan command to scaffold a complete module:
```bash
# Basic module (no model)
php artisan make:module ProductCatalog
# With a model + Filament resource
php artisan make:module ProductCatalog --model=Product
# With API routes
php artisan make:module ProductCatalog --model=Product --api
# Without Filament admin
php artisan make:module ProductCatalog --model=Product --no-filament
```
This creates:
- **Model**: `app/Modules/ProductCatalog/Models/ProductCatalog.php`
- **Controller**: `app/Modules/ProductCatalog/Controllers/ProductCatalogController.php`
- **Routes**: `app/Modules/ProductCatalog/routes.php`
- **Views**: `app/Modules/ProductCatalog/Views/`
- **Migration**: `database/migrations/YYYY_MM_DD_HHMMSS_create_product_catalogs_table.php`
- **Filament Resource**: `app/Filament/Resources/ProductCatalogResource.php`
- **Tests**: `tests/Modules/ProductCatalog/ProductCatalogTest.php`
## Module Structure
## What Gets Created
### Basic module (`make:module Name`):
```
app/Modules/
── ProductCatalog/
├── Models/
│ └── ProductCatalog.php
├── Controllers/
│ └── ProductCatalogController.php
├── Views/
│ ├── index.blade.php
│ ├── create.blade.php
│ ├── edit.blade.php
│ └── show.blade.php
── routes.php
app/Modules/Name/
── Config/name.php # Module config
├── Database/Migrations/ # Module migrations
├── Database/Seeders/
├── Filament/Resources/ # Admin resources
├── Http/Controllers/NameController.php
├── Http/Middleware/
├── Http/Requests/
├── Models/
├── Policies/
├── Services/
── Routes/web.php # Frontend routes
├── Resources/views/index.blade.php
├── Permissions.php # Module permissions
└── NameServiceProvider.php # Auto-registered
```
## Registering Module Routes
### With `--model=Product`:
Adds:
- `Models/Product.php` (with auditing)
- `Database/Migrations/create_products_table.php`
- `Filament/Resources/ProductResource.php` (full CRUD)
After creating a module, register its routes in `routes/web.php`:
```php
require __DIR__.'/../app/Modules/ProductCatalog/routes.php';
```
### With `--api`:
Adds:
- `Routes/api.php` (Sanctum-protected)
## Module Features
Each module includes:
**CRUD Operations** - Full Create, Read, Update, Delete functionality
**Authentication** - Routes protected by auth middleware
**Filament Admin** - Auto-generated admin panel resource
**Blade Views** - Responsive Tailwind CSS templates
**Tests** - Pest test suite for module functionality
**Permissions** - Ready for role-based access control
**ServiceProvider** - Auto-registered, loads routes/views/migrations
**Config** - Module-specific settings with audit configuration
**Permissions** - Pre-defined CRUD permissions
**Filament Admin** - Full CRUD resource with navigation group
**Auditing** - Track all changes via ModuleAuditable trait
**Tests** - Feature tests with permission checks
## Example Usage
```bash
# Create an Inventory module
php artisan make:module Inventory
# Create a Stock Management module with Product model
php artisan make:module StockManagement --model=Product --api
# Run migrations
php artisan migrate
# Register routes in routes/web.php
require __DIR__.'/../app/Modules/Inventory/routes.php';
# Seed permissions
php artisan db:seed --class=RolePermissionSeeder
# Access at: http://localhost:8080/inventory
# Admin panel: http://localhost:8080/admin/inventories
# Clear caches
php artisan optimize:clear
```
## Adding Permissions
**Access:**
- Frontend: `http://localhost:8080/stock-management`
- Admin: `http://localhost:8080/admin` → Stock Management section
- API: `http://localhost:8080/api/stock-management`
To add module-specific permissions:
## Permissions
Each module creates these permissions in `Permissions.php`:
```php
// In database/seeders/RolePermissionSeeder.php
$permissions = [
'inventory.view',
'inventory.create',
'inventory.edit',
'inventory.delete',
return [
'stock_management.view' => 'View Stock Management',
'stock_management.create' => 'Create Stock Management records',
'stock_management.edit' => 'Edit Stock Management records',
'stock_management.delete' => 'Delete Stock Management records',
];
```
Use in Blade:
```blade
@can('stock_management.view')
<a href="{{ route('stock-management.index') }}">Stock</a>
@endcan
```
Use in Controller:
```php
$this->authorize('stock_management.view');
```
## Audit Configuration
Edit `Config/module_name.php`:
```php
'audit' => [
'enabled' => true,
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
'include' => [], // Fields to audit (if strategy='include')
'exclude' => ['password'], // Fields to exclude
],
```
## Best Practices
1. **Keep modules self-contained** - Each module should be independent
2. **Use namespaces** - Follow the `App\Modules\{ModuleName}` pattern
3. **Test your modules** - Run `php artisan test` after creating
4. **Document changes** - Update module README if you modify structure
5. **Use Filament** - Leverage the admin panel for quick CRUD interfaces
1. **Keep modules independent** - Avoid tight coupling between modules
2. **Use Services** - Put business logic in `Services/`, not controllers
3. **Define clear permissions** - One permission per action
4. **Test your modules** - Run `php artisan test` after creating
5. **Use Filament** - Leverage admin panel for quick CRUD

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Traits;
use OwenIt\Auditing\Auditable;
trait ModuleAuditable
{
use Auditable;
/**
* Get the audit configuration from module config.
*/
public function getAuditConfig(): array
{
$moduleName = $this->getModuleName();
return config("{$moduleName}.audit", [
'enabled' => true,
'strategy' => 'all',
'include' => [],
'exclude' => [],
]);
}
/**
* Determine if auditing is enabled for this model.
*/
public function isAuditingEnabled(): bool
{
$config = $this->getAuditConfig();
return $config['enabled'] ?? true;
}
/**
* Get fields to include in audit (if strategy is 'include').
*/
public function getAuditInclude(): array
{
$config = $this->getAuditConfig();
if (($config['strategy'] ?? 'all') === 'include') {
return $config['include'] ?? [];
}
return [];
}
/**
* Get fields to exclude from audit.
*/
public function getAuditExclude(): array
{
$config = $this->getAuditConfig();
return array_merge(
$this->auditExclude ?? [],
$config['exclude'] ?? []
);
}
/**
* Determine the module name from the model's namespace.
*/
protected function getModuleName(): string
{
$class = get_class($this);
// Extract module name from namespace: App\Modules\{ModuleName}\Models\...
if (preg_match('/App\\\\Modules\\\\([^\\\\]+)\\\\/', $class, $matches)) {
return \Illuminate\Support\Str::snake($matches[1]);
}
return 'app';
}
}