From Static to Dynamic: Creating a Powerful Form Builder with Livewire

Learn how to build a dynamic form builder in Laravel Livewire with repeaters, file uploads, and component-based fields. Step-by-step guide with code examples.

Building dynamic forms is a common requirement in modern applications. Instead of hardcoding fields, you might want users to add components (like text inputs, dropdowns, file uploads, or repeaters) that automatically render inside a master form. With Laravel Livewire, you can develop dynamic form builder in a clean and interactive way without writing tons of JavaScript.

Step 1: Create a Livewire Component

Run the artisan command to create your base form builder component with livewire:

php artisan make:livewire FormBuilder

This creates two files:

  • app/Http/Livewire/FormBuilder.php (backend logic)
  • resources/views/livewire/form-builder.blade.php (frontend view)

Step 2: Define Available Components

Inside app/Http/Livewire/FormBuilder.php, let’s define a list of available components. Each component has a name and a JSON configuration of its fields.

public $availableComponents = [
    [
        'name' => 'Text Input',
        'fields' => [
            ['type' => 'text', 'label' => 'Enter Text', 'value' => '']
        ]
    ],
    [
        'name' => 'Email',
        'fields' => [
            ['type' => 'email', 'label' => 'Enter Email', 'value' => '']
        ]
    ],
    [
        'name' => 'File Upload',
        'fields' => [
            ['type' => 'file', 'label' => 'Upload File', 'value' => '']
        ]
    ],
    [
        'name' => 'Repeater',
        'fields' => [
            ['type' => 'repeater', 'label' => 'Repeater Field', 'items' => []]
        ]
    ],
];

We’ll store user-selected components in:

public $formComponents = [];

Step 3: Add Components to the Form

When a user clicks a component, push it to the form array:

public function addComponent($index)
{
    $component = $this->availableComponents[$index];
    $this->formComponents[] = $component;
}

Step 4: Render Dynamic Fields

Render all selected components to form-builder.blade.php:

<div>
    <!-- Component Menu -->
    <h3>Available Components</h3>
    @foreach ($availableComponents as $i => $comp)
        <button wire:click="addComponent({{ $i }})">
            {{ $comp['name'] }}
        </button>
    @endforeach

    <hr>

    <!-- Dynamic Form -->
    <form wire:submit.prevent="save">
        @foreach ($formComponents as $cIndex => $component)
            <div class="component">
                <h4>{{ $component['name'] }}</h4>

                @foreach ($component['fields'] as $fIndex => $field)
                    @if ($field['type'] === 'text')
                        <input type="text"
                               wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.value"
                               placeholder="{{ $field['label'] }}">
                    @elseif ($field['type'] === 'email')
                        <input type="email"
                               wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.value"
                               placeholder="{{ $field['label'] }}">
                    @elseif ($field['type'] === 'file')
                        <input type="file"
                               wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.value">
                    @elseif ($field['type'] === 'repeater')
                        <button type="button"
                                wire:click="addRepeaterItem({{ $cIndex }}, {{ $fIndex }})">
                            ➕ Add Row
                        </button>

                        @foreach ($field['items'] as $rIndex => $item)
                            <input type="text"
                                   wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.items.{{ $rIndex }}.value"
                                   placeholder="Repeater Item {{ $rIndex + 1 }}">
                        @endforeach
                    @endif
                @endforeach
            </div>
        @endforeach

        <button type="submit">Save Form</button>
    </form>
</div>

Step 5: Handle Repeaters in Form

If clicked component is repeater, push it to the form array as follows:

public function addRepeaterItem($cIndex, $fIndex)
{
    $this->formComponents[$cIndex]['fields'][$fIndex]['items'][] = ['value' => ''];
}

Step 6: Save the Form Data

Finally, handle form saving:

public function save()
{
    // Just dump the data for now
    dd($this->formComponents);

    // In real apps, store in DB as JSON
}

Conclusion

Dynamic form builders don’t have to be complicated. With Laravel Livewire, you can build fully interactive forms that support repeaters, file uploads, and dynamic components without writing heavy JavaScript.

This approach keeps your codebase clean, your forms flexible, and your users happy. Once you have the basics working, it’s easy to extend the builder with drag-and-drop sorting, conditional fields, or rich text editors.

Whether you’re building an admin panel, a survey tool, or a custom CMS, this Livewire-powered dynamic form structure gives you a strong foundation to adapt to almost any use case.

Building a Dynamic XML Sitemap in Laravel: A Step-by-Step Guide

Learn how to create a dynamic XML sitemap in Laravel from a list of links. Step-by-step guide with controller, Blade template, and route setup for your web app.

If you’ve ever wondered how search engines like Google actually find and index your Laravel-powered website, the answer often lies in a well-structured XML sitemap. Without one, even your best content might stay hidden in the shadows. The good news? Generating a dynamic XML sitemap in Laravel is easier than you think.

In this guide, I’ll walk you through a step-by-step process to build a dynamic XML sitemap in Laravel from any list of links—helping your site become more SEO-friendly and crawlable in just minutes.

Step 1: Create a Controller

We’ll start by creating a SitemapController to generate the XML response.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Response;

class SitemapController extends Controller
{
    public function index()
    {
        // Example list of links (this can come from DB or config)
        $links = [
            ['url' => url('/'), 'updated_at' => now()],
            ['url' => url('/about'), 'updated_at' => now()->subDays(2)],
            ['url' => url('/contact'), 'updated_at' => now()->subDays(5)],
        ];

        $content = view('sitemap', compact('links'));

        return response($content, 200)
            ->header('Content-Type', 'application/xml');
    }
}

You can extend this controller to fetche links from your database (e.g., posts, products, categories) automatically instead of a manual list like below:

<?php

namespace App\Http\Controllers;

use App\Models\Page;
use Illuminate\Http\Response;

class SitemapController extends Controller
{
    public function index()
    {
        // Example list of links (this can come from DB or config)
        $links = Page::all();

        $content = view('sitemap', compact('links'));

        return response($content, 200)
            ->header('Content-Type', 'application/xml');
    }
}

Alternative Solution

If you don’t want to create controller, you can add this code directly to routes/web.php route as follows:

<?php

use App\Models\Page;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Route;

Route::get('/sitemap.xml', function () {
    $links = Page::all();

    $content = view('sitemap', [
        'links' => $links,
    ]);

    return response($content, 200)
            ->header('Content-Type', 'application/xml');
});

Step 2: Create the Blade View

Create a new file at resources/views/sitemap.blade.php and paste the following code:

{!! '<?xml version="1.0" encoding="UTF-8"?>' !!}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    @foreach ($links as $link)
        <url>
            <loc>{{ $link->url }}</loc>
            <lastmod>{{ \Carbon\Carbon::parse($link->updated_at)->toAtomString() }}</lastmod>
            <changefreq>weekly</changefreq>
            <priority>0.8</priority>
        </url>
    @endforeach
</urlset>

Note: We use {!! '<?xml version="1.0" encoding="UTF-8"?>' !!} because Blade treats <?xml ... ?> as a PHP tag, which causes a syntax error.


Step 3: Add the Route

Open routes/web.php and add:

use App\Http\Controllers\SitemapController;

Route::get('/sitemap.xml', [SitemapController::class, 'index']);

Note: If you have directly added the code of sitemap to this file, then you can skip this step.


Step 4: Test Your Sitemap

Now, visit:

https://yourapp.com/sitemap.xml

You should see a valid sitemap like this:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>https://yourapp.com/</loc>
        <lastmod>2025-09-17T09:10:00+00:00</lastmod>
        <changefreq>weekly</changefreq>
        <priority>0.8</priority>
    </url>
    <url>
        <loc>https://yourapp.com/about</loc>
        <lastmod>2025-09-15T09:10:00+00:00</lastmod>
        <changefreq>weekly</changefreq>
        <priority>0.8</priority>
    </url>
</urlset>

Conclusion

With just a few steps, you’ve created a dynamic XML sitemap in Laravel. You can extend this by fetching links from your database (e.g., blog posts, products, categories) instead of a static list.

This setup ensures your site is search engine friendly and keeps your content crawlable and indexable.

Boost Your Filament Resources with Excel Export in Minutes

Learn how to add an Export to Excel button in Filament using Laravel Excel. Includes filter-aware exports and reusable QueryExport class.

Exporting data is a common requirement in admin panels. If you’re using Filament with Laravel, you can easily integrate Export to Excel functionality using the Maatwebsite/Laravel-Excel package.

This guide will show you step by step how to:

  • Install Laravel Excel
  • Create a reusable export class
  • Add an “Export to Excel” button in your Filament resource
  • Ensure exports respect filters and tabs
  • Match Filament table columns in Excel

Step 1: Install Laravel Excel

Run the following command in your project root folder to install Laravel Excel package:

composer require maatwebsite/excel

Step 2: Create a Generic Export Class

Instead of creating a new export class for every model, you can build one reusable export class:

<?php

namespace App\Exports;

use Illuminate\Database\Eloquent\Builder;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;

class QueryExport implements FromCollection, WithHeadings, WithMapping
{
    protected Builder $query;
    protected array $columns;

    public function __construct(Builder $query, array $columns = [])
    {
        $this->query = $query;
        $this->columns = $columns;
    }

    public function collection()
    {
        return $this->query->get();
    }

    public function headings(): array
    {
        return array_merge(
            ['ID'],
            collect($this->columns)
                ->map(fn($col) => $col->getLabel() ?? $col->getName())
                ->toArray()
        );
    }

    public function map($row): array
    {
        return array_merge(
            [$row->id],
            collect($this->columns)
                ->map(fn($col) => data_get($row, $col->getName()))
                ->toArray()
        );
    }
}

This class is reusable across all models/resources.

Step 3: Add Export Action in Filament

In your ListBrands (or any other List<Resource>) page, add a header action:

use App\Exports\QueryExport;
use Maatwebsite\Excel\Facades\Excel;
use Filament\Tables\Actions\Action;

Action::make('export')
    ->label('Export to Excel')
    ->icon('heroicon-o-arrow-down-tray')
    ->action(function ($livewire) {
        $query   = $livewire->getFilteredTableQuery(); // respects filters & tabs
        $columns = $livewire->getTable()->getColumns();

        return Excel::download(new QueryExport($query, $columns), 'brands.xlsx');
    }),

Step 4: How It Works

  • The export respects all filters, search, and tab constraints thanks to getFilteredTableQuery().
  • The export file includes the same columns as your Filament table.
  • The ID column is automatically prepended.

Conclusion

With just a few steps, you can add a powerful Excel export feature to any Filament resource. The approach we used:

  • Reusable QueryExport class
  • Filter & tab aware exports
  • Matching Filament table columns
  • ID column always included

This setup gives your admin panel professional-grade export functionality while staying flexible across different resources.

Filament Table Actions: Pro Tips Every Developer Should Know

Discover advanced tips to customize Filament table actions in Laravel. Learn how to auto-mutate data, load custom views in modals, and create state-toggling actions with confirmation and notifications.

Filament resource table actions offer powerful customization for crafting seamless admin experiences. Let’s explore some high-impact tricks every Laravel developer should know:

Trick 1: Automatically Mutate Form Data in the Edit Action

Need to auto-update a field every time you edit from the table? You can do this with the mutateFormDataUsing() method in your EditAction:

use Filament\Tables;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // Your columns...
        ])
        ->actions([
            Tables\Actions\EditAction::make()
                ->mutateFormDataUsing(function (array $data): array {
                    $data['status'] = 'updated'; // auto-set status before save
                    return $data;
                }),
        ]);
}

This ensures the status field is automatically set to "updated" whenever an edit occurs.

Trick 2: Customize an Action to Load a View in a Modal

Standard actions don’t always cut it. For instance, you might want to display a list of users who liked a blog post in a modal. Here’s a clean way to do it:

Action::make('showLikes')
    ->label('Likes')
    ->icon('heroicon-o-heart')
    ->visible(fn($record) => $record->likes()->count() > 0)
    ->modalHeading('Likes')
    ->modalContent(function ($record) {
        return view('filament.components.likes-list', [
            'blogId' => $record->id,
        ]);
    })
    ->modalSubmitAction(false)
    ->modalCancelAction(false)
    ->modalWidth('lg'),

This opens a modal showing your custom view, without extra buttons.

Trick 3: Add a State-Toggling Action with Confirmation and Notification

Want a sleek toggle for boolean flags like activation status, complete with confirmation and feedback? Try this:

Tables\Actions\Action::make('toggleActive')
    ->label(fn($record) => $record->is_active ? 'Deactivate' : 'Activate')
    ->icon(fn($record) => $record->is_active ? 'heroicon-o-x-circle' : 'heroicon-o-check-circle')
    ->color(fn($record) => $record->is_active ? 'danger' : 'success')
    ->requiresConfirmation()
    ->action(function ($record) {

        $actionString = $record->is_active ? 'deactivated' : 'activated';

        $record->update([
            'is_active' => !$record->is_active,
        ]);

        Notification::make()
            ->title('Blog has been ' . $actionString . ' successfully')
            ->success()
            ->send();
    }),

This delivers an intuitive toggle experience—confirm first, then notify on change.


Why These Tips Matter

These action enhancements help you:

  • Automate workflows (e.g., setting status instantly in edit forms).
  • Visualize complex data elegantly (e.g., view likes in a modal).
  • Ensure user clarity and feedback with confirmation dialogs and notifications.

This is especially valuable when building advanced admin panels with Filament—turning UI logic into polished user experiences.

Mastering Version Control in Laravel Filament: Build Your Own Rollback System

Learn how to implement a powerful versioning and rollback system in Laravel using Filament. Track model changes, record history, and rollback updates, deletions, or creations with ease.

When working on Filament admin panels, sometimes you need more than Git history, because git is storing only code changes. You may want to track every change to models/resources directly in your application, and even rollback changes to a previous state. For this reason, you should have a versioning system inside your Laravel Filament admin panel.

This guide shows you how to build a database-driven versioning system for Laravel Filament that:

  • Records model/resource changes (create, update, delete)
  • Stores version history in a dedicated table
  • Lets you rollback individual changes from the Filament admin panel

Step 1: Create the versions Table

Start by creating a migration for versions table to store version details of all model histories as follows:

Schema::create('versions', function (Blueprint $table) {
    $table->id();
    $table->string('version');
    $table->string('change_type'); 
    $table->morphs('versionable'); 
    $table->json('previous_values')->nullable();
    $table->json('new_values')->nullable();
    $table->json('changed_attributes')->nullable();
    $table->foreignId('user_id')->nullable()->references('id')->on('users');
    $table->timestamps();
});

Step 2: Define the Version Model

Create a Version model as follows:

class Version extends Model
{
    protected $fillable = [
        'version', 'change_type', 'versionable_type', 'versionable_id',
        'previous_values', 'new_values', 'changed_attributes', 'user_id',
    ];

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

    public function versionable(): MorphTo
    {
        return $this->morphTo();
    }

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

Step 3: The Versioning Service

A service to manage version creation and rollbacks. It will perform the following operations:

  • Mute mechanism: Prevent logging during rollback operations.
  • Version bumping: Automatically increments patch version numbers.
  • Recording methods:
    • recordCreated() for creations,
    • recordUpdated() for updates,
    • recordDeleted() for deletions.

Rollback logic:

  • Handles three types—model_created, model_updated, model_deleted.
  • Performs the inverse action (e.g. deletes a created record, restores previous values, or recreates a deleted record).
  • Logs the rollback as a new entry with change_type rollback.
class VersioningService
{
    /** @var int depth counter to silence logging while rolling back */
    protected static int $muted = 0;

    public static function muted(): bool
    {
        return self::$muted > 0;
    }

    public static function mute(callable $callback)
    {
        self::$muted++;
        try {
            return $callback();
        } finally {
            self::$muted--;
        }
    }

    public function currentVersion(): string
    {
        return Version::query()->latest('id')->value('version') ?? '0.0.0';
    }

    protected function bumpPatch(string $changeType, Model $model, array $before, array $after, array $changed): Version
    {
        [$major, $minor, $patch] = array_map('intval', explode('.', $this->currentVersion()));
        $patch++;
        $new = implode('.', [$major, $minor, $patch]);

        return Version::create([
            'version'            => $new,
            'change_type'        => $changeType,
            'versionable_type'   => $model::class,
            'versionable_id'     => $model->getKey(),
            'previous_values'    => $before,
            'new_values'         => $after,
            'changed_attributes' => $changed,
            'user_id'            => optional(auth())->id(),
        ]);
    }

    public function recordCreated(Model $model, array $after): void
    {
        if (self::muted()) return;
        $this->bumpPatch('model_created', $model, [], $after, array_keys($after));
    }

    public function recordUpdated(Model $model, array $before, array $after, array $changed): void
    {
        if (self::muted()) return;
        $this->bumpPatch('model_updated', $model, $before, $after, $changed);
    }

    public function recordDeleted(Model $model, array $before): void
    {
        if (self::muted()) return;
        $this->bumpPatch('model_deleted', $model, $before, [], array_keys($before));
    }

    /**
     * Roll back one specific version row by ID.
     * - model_created  -> delete the created row
     * - model_updated  -> restore previous_values
     * - model_deleted  -> recreate from previous_values
     */
    public function rollback(int $versionId): Version
    {
        $v = Version::query()->findOrFail($versionId);

        return DB::transaction(function () use ($v) {
            $modelClass = $v->versionable_type;
            /** @var Model $proto */
            $proto = new $modelClass;

            $rolledBack = self::mute(function () use ($v, $modelClass, $proto) {
                if ($v->change_type === 'model_created') {
                    $this->rollbackCreated($modelClass, $v);
                } elseif ($v->change_type === 'model_updated') {
                    $this->rollbackUpdated($modelClass, $v);
                } elseif ($v->change_type === 'model_deleted') {
                    $this->rollbackDeleted($modelClass, $v);
                } else {
                    // no-op for "rollback" entries or unknown types
                }
            });

            // Log a rollback entry (visible in history)
            return Version::create([
                'version'            => $this->nextPatchAfter($v->version),
                'change_type'        => 'rollback',
                'versionable_type'   => $v->versionable_type,
                'versionable_id'     => $v->versionable_id,
                'previous_values'    => $v->new_values,
                'new_values'         => $v->previous_values,
                'changed_attributes' => $v->changed_attributes,
                'user_id'            => optional(auth())->id(),
            ]);
        });
    }

    protected function rollbackCreated(string $modelClass, Version $v): void
    {
        /** @var Model $modelClass */
        $instance = $modelClass::query()->withoutGlobalScopes()
            ->find($v->versionable_id);

        if ($instance) {
            $instance->forceDelete(); // if soft deletes are used, you might prefer delete()
        }
    }

    protected function rollbackUpdated(string $modelClass, Version $v): void
    {
        /** @var Model $instance */
        $instance = $modelClass::query()->withoutGlobalScopes()
            ->findOrFail($v->versionable_id);

        $prev = $v->previous_values ?? [];
        // Only set attributes that actually exist/fillable
        $fillable = $instance->getFillable();
        $attrs = $fillable ? array_intersect_key($prev, array_flip($fillable)) : $prev;

        // Quiet update to avoid firing observers
        $instance->unguarded(function () use ($instance, $attrs) {
            $instance->fill($attrs);
            $instance->saveQuietly();
        });
    }

    protected function rollbackDeleted(string $modelClass, Version $v): void
    {
        $prev = $v->previous_values ?? [];
        if (empty($prev)) return;

        /** @var Model $model */
        $model = new $modelClass;
        $model->unguarded(function () use ($model, $prev) {
            // keep original primary key if present
            $model->fill($prev);
            // ensure key is set before save
            if (array_key_exists($model->getKeyName(), $prev)) {
                $model->setAttribute($model->getKeyName(), $prev[$model->getKeyName()]);
            }
            $model->saveQuietly();
        });
    }

    protected function nextPatchAfter(string $base): string
    {
        [$M, $m, $p] = array_pad(array_map('intval', explode('.', $base)), 3, 0);
        return implode('.', [$M, $m, $p + 1]);
    }
}

Step 4: The Versionable Trait

Incorporate this trait to any model you want to track (e.g., Post or Category):

  • On deleted: Record deletion with previous values.
  • On updating: Cache “before” state of attributes.
  • On updated: Compare before/after, then record updates.
  • On created: Save the full initial state.
  • On deleting: Cache attributes before deletion.
trait Versionable
{
    // this is a PHP-only property, never touched by Eloquent
    protected array $versioningCache = [];

    public static function bootVersionable(): void
    {
        // Track "before" snapshot only of fields that are dirty
        static::updating(function ($model) {
            $dirty = $model->getDirty(); // fields being changed
            $before = array_intersect_key($model->getOriginal(), $dirty);

            $model->versioningCache['before'] = $before;
        });

        static::updated(function ($model) {
            if (VersioningService::muted()) return;

            $before = $model->versioningCache['before'] ?? [];
            $after  = array_intersect_key($model->getAttributes(), $before); // only changed
            $changed = array_keys($before);

            app(VersioningService::class)->recordUpdated($model, $before, $after, $changed);

            unset($model->versioningCache['before']);
        });

        // For created: store only filled values
        static::created(function ($model) {
            if (VersioningService::muted()) return;

            $after = $model->getAttributes();
            app(VersioningService::class)->recordCreated($model, $after);
        });

        // For deleted: store full row (since everything is removed)
        static::deleting(function ($model) {
            $model->versioningCache['before'] = $model->getAttributes();
        });

        static::deleted(function ($model) {
            if (VersioningService::muted()) return;

            $before = $model->versioningCache['before'] ?? [];
            app(VersioningService::class)->recordDeleted($model, $before);

            unset($model->versioningCache['before']);
        });
    }

    protected static function snapshot($model, bool $useOriginal = false): array
    {
        $arr = $useOriginal ? $model->getOriginal() : $model->getAttributes();

        foreach ($model->getHidden() as $hidden) {
            unset($arr[$hidden]);
        }

        return $arr;
    }
}

Step 5: Create VersionResource

Create a VersionResource to list all versions stored in the database with rollback action as follows:

class VersionResource extends Resource
{
    protected static ?string $model = Version::class;

    protected static ?string $navigationIcon = 'heroicon-o-clock';

    public static function infolist(Infolist $infolist): Infolist
    {
        return $infolist
            ->schema([
                Section::make()
                    ->schema([
                        TextEntry::make('versionable_type')
                            ->label('Model'),
                        TextEntry::make('created_at')
                            ->label('Date')
                            ->dateTime(),
                        TextEntry::make('user.name')
                            ->label('User'),
                    ])
                    ->columns(3),
                Section::make()
                    ->schema([
                        KeyValueEntry::make('previous_values')
                            ->keyLabel('Field'),
                    ]),
                Section::make()
                    ->schema([
                        KeyValueEntry::make('new_values')
                            ->keyLabel('Field'),
                    ])
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('version')
                    ->searchable(),
                Tables\Columns\TextColumn::make('change_type')
                    ->colors([
                        'success' => 'model_created',
                        'warning' => 'model_updated',
                        'danger'  => 'model_deleted',
                        'gray'    => 'rollback',
                    ])
                    ->searchable(),
                Tables\Columns\TextColumn::make('versionable_type')
                    ->label('Model')
                    ->wrap()
                    ->searchable(),
                Tables\Columns\TextColumn::make('versionable_id')
                    ->label('ID'),
                Tables\Columns\TextColumn::make('user.name'),
                Tables\Columns\TextColumn::make('created_at')
                    ->dateTime()
                    ->sortable(),
            ])
            ->filters([
                //
            ])
            ->actions([
                Tables\Actions\ViewAction::make(),
                Tables\Actions\Action::make('rollback')
                    ->label('Rollback')
                    ->icon('heroicon-o-arrow-uturn-left')
                    ->requiresConfirmation()
                    ->modalHeading('Confirm Rollback')
                    ->modalDescription('This will revert the model to its previous state for this version. No schema/code will be touched.')
                    ->action(function (Version $record) {
                        app(\App\Services\VersioningService::class)->rollback($record->id);
                        \Filament\Notifications\Notification::make()
                            ->title('Rollback completed')
                            ->success()
                            ->send();
                    })
                    ->visible(
                        fn(Version $record) =>
                        in_array($record->change_type, ['model_created', 'model_updated', 'model_deleted'])
                    ),
            ])
            ->defaultSort('created_at', 'desc');
    }

    public static function getRelations(): array
    {
        return [
            //
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListVersions::route('/'),
        ];
    }
}

On clicking on any version entry, the details will be opened in popup window.

All entries are also contains rollback action, when triggered, it reverts changes and shows a notification like Rollback completed.

Example Workflow

  • Admin updates a model (e.g., a Post).
  • Version record is created capturing changes.
  • Admin clicks “Rollback” in the panel.
  • Model is reverted to its previous state, without altering schema or code.

This system stores only the changed fields, supports create/update/delete, and provides per-version rollback — ideal for auditing, debugging, or restoring data on the admin side.

Conclusion

You can enhance your Filament admin panel by introducing a system that captures and lets you rollback model changes—right within your application, beyond just Git history. You can also use this same system for auditing purpose as well, like when any changes was done and who has done those changes.

This is a very useful and lightweight database-driven versioning system, which can be used for different purpose.