diff --git a/src/app/Console/Commands/MakeModuleCommand.php b/src/app/Console/Commands/MakeModuleCommand.php index e7e709b..0838301 100644 --- a/src/app/Console/Commands/MakeModuleCommand.php +++ b/src/app/Console/Commands/MakeModuleCommand.php @@ -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: php artisan migrate"); + $this->line(" 2. Seed permissions: php artisan db:seed --class=RolePermissionSeeder"); + $this->line(" 3. Clear caches: php artisan optimize:clear"); + $this->newLine(); + $this->line(" Access at:"); + $this->line(" - Frontend: /{$this->kebabName}"); + if ($this->modelName && !$this->option('no-filament')) { + $this->line(" - Admin: /admin → {$this->studlyName}"); + } + if ($this->option('api')) { + $this->line(" - API: /api/{$this->kebabName}"); + } 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 = <<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 = << '{$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 = <<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 = <<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 = <<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 = <<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 = << + +

+ {{ __('{$this->studlyName}') }} +

+
+ +
+
+
+
+ {{ __('{$this->studlyName} module is working!') }} +
+
+
+
+ +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 = <<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 = <<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 = <<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 = <<modulePath}/Filament/Resources/{$this->modelName}Resource/Pages", 0755, true); + + // Create List page + $listStub = <<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 = << - -

- {{ __('{$plural}') }} -

-
- -
-
-
-
-
-

{$plural} List

- - Create New - -
- - @if(session('success')) -
- {{ session('success') }} -
- @endif - - - - - - - - - - - @forelse(\${$pluralVariable} as \${$variable}) - - - - - - @empty - - - - @endforelse - -
NameDescriptionActions
{{ \${$variable}->name }}{{ \${$variable}->description }} - Edit -
- @csrf - @method('DELETE') - -
-
No {$pluralVariable} found.
- -
- {{ \${$pluralVariable}->links() }} -
-
-
-
-
- -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 = <<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 = <<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 = <<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"); + } + } } diff --git a/src/app/Modules/README.md b/src/app/Modules/README.md index c0bf467..1f98389 100644 --- a/src/app/Modules/README.md +++ b/src/app/Modules/README.md @@ -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') + Stock +@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 diff --git a/src/app/Traits/ModuleAuditable.php b/src/app/Traits/ModuleAuditable.php new file mode 100644 index 0000000..4e56fc8 --- /dev/null +++ b/src/app/Traits/ModuleAuditable.php @@ -0,0 +1,77 @@ +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'; + } +}