Curso de Laravel Gohu

Crear los modelos Eloquent en Laravel con sus relaciones

Por Víctor Peña · Actualizado el

Crear los modelos Eloquent en Laravel

Hola, ¿cómo están? En la lección anterior definimos la base de datos del sistema de Mascotas y vimos qué método de Eloquent corresponde a cada relación del diagrama.

Hoy toca escribirlos. Sin los modelos, las consultas de las próximas lecciones no tienen sobre qué trabajar.

¡Empecemos!

Qué es Eloquent ORM

Eloquent es el ORM que trae Laravel. Las siglas significan Object-Relational Mapping: un traductor entre las tablas de la base de datos y los objetos de tu código.

En lugar de escribir SQL a mano, trabajas con clases:

// Sin ORM
$sql = "SELECT * FROM mascotas WHERE tipo = 'Perro'";

// Con Eloquent
Mascota::where('tipo', 'Perro')->get();

Cada tabla tiene su modelo, y cada fila se convierte en un objeto de esa clase.

Representación de Eloquent ORM en Laravel: la tabla se traduce en un modelo y cada fila en un objeto

La ventaja no es solo escribir menos: es que el código queda expresivo y las relaciones entre tablas se recorren como propiedades del objeto.

Crear un modelo

El comando de Artisan:

php artisan make:model Mascota

Esto crea el archivo app/Models/Mascota.php.

Si además quieres la migración, el factory y el seeder de una vez:

php artisan make:model Mascota -mfs

Y para generarlo todo, incluido el controlador con recursos:

php artisan make:model Mascota --all

La convención de nombres

Aquí está lo que hay que entender de Eloquent: funciona por convención. Si sigues las reglas, no tienes que configurar nada.

Elemento Convención Ejemplo
Modelo Singular, PascalCase Mascota
Tabla Plural, snake_case mascotas
Clave primaria id id
Clave foránea modelo_id refugio_id
Tabla pivote Los dos modelos en singular, alfabético mascota_vacuna

Eloquent deduce el nombre de la tabla pasando el modelo a plural. Con Mascota busca mascotas.

Un detalle para nombres en español: Laravel pluraliza en inglés, así que a veces se equivoca. Si el nombre no coincide, decláralo explícitamente:

protected $table = 'doctores';

Es preferible eso a renombrar la tabla para complacer al framework.

Anatomía de un modelo

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Mascota extends Model
{
    use HasFactory;

    protected $fillable = [
        'nombre',
        'codigo',
        'tipo',
        'raza',
        'color',
        'edad',
        'refugio_id',
    ];

    protected function casts(): array
    {
        return [
            'edad' => 'integer',
            'pedigree' => 'boolean',
        ];
    }
}

fillable: la protección contra asignación masiva

Esta propiedad no es opcional, y conviene entender por qué existe.

Cuando haces Mascota::create($request->all()), Eloquent intenta asignar todos los campos que llegaron del formulario. Si alguien añade a mano un campo refugio_id o id en la petición, podría modificar datos que no le corresponden.

$fillable es la lista blanca: solo esos campos se pueden asignar en masa. Cualquier otro se ignora.

Si intentas crear un registro con un campo que no está en la lista, Laravel lanza un error de asignación masiva. Como vimos en la lección de CRUD, es de los errores que más aparecen al empezar.

Existe la alternativa $guarded = [], que permite todo. Evítala: renuncias a la protección por comodidad.

casts: convertir los tipos automáticamente

MySQL devuelve casi todo como texto. casts() le dice a Laravel cómo convertirlo:

protected function casts(): array
{
    return [
        'edad' => 'integer',
        'pedigree' => 'boolean',
        'fecha_ingreso' => 'date',
        'precio' => 'decimal:2',
    ];
}

Sin esto, $mascota->pedigree devolvería "1" en lugar de true, y las comparaciones estrictas fallarían.

El cast date es especialmente útil: convierte la columna en un objeto de fecha con el que puedes hacer ->format('d/m/Y') o calcular diferencias directamente.

Declarar las relaciones

Aquí está el corazón de la lección. Cada relación del diagrama se declara como un método en el modelo.

Uno a muchos: hasMany y belongsTo

Un refugio tiene muchas mascotas; una mascota pertenece a un refugio.

En el modelo Refugio —el lado «uno»:

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

En el modelo Mascota —el lado «muchos», donde vive la clave foránea:

public function refugio(): BelongsTo
{
    return $this->belongsTo(Refugio::class);
}

Fíjate en el nombre de los métodos: plural si devuelve varios, singular si devuelve uno. No es capricho, es lo que hace legible el código que los usa:

$refugio->mascotas;   // una colección
$mascota->refugio;    // un solo objeto

Eloquent deduce que la clave foránea es refugio_id. Si tu columna se llama distinto:

return $this->belongsTo(Refugio::class, 'id_refugio');

Uno a uno: hasOne y belongsTo

Una mascota tiene una única adopción.

// En Mascota
public function adopcion(): HasOne
{
    return $this->hasOne(Adopcion::class);
}

// En Adopcion
public function mascota(): BelongsTo
{
    return $this->belongsTo(Mascota::class);
}

La diferencia con hasMany es solo que devuelve un objeto en lugar de una colección. La clave foránea sigue estando en el mismo lado.

Muchos a muchos: belongsToMany

Una mascota tiene muchas vacunas, y una vacuna se aplica a muchas mascotas.

// En Mascota
public function vacunas(): BelongsToMany
{
    return $this->belongsToMany(Vacuna::class);
}

// En Vacuna
public function mascotas(): BelongsToMany
{
    return $this->belongsToMany(Mascota::class);
}

En ambos lados es belongsToMany, porque la relación es simétrica.

Eloquent busca la tabla pivote con los dos nombres en singular y en orden alfabético: mascota_vacuna. Si la tuya se llama distinto:

return $this->belongsToMany(Vacuna::class, 'vacunas_aplicadas');

Datos extra en la tabla pivote

Como vimos, la tabla intermedia suele llevar información propia —la fecha de aplicación, por ejemplo—. Para acceder a ella hay que declararlo:

public function vacunas(): BelongsToMany
{
    return $this->belongsToMany(Vacuna::class)
        ->withPivot('fecha', 'lote')
        ->withTimestamps();
}

Y al consultarla:

foreach ($mascota->vacunas as $vacuna) {
    echo $vacuna->nombre;
    echo $vacuna->pivot->fecha;   // el dato de la tabla intermedia
}

Ese ->pivot-> es la forma de llegar a las columnas de la tabla intermedia, y es algo que confunde bastante la primera vez.

Los modelos del sistema completo

Así quedan los cinco modelos de nuestro sistema:

// app/Models/Refugio.php
class Refugio extends Model
{
    protected $fillable = ['nombre', 'ciudad', 'direccion', 'telefono', 'encargado'];

    public function mascotas(): HasMany
    {
        return $this->hasMany(Mascota::class);
    }
}
// app/Models/Mascota.php
class Mascota extends Model
{
    protected $fillable = ['nombre', 'codigo', 'tipo', 'raza', 'color', 'edad', 'refugio_id'];

    public function refugio(): BelongsTo
    {
        return $this->belongsTo(Refugio::class);
    }

    public function vacunas(): BelongsToMany
    {
        return $this->belongsToMany(Vacuna::class)->withPivot('fecha');
    }

    public function adopcion(): HasOne
    {
        return $this->hasOne(Adopcion::class);
    }
}
// app/Models/Vacuna.php
class Vacuna extends Model
{
    protected $fillable = ['tipo', 'precio'];

    public function mascotas(): BelongsToMany
    {
        return $this->belongsToMany(Mascota::class)->withPivot('fecha');
    }
}
// app/Models/Persona.php
class Persona extends Model
{
    protected $fillable = ['nombre', 'apellido', 'dni', 'direccion', 'telefono'];

    public function adopciones(): HasMany
    {
        return $this->hasMany(Adopcion::class);
    }
}
// app/Models/Adopcion.php
class Adopcion extends Model
{
    protected $table = 'adopciones';   // Laravel pluralizaría "adopcions"

    protected $fillable = ['fecha', 'detalle', 'mascota_id', 'persona_id'];

    public function mascota(): BelongsTo
    {
        return $this->belongsTo(Mascota::class);
    }

    public function persona(): BelongsTo
    {
        return $this->belongsTo(Persona::class);
    }
}

Fíjate en Adopcion: es justo el caso donde la pluralización en inglés falla, así que declaramos $table explícitamente.

Comprobar que funciona

Antes de seguir, vale la pena verificar que las relaciones responden. Con Laravel Gohu es inmediato:

use App\Models\Refugio;
Refugio::with('mascotas')->first();

Si devuelve el refugio con su colección de mascotas anidada, los modelos están bien.

Si en cambio obtienes un error de tabla o columna inexistente, casi siempre es un problema de convención: el nombre de la tabla o el de la clave foránea no coinciden con lo que Eloquent espera.

Relaciones inversas y anidadas

Con las relaciones declaradas, puedes recorrerlas en cualquier dirección:

$mascota->refugio->ciudad;              // de mascota a refugio
$refugio->mascotas->count();            // cuántas mascotas tiene
$mascota->adopcion->persona->nombre;    // encadenando tres modelos

Ese último caso es potente y también peligroso: cada -> que atraviesa una relación no cargada dispara una consulta. Es la raíz del problema N+1 que veremos al final del curso.

Errores comunes

  • Olvidar $fillable y encontrarte con el error de asignación masiva.
  • Nombre de método en plural para una relación que devuelve uno solo, o al revés.
  • Confundir dónde va belongsTo. Siempre en el modelo cuya tabla tiene la clave foránea.
  • No declarar $table cuando la pluralización en inglés no coincide con tu nombre en español.
  • Olvidar withPivot() y no poder acceder a los datos de la tabla intermedia.
  • Usar $guarded = [] para evitar configurar $fillable.

Para cerrar

Los modelos son la traducción de tu diagrama entidad-relación al código. Bien declarados, todo lo que viene después —consultas, relaciones anidadas, conteos— se escribe casi solo.

Las dos reglas que resumen la lección: belongsTo va donde está la clave foránea, y el nombre del método en plural o singular según lo que devuelva.

Con los modelos listos, en la siguiente lección empezamos a consultarlos con Eloquent desde Laravel Gohu.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp