AI-ready development

Thunderclap Task Recipes

Thunderclap Task Recipes

Thunderclap generates a working CRUD module, but production features need customization. This page shows practical post-generation workflows with concrete before/after examples.

Recipe format

Each recipe follows this structure:

  1. Context: What was generated
  2. Task: What needs to change
  3. Files to modify: Exact paths
  4. Before: Generated code
  5. After: Customized code
  6. Verification: Commands to prove it works

Recipe 1: Add validation rules

Context

Thunderclap generated StoreProductRequest and UpdateProductRequest with basic validation.

Task

Add business rules:

  • name: required, max 255, unique
  • sku: required, max 50, unique, uppercase
  • price: required, numeric, min 0.01
  • description: nullable, max 1000

Files to modify

  • modules/Product/Requests/StoreProductRequest.php
  • modules/Product/Requests/UpdateProductRequest.php

Before (generated)

PHP
// modules/Product/Requests/StoreProductRequest.php
public function rules(): array
{
return [
'name' => ['required'],
'sku' => ['required'],
'price' => ['required'],
'description' => ['nullable'],
];
}

After (customized)

PHP
// modules/Product/Requests/StoreProductRequest.php
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255', 'unique:products,name'],
'sku' => ['required', 'string', 'max:50', 'unique:products,sku', 'uppercase'],
'price' => ['required', 'numeric', 'min:0.01'],
'description' => ['nullable', 'string', 'max:1000'],
];
}
// modules/Product/Requests/UpdateProductRequest.php
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255', 'unique:products,name,' . $this->route('product')->id],
'sku' => ['required', 'string', 'max:50', 'unique:products,sku,' . $this->route('product')->id, 'uppercase'],
'price' => ['required', 'numeric', 'min:0.01'],
'description' => ['nullable', 'string', 'max:1000'],
];
}

Verification

Bash
# Test validation
php artisan test --filter=ProductTest
# Manual test: try creating product with duplicate SKU
# Expected: validation error "The sku has already been taken."

Recipe 2: Customize table columns

Context

Generated ProductTable shows all columns with basic text formatting.

Task

Customize listing:

  • Show only: name, sku, price, is_active, created_at
  • Format price as currency (IDR)
  • Show is_active as badge (green/red)
  • Make name, sku, price sortable
  • Add search on name and sku

Files to modify

  • modules/Product/TableView.php

Before (generated)

PHP
use Laravolt\Suitable\Columns\Text;
public function columns(): array
{
return [
Text::make('id'),
Text::make('name'),
Text::make('sku'),
Text::make('description'),
Text::make('price'),
Text::make('is_active'),
Text::make('created_at'),
Text::make('updated_at'),
];
}

After (customized)

PHP
use Laravolt\Suitable\Columns\Text;
use Laravolt\Suitable\Columns\Boolean;
use Laravolt\Suitable\Columns\DateTime;
use Laravolt\Suitable\Columns\Number;
public function columns(): array
{
return [
Text::make('name', 'Product Name')->sortable(),
Text::make('sku', 'SKU')->sortable(),
Number::make('price', 'Price')
->sortable()
->prefix('Rp ')
->decimal(0),
Boolean::make('is_active', 'Active'),
DateTime::make('created_at', 'Created')->sortable(),
];
}
public function searchable(): array
{
return ['name', 'sku'];
}

Verification

Bash
# Visit listing page
php artisan serve
# Open: http://localhost:8000/product
# Check:
# - Price shows "Rp 10,000" format
# - Active shows green/red badge
# - Search box filters by name/sku
# - Column headers are clickable for sort

Recipe 3: Add relationships

Context

Product belongs to Category. Category table exists.

Task

  • Add category_id foreign key to products table
  • Add relationship to Product model
  • Show category in listing
  • Add category select in form

Files to modify

  1. New migration
  2. modules/Product/Models/Product.php (or app/Models/Product.php)
  3. modules/Product/TableView.php
  4. modules/Product/resources/views/form.blade.php
  5. modules/Product/Requests/StoreProductRequest.php
  6. modules/Product/Requests/UpdateProductRequest.php

Step 1: Migration

Bash
php artisan make:migration add_category_id_to_products_table
PHP
public function up()
{
Schema::table('products', function (Blueprint $table) {
$table->foreignId('category_id')->nullable()->constrained()->cascadeOnDelete();
});
}
public function down()
{
Schema::table('products', function (Blueprint $table) {
$table->dropForeign(['category_id']);
$table->dropColumn('category_id');
});
}
Bash
php artisan migrate

Step 2: Add relationship to model

PHP
// modules/Product/Models/Product.php
use App\Models\Category;
protected $fillable = [
'name',
'sku',
'description',
'price',
'is_active',
'category_id', // Add this
];
public function category()
{
return $this->belongsTo(Category::class);
}

Step 3: Show category in table

PHP
// modules/Product/TableView.php
use Laravolt\Suitable\Columns\Text;
public function columns(): array
{
return [
Text::make('name', 'Product Name')->sortable(),
Text::make('sku', 'SKU')->sortable(),
Text::make('category.name', 'Category'), // Add this
// ... other columns
];
}
public function query()
{
return Product::with('category'); // Eager load
}

Step 4: Add category select in form

PHP
// modules/Product/resources/views/form.blade.php
{!! PrelineForm::text('name')->label('Product Name')->required() !!}
{!! PrelineForm::text('sku')->label('SKU')->required() !!}
{{-- Add this --}}
{!! PrelineForm::select('category_id', $categories)
->label('Category')
->placeholder('-- Select Category --')
->required() !!}
{!! PrelineForm::number('price')->label('Price')->required() !!}
{!! PrelineForm::textarea('description')->label('Description')->rows(5) !!}
{!! PrelineForm::checkbox('is_active', 1)->label('Active')->checked(old('is_active', $product->is_active ?? true)) !!}

Step 5: Pass categories to view

PHP
// modules/Product/Controllers/ProductController.php
public function create()
{
$categories = \App\Models\Category::pluck('name', 'id');
return view('product::create', compact('categories'));
}
public function edit(Product $product)
{
$categories = \App\Models\Category::pluck('name', 'id');
return view('product::edit', compact('product', 'categories'));
}

Step 6: Update validation

PHP
// modules/Product/Requests/StoreProductRequest.php
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'sku' => ['required', 'string', 'max:50', 'unique:products,sku'],
'category_id' => ['required', 'exists:categories,id'], // Add this
'price' => ['required', 'numeric', 'min:0.01'],
'description' => ['nullable', 'string', 'max:1000'],
'is_active' => ['boolean'],
];
}

Verification

Bash
# Run tests
php artisan test --filter=ProductTest
# Check routes
php artisan route:list --name=product
# Manual test:
# 1. Visit /product/create
# 2. Check category dropdown appears
# 3. Try submitting without category (should fail validation)
# 4. Create product with category
# 5. Check listing shows category name

Recipe 4: Override generated views

Context

Generated form view uses default layout. Need custom styling.

Task

Wrap form fields in card, add section headers, use 2-column grid for name/SKU.

Files to modify

  • modules/Product/resources/views/form.blade.php

Before (generated)

Blade
{!! PrelineForm::text('name')->label('Product Name')->required() !!}
{!! PrelineForm::text('sku')->label('SKU')->required() !!}
{!! PrelineForm::number('price')->label('Price')->required() !!}
{!! PrelineForm::textarea('description')->label('Description') !!}
{!! PrelineForm::checkbox('is_active', 1)->label('Active') !!}

After (customized)

Blade
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-6">
{{-- Basic Information --}}
<div>
<h3 class="text-lg font-semibold mb-4">Basic Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{!! PrelineForm::text('name')->label('Product Name')->required() !!}
{!! PrelineForm::text('sku')
->label('SKU')
->required()
->help('Format: PROD-XXXX') !!}
</div>
</div>
{{-- Pricing --}}
<div>
<h3 class="text-lg font-semibold mb-4">Pricing</h3>
{!! PrelineForm::number('price')
->label('Price (IDR)')
->required()
->min(0)
->step(0.01) !!}
</div>
{{-- Details --}}
<div>
<h3 class="text-lg font-semibold mb-4">Details</h3>
{!! PrelineForm::textarea('description')
->label('Description')
->rows(5)
->help('Maximum 1000 characters') !!}
</div>
{{-- Status --}}
<div>
<h3 class="text-lg font-semibold mb-4">Status</h3>
{!! PrelineForm::checkbox('is_active', 1)
->label('Active')
->checked(old('is_active', $product->is_active ?? true)) !!}
</div>
</div>

Verification

Bash
# Visit create/edit pages
# Check:
# - Form is wrapped in card
# - Sections have headers
# - Name and SKU are side-by-side on desktop
# - Help text appears under fields

Recipe 5: Add custom actions

Context

Need "Duplicate" action on product listing.

Task

Add "Duplicate" button to table row actions that copies product with new SKU.

Files to modify

  1. modules/Product/TableView.php
  2. modules/Product/Controllers/ProductController.php
  3. modules/Product/routes/web.php

Step 1: Add route

PHP
// modules/Product/routes/web.php
Route::post('product/{product}/duplicate', [ProductController::class, 'duplicate'])
->name('product.duplicate');

Step 2: Add controller method

PHP
// modules/Product/Controllers/ProductController.php
public function duplicate(Product $product)
{
$this->authorize('create', Product::class);
$newProduct = $product->replicate();
$newProduct->sku = $product->sku . '-COPY-' . time();
$newProduct->name = $product->name . ' (Copy)';
$newProduct->save();
return redirect()
->route('product.index')
->with('success', 'Product duplicated successfully.');
}

Step 3: Add action to table

PHP
// modules/Product/TableView.php
use Laravolt\Suitable\Columns\Action;
public function columns(): array
{
return [
// ... other columns
Action::make('actions')
->label('Actions')
->actions(function ($row) {
return [
[
'label' => 'View',
'url' => route('product.show', $row),
],
[
'label' => 'Edit',
'url' => route('product.edit', $row),
],
[
'label' => 'Duplicate',
'url' => route('product.duplicate', $row),
'method' => 'POST',
'confirm' => 'Duplicate this product?',
],
[
'label' => 'Delete',
'url' => route('product.destroy', $row),
'method' => 'DELETE',
'confirm' => 'Delete this product?',
],
];
}),
];
}

Verification

Bash
# Check route exists
php artisan route:list --name=product.duplicate
# Manual test:
# 1. Visit /product
# 2. Click "Duplicate" on any product
# 3. Confirm dialog appears
# 4. Check new product created with "-COPY-" suffix

Common pitfalls

1. Forgetting to update both Store and Update requests

Problem: Added validation to StoreProductRequest but not UpdateProductRequest.

Fix: Always update both request classes with the same rules (except unique constraints).

2. Not eager loading relationships

Problem: N+1 query when showing category in product listing.

Fix: Add with('category') to table query method.

3. Missing authorization checks

Problem: Generated controller has no policy checks.

Fix: Add policy and $this->authorize() calls in controller methods.

4. Hardcoded values in views

Problem: Form has hardcoded category options.

Fix: Pass dynamic data from controller to view.

AI-friendly task templates

Use these templates when prompting AI agents:

Template: Add validation

Plain Text
Update [Module] validation:
- [field]: [rules]
- [field]: [rules]
Files: modules/[Module]/Requests/Store[Module]Request.php, Update[Module]Request.php
Return: updated rules, test command

Template: Customize table

Plain Text
Update [Module] table:
- Show columns: [list]
- Format [column] as [type]
- Make [columns] sortable
- Add search on [columns]
File: modules/[Module]/TableView.php
Return: updated columns() method

Template: Add relationship

Plain Text
Add [relationship] to [Module]:
1. Migration: add [foreign_key] to [table]
2. Model: add [relationship]() method
3. Table: show [related_field]
4. Form: add [select/input] for [relationship]
5. Validation: add [foreign_key] rules
Return: migration, model, table, form, validation changes
Previous
AI Coding Quickstart