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),
Análise (O truque final):
- Invocando o Serviço: Chamamos nosso bibliotecário (ManageCustomFields) e pedimos todos os campos que se aplicam a users.
- Mapeamento: Usamos o map do Laravel Collections para iterar sobre cada CustomField retornado.
- 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.
- 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