Curso de Laravel Gohu
Crear los modelos Eloquent en Laravel con sus relaciones
Por Víctor Peña · Actualizado el

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.

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
$fillabley 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
$tablecuando 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.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp