DEV3 Technologies home

Filament export action with a custom exporter

A Filament 4 exporter class with formatted columns and a custom completion message, plus the list-page header and bulk export actions.

FilamentUtilitiesfilamentexportcsvexporter

Code

<?php

namespace App\Filament\Exports;

use App\Models\Candidate;
use Filament\Actions\Exports\Enums\ExportFormat;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Number;

class CandidateExporter extends Exporter
{
    protected static ?string $model = Candidate::class;

    public static function getColumns(): array
    {
        return [
            ExportColumn::make('id')->label('ID'),
            ExportColumn::make('name'),
            ExportColumn::make('job.title')->label('Job'),
            ExportColumn::make('stage.name')->label('Stage'),
            ExportColumn::make('rating')
                ->formatStateUsing(fn (?int $state): string => $state === null ? '' : "$state / 5"),
            ExportColumn::make('expected_salary')
                ->formatStateUsing(fn (?int $state): string => $state === null ? '' : Number::format($state / 100, 2)),
            ExportColumn::make('tags_list')
                ->label('Tags')
                ->state(fn (Candidate $record): string => $record->tags->pluck('name')->implode('; ')),
            ExportColumn::make('applied_at')
                ->formatStateUsing(fn ($state): string => $state?->format('Y-m-d') ?? ''),
        ];
    }

    // CSV only: skip the XLSX option and its extra dependency.
    public function getFormats(): array
    {
        return [ExportFormat::Csv];
    }

    // Avoid N+1 queries while the exporter walks the rows.
    public static function modifyQuery(Builder $query): Builder
    {
        return $query->with(['job', 'stage', 'tags']);
    }

    public static function getCompletedNotificationBody(Export $export): string
    {
        $body = 'Your candidate export is ready: '.Number::format($export->successful_rows).' rows.';

        if ($failed = $export->getFailedRowsCount()) {
            $body .= ' '.Number::format($failed).' rows failed.';
        }

        return $body;
    }
}

Needs: filament/filament@^4.0

How it works

What it solves

Recruiters and finance teams ask for "just give me a spreadsheet" on almost every list. Filament's exporter queues the work, handles large tables and delivers a download link in a notification, so you only describe the columns.

How it works

CandidateExporter declares getColumns(). Plain columns map straight to attributes, dotted names such as job.title traverse relations, and formatStateUsing turns raw values into spreadsheet-friendly text (a rating as "4 / 5", cents as a decimal, a date as Y-m-d). tags_list uses state() to build a computed value from a loaded relation.

modifyQuery() eager-loads the relations used above, which prevents one query per row on big exports. getFormats() restricts the choice to CSV, and getCompletedNotificationBody() customises the notification text with row counts.

On the list page, ExportAction in the header exports whatever the table query currently matches, so applied filters and the search box affect the file. The trailing comment shows the matching bulk action for selected rows only.

Adapting it

Rename the model and columns. Add ExportFormat::Xlsx back if you want spreadsheets (it needs the OpenSpout package available through Filament). Use enabledByDefault(false) on rarely needed columns so users can opt in from the modal.

Pitfalls

  • Exports run on the queue and use the exports, export_rows style tables: run php artisan make:queue-batches-table, make:notifications-table and vendor:publish --tag=filament-actions-migrations, then migrate.
  • Do not export fields that only some roles may see unless you check permissions in modifyQuery().
  • Spreadsheet formulas: values starting with = can execute in Excel, prefix untrusted text with a quote.

How to use it

  1. Publish and run the migrations: php artisan vendor:publish --tag=filament-actions-migrations, php artisan make:queue-batches-table, php artisan make:notifications-table, then php artisan migrate.
  2. Add the exporter to app/Filament/Exports/ and edit getColumns() for your model.
  3. Use ExportAction::make()->exporter(CandidateExporter::class) in a list page's header actions.
  4. Keep a queue worker running; the user is notified with a download link when the file is ready.
  5. Optional: add ExportBulkAction to the table toolbar for selected rows.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets