thumbnail

Campos Customizáveis com Laravel e FilamentPHP

Yuri do Monte Yuri do Monte | 10 min de leitura
há 4 meses

Dando Superpoderes aos Clientes: Campos Customizáveis com Laravel e FilamentPHP


Em um cenário de software como serviço (SaaS), a personalização é a chave para o sucesso. Clientes diferentes têm necessidades diferentes, e forçá-los a usar um sistema rígido é a receita para o churn. Mas o que fazer quando um cliente precisa de um campo "Tipo de Contrato" e outro precisa de um "Sabor de Pizza Favorito" no cadastro de usuários? Abrir um chamado para o desenvolvimento toda vez? Ninguém tem tempo para isso!

Pensando nisso, desenvolvi uma funcionalidade (inspirado em certas fontes brilhantes) que transforma a maneira como nossos clientes adaptam o sistema: um sistema de campos customizáveis com Laravel e FilamentPHP. Chega de sofrer. Agora o sofrimento é automatizado.

Imagine a cena: seu cliente, com um café na mão, decide que precisa de um novo campo. Em vez de abrir um ticket e esperar dias, ele mesmo acessa a área de "Custom Fields", define o nome, o tipo (texto, data, seleção), em qual tela o campo deve aparecer e... voilà! O campo surge no formulário, pronto para uso, como se um gênio da programação o tivesse colocado ali.

Vamos desvendar como essa mágica (que na verdade é só código bem escrito) funciona.


A Anatomia de um Campo: O Model CustomField

Todo super-herói tem uma origem, e a do nosso campo customizável é o seu Model. Ele é o DNA, a estrutura que define o que um campo é e como ele se comporta.


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class CustomField extends Model
{
    protected $fillable = [
        'enterprise_id', // A qual empresa pertence
        'name',          // Nome amigável (ex: "Tipo de Contrato")
        'slug',          // Identificador único (ex: "tipo_de_contrato")
        'type',          // O tipo: text, number, date, select, etc.
        'apply_to',      // Onde ele aparece? ["users", "assets"]
        'options',       // Opções para campos select, radio, etc.
        'required',      // É obrigatório?
        'active',        // Está ativo?
        'sort_order',    // Ordem de exibição
        'description',   // Texto de ajuda
        'group',         // Grupo ao qual pertence (para organização)
    ];

    protected $casts = [
        'options' => 'array',    // Converte o JSON de opções para array
        'apply_to' => 'array',   // O mesmo para as telas de aplicação
        'required' => 'boolean',
        'active' => 'boolean',
    ];
}

Análise (não tão) Secreta:

$fillable: Nosso guarda-costas, garantindo que apenas os dados certos entrem no banco.
$casts: O tradutor. Ele pega o JSON que vem do banco (para options e apply_to) e o transforma em um array PHP, que é muito mais fácil de manipular. Chega de json_decode() espalhado pelo código!


A Sala de Controle: CustomFieldResource

Com o DNA definido, precisamos de um lugar para criar e gerenciar esses campos. É aqui que o Filament entra em cena com seu poder de criar CRUDs (Create, Read, Update, Delete) em minutos. O CustomFieldResource é a interface onde o cliente (ou você) vai brincar de Deus, criando campos do nada.


<?php

namespace App\Filament\App\Resources\CustomFields;

use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\TagsInput;
use Filament\Schemas\Schema;

class CustomFieldResource extends Resource
{
    protected static ?string $model = CustomField::class;

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            TextInput::make('name')
                ->live(onBlur: true)
                ->afterStateUpdated(fn (Set $set, ?string $state) => $set('slug', Str::slug($state)))
                ->required(),
            TextInput::make('slug')
                ->unique(ignoreRecord: true)
                ->required(),
            Select::make('type')
                ->options([
                    'text' => 'Text', 'number' => 'Number', 'date' => 'Date',
                    'select' => 'Select', 'multiselect' => 'Multi Select',
                    'checkbox' => 'Checkbox', 'radio' => 'Radio',
                ])
                ->live()
                ->required(),
            Select::make('apply_to')
                ->options(['users' => 'Users', 'assets' => 'Assets'])
                ->multiple()
                ->required(),
            TagsInput::make('options')
                ->placeholder('Digite uma opção e tecle Enter')
                ->visible(fn(Get $get) => in_array($get('type'), ['select', 'multiselect', 'radio']))
                ->required(fn(Get $get) => in_array($get('type'), ['select', 'multiselect', 'radio'])),
            Toggle::make('required'),
            Toggle::make('active')->default(true),
        ]);
    }
}

Análise (com um toque de sarcasmo):


  • live()** e ****afterStateUpdated**: O campo name gera o slug automaticamente. É a automação que amamos, nos poupando de ter que pensar em um slug ou, pior, deixar o cliente inventar um com espaços e emojis.
  • visible(fn(Get $get) => ...): Essa é a mágica do Filament. O campo options (para select, radio, etc.) só aparece se o type selecionado for um desses. É o formulário inteligente, que não te mostra o que não é da sua conta.


O Bibliotecário: O Serviço ManageCustomFields

Agora que temos campos sendo criados, precisamos de uma forma organizada de buscá-los. Em vez de espalhar queries pelo código, criamos um serviço para centralizar essa lógica. Ele é o nosso bibliotecário: você pede os livros (campos) de uma certa seção (ex: users), e ele te entrega.


<?php
namespace App\Services;

use App\Models\CustomField;
use Illuminate\Database\Eloquent\Collection;

class ManageCustomFields
{
    public function getCustomFields(?array $apply_to = null): Collection
    {
        $user = auth()->user();
        $query = CustomField::where('enterprise_id', $user->enterprise_id)->where('active', true);

        if (is_null($apply_to) || empty($apply_to)) {
            return $query->get();
        }

        $query->where(function($q) use ($apply_to) {
            foreach ($apply_to as $value) {
                $q->orWhereJsonContains('apply_to', $value);
            }
        });

        return $query->get();
    }
}


Análise (sem jargões chatos):


  • O serviço busca apenas os campos da empresa (enterprise_id) do usuário logado. Segurança em primeiro lugar!
  • O orWhereJsonContains é o herói aqui. Ele consegue buscar dentro da coluna JSON apply_to, encontrando os campos que devem aparecer em 'users', 'assets', ou onde quer que você tenha definido.


O Grand Finale: Renderização Dinâmica no UsersResource

Esta é a parte onde a mágica realmente acontece. Como fazemos os campos aparecerem no formulário de Usuários? Com uma combinação de Section, um map e um match que parece mais uma invocação arcana.

No UsersResource, dentro do método form(), criamos uma seção que busca e renderiza os campos.


// No UsersResource.php

Section::make('Informações adicionais')
    ->schema(function (?Model $record) {
        // 1. Pega os campos customizáveis para 'users'
        $customFields = app(ManageCustomFields::class)->getCustomFields(['users']);

        if ($customFields->isEmpty()) {
            return []; // Se não tem, não faz nada. Simples.
        }

        // 2. Mapeia cada campo para um componente do Filament
        return $customFields->map(function ($field) use ($record) {
            $type = $field->type;
            $fieldName = 'custom_fields.' . $field->slug; // Importante para o statePath

            // 3. O Match Mágico!
            return match ($type) {
                'text'   => TextInput::make($fieldName)->label($field->name),
                'number' => TextInput::make($fieldName)->label($field->name)->numeric(),
                'date'   => DatePicker::make($fieldName)->label($field->name),
                'select' => Select::make($fieldName)->label($field->name)->options($field->options),
                // ... e assim por diante para cada tipo
            };
        })
        ->all();
    })
    ->statePath('custom_fields') // 4. Agrupa tudo num array só!
    ->columns(2),


3LSQBVFBpiV6ERbNTJAuItMXSrc2FcP4XOF6kwHH.png 119.34 KB

YuHBM6YcyGbPGgKjUQ6sJabHJ4CCC0Gxwc6or3XB.png 117.67 KB

yTRW8pGWJ0mBoCglMkpq75gjwHfSLyzzJBqp1uUU.png 202.55 KB

Análise (O truque final):


  1. Invocando o Serviço: Chamamos nosso bibliotecário (ManageCustomFields) e pedimos todos os campos que se aplicam a users.
  2. Mapeamento: Usamos o map do Laravel Collections para iterar sobre cada CustomField retornado.
  3. O match Mágico: Esta é a estrela do show. Com base no $field->type, o match retorna o componente de formulário correspondente do Filament (TextInput, DatePicker, Select, etc.). É uma forma muito mais limpa e legível do que um if/else gigante ou um switch.
  4. statePath('custom_fields'): Este comando é crucial. Ele diz ao Filament para armazenar todos os valores desses campos dinâmicos dentro de um único array chamado custom_fields no model User. Isso significa que não precisamos criar uma nova coluna no banco para cada campo customizável. Tudo fica organizado em uma única coluna JSON. Genial, não?


Conclusão: O Poder na Mão do Cliente

E é isso! Com uma estrutura de dados flexível, um serviço para organizar a lógica e o poder dos componentes dinâmicos do Filament, criamos um sistema onde o cliente tem autonomia para moldar a aplicação às suas necessidades.

O resultado? Clientes mais felizes, menos chamados de suporte para o time de desenvolvimento e um sistema muito mais adaptável e robusto. Agora, se um cliente pedir um campo para "Nível de Habilidade em Just Dance", você pode simplesmente sorrir e dizer: "Pode criar você mesmo!".


Comentários

Realize login para comentar neste post
Este post não possui comentários