Filament v4 Beta: Análisis completo de las características revolucionarias que transformarán el desarrollo de aplicaciones Laravel

Filament v4 Beta: Análisis completo de las características revolucionarias que transformarán el desarrollo de aplicaciones Laravel

La Filament v4 Beta ha llegado oficialmente, y es sin duda la actualización más ambiciosa y completa en la historia de este framework. Después de explorar en detalle todas las nuevas características, puedo afirmar que esta versión representa un salto cuántico en términos de rendimiento, facilidad de uso y capacidades de desarrollo.

En este análisis exhaustivo, vamos a explorar cada una de las nuevas características de Filament v4, explicando no solo qué es nuevo, sino también cómo estas mejoras pueden transformar tu flujo de trabajo y las posibilidades de tus aplicaciones.

Rendimiento: La Revolución Invisible

Optimizaciones de Rendering

Una de las mejoras más significativas, aunque menos visible, es la optimización masiva del rendimiento de renderizado. Filament v4 ha reescrito internamente muchas plantillas Blade para reducir drásticamente el número de vistas que se renderizan.

¿Cómo lo han logrado?

  • Utilizando objetos PHP existentes para renderizar HTML en lugar de incluir nuevos archivos
  • Reduciendo el número de archivos que necesitan ser cargados
  • Extrayendo grupos de clases Tailwind CSS en clases dedicadas
  • Minimizando la cantidad de HTML que necesita ser renderizado

El resultado: Páginas más rápidas, menor tamaño de respuesta y mejor experiencia del usuario, especialmente notable en tablas grandes con muchos registros.

Mejoras Específicas en Tablas

Las tablas han recibido atención especial en términos de rendimiento:

// Antes: Procesamiento lento con muchos registros
Table::make()
    ->columns([
        TextColumn::make('name'),
        TextColumn::make('email'),
        // ... muchas más columnas
    ])
    ->records($thousandsOfRecords); // Lento en v3

// Ahora: Renderizado optimizado automáticamente en v4
Table::make()
    ->columns([
        TextColumn::make('name'),
        TextColumn::make('email'),
        // ... mismas columnas, pero mucho más rápido
    ])
    ->records($thousandsOfRecords); // Optimizado en v4

Tailwind CSS v4: El Futuro del Diseño Web

Sistema de Colores Modernizado

Filament v4 adopta Tailwind CSS v4, que incluye un cambio revolucionario del espacio de color RGB a OKLCH, utilizando el gama de colores P3 más amplio para producir colores más vivos y precisos.

/* Antes (RGB) */
.bg-blue-500 { background-color: rgb(59 130 246); }

/* Ahora (OKLCH) */
.bg-blue-500 { background-color: oklch(0.7 0.15 252); }

Beneficios del nuevo sistema:

  • Colores más vivos: Aprovecha el espacio de color P3
  • Mejor consistencia: Percepción de luminosidad más uniforme
  • Mejor accesibilidad: Cumplimiento mejorado con directrices WCAG

Configuración Simplificada

Tailwind v4 introduce un sistema de configuración renovado que es más intuitivo y poderoso:

// tailwind.config.js (v4)
export default {
  content: ['./src/**/*.{html,js,php}'],
  theme: {
    extend: {
      colors: {
        brand: 'oklch(0.7 0.15 252)'
      }
    }
  }
}

Autenticación Multi-Factor (MFA): Seguridad de Nivel Empresarial

Configuración Sencilla

La implementación de MFA en Filament v4 es sorprendentemente simple:

use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Auth\MultiFactor\Email\EmailAuthentication;

public function panel(Panel $panel): Panel 
{
    return $panel
        ->multiFactorAuthentication([
            AppAuthentication::make(),
            EmailAuthentication::make()->codeExpiryMinutes(5),
        ], isRequired: true);
}

Métodos Disponibles

1. Autenticación por Aplicación:

  • Compatible con Google Authenticator
  • Soporte para Authy
  • Microsoft Authenticator
  • Cualquier app compatible con TOTP

2. Autenticación por Email:

  • Códigos de un solo uso enviados por correo
  • Tiempo de expiración configurable
  • Templates de email personalizables

Extensibilidad

use Filament\Auth\MultiFactor\MfaProvider;

class SmsAuthentication extends MfaProvider
{
    public function getId(): string
    {
        return 'sms';
    }
    
    public function getName(): string
    {
        return 'SMS Verification';
    }
    
    public function generateChallenge(User $user): string
    {
        $code = sprintf('%06d', mt_rand(0, 999999));
        // Enviar SMS con $code
        return $code;
    }
    
    public function verifyChallenge(User $user, string $code): bool
    {
        // Verificar el código SMS
        return $this->isValidSmsCode($user, $code);
    }
}

Iconos Heroicons: Adiós a las Cadenas Mágicas

Autocompletado Inteligente

La nueva clase enum Heroicon proporciona autocompletado completo en tu IDE:

use Filament\Support\Enums\Heroicon;

// ❌ Antes: Cadenas mágicas propensas a errores
Action::make('edit')
    ->icon('heroicon-o-pencil-square') // Fácil de escribir mal

// ✅ Ahora: Autocompletado y verificación de tipos
Action::make('edit')
    ->icon(Heroicon::OutlinedPencilSquare) // IDE autocompleta
    
// Variantes disponibles automáticamente
->icon(Heroicon::PencilSquare)          // Sólido
->icon(Heroicon::OutlinedPencilSquare)  // Outlined
->icon(Heroicon::MiniPencilSquare)      // Mini (16px)

Selección Automática de Tamaño

Filament selecciona automáticamente el tamaño apropiado del icono según el contexto:

  • 16px para elementos pequeños
  • 20px para elementos medianos
  • 24px para elementos grandes

Configuración Global de Zona Horaria

FilamentTimezone Facade

use Filament\Support\Facades\FilamentTimezone;

// Configuración global en AppServiceProvider
public function boot(): void
{
    FilamentTimezone::set('Europe/Madrid');
}

Componentes Afectados

Esta configuración impacta automáticamente en:

  • DateTimePicker
  • TextColumn con fechas
  • TextEntry con fechas
  • Cualquier componente que maneje fechas/horas

Formatos ISO Estándar

TextColumn::make('created_at')
    ->dateTime('iso') // Formato ISO 8601 automático
    
TextColumn::make('updated_at')
    ->date('iso-date') // Solo fecha ISO

Recursos Anidados: La Funcionalidad Más Solicitada

El Problema Anterior

Antes de v4, si tenías una estructura como CourseLessons, solo podías editar lecciones en modales dentro del recurso del curso.

La Solución: Nested Resources

# Crear un recurso anidado
php artisan make:filament-resource LessonResource --nested

Configuración de Recursos Anidados

// app/Filament/Resources/LessonResource.php
class LessonResource extends Resource
{
    protected static ?string $model = Lesson::class;
    
    // Definir la relación padre
    public static function getParentResource(): ?string
    {
        return CourseResource::class;
    }
    
    public static function getParentRelationshipName(): string
    {
        return 'course';
    }
    
    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('title')->required(),
                Textarea::make('content'),
                TimePicker::make('duration'),
                FileUpload::make('video_file'),
            ]);
    }
    
    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('title'),
                TextColumn::make('duration'),
                ToggleColumn::make('is_published'),
            ]);
    }
}

Beneficios de los Recursos Anidados

  1. Páginas completas: En lugar de modales limitados
  2. Mejor UX: Navegación más intuitiva
  3. Contexto claro: Siempre sabes en qué curso estás
  4. Funcionalidad completa: Todas las características de recursos normales

Esquemas Unificados: Server-Driven UI

Concepto Fundamental

Los esquemas forman la base del enfoque Server-Driven UI de Filament. Te permiten construir interfaces en PHP usando objetos de configuración estructurados.

use Filament\Schemas\Schema;
use Filament\Schemas\Components\Text;
use Filament\Schemas\Components\Button;

$schema = Schema::make()
    ->components([
        Text::make('Título Principal')
            ->size('xl')
            ->weight('bold'),
            
        Text::make('Descripción detallada del contenido')
            ->color('gray'),
            
        Button::make('Acción Principal')
            ->color('primary')
            ->action(fn() => $this->doSomething()),
    ]);

Componentes Disponibles

Prime Components:

  • Text - Para mostrar texto
  • Button - Botones interactivos
  • Icon - Iconos standalone
  • Link - Enlaces

Layout Components:

  • Grid - Layouts en cuadrícula
  • Flex - Layouts flexibles
  • Stack - Apilamiento vertical/horizontal
  • Split - División de pantalla

Form Fields: Todos los campos de formulario existentes

Infolist Entries: Todas las entradas de lista de información

Personalización de Páginas

// Personalizar completamente el layout de una página
public function content(): array
{
    return [
        Grid::make(2)
            ->schema([
                // Columna izquierda
                Stack::make([
                    Text::make('Panel de Control'),
                    $this->getInfolist(),
                ]),
                
                // Columna derecha  
                Stack::make([
                    Text::make('Acciones Rápidas'),
                    $this->getActions(),
                ]),
            ]),
    ];
}

Mejoras en Formularios: Potencia y Flexibilidad

Rich Editor con Tiptap

El editor rico ahora utiliza Tiptap, un framework moderno y extensible:

RichEditor::make('content')
    ->json() // Almacenar como JSON en lugar de HTML
    ->customBlocks([
        // Bloques personalizados arrastrables
        'call_to_action' => [
            'label' => 'Llamada a la Acción',
            'schema' => [
                TextInput::make('title'),
                TextInput::make('button_text'),
                TextInput::make('button_url'),
            ],
        ],
    ])
    ->mergeTags([
        // Tags dinámicos como {{ name }}
        'user_name' => 'Nombre del Usuario',
        'today' => 'Fecha de Hoy',
        'company' => 'Nombre de la Empresa',
    ]);

Componente Slider

Slider::make('rating')
    ->min(1)
    ->max(5)
    ->step(0.5)
    ->marks([1, 2, 3, 4, 5]) // Marcas visibles
    ->displayValue() // Mostrar valor actual
    ->formatValue(fn($value) => $value . ' estrellas');

// Slider de rango
Slider::make('price_range')
    ->range() // Habilitar modo rango
    ->min(0)
    ->max(1000)
    ->formatValue(fn($value) => '€' . number_format($value));

Editor de Código

CodeEditor::make('custom_css')
    ->language('css')
    ->lineNumbers()
    ->theme('dark')
    ->height('300px');

CodeEditor::make('api_response')
    ->language('json')
    ->readonly()
    ->formatUsing(function ($state) {
        return json_encode($state, JSON_PRETTY_PRINT);
    });

Table Repeaters

Repeater::make('products')
    ->table() // Mostrar como tabla
    ->schema([
        TextInput::make('name'),
        TextInput::make('price'),
        Select::make('category'),
    ])
    ->table([
        // Configurar columnas de tabla
        TableColumn::make('name')
            ->width('40%'),
        TableColumn::make('price')
            ->width('20%')
            ->alignment('end'),
        TableColumn::make('category')
            ->width('40%'),
    ]);

ModalTableSelect

Select::make('customer_id')
    ->modalTable() // Selección desde tabla modal
    ->relationship('customer', 'name')
    ->modalTableColumns([
        TextColumn::make('name'),
        TextColumn::make('email'),
        TextColumn::make('company'),
    ])
    ->modalTableFilters([
        SelectFilter::make('company'),
    ])
    ->modalTableSearch();

JavaScript Optimizado

// Visibilidad con JavaScript (sin recarga)
TextInput::make('other_reason')
    ->visibleJs("$get('reason') === 'other'") // Evaluación instantánea

// Contenido dinámico con JavaScript
Text::make()
    ->label(JsContent::make("'Total: $' + ($get('price') * $get('quantity'))"))

// Actualizaciones sin recarga
TextInput::make('quantity')
    ->afterStateUpdatedJs("$set('total', $get('price') * $state)")

FusedGroup

FusedGroup::make([
    TextInput::make('first_name')
        ->placeholder('Nombre'),
    TextInput::make('last_name')
        ->placeholder('Apellido'),
])
->label('Nombre Completo')
->columns(2);

Renderizado Parcial

TextInput::make('search')
    ->live()
    ->partiallyRenderComponentsAfterStateUpdated(['results_table']) // Solo re-renderizar tabla
    
Select::make('category')
    ->live()
    ->partiallyRenderAfterStateUpdated() // Solo re-renderizar este campo

TextInput::make('notes')
    ->live()
    ->skipRenderAfterStateUpdated() // No re-renderizar nada

Tablas con Datos Estáticos: Flexibilidad Total

Datos Simples

Table::make()
    ->records([
        ['name' => 'Juan Pérez', 'email' => 'juan@example.com', 'role' => 'Admin'],
        ['name' => 'María García', 'email' => 'maria@example.com', 'role' => 'User'],
        ['name' => 'Pedro López', 'email' => 'pedro@example.com', 'role' => 'Editor'],
    ])
    ->columns([
        TextColumn::make('name')
            ->searchable()
            ->sortable(),
        TextColumn::make('email'),
        BadgeColumn::make('role')
            ->colors([
                'success' => 'Admin',
                'warning' => 'Editor',
                'secondary' => 'User',
            ]),
    ]);

Datos de API Externa

use Illuminate\Support\Facades\Http;

public function getTableRecords(): array
{
    $response = Http::get('https://api.example.com/users');
    
    return $response->json('data');
}

Table::make()
    ->records($this->getTableRecords())
    ->columns([
        TextColumn::make('name'),
        TextColumn::make('email'),
        TextColumn::make('created_at')
            ->dateTime(),
    ])
    ->actions([
        Action::make('sync')
            ->action(function ($record) {
                // Sincronizar con sistema local
                User::updateOrCreate(
                    ['external_id' => $record['id']],
                    $record
                );
            }),
    ]);

Paginación y Búsqueda Personalizadas

use Illuminate\Pagination\LengthAwarePaginator;

public function getTableRecords(): LengthAwarePaginator
{
    $allData = $this->fetchAllDataFromSomewhere();
    
    // Aplicar búsqueda
    if ($search = $this->getTableSearch()) {
        $allData = array_filter($allData, function ($item) use ($search) {
            return str_contains(strtolower($item['name']), strtolower($search));
        });
    }
    
    // Crear paginador
    $perPage = 10;
    $currentPage = request()->get('page', 1);
    $currentItems = array_slice($allData, ($currentPage - 1) * $perPage, $perPage);
    
    return new LengthAwarePaginator(
        $currentItems,
        count($allData),
        $perPage,
        $currentPage,
        ['path' => request()->url()]
    );
}

Acciones Unificadas: Consistencia Total

Namespace Único

use Filament\Actions\Action;  // ✅ Un solo namespace para todo

// Antes (v3): Diferentes clases para diferentes contextos
use Filament\Tables\Actions\Action as TableAction;
use Filament\Forms\Actions\Action as FormAction;
use Filament\Actions\Action as PageAction;

// Ahora (v4): Una sola clase para todos los contextos
use Filament\Actions\Action; // Para tablas, formularios, páginas, etc.

Toolbar Actions

Table::make()
    ->toolbarActions([
        // Acciones regulares
        Action::make('create')
            ->label('Nuevo Usuario')
            ->icon(Heroicon::Plus)
            ->action(fn() => $this->createUser()),
            
        // Acciones bulk en toolbar
        BulkAction::make('export_selected')
            ->label('Exportar Seleccionados')
            ->icon(Heroicon::DocumentArrowDown)
            ->action(fn($records) => $this->exportUsers($records)),
    ]);

Bulk Actions Optimizadas

BulkAction::make('update_status')
    ->label('Actualizar Estado')
    ->icon(Heroicon::CheckCircle)
    ->chunkSelectedRecords(100) // Procesar en chunks de 100
    ->authorizeIndividualRecords() // Verificar permisos por registro
    ->action(function ($records) {
        $successCount = 0;
        
        foreach ($records as $record) {
            try {
                $record->update(['status' => 'approved']);
                $successCount++;
            } catch (Exception $e) {
                // Manejar errores individuales
            }
        }
        
        return $successCount;
    })
    ->successNotificationTitle(fn($successCount) => 
        "Se actualizaron {$successCount} registros correctamente"
    )
    ->failureNotificationTitle(fn($failureCount) => 
        "Fallaron {$failureCount} registros"
    );

Rate Limiting

Action::make('send_email')
    ->rateLimit(5) // Máximo 5 veces por minuto por IP
    ->action(function () {
        // Enviar email
    });

Tooltips en Botones Deshabilitados

Action::make('delete')
    ->disabled(fn($record) => $record->has_dependencies)
    ->tooltip(fn($record) => 
        $record->has_dependencies 
            ? 'No se puede eliminar: tiene dependencias'
            : null
    );

Widgets Mejorados: Flexibilidad y Rendimiento

Charts Colapsables

class SalesChart extends ChartWidget
{
    protected static ?string $heading = 'Ventas Mensuales';
    protected bool $isCollapsible = true; // ✨ Nuevo
    
    protected function getData(): array
    {
        return [
            'datasets' => [
                [
                    'label' => 'Ventas',
                    'data' => [12, 19, 3, 5, 2, 3],
                    'backgroundColor' => 'rgba(59, 130, 246, 0.1)',
                ],
            ],
            'labels' => ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun'],
        ];
    }
}

Filtros Personalizados en Charts

use Filament\Widgets\Concerns\HasFiltersSchema;

class RevenueChart extends ChartWidget
{
    use HasFiltersSchema;
    
    protected function filtersSchema(): array
    {
        return [
            Select::make('period')
                ->options([
                    '7' => 'Últimos 7 días',
                    '30' => 'Últimos 30 días',
                    '90' => 'Últimos 90 días',
                ])
                ->default('30'),
                
            DatePicker::make('start_date')
                ->label('Fecha inicio'),
                
            DatePicker::make('end_date')
                ->label('Fecha fin'),
        ];
    }
    
    protected function getData(): array
    {
        $period = $this->filters['period'] ?? 30;
        $startDate = $this->filters['start_date'];
        $endDate = $this->filters['end_date'];
        
        // Obtener datos basados en filtros
        return $this->getChartData($period, $startDate, $endDate);
    }
}

Sistema de Grid Responsivo

class DashboardPage extends Dashboard
{
    public function getWidgets(): array
    {
        return [
            StatsOverviewWidget::class => [
                'columnSpan' => 'full', // Ancho completo
            ],
            SalesChart::class => [
                'columnSpan' => [
                    'sm' => 1,  // 1 columna en móvil
                    'md' => 2,  // 2 columnas en tablet
                    'lg' => 3,  // 3 columnas en desktop
                ],
            ],
            RecentOrdersWidget::class => [
                'columnSpan' => [
                    'sm' => 1,
                    'lg' => 2,
                ],
                'columnStart' => [
                    'lg' => 1, // Empezar en columna 1 en desktop
                ],
            ],
        ];
    }
    
    public function getColumns(): int|string|array
    {
        return [
            'sm' => 1,
            'md' => 2,
            'lg' => 4,
        ];
    }
}

Multi-Tenancy: Aislamiento Automático

Configuración Automática

use Filament\Users\Tenant;

class TenantAwareModel extends Model
{
    protected static function booted(): void
    {
        // Los scopes globales se aplican automáticamente
        static::addGlobalScope(new TenantScope);
        
        // Los eventos del ciclo de vida también
        static::creating(function ($model) {
            $model->tenant_id = Filament::getTenant()->id;
        });
    }
}

Validación con Scope

// ❌ Problema: Laravel bypasses los global scopes
$rules = [
    'email' => 'unique:users,email',
];

// ✅ Solución: Validación con scope de tenant
use Filament\Support\Rules\ScopedUnique;
use Filament\Support\Rules\ScopedExists;

$rules = [
    'email' => [
        'required',
        'email',
        ScopedUnique::make('users', 'email'),
    ],
    'manager_id' => [
        'nullable',
        ScopedExists::make('users', 'id'),
    ],
];

Mejoras en Accesibilidad

Etiquetas Dinámicas

// Las etiquetas se ajustan automáticamente según la profundidad
TextInput::make('name')
    ->label('Nombre') // Se renderiza como h2, h3, h4, etc. según contexto

Container Queries

Grid::make()
    ->containerQueries([
        'sm' => '(min-width: 300px)',
        'md' => '(min-width: 600px)',
        'lg' => '(min-width: 900px)',
    ])
    ->schema([
        // Los componentes responden al tamaño del contenedor padre
        TextInput::make('title')
            ->columnSpan([
                'default' => 1,
                'container-md' => 2, // 2 columnas cuando el contenedor es ≥ 600px
            ]),
    ]);

Colores OKLCH para WCAG

Los nuevos colores OKLCH garantizan mejor contraste y cumplimiento con directrices de accesibilidad:

/* Colores optimizados automáticamente para contraste */
.text-primary-600 { color: oklch(0.5 0.2 252); }
.text-primary-700 { color: oklch(0.4 0.2 252); }

Configuración del Panel: Control Total

Autorización Estricta

Panel::make()
    ->strictAuthorization() // ✨ Nuevo: Requiere policies explícitas
    ->default();

Posición de Sub-navegación

Panel::make()
    ->subNavigationPosition(SubNavigationPosition::Top) // Como tabs
    // o SubNavigationPosition::End para bottom
    // o SubNavigationPosition::Start para sidebar (default)

Redirección Después de Crear

Panel::make()
    ->createRedirect(CreateRedirect::View) // Redirigir a vista
    // o CreateRedirect::Edit para edición
    // o CreateRedirect::Index para listado (default)

Fuente Local

Panel::make()
    ->font('Inter') // Cargada localmente, no desde CDN
    // o cualquier fuente personalizada

Notificaciones de Error Personalizadas

Panel::make()
    ->errorNotifications(false) // Deshabilitar notificaciones de error
    ->registerErrorNotification(function ($title, $body, $statusCode) {
        // Lógica personalizada para manejar errores
        if ($statusCode === 403) {
            Notification::make()
                ->title('Sin permisos')
                ->body('No tienes autorización para esta acción')
                ->danger()
                ->send();
        }
    });

Testing: Simplificado y Potente

Testing de Acciones

use Filament\Actions\Testing\TestsActions;

class UserResourceTest extends TestCase
{
    use TestsActions;
    
    public function test_can_create_user(): void
    {
        $this->actingAs($admin)
            ->livewire(UserResource\Pages\CreateUser::class)
            ->fillForm([
                'name' => 'Juan Pérez',
                'email' => 'juan@example.com',
                'password' => 'password123',
            ])
            ->call('create')
            ->assertHasNoFormErrors()
            ->assertDatabaseHas('users', [
                'name' => 'Juan Pérez',
                'email' => 'juan@example.com',
            ]);
    }
    
    public function test_can_bulk_delete_users(): void
    {
        $users = User::factory(3)->create();
        
        $this->actingAs($admin)
            ->livewire(UserResource\Pages\ListUsers::class)
            ->selectTableRecords($users->pluck('id')->toArray())
            ->callTableBulkAction('delete')
            ->assertTableBulkActionHasNoErrors()
            ->assertDatabaseMissing('users', ['id' => $users->first()->id]);
    }
}

Casos de Uso Reales: Ejemplos Prácticos

Sistema de E-learning

// CourseResource con lecciones anidadas
class CourseResource extends Resource
{
    public static function form(Form $form): Form
    {
        return $form->schema([
            TextInput::make('title')->required(),
            RichEditor::make('description')
                ->customBlocks([
                    'video' => [
                        'label' => 'Video',
                        'schema' => [
                            TextInput::make('video_url'),
                            TextInput::make('duration'),
                        ],
                    ],
                ])
                ->mergeTags([
                    'instructor_name' => 'Nombre del Instructor',
                    'course_duration' => 'Duración del Curso',
                ]),
            Slider::make('difficulty')
                ->min(1)->max(5)
                ->marks([1, 2, 3, 4, 5]),
        ]);
    }
}

// LessonResource anidado
class LessonResource extends Resource
{
    public static function getParentResource(): string
    {
        return CourseResource::class;
    }
    
    public static function form(Form $form): Form
    {
        return $form->schema([
            TextInput::make('title'),
            CodeEditor::make('exercise_code')
                ->language('php'),
            Slider::make('estimated_minutes')
                ->min(5)->max(120)->step(5),
        ]);
    }
}

Dashboard de Analytics

class AnalyticsDashboard extends Dashboard
{
    protected function getWidgets(): array
    {
        return [
            SalesChart::class,
            UserGrowthChart::class,
            RevenueStatsWidget::class,
        ];
    }
}

class SalesChart extends ChartWidget
{
    use HasFiltersSchema;
    protected bool $isCollapsible = true;
    
    protected function filtersSchema(): array
    {
        return [
            Select::make('period')
                ->options([
                    '7' => 'Últimos 7 días',
                    '30' => 'Últimos 30 días',
                    '90' => 'Últimos 90 días',
                ])
                ->live(),
        ];
    }
    
    protected function getData(): array
    {
        return Cache::remember(
            "sales_chart_{$this->filters['period']}",
            now()->addMinutes(10),
            fn() => $this->calculateSalesData()
        );
    }
}

Sistema de Gestión de Inventario

class ProductResource extends Resource
{
    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('name')->searchable(),
                TextColumn::make('sku')->copyable(),
                TextColumn::make('stock')
                    ->color(fn($state) => $state < 10 ? 'danger' : 'success'),
                TextColumn::make('price')
                    ->formatStateUsing(fn($state) => '€' . number_format($state, 2)),
            ])
            ->toolbarActions([
                Action::make('import_csv')
                    ->form([
                        FileUpload::make('csv_file')
                            ->acceptedFileTypes(['text/csv']),
                    ])
                    ->action(function ($data) {
                        $this->importFromCsv($data['csv_file']);
                    }),
            ])
            ->bulkActions([
                BulkAction::make('update_prices')
                    ->form([
                        TextInput::make('percentage')
                            ->numeric()
                            ->suffix('%'),
                    ])
                    ->chunkSelectedRecords(50)
                    ->action(function ($records, $data) {
                        $factor = 1 + ($data['percentage'] / 100);
                        
                        foreach ($records as $record) {
                            $record->update([
                                'price' => $record->price * $factor
                            ]);
                        }
                    }),
            ]);
    }
}

Migración desde v3: Guía Práctica

Cambios Principales

// ❌ v3: Namespaces específicos
use Filament\Tables\Actions\Action as TableAction;
use Filament\Forms\Actions\Action as FormAction;

// ✅ v4: Namespace unificado
use Filament\Actions\Action;

// ❌ v3: Iconos como strings
->icon('heroicon-o-pencil')

// ✅ v4: Enum de iconos
->icon(Heroicon::OutlinedPencil)

// ❌ v3: Configuración de timezone individual
DateTimePicker::make('date')->timezone('Europe/Madrid')

// ✅ v4: Configuración global
FilamentTimezone::set('Europe/Madrid'); // En AppServiceProvider
DateTimePicker::make('date') // Usa timezone global automáticamente

Script de Migración Asistida

# Instalar herramienta de migración
composer require filament/upgrade-helper

# Ejecutar análisis de código
php artisan filament:upgrade-analyze

# Aplicar cambios automáticos
php artisan filament:upgrade-migrate

Reflexiones Finales: El Futuro de Filament

Lo Que Más Me Impresiona

  1. Rendimiento sin sacrificios: Las optimizaciones son transparentes
  2. Developer Experience: Todo está pensado para productividad
  3. Flexibilidad extrema: Desde aplicaciones simples hasta complejas
  4. Consistencia: APIs unificadas en toda la plataforma

Casos de Uso Ideales para v4

  • Startups técnicas que necesitan MVPs robustos rápidamente
  • Empresas medianas con requisitos complejos de gestión
  • Aplicaciones SaaS que requieren multi-tenancy sofisticado
  • Dashboards corporativos con necesidades de visualización avanzada

El Ecosistema Filament en 2025

Con v4, Filament se posiciona no solo como una herramienta para paneles administrativos, sino como una plataforma completa para aplicaciones business. La combinación de:

  • Server-Driven UI con esquemas
  • Componentes altamente personalizables
  • Rendimiento optimizado
  • Seguridad empresarial (MFA)
  • Testing integrado

…hace que Filament v4 sea una opción seria para aplicaciones de producción a escala empresarial.

Recomendaciones

¿Deberías migrar ahora?

  • Para proyectos nuevos: Definitivamente sí
  • Para proyectos existentes críticos: Espera a la versión estable
  • Para proyectos de aprendizaje: Es el momento perfecto

¿Vale la pena el esfuerzo de migración? Si tu aplicación se beneficia de:

  • Mejor rendimiento en tablas grandes
  • Recursos anidados
  • Autenticación multi-factor
  • Flexibilidad en layouts personalizados

La respuesta es un rotundo .


Filament v4 Beta representa mucho más que una actualización incremental; es una reimaginación completa de lo que puede ser una herramienta de desarrollo de aplicaciones web. Dan Harrin y el equipo han creado algo verdaderamente especial que va a cambiar la forma en que desarrollamos aplicaciones Laravel.

La beta está disponible ahora, y aunque aún no es estable para producción, es el momento perfecto para explorar estas nuevas capacidades y prepararse para el futuro de Filament.

¿Has probado ya Filament v4 Beta? ¿Qué característica te parece más emocionante? ¡Comparte tu experiencia en los comentarios!


Enlaces importantes:

Relacionados