En este artículo vamos a crear un panel de administración para una clínica veterinaria usando Laravel 12 y FilamentPHP 3.

Este ejemplo está basado en la demo oficial de Filament, pero adaptado al español, siguiendo buenas prácticas como nombrar los modelos en inglés, en singular y en Pascal Case. Si aún no tienes Filament instalado, te recomiendo leer nuestro artículo anterior:

👉 Cómo instalar FilamentPHP en Laravel (2025)


🧱 Estructura de la aplicación

Nuestra aplicación tendrá tres entidades principales:

  • Owner: representa al dueño de la mascota.
  • Patient: representa a la mascota.
  • Treatment: representa un tratamiento médico aplicado a una mascota.

Relaciones:

  • Un Owner tiene muchas Patients (1:N).
  • Un Patient pertenece a un Owner.
  • Un Patient puede tener muchos Treatments (1:N).
  • Un Treatment pertenece a un Patient.

🛠️ Paso 1: Crear modelos y migraciones

Ejecutamos los siguientes comandos:

php artisan make:model Owner -m
php artisan make:model Patient -m
php artisan make:model Treatment -m

Ahora definimos las columnas básicas en las migraciones:

🔹 database/migrations/xxxx_create_owners_table.php

Schema::create('owners', function (Blueprint $table) {
    $table->id();
    $table->string('email');
    $table->string('name');
    $table->string('phone');
    $table->timestamps();
});

🔹 database/migrations/xxxx_create_patients_table.php

Schema::create('patients', function (Blueprint $table) {
    $table->id();
    $table->date('date_of_birth');
    $table->string('name');
    $table->foreignId('owner_id')->constrained('owners')->cascadeOnDelete();
    $table->string('type');
    $table->timestamps();
});

🔹 database/migrations/xxxx_create_treatments_table.php

Schema::create('treatments', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->text('notes')->nullable();
    $table->foreignId('patient_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
});

Luego ejecutamos:

php artisan migrate

🔓 Desprotegiendo todos los modelos

Para abreviar esta guía, deshabilitaremos la protección de asignación masiva de Laravel . Filament solo guarda datos válidos en los modelos, por lo que estos pueden desprotegerse de forma segura. Para desproteger todos los modelos de Laravel a la vez, agregue Model::unguard() al método boot() de app/Providers/AppServiceProvider.php:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::unguard();
}

🔄 Paso 2: Definir relaciones en los modelos

🔹 app/Models/Owner.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Owner extends Model
{
    public function patients(): HasMany
    {
        return $this->hasMany(Patient::class);
    }
}

🔹 app/Models/Patient.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Patient extends Model
{
    public function owner(): BelongsTo
    {
        return $this->belongsTo(Owner::class);
    }

    public function treatments(): HasMany
    {
        return $this->hasMany(Treatment::class);
    }
}

🔹 app/Models/Treatment.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Treatment extends Model
{
    public function patient(): BelongsTo
    {
        return $this->belongsTo(Patient::class);
    }
}

🧩 Paso 3: Crear los recursos de Filament

Ejecutamos:

php artisan make:filament-resource Patient

Esto crea los archivos necesarios en app/Filament/Resources/.

panel con el nuevo recurso

📝 Paso 4: Configurar los formularios y tablas

Si abre el PatientResource.php, encontrará un método form()con una matriz vacía schema([...]). Al agregar campos de formulario a este esquema, se creará un formulario que permite crear y editar nuevos pacientes.

Entrada de texto «Nombre»

Filament agrupa una amplia selección de campos de formulario . Comencemos con un campo de entrada de texto simple :

Visite /admin/patients/create(o haga clic en el botón “Nuevo paciente”) y observe que se agregó un campo de formulario para el nombre del paciente.

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name'),
        ]);
}

Dado que este campo es obligatorio en la base de datos y tiene una longitud máxima de 255 caracteres, agreguemos dos reglas de validación al campo de nombre:

use Filament\Forms;

Forms\Components\TextInput::make('name')
    ->required()
    ->maxLength(255)

Intente enviar el formulario para crear un nuevo paciente sin nombre y observe que se muestra un mensaje informándole que el campo de nombre es obligatorio.

Seleccionar «Tipo»

Agreguemos un segundo campo para el tipo de paciente: se puede elegir entre gato, perro o conejo. Dado que hay un conjunto fijo de opciones, un campo de selección funciona bien:

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name')
                ->required()
                ->maxLength(255),
            Forms\Components\Select::make('type')
                ->options([
                    'cat' => 'Gato',
                    'dog' => 'Perro',
                    'rabbit' => 'Conejo',
                ]),
        ]);
}

El método options() del campo Seleccionar acepta una matriz de opciones para que el usuario elija. Las claves de la matriz deben coincidir con la base de datos, y los valores se utilizan como etiquetas del formulario. Puede agregar tantos animales como desee a esta matriz.

Como este campo también es obligatorio en la base de datos, agreguemos la regla de validación required():

use Filament\Forms;

Forms\Components\Select::make('type')
    ->options([
        'cat' => 'Gato',
        'dog' => 'Perro',
        'rabbit' => 'Conejo',
    ])
    ->required()

Selector de “Fecha de nacimiento”

Agreguemos un campo selector de fecha para la columna date_of_birth junto con la validación (la fecha de nacimiento es obligatoria y la fecha no debe ser posterior al día actual).

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name')
                ->required()
                ->maxLength(255),
            Forms\Components\Select::make('type')
                ->options([
                    'cat' => 'Gato',
                    'dog' => 'Perro',
                    'rabbit' => 'Conejo',
                ])
                ->required(),
            Forms\Components\DatePicker::make('date_of_birth')
                ->required()
                ->maxDate(now()),
        ]);
}

Seleccionar «Propietario»

También debemos agregar un propietario al crear un nuevo paciente. Dado que agregamos una relación BelongsTo en el modelo Patient (asociándolo al modelo Owner modelo relacionado), podemos usar el método relationship() del campo de selección para cargar una lista de propietarios entre los que elegir:

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name')
                ->required()
                ->maxLength(255),
            Forms\Components\Select::make('type')
                ->options([
                    'cat' => 'Gato',
                    'dog' => 'Pero',
                    'rabbit' => 'Conejo',
                ])
                ->required(),
            Forms\Components\DatePicker::make('date_of_birth')
                ->required()
                ->maxDate(now()),
            Forms\Components\Select::make('owner_id')
                ->relationship('owner', 'name')
                ->required(),
        ]);
}

El primer argumento del método relationship() es el nombre de la función que define la relación en el modelo (usada por Filament para cargar las opciones de selección); en este caso, owner. El segundo argumento es el nombre de la columna de la tabla relacionada que se usará; en este caso, name.

También haremos que el campo owner sea obligatorio, searchable()preload() los primeros 50 propietarios en la lista de búsqueda (en caso de que la lista sea larga):

use Filament\Forms;

Forms\Components\Select::make('owner_id')
    ->relationship('owner', 'name')
    ->searchable()
    ->preload()
    ->required()

Creando nuevos propietarios sin salir de la página

Actualmente, no hay propietarios en nuestra base de datos. En lugar de crear un recurso independiente para propietarios de filament, ofrecemos a los usuarios una forma más sencilla de agregar propietarios mediante un formulario modal (accesible mediante un botón + junto al botón de selección). Utilice el método createOptionForm() para integrar un formulario modal con campos TextInput para el nombre, la dirección de correo electrónico y el número de teléfono del propietario:

use Filament\Forms;

Forms\Components\Select::make('owner_id')
    ->relationship('owner', 'name')
    ->searchable()
    ->preload()
    ->createOptionForm([
        Forms\Components\TextInput::make('name')
            ->required()
            ->maxLength(255),
        Forms\Components\TextInput::make('email')
            ->label('Correo Electrónico')
            ->email()
            ->required()
            ->maxLength(255),
        Forms\Components\TextInput::make('phone')
            ->label('Teléfono')
            ->tel()
            ->required(),
    ])
    ->required()

En este ejemplo se utilizaron algunos métodos nuevos en TextInput:

  • label() Anula la etiqueta generada automáticamente para cada campo. En este caso, queremos que la etiqueta Email sea Correo Electrónico, y la etiqueta Phone sea Telefono.
  • email() Garantiza que solo se puedan introducir direcciones de correo electrónico válidas en el campo. También cambia la distribución del teclado en dispositivos móviles.
  • tel() Garantiza que solo se puedan introducir números de teléfono válidos en el campo. También cambia la distribución del teclado en dispositivos móviles.

¡El formulario ya debería funcionar! Intenta crear un nuevo paciente y su propietario. Una vez creado, serás redirigido a la página Editar, donde podrás actualizar sus datos.

Te invito a que sigas agregando en los demás campos del formulario las etiquetas label() para que así tu panel tenga los formularios totalmente en español.

resultado final del formulario

Preparación de la Tabla del paciente

Visita la página /admin/patients de nuevo. Si has creado un paciente, debería haber una fila vacía en la tabla, con un botón de edición. Añadamos algunas columnas a la tabla para poder ver los datos del paciente.

Abre el archivo PatientResource.php. Deberías ver un método table() con una matriz vacía columns([…]). Puedes usar esta matriz para agregar columnas a la tabla patients.

Agregar columnas de texto

Filament tiene una gran selección de columnas de tabla . Usemos una columna de texto simple para todos los campos de la tabla patients:

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

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name'),
            Tables\Columns\TextColumn::make('type'),
            Tables\Columns\TextColumn::make('date_of_birth'),
            Tables\Columns\TextColumn::make('owner.name'),
        ]);
}

Filament utiliza la notación de puntos para la carga rápida de datos relacionados owner.name. En nuestra tabla, mostramos una lista de nombres de propietarios en lugar del ID menos informativos. También se pueden agregar columnas para la dirección de correo electrónico y el número de teléfono del propietario.

Hacer que las columnas sean buscables

La posibilidad de buscar pacientes directamente en la tabla sería útil a medida que una clínica veterinaria crece. Se pueden hacer búsquedas en las columnas encadenando el método searchable() a la columna. Hagamos que el nombre del paciente y el nombre del propietario sean buscables.

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

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name')
                ->searchable(),
            Tables\Columns\TextColumn::make('type'),
            Tables\Columns\TextColumn::make('date_of_birth'),
            Tables\Columns\TextColumn::make('owner.name')
                ->searchable(),
        ]);
}

Vuelva a cargar la página y observe un nuevo campo de entrada de búsqueda en la tabla que filtra las entradas de la tabla utilizando los criterios de búsqueda.

Hacer que las columnas sean ordenables

Para que la tabla Patients se pueda ordenar por edad, agregue el método sortable() a la columna date_of_birth:

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

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name')
                ->searchable(),
            Tables\Columns\TextColumn::make('type'),
            Tables\Columns\TextColumn::make('date_of_birth')
                ->sortable(),
            Tables\Columns\TextColumn::make('owner.name')
                ->searchable(),
        ]);
}

Esto añadirá un icono de ordenación al encabezado de la columna. Al hacer clic en él, la tabla se ordenará por fecha de nacimiento.

tabla. depacientes

Filtrar la tabla por tipo de paciente

Si bien puedes hacer que el campo type se pueda buscar, hacer un filtro es una experiencia de usuario mucho mejor.

Las tablas de filament pueden tener filtros , que son componentes que reducen el número de registros de una tabla añadiendo un ámbito a la consulta de Eloquent. Los filtros pueden incluso contener componentes de formulario personalizados, lo que los convierte en una herramienta eficaz para crear interfaces.

Filament incluye una funcion prediseñada SelectFilter que puedes agregar a la tabla filters():

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

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->filters([
            Tables\Filters\SelectFilter::make('type')
                ->options([
                    'cat' => 'Gato',
                    'dog' => 'Perro',
                    'rabbit' => 'Conejo',
                ]),
        ]);
}
filtros por tipo de mascota

Presentamos a los gestores de relaciones

Actualmente, los pacientes pueden asociarse con sus dueños en nuestro sistema. Pero ¿qué ocurre si queremos un tercer nivel? Los pacientes acuden a la clínica veterinaria para recibir tratamiento, y el sistema debería poder registrar estos tratamientos y asociarlos con un paciente.

Una opción es crear un nuevo Recurso TreatmentResource para asociar tratamientos a un paciente. Sin embargo, gestionar los tratamientos por separado del resto de la información del paciente resulta engorroso para el usuario. Filament utiliza «gestores de relaciones» para solucionar este problema.

Los gestores de relaciones son tablas que muestran los registros relacionados de un recurso existente en la pantalla de edición del recurso principal. Por ejemplo, en nuestro proyecto, podría ver y administrar los tratamientos de un paciente directamente debajo de su formulario de edición.

También puede utilizar las “acciones” de Filament para abrir un formulario modal para crear, editar y eliminar tratamientos directamente desde la tabla del paciente.

Utilice el comando artisan make:filament-relation-manager para crear rápidamente un gestor de relaciones, conectando el recurso del paciente con los tratamientos relacionados:

php artisan make:filament-relation-manager PatientResource treatments description
  • PatientResource Es el nombre de la clase del recurso del modelo propietario. Dado que los tratamientos pertenecen a los pacientes, deben mostrarse en la página «Editar paciente».
  • treatments es el nombre de la relación en el modelo Paciente que creamos anteriormente.
  • description Es la columna a mostrar de la tabla de tratamientos.

Esto creará un PatientResource/RelationManagers/TreatmentsRelationManager.php archivo. Debe registrar el nuevo gestor de relaciones en el método getRelations() en el PatientResource.php:

use App\Filament\Resources\PatientResource\RelationManagers;

public static function getRelations(): array
{
    return [
        RelationManagers\TreatmentsRelationManager::class,
    ];
}

El archivo TreatmentsRelationManager.php contiene una clase precargada con un formulario y una tabla usando los parámetros del comando «Artisan»
make:filament-relation-manager. Puede personalizar los campos y columnas en el administrador de relaciones de forma similar a como lo haría en un recurso:

use Filament\Forms;
use Filament\Forms\Form;
use Filament\Tables;
use Filament\Tables\Table;

public function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('description')
                ->required()
                ->maxLength(255),
        ]);
}

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('description'),
        ]);
}

Visita la página Editar de uno de tus pacientes. Ya deberías poder crear, editar, eliminar y listar tratamientos para ese paciente.

Configuración del formulario de tratamiento

De forma predeterminada, los campos de texto solo ocupan la mitad del ancho del formulario. Dado que el campo description puede contener mucha información, agregue un método columnSpan('full') para que ocupe todo el ancho del formulario modal:

use Filament\Forms;

Forms\Components\TextInput::make('description')
    ->required()
    ->maxLength(255)
    ->columnSpan('full')

Agreguemos el campo notes, que permite añadir más detalles sobre el tratamiento. Podemos usar un campo de área de texto con un columnSpan('full'):

use Filament\Forms;
use Filament\Forms\Form;

public function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('description')
                ->required()
                ->maxLength(255)
                ->columnSpan('full'),
            Forms\Components\Textarea::make('notes')
                ->maxLength(65535)
                ->columnSpan('full'),
        ]);
}

Configurando el campo price

Agreguemos un campo price para el tratamiento. Podemos usar una entrada de texto con algunas personalizaciones para que sea compatible con la entrada de moneda. Debería ser numeric(), lo que añade validación y cambia la distribución del teclado en dispositivos móviles. Agregue el prefijo de moneda que prefiera usando el método prefix(); por ejemplo, prefix('€') se agregará un  antes de la entrada sin afectar el valor de salida guardado:

use Filament\Forms;
use Filament\Forms\Form;

public function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('description')
                ->required()
                ->maxLength(255)
                ->columnSpan('full'),
            Forms\Components\Textarea::make('notes')
                ->maxLength(65535)
                ->columnSpan('full'),
            Forms\Components\TextInput::make('price')
                ->numeric()
                ->prefix('')
                ->maxValue(42949672.95),
        ]);
}
formulario modal de tratamientos

Preparación de la tabla de tratamientos

Al generar el gestor de relaciones previamente, la columna de texto description se agregó automáticamente. Agreguemos también una columna sortable() para el price con un prefijo de moneda. Use el método money() de Filament para formatear la columna price como dinero; en este caso, para EUR[]:

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

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('description'),
            Tables\Columns\TextColumn::make('price')
                ->money('EUR')
                ->sortable(),
        ]);
}

Agreguemos también una columna para indicar cuándo se administró el tratamiento usando la marca de tiempo predeterminada created_at . Usemos el método dateTime() para mostrar la fecha y la hora en un formato legible:

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

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('description'),
            Tables\Columns\TextColumn::make('price')
                ->money('EUR')
                ->sortable(),
            Tables\Columns\TextColumn::make('created_at')
                ->dateTime(),
        ]);
}

Puede pasar cualquier cadena de formato de fecha PHP válida al método dateTime() (por ejemplo dateTime('m-d-Y h:i A')).

gestor de recursos tratamientos

🧠 Buenas prácticas

Aunque estamos construyendo una app en español, los modelos deben mantenerse en inglés y singular por convención en Laravel. Por ejemplo:

  • ✅ Owner en lugar de Dueño
  • ✅ Patient en lugar de Mascota
  • ✅ Treatment en lugar de Tratamiento

Esto facilita el mantenimiento y la consistencia con la comunidad.


🔗 Conclusión

Ya tienes una aplicación básica con Laravel y Filament para gestionar una clínica veterinaria. En próximos artículos veremos cómo:

  • crear widgets de estadísticas
  • Personalizar el panel (colores, iconos, navegación, idioma)
  • Aplicar roles y permisos con Spatie
  • Validar campos y añadir lógica avanzada

¿Te gustó el tutorial? ¡Compártelo y sígueme en Twitter para más contenido sobre Laravel y Filament en español!

🐱 Repositorio del Proyecto

Puedes todo el código de este y otros tutoriales en el siguiente repositorio de Github. Si te ayudo puedes dejarle una estrella así sabré que te a gustado.

Categorizado en:

FilamentPHP, Laravel, Tutoriales,