# Custom Thunderclap Generators

This guide covers extending Thunderclap to create custom generators, modify stub templates, and build domain-specific scaffolding.

## Overview

Thunderclap's generator system is extensible, allowing you to:
- Create custom stub templates
- Add new generator commands
- Hook into generation lifecycle
- Build domain-specific generators (approval workflows, audit logs, etc.)

**When to Create Custom Generators:**
- Repetitive patterns in your domain (approval flows, versioning)
- Company-specific conventions (naming, structure)
- Integration with third-party services (payment gateways, CRMs)
- Compliance requirements (audit trails, data retention)

---

## Understanding Thunderclap Architecture

### Generator Lifecycle

```
1. Command Execution
   ↓
2. Parse Arguments (model name, options)
   ↓
3. Load Stub Templates
   ↓
4. Replace Placeholders
   ↓
5. Fire "Generating" Event
   ↓
6. Write Files to Disk
   ↓
7. Fire "Generated" Event
   ↓
8. Run Post-Generation Hooks
```

### Key Components

- **Stubs:** Template files with placeholders (`{{ model }}`, `{{ namespace }}`)
- **Generators:** Classes that orchestrate file creation
- **Events:** Hooks for custom logic (`Thunderclap\Events\ModelGenerated`)
- **Config:** `config/thunderclap.php` for paths and namespaces

---

## Custom Stub Templates

### Step 1: Publish Default Stubs

```bash
php artisan vendor:publish --tag=thunderclap-stubs
```

This creates `resources/stubs/thunderclap/` with default templates:
```
resources/stubs/thunderclap/
├── model.stub
├── migration.stub
├── controller.stub
├── request.stub
├── resource.stub
├── factory.stub
└── test.stub
```

### Step 2: Customize Model Stub

```php
// resources/stubs/thunderclap/model.stub
<?php

namespace {{ namespace }};

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use App\Traits\Auditable; // Custom trait

class {{ model }} extends Model
{
    use HasFactory, SoftDeletes, Auditable;

    protected $fillable = [
        {{ fillable }}
    ];

    protected $casts = [
        {{ casts }}
    ];

    // Custom: Auto-generate UUID on creation
    protected static function booted()
    {
        static::creating(function ($model) {
            if (empty($model->uuid)) {
                $model->uuid = \Illuminate\Support\Str::uuid();
            }
        });
    }

    // Custom: Audit trail relationship
    public function auditLogs()
    {
        return $this->morphMany(\App\Models\AuditLog::class, 'auditable');
    }
}
```

### Step 3: Add Custom Placeholders

```php
// config/thunderclap.php
return [
    'stubs' => [
        'model' => resource_path('stubs/thunderclap/model.stub'),
        // ... other stubs
    ],

    'placeholders' => [
        'author' => env('THUNDERCLAP_AUTHOR', 'Generated by Thunderclap'),
        'company' => env('COMPANY_NAME', 'Acme Corp'),
        'license' => 'MIT',
    ],
];
```

```php
// Updated stub with custom placeholders
/**
 * {{ model }} Model
 *
 * @author {{ author }}
 * @copyright {{ company }}
 * @license {{ license }}
 */
class {{ model }} extends Model
{
    // ...
}
```

### Step 4: Generate with Custom Stub

```bash
php artisan thunderclap:generate Product

# Generated model now includes:
# - Auditable trait
# - UUID auto-generation
# - Audit log relationship
# - Custom docblock
```

---

## Creating a Custom Generator Command

### Example: Approval Workflow Generator

This generator creates a complete approval workflow with states, transitions, and notifications.

### Step 1: Create Generator Command

```bash
php artisan make:command GenerateApprovalWorkflow
```

### Step 2: Implement Generator Logic

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;

class GenerateApprovalWorkflow extends Command
{
    protected $signature = 'generate:approval-workflow 
                            {model : The model name (e.g., PurchaseOrder)}
                            {--states=pending,approved,rejected : Comma-separated workflow states}
                            {--domain= : Domain namespace (optional)}';

    protected $description = 'Generate approval workflow for a model';

    public function handle()
    {
        $model = $this->argument('model');
        $states = explode(',', $this->option('states'));
        $domain = $this->option('domain');

        $this->info("Generating approval workflow for {$model}...");

        // 1. Generate base model with Thunderclap
        $this->call('thunderclap:generate', [
            'model' => $model,
            '--domain' => $domain,
        ]);

        // 2. Add workflow state column migration
        $this->generateStateMigration($model, $states);

        // 3. Generate state enum
        $this->generateStateEnum($model, $states, $domain);

        // 4. Add workflow trait to model
        $this->addWorkflowTrait($model, $domain);

        // 5. Generate approval controller
        $this->generateApprovalController($model, $domain);

        // 6. Generate approval notification
        $this->generateApprovalNotification($model, $domain);

        // 7. Add routes
        $this->addApprovalRoutes($model);

        $this->info("✅ Approval workflow generated successfully!");
        $this->info("Next steps:");
        $this->info("  1. Run: php artisan migrate");
        $this->info("  2. Review: app/Domains/{$domain}/Enums/{$model}State.php");
        $this->info("  3. Customize: app/Domains/{$domain}/Http/Controllers/{$model}ApprovalController.php");
    }

    protected function generateStateMigration(string $model, array $states)
    {
        $table = Str::snake(Str::pluralStudly($model));
        $timestamp = date('Y_m_d_His');
        $filename = "database/migrations/{$timestamp}_add_workflow_to_{$table}_table.php";

        $stub = <<<PHP
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::table('{$table}', function (Blueprint \$table) {
            \$table->string('status')->default('{$states[0]}')->after('id');
            \$table->foreignId('approved_by')->nullable()->constrained('users');
            \$table->timestamp('approved_at')->nullable();
            \$table->text('approval_notes')->nullable();
        });
    }

    public function down()
    {
        Schema::table('{$table}', function (Blueprint \$table) {
            \$table->dropColumn(['status', 'approved_by', 'approved_at', 'approval_notes']);
        });
    }
};
PHP;

        File::put(base_path($filename), $stub);
        $this->info("✓ Created migration: {$filename}");
    }

    protected function generateStateEnum(string $model, array $states, ?string $domain)
    {
        $namespace = $domain 
            ? "App\\Domains\\{$domain}\\Enums"
            : "App\\Enums";

        $directory = $domain
            ? "app/Domains/{$domain}/Enums"
            : "app/Enums";

        File::ensureDirectoryExists(base_path($directory));

        $cases = collect($states)
            ->map(fn($state) => "    case " . Str::studly($state) . " = '{$state}';")
            ->implode("\n");

        $labels = collect($states)
            ->map(fn($state) => "            self::" . Str::studly($state) . " => '" . Str::title($state) . "',")
            ->implode("\n");

        $stub = <<<PHP
<?php

namespace {$namespace};

enum {$model}State: string
{
{$cases}

    public function label(): string
    {
        return match(\$this) {
{$labels}
        };
    }

    public function canTransitionTo(self \$newState): bool
    {
        return match(\$this) {
            self::Pending => in_array(\$newState, [self::Approved, self::Rejected]),
            self::Approved => false, // Terminal state
            self::Rejected => in_array(\$newState, [self::Pending]), // Allow resubmission
        };
    }
}
PHP;

        $filename = "{$directory}/{$model}State.php";
        File::put(base_path($filename), $stub);
        $this->info("✓ Created enum: {$filename}");
    }

    protected function addWorkflowTrait(string $model, ?string $domain)
    {
        $modelPath = $domain
            ? "app/Domains/{$domain}/Models/{$model}.php"
            : "app/Models/{$model}.php";

        $content = File::get(base_path($modelPath));

        // Add trait use statement
        $traitUse = "use App\\Traits\\HasApprovalWorkflow;";
        if (!Str::contains($content, $traitUse)) {
            $content = Str::replaceFirst(
                "use Illuminate\Database\Eloquent\Model;",
                "use Illuminate\Database\Eloquent\Model;\n{$traitUse}",
                $content
            );
        }

        // Add trait to class
        $traitInClass = "use HasApprovalWorkflow;";
        if (!Str::contains($content, $traitInClass)) {
            $content = Str::replaceFirst(
                "use HasFactory;",
                "use HasFactory, HasApprovalWorkflow;",
                $content
            );
        }

        File::put(base_path($modelPath), $content);
        $this->info("✓ Added HasApprovalWorkflow trait to {$model}");
    }

    protected function generateApprovalController(string $model, ?string $domain)
    {
        $namespace = $domain
            ? "App\\Domains\\{$domain}\\Http\\Controllers"
            : "App\\Http\\Controllers";

        $directory = $domain
            ? "app/Domains/{$domain}/Http/Controllers"
            : "app/Http/Controllers";

        File::ensureDirectoryExists(base_path($directory));

        $modelClass = $domain
            ? "App\\Domains\\{$domain}\\Models\\{$model}"
            : "App\\Models\\{$model}";

        $stub = <<<PHP
<?php

namespace {$namespace};

use {$modelClass};
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class {$model}ApprovalController extends Controller
{
    public function approve(Request \$request, {$model} \$model)
    {
        \$request->validate([
            'notes' => 'nullable|string|max:1000',
        ]);

        \$model->approve(
            approver: \$request->user(),
            notes: \$request->notes
        );

        return redirect()
            ->route('{$model}.show', \$model)
            ->with('success', '{$model} approved successfully');
    }

    public function reject(Request \$request, {$model} \$model)
    {
        \$request->validate([
            'notes' => 'required|string|max:1000',
        ]);

        \$model->reject(
            approver: \$request->user(),
            notes: \$request->notes
        );

        return redirect()
            ->route('{$model}.show', \$model)
            ->with('success', '{$model} rejected');
    }
}
PHP;

        $filename = "{$directory}/{$model}ApprovalController.php";
        File::put(base_path($filename), $stub);
        $this->info("✓ Created controller: {$filename}");
    }

    protected function generateApprovalNotification(string $model, ?string $domain)
    {
        $this->call('make:notification', [
            'name' => "{$model}ApprovalRequested",
        ]);

        $this->info("✓ Created notification: {$model}ApprovalRequested");
    }

    protected function addApprovalRoutes(string $model)
    {
        $routeName = Str::kebab(Str::pluralStudly($model));
        $routes = <<<PHP

// {$model} Approval Routes
Route::post('{$routeName}/{{$model}}/approve', [{$model}ApprovalController::class, 'approve'])
    ->name('{$model}.approve');
Route::post('{$routeName}/{{$model}}/reject', [{$model}ApprovalController::class, 'reject'])
    ->name('{$model}.reject');
PHP;

        File::append(base_path('routes/web.php'), $routes);
        $this->info("✓ Added approval routes to routes/web.php");
    }
}
```

### Step 3: Use Custom Generator

```bash
php artisan generate:approval-workflow PurchaseOrder \
  --states=draft,pending,approved,rejected \
  --domain=Procurement

# Generates:
# ✓ Base CRUD (via Thunderclap)
# ✓ Migration with workflow columns
# ✓ PurchaseOrderState enum
# ✓ HasApprovalWorkflow trait added to model
# ✓ PurchaseOrderApprovalController
# ✓ PurchaseOrderApprovalRequested notification
# ✓ Approval routes
```

---

## Generator Hooks and Events

### Listening to Thunderclap Events

```php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravolt\Thunderclap\Events\ModelGenerated;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Hook into model generation
        ModelGenerated::listen(function ($event) {
            $model = $event->model;
            $path = $event->path;

            // Auto-add to Git
            exec("git add {$path}");

            // Log generation
            \Log::info("Generated model: {$model} at {$path}");

            // Trigger IDE indexing (PHPStorm)
            exec("php artisan ide-helper:models {$model}");
        });
    }
}
```

---

## Complete Example: Audit Log Generator

This generator creates a polymorphic audit log system for any model.

### Step 1: Create Generator Command

```bash
php artisan make:command GenerateAuditLog
```

### Step 2: Implement Generator

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class GenerateAuditLog extends Command
{
    protected $signature = 'generate:audit-log {model}';
    protected $description = 'Add audit logging to a model';

    public function handle()
    {
        $model = $this->argument('model');

        // 1. Create audit_logs table if not exists
        if (!$this->auditTableExists()) {
            $this->createAuditMigration();
        }

        // 2. Create AuditLog model if not exists
        if (!File::exists(app_path('Models/AuditLog.php'))) {
            $this->createAuditModel();
        }

        // 3. Create Auditable trait if not exists
        if (!File::exists(app_path('Traits/Auditable.php'))) {
            $this->createAuditableTrait();
        }

        // 4. Add trait to target model
        $this->addTraitToModel($model);

        $this->info("✅ Audit logging added to {$model}");
    }

    protected function auditTableExists(): bool
    {
        return \Schema::hasTable('audit_logs');
    }

    protected function createAuditMigration()
    {
        $timestamp = date('Y_m_d_His');
        $filename = "database/migrations/{$timestamp}_create_audit_logs_table.php";

        $stub = <<<'PHP'
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('audit_logs', function (Blueprint $table) {
            $table->id();
            $table->morphs('auditable');
            $table->foreignId('user_id')->nullable()->constrained();
            $table->string('event'); // created, updated, deleted
            $table->json('old_values')->nullable();
            $table->json('new_values')->nullable();
            $table->string('ip_address', 45)->nullable();
            $table->string('user_agent')->nullable();
            $table->timestamps();

            $table->index(['auditable_type', 'auditable_id']);
            $table->index('event');
            $table->index('created_at');
        });
    }

    public function down()
    {
        Schema::dropIfExists('audit_logs');
    }
};
PHP;

        File::put(base_path($filename), $stub);
        $this->call('migrate');
        $this->info("✓ Created audit_logs table");
    }

    protected function createAuditModel()
    {
        $stub = <<<'PHP'
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class AuditLog extends Model
{
    protected $fillable = [
        'auditable_type',
        'auditable_id',
        'user_id',
        'event',
        'old_values',
        'new_values',
        'ip_address',
        'user_agent',
    ];

    protected $casts = [
        'old_values' => 'array',
        'new_values' => 'array',
    ];

    public function auditable()
    {
        return $this->morphTo();
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
PHP;

        File::put(app_path('Models/AuditLog.php'), $stub);
        $this->info("✓ Created AuditLog model");
    }

    protected function createAuditableTrait()
    {
        File::ensureDirectoryExists(app_path('Traits'));

        $stub = <<<'PHP'
<?php

namespace App\Traits;

use App\Models\AuditLog;

trait Auditable
{
    protected static function bootAuditable()
    {
        static::created(function ($model) {
            $model->logAudit('created', null, $model->getAttributes());
        });

        static::updated(function ($model) {
            $model->logAudit('updated', $model->getOriginal(), $model->getChanges());
        });

        static::deleted(function ($model) {
            $model->logAudit('deleted', $model->getAttributes(), null);
        });
    }

    public function auditLogs()
    {
        return $this->morphMany(AuditLog::class, 'auditable');
    }

    protected function logAudit(string $event, ?array $oldValues, ?array $newValues)
    {
        AuditLog::create([
            'auditable_type' => get_class($this),
            'auditable_id' => $this->id,
            'user_id' => auth()->id(),
            'event' => $event,
            'old_values' => $oldValues,
            'new_values' => $newValues,
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
        ]);
    }
}
PHP;

        File::put(app_path('Traits/Auditable.php'), $stub);
        $this->info("✓ Created Auditable trait");
    }

    protected function addTraitToModel(string $model)
    {
        $modelPath = app_path("Models/{$model}.php");

        if (!File::exists($modelPath)) {
            $this->error("Model not found: {$modelPath}");
            return;
        }

        $content = File::get($modelPath);

        // Add trait use statement
        if (!str_contains($content, 'use App\Traits\Auditable;')) {
            $content = str_replace(
                "use Illuminate\Database\Eloquent\Model;",
                "use Illuminate\Database\Eloquent\Model;\nuse App\Traits\Auditable;",
                $content
            );
        }

        // Add trait to class
        if (!str_contains($content, 'use Auditable;')) {
            $content = str_replace(
                "use HasFactory;",
                "use HasFactory, Auditable;",
                $content
            );
        }

        File::put($modelPath, $content);
        $this->info("✓ Added Auditable trait to {$model}");
    }
}
```

### Step 3: Use Audit Log Generator

```bash
# Add audit logging to Product model
php artisan generate:audit-log Product

# Now all Product changes are automatically logged
$product = Product::create(['name' => 'Widget', 'price' => 99.99]);
// AuditLog created: event=created, new_values={name: Widget, price: 99.99}

$product->update(['price' => 89.99]);
// AuditLog created: event=updated, old_values={price: 99.99}, new_values={price: 89.99}

$product->delete();
// AuditLog created: event=deleted, old_values={...}

// Query audit trail
$product->auditLogs; // All changes
$product->auditLogs()->where('event', 'updated')->get(); // Only updates
```

---

## Best Practices

### 1. Keep Generators Focused
- One generator = one responsibility
- Compose multiple generators for complex workflows
- Avoid monolithic "generate everything" commands

### 2. Make Generators Idempotent
- Check if files exist before creating
- Use `--force` flag for overwrites
- Provide clear feedback on what was skipped

### 3. Validate Input
- Check model exists before adding traits
- Validate state names, field types
- Provide helpful error messages

### 4. Document Generated Code
- Add docblocks explaining purpose
- Include usage examples in comments
- Link to relevant documentation

### 5. Test Generators
```php
public function test_approval_workflow_generator()
{
    Artisan::call('generate:approval-workflow', [
        'model' => 'TestOrder',
        '--states' => 'pending,approved',
    ]);

    $this->assertFileExists(app_path('Models/TestOrder.php'));
    $this->assertFileExists(app_path('Enums/TestOrderState.php'));
    $this->assertFileExists(app_path('Http/Controllers/TestOrderApprovalController.php'));
}
```

---

## Verification

```bash
# Test custom generator
php artisan generate:approval-workflow Invoice --domain=Billing

# Verify generated files
ls -la app/Domains/Billing/Models/Invoice.php
ls -la app/Domains/Billing/Enums/InvoiceState.php
ls -la app/Domains/Billing/Http/Controllers/InvoiceApprovalController.php

# Run migrations
php artisan migrate

# Test workflow
php artisan tinker
```

```php
$invoice = App\Domains\Billing\Models\Invoice::create([
    'amount' => 1000,
    'status' => 'pending',
]);

$invoice->approve(auth()->user(), 'Approved by manager');
$invoice->status; // 'approved'
$invoice->approved_at; // Carbon instance
$invoice->approved_by; // User ID
```

---

## Summary

Custom Thunderclap generators enable:
- ✅ Domain-specific scaffolding (approval workflows, audit logs)
- ✅ Company-wide conventions enforcement
- ✅ Reduced boilerplate and repetition
- ✅ Consistent code structure across teams
- ✅ Faster onboarding for new developers

**Next Steps:**
1. Identify repetitive patterns in your codebase
2. Extract common logic into reusable generators
3. Document generator usage in team wiki
4. Share generators across projects via Composer package