miércoles, 1 de julio de 2026

Eloquent, atributos dinámicos y autocompletado en PHP y en Typescript

 Tengo un modelo Eloquent que fue generado con asistencia de IA.

El modelo es Paciente: class Paciente extends Model.

Y tiene métodos que hacen algo con los atributos del modelo, algo que todavía no entiendo bien.

Estos son:

    public function getFullNameAttribute()

    public function getFormattedAddressAttribute()

    public function getFormattedPhoneAttribute()

    public function getSocialSecurityInfoAttribute()


¿Por qué el asistente de código de IA hizo que todos estos métodos que él creó sean nombrados de modo que todos llevan el sufijo Attribute?


La IA lo hizo así porque está siguiendo una convención propia de Eloquent. Esos métodos no son métodos "comunes", sino accessors (accesores), es decir, métodos especiales que Laravel invoca automáticamente cuando accedes a un atributo del modelo.

Por ejemplo, si tienes:

class Paciente extends Model
{
    public function getFullNameAttribute()
    {
        return "{$this->apellido}, {$this->nombre}";
    }
}

Puedes escribir:

echo $paciente->full_name;

aunque la columna full_name no exista en la base de datos.

Laravel hace internamente algo equivalente a:

"¿Me están pidiendo el atributo full_name? Entonces busco un método llamado getFullNameAttribute()."

Por eso el nombre tiene esa estructura tan rígida:

get + NombreDelAtributo + Attribute

No es casualidad; Laravel la interpreta automáticamente.


¿Cómo convierte el nombre?

El atributo:

full_name

se convierte en el método:

getFullNameAttribute

Otro ejemplo:

formatted_phone

getFormattedPhoneAttribute

Y luego puedes hacer:

$paciente->formatted_phone;

como si fuera una propiedad normal.


¿Para qué sirven?

Se usan para exponer atributos calculados.

Por ejemplo, si la tabla tiene:

nombreapellido
JuanPérez

puedes crear:

public function getFullNameAttribute()
{
    return "{$this->nombre} {$this->apellido}";
}

y luego:

echo $paciente->full_name;

obtiene:

Juan Pérez

sin necesidad de almacenar full_name en la base.


En tu modelo

Por los nombres que mencionas, imagino que hacen algo parecido a esto:

getFullNameAttribute()

devuelve:

Juan Pérez

getFormattedAddressAttribute()

devuelve:

Av. Corrientes 1234, CABA

getFormattedPhoneAttribute()

devuelve:

(11) 4321-1234

getSocialSecurityInfoAttribute()

devuelve algo como:

OSDE 210 - Nº 123456

¿Se usan solo para leer?

No. Existe el mecanismo inverso: los mutators.

Por ejemplo:

public function setNombreAttribute($value)
{
    $this->attributes['nombre'] = strtoupper($value);
}

Entonces:

$paciente->nombre = "juan";

guardará:

JUAN

en la base.


Laravel moderno

Desde versiones recientes de Laravel, la forma recomendada es usar la clase Eloquent ORM Attribute, que reúne el accessor y el mutator en un único método:

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function fullName(): Attribute
{
    return Attribute::make(
        get: fn () => "{$this->apellido}, {$this->nombre}",
    );
}

El uso desde el resto de la aplicación sigue siendo el mismo:

$paciente->full_name;

Un detalle importante

Tus métodos devuelven información derivada de los datos del paciente, no realizan acciones. Por eso el asistente de IA los implementó como accessors y no como métodos comunes.

Si en cambio el modelo tuviera un método como:

$paciente->enviarRecordatorio();

ese no llevaría el sufijo Attribute, porque representa un comportamiento (una acción), no un atributo calculado.

En resumen, el sufijo Attribute le indica a Eloquent: "este método define cómo obtener el valor de un atributo del modelo". Es una convención del framework para que puedas acceder a esos valores como si fueran propiedades normales del objeto.


¿Es mucho pedir tener asistencia de autocompletado de atributos del modelo en el IDE?


No, de hecho es algo esperable. El problema es que Eloquent usa muchas propiedades "mágicas" (__get y __set), y un IDE no puede inferirlas por sí solo. Afortunadamente, hay varias soluciones.

Opción 1 (la más usada): Laravel Idea (de pago)

Si usas PhpStorm, el plugin Laravel Idea ofrece un autocompletado excelente:

  • Atributos de modelos.

  • Relaciones.

  • Accessors y mutators.

  • Rutas.

  • Vistas.

  • Configuración.

  • Mucho más.

Es probablemente la mejor experiencia para desarrollar con Laravel.


Opción 2 (gratuita): barryvdh/laravel-ide-helper

Es el estándar de facto para mejorar el autocompletado.

Se instala como dependencia de desarrollo y genera anotaciones PHPDoc en los modelos o en archivos auxiliares.

Por ejemplo, tu modelo Paciente puede quedar anotado así:

/**
 * @property int $id
 * @property string $nombre
 * @property string $apellido
 * @property string $telefono
 * @property-read string $full_name
 * @property-read string $formatted_phone
 */
class Paciente extends Model
{
    ...
}

Entonces el IDE sabe que existen:

$paciente->nombre;
$paciente->apellido;
$paciente->full_name;
$paciente->formatted_phone;

y ofrece autocompletado y análisis estático.


Opción 3: Anotarlo a mano

Puedes escribir las anotaciones tú mismo:

/**
 * @property string $nombre
 * @property string $apellido
 * @property-read string $full_name
 */
class Paciente extends Model
{
}

Funciona, pero es tedioso y hay que mantenerlo actualizado.


Opción 4: PHPStan + Larastan

Si además quieres análisis estático potente, puedes usar PHPStan junto con Larastan.

No solo mejora el autocompletado, sino que detecta errores como:

  • acceder a un atributo inexistente;

  • llamar a una relación que no existe;

  • errores de tipos.

Es muy recomendable cuando el proyecto crece.


En tu caso

Como conozco un poco tu forma de trabajar (te gusta aprovechar las capacidades del IDE y mantener el código limpio), yo usaría:

  • barryvdh/laravel-ide-helper si buscas una solución gratuita.

  • Laravel Idea si utilizas PhpStorm y no te importa pagar una licencia.

Con cualquiera de las dos tendrás autocompletado de:

  • columnas de la base de datos;

  • relaciones ($paciente->odontogramas, por ejemplo);

  • accessors como full_name y formatted_phone;

  • muchos otros elementos del ecosistema Laravel.

Es una mejora de productividad muy notable cuando trabajas con modelos Eloquent.


¿Puedo tener autocompletado en Typescript de atributos de modelos de Eloquent que le paso a la página de Inertia?


Sí. De hecho, es una de las mejores prácticas cuando se trabaja con Laravel + Inertia + React + TypeScript.

La idea es que no tipas el modelo Eloquent, sino el JSON que Inertia le envía a React.

Por ejemplo, si desde Laravel haces:

return Inertia::render('Paciente/Edit', [
    'paciente' => $paciente,
]);

En el navegador ya no existe un objeto Eloquent. Lo que recibe React es algo equivalente a:

{
  "id": 15,
  "nombre": "Juan",
  "apellido": "Pérez",
  "fecha_nacimiento": "1980-05-01"
}

Entonces defines una interfaz en TypeScript:

export interface Paciente {
    id: number;
    nombre: string;
    apellido: string;
    fecha_nacimiento: string | null;
}

Y luego tipas las props de la página:

interface Props {
    paciente: Paciente;
}

const { paciente } = usePage<Props>().props;

console.log(paciente.nombre);     // ✔ autocompletado
console.log(paciente.apellido);   // ✔ autocompletado

¿Qué pasa con los accessors?

Supongamos que en Eloquent tienes:

public function getFullNameAttribute()
{
    return "{$this->apellido}, {$this->nombre}";
}

Si haces que ese atributo forme parte del JSON (por ejemplo agregándolo al arreglo $appends del modelo):

protected $appends = [
    'full_name',
];

entonces Inertia enviará:

{
    "nombre": "Juan",
    "apellido": "Pérez",
    "full_name": "Pérez, Juan"
}

y simplemente lo agregas a la interfaz:

export interface Paciente {
    id: number;
    nombre: string;
    apellido: string;
    full_name: string;
}

También tendrás autocompletado para paciente.full_name.


El inconveniente

Hay una desventaja: duplicas la definición del modelo.

Lo tienes en PHP:

class Paciente extends Model

y otra vez en TypeScript:

interface Paciente

Si agregas una columna nueva, debes actualizar ambos.


¿Hay forma de evitar la duplicación?

Sí. Existen herramientas que generan automáticamente las interfaces TypeScript a partir de tus modelos o de tus recursos de API.

Algunas de las más utilizadas son:

  • Spatie Laravel Data

  • Laravel TypeScript Transformer

  • Momentum Trail

Estas herramientas mantienen sincronizados los tipos entre PHP y TypeScript.


Para tu proyecto

Por lo que conozco de Dentalia, todavía no introduciría una herramienta de generación automática.

Crearía un archivo como:

resources/js/types/paciente.ts

con:

export interface Paciente {
    id: number;
    nombre: string;
    apellido: string;
    // ...
}

y reutilizaría esa interfaz en todas las páginas y componentes.

Cuando el proyecto crezca (decenas de modelos y cientos de props), ahí sí evaluaría incorporar una solución que genere automáticamente los tipos desde PHP. En esta etapa, mantener unas pocas interfaces TypeScript a mano suele ser más simple y te da exactamente el autocompletado que buscas.

No hay comentarios: