Skip to content

5. Modelos en Laravel

Un modelo en Laravel representa una tabla en la base de datos y actúa como una capa de abstracción que permite interactuar con los datos utilizando métodos y propiedades orientados a objetos. Laravel implementa el patrón Active Record a través de su ORM (Object-Relational Mapping) llamado Eloquent.

Cada modelo corresponde a una tabla específica en la base de datos y simplifica las operaciones comunes como:

  • Crear, leer, actualizar y eliminar registros (CRUD)
  • Definir relaciones entre tablas (one-to-one, one-to-many, many-to-many)
  • Aplicar reglas de negocio a nivel de datos
  • Realizar validaciones y transformaciones de datos
Ejemplo básico de un modelo en Laravel
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
// Un modelo básico en Laravel
}

Laravel proporciona el comando make:model en Artisan para generar rápidamente modelos:

Comando básico para crear un modelo
php artisan make:model Product

Este comando creará un archivo Product.php en el directorio app/Models/.

Opciones del comando make:model
# Crear un modelo con migración
php artisan make:model Product -m
# Crear un modelo con controlador
php artisan make:model Product -c
# Crear un modelo con migración, factory, seeder y controlador
php artisan make:model Product -mfsc
# Crear un modelo con todo (migración, factory, seeder, controlador y policy)
php artisan make:model Product --all
# Crear un modelo en un subdirectorio (namespace)
php artisan make:model Admin/Product

Ubicación y Convención de Nombres de Modelos

Section titled “Ubicación y Convención de Nombres de Modelos”

Por convención, los modelos se almacenan en el directorio app/Models/ (a partir de Laravel 8) o app/ (en versiones anteriores).

  • Los nombres de los modelos deben estar en singular y en PascalCase
  • El nombre de la tabla correspondiente debe estar en plural y en snake_case
Convención de nombres de modelos
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model // Nombre en singular y PascalCase
{
// Laravel asumirá que la tabla se llama 'products' (plural y snake_case)
}

La asignación masiva permite crear o actualizar un registro utilizando un array de datos. Para controlar qué atributos pueden ser asignados masivamente, Laravel ofrece dos propiedades: $fillable y $guarded.

$fillable define una lista de atributos que están permitidos para asignación masiva (enfoque de lista blanca).

Ejemplo de uso de $fillable
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* Los atributos que son asignables masivamente.
*
* @var array
*/
protected $fillable = [
'name',
'description',
'price',
'category_id'
];
// Ahora podemos usar:
// Product::create(['name' => 'Producto 1', 'price' => 99.99, ...]);
}

$guarded define una lista de atributos que están protegidos contra la asignación masiva (enfoque de lista negra).

Ejemplo de uso de $guarded
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* Los atributos que no son asignables masivamente.
*
* @var array
*/
protected $guarded = [
'id',
'admin_flag'
];
// Todos los atributos excepto 'id' y 'admin_flag' serán asignables masivamente
}

Configuración de la Tabla Manual ($table)

Section titled “Configuración de la Tabla Manual ($table)”

Si el nombre de tu tabla no sigue la convención de Laravel (plural del nombre del modelo), puedes especificarlo manualmente usando la propiedad $table:

Especificar manualmente el nombre de la tabla
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* El nombre de la tabla asociada con el modelo.
*
* @var string
*/
protected $table = 'inventory_products';
// Laravel usará la tabla 'inventory_products' en lugar de 'products'
}

Uso de Claves Primarias Personalizadas ($primaryKey)

Section titled “Uso de Claves Primarias Personalizadas ($primaryKey)”

Por defecto, Laravel asume que cada tabla tiene una clave primaria llamada id. Si tu tabla utiliza un nombre diferente para la clave primaria, puedes especificarlo usando la propiedad $primaryKey:

Definir una clave primaria personalizada
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* La clave primaria asociada con la tabla.
*
* @var string
*/
protected $primaryKey = 'product_id';
}

Si tu clave primaria no es un entero autoincremental, puedes especificar su tipo:

Configurar clave primaria no incremental
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class ApiKey extends Model
{
/**
* La clave primaria asociada con la tabla.
*
* @var string
*/
protected $primaryKey = 'key';
/**
* Indica si la clave primaria es autoincremental.
*
* @var bool
*/
public $incrementing = false;
/**
* El tipo de dato de la clave primaria.
*
* @var string
*/
protected $keyType = 'string';
}

Activar o Desactivar Timestamps ($timestamps)

Section titled “Activar o Desactivar Timestamps ($timestamps)”

Por defecto, Laravel mantiene automáticamente las columnas created_at y updated_at en tus tablas. Si no deseas utilizar estas columnas, puedes desactivarlas:

Desactivar timestamps en un modelo
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* Indica si el modelo debe registrar timestamps.
*
* @var bool
*/
public $timestamps = false;
}

Personalizar los nombres de las columnas de timestamps

Section titled “Personalizar los nombres de las columnas de timestamps”

Si utilizas nombres diferentes para las columnas de timestamps, puedes personalizarlos:

Personalizar nombres de columnas de timestamps
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* El nombre de la columna "created at".
*
* @var string
*/
const CREATED_AT = 'fecha_creacion';
/**
* El nombre de la columna "updated at".
*
* @var string
*/
const UPDATED_AT = 'fecha_actualizacion';
}

Puedes personalizar el formato de almacenamiento de los timestamps:

Personalizar formato de fecha
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* Prepara una fecha para el almacenamiento en la base de datos.
*
* @param DateTimeInterface $date
* @return string
*/
protected function serializeDate(DateTimeInterface $date)
{
return $date->format('Y-m-d');
}
}

Eloquent permite convertir automáticamente ciertos atributos a tipos de datos nativos de PHP cuando los recuperas del modelo, y viceversa cuando los almacenas en la base de datos.

Definir casts de atributos
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* Los atributos que deben ser convertidos a tipos nativos.
*
* @var array
*/
protected $casts = [
'price' => 'float',
'active' => 'boolean',
'options' => 'array',
'released_at' => 'datetime',
'metadata' => 'json',
'dimensions' => 'object',
'sale_dates' => 'collection',
];
}
  • integer/int: Convierte a entero
  • real/float/double: Convierte a número de punto flotante
  • string: Convierte a cadena de texto
  • boolean/bool: Convierte a booleano
  • object: Convierte JSON a objeto PHP stdClass
  • array: Convierte JSON a array PHP
  • collection: Convierte JSON a una colección de Laravel
  • json: Convierte a JSON al guardar y de JSON al cargar
  • datetime: Convierte a instancia Carbon/DateTime
  • timestamp: Convierte a timestamp Unix
  • date: Convierte a instancia Carbon con solo la fecha
  • encrypted: Encripta/desencripta automáticamente los valores
  • decimal:<precision>: Convierte a decimal con precisión específica (ej. decimal:2)

Puedes crear tus propios casts personalizados:

Crear un cast personalizado
<?php
namespace AppCasts;
use IlluminateContractsDatabaseEloquentCastsAttributes;
class Money implements CastsAttributes
{
/**
* Cast el valor dado.
*
* @param IlluminateDatabaseEloquentModel $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
*/
public function get($model, $key, $value, $attributes)
{
return new AppValueObjectsMoney($value / 100);
}
/**
* Prepara el valor dado para el almacenamiento.
*
* @param IlluminateDatabaseEloquentModel $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
*/
public function set($model, $key, $value, $attributes)
{
return [
$key => $value instanceof AppValueObjectsMoney
? $value->getAmount() * 100
: $value * 100,
];
}
}

Los accesores y mutadores te permiten modificar los atributos cuando accedes a ellos o cuando los estableces en un modelo.

Los accesores transforman los datos cuando los recuperas del modelo. Definen un formato de visualización sin alterar los datos almacenados en la base de datos.

Accesores en Laravel 8 y anteriores
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* Obtiene el nombre del usuario en formato capitalizado.
*
* @param string $value
* @return string
*/
public function getNameAttribute($value)
{
return ucfirst($value);
}
/**
* Obtiene el nombre completo del usuario.
*
* @return string
*/
public function getFullNameAttribute()
{
return "{$this->name} {$this->lastname}";
}
}
// Uso:
$user = User::find(1);
echo $user->name; // Aplica el accesor automáticamente
echo $user->full_name; // Atributo virtual (no existe en la base de datos)

Los mutadores transforman los datos antes de guardarlos en la base de datos:

Mutadores en Laravel 8 y anteriores
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* Establece el atributo de contraseña.
*
* @param string $value
* @return void
*/
public function setPasswordAttribute($value)
{
$this->attributes['password'] = bcrypt($value);
}
/**
* Establece el atributo de email en minúsculas.
*
* @param string $value
* @return void
*/
public function setEmailAttribute($value)
{
$this->attributes['email'] = strtolower($value);
}
}
// Uso:
$user = new User;
$user->password = 'password123'; // Se aplica el hash automáticamente
$user->email = 'Usuario@EJEMPLO.com'; // Se convierte a minúsculas
$user->save();

Los scopes te permiten encapsular lógica de consulta reutilizable.

Los scopes locales permiten definir consultas reutilizables que pueden ser encadenadas con otras consultas:

Definición y uso de scopes locales
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentBuilder;
class Product extends Model
{
/**
* Scope para productos activos.
*
* @param IlluminateDatabaseEloquentBuilder $query
* @return IlluminateDatabaseEloquentBuilder
*/
public function scopeActive(Builder $query)
{
return $query->where('active', true);
}
/**
* Scope para productos en stock.
*
* @param IlluminateDatabaseEloquentBuilder $query
* @return IlluminateDatabaseEloquentBuilder
*/
public function scopeInStock(Builder $query)
{
return $query->where('stock', '>', 0);
}
/**
* Scope para productos de una categoría específica.
*
* @param IlluminateDatabaseEloquentBuilder $query
* @param int $categoryId
* @return IlluminateDatabaseEloquentBuilder
*/
public function scopeByCategory(Builder $query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
}
// Uso:
$featuredProducts = Product::active()->inStock()->byCategory(1)->get();

Los scopes globales se aplican automáticamente a todas las consultas de un modelo:

Definición de una clase Scope Global
<?php
namespace AppScopes;
use IlluminateDatabaseEloquentBuilder;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentScope;
class ActiveScope implements Scope
{
/**
* Aplicar el scope a una consulta Eloquent.
*
* @param IlluminateDatabaseEloquentBuilder $builder
* @param IlluminateDatabaseEloquentModel $model
* @return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->where('active', true);
}
}
Aplicación de un Scope Global
<?php
namespace AppModels;
use AppScopesActiveScope;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* El método "booted" del modelo.
*
* @return void
*/
protected static function booted()
{
static::addGlobalScope(new ActiveScope);
}
}
// Todas las consultas ahora incluirán automáticamente WHERE active = true
$products = Product::all();
// Para ignorar un scope global:
$allProducts = Product::withoutGlobalScope(ActiveScope::class)->get();
// O ignorar todos los scopes globales:
$allProducts = Product::withoutGlobalScopes()->get();

Los modelos Eloquent disparan eventos durante su ciclo de vida, lo que te permite ejecutar código cuando ocurren acciones específicas.

  • retrieved: Después de que un modelo es recuperado de la base de datos
  • creating: Antes de que un nuevo modelo sea guardado por primera vez
  • created: Después de que un nuevo modelo ha sido guardado por primera vez
  • updating: Antes de guardar un modelo existente
  • updated: Después de guardar un modelo existente
  • saving: Antes de guardar un modelo (nuevo o existente)
  • saved: Después de guardar un modelo (nuevo o existente)
  • deleting: Antes de eliminar un modelo
  • deleted: Después de eliminar un modelo
  • trashed: Después de que un modelo ha sido marcado como eliminado (soft delete)
  • forceDeleting: Antes de eliminar permanentemente un modelo
  • forceDeleted: Después de eliminar permanentemente un modelo
  • restoring: Antes de restaurar un modelo eliminado (soft delete)
  • restored: Después de restaurar un modelo eliminado (soft delete)
Observer de modelo
<?php
namespace AppObservers;
use AppModelsUser;
use IlluminateSupportFacadesLog;
class UserObserver
{
/**
* Manejar el evento "created" del modelo.
*
* @param AppModelsUser $user
* @return void
*/
public function created(User $user)
{
Log::info('Se ha creado un nuevo usuario: ' . $user->name);
}
/**
* Manejar el evento "updated" del modelo.
*
* @param AppModelsUser $user
* @return void
*/
public function updated(User $user)
{
Log::info('El usuario ha sido actualizado: ' . $user->name);
}
/**
* Manejar el evento "deleted" del modelo.
*
* @param AppModelsUser $user
* @return void
*/
public function deleted(User $user)
{
Log::info('El usuario ha sido eliminado: ' . $user->name);
}
}
Eventos directamente en el modelo
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
use IlluminateSupportFacadesLog;
class User extends Model
{
/**
* El método "booted" del modelo.
*
* @return void
*/
protected static function booted()
{
// Usando closures para los eventos
static::created(function ($user) {
Log::info('Nuevo usuario creado: ' . $user->name);
});
static::updated(function ($user) {
Log::info('Usuario actualizado: ' . $user->name);
});
static::deleted(function ($user) {
Log::info('Usuario eliminado: ' . $user->name);
});
}
}

Los modelos Eloquent ofrecen una variedad de métodos útiles para operaciones comunes.

Operaciones CRUD con modelos
<?php
// Crear una instancia sin guardar
$user = new User;
$user->name = 'Nuevo Usuario';
$user->email = 'usuario@ejemplo.com';
// Guardar la instancia en la base de datos
$user->save();
// Crear una instancia y guardarla de inmediato
$user = User::create([
'name' => 'Nuevo Usuario',
'email' => 'usuario@ejemplo.com',
'password' => bcrypt('password'),
]);
// Actualizar un modelo existente
$user = User::find(1);
$user->email = 'nuevo_email@ejemplo.com';
$user->save();
// Actualizar múltiples atributos a la vez
$user->update([
'name' => 'Nombre Actualizado',
'email' => 'email_actualizado@ejemplo.com',
]);
// Actualizar o crear un registro
$user = User::updateOrCreate(
['email' => 'usuario@ejemplo.com'], // atributos de búsqueda
['name' => 'Actualizado', 'active' => true] // atributos para actualizar o crear
);
// Borrar un modelo
$user->delete();
// Borrar por ID
User::destroy(1);
User::destroy([1, 2, 3]); // múltiples IDs
// Usar soft deletes (requiere columna deleted_at y trait SoftDeletes)
$user->delete(); // marca como eliminado
$user->trashed(); // verifica si está eliminado
$user->forceDelete(); // elimina permanentemente
// Restaurar un modelo eliminado con soft delete
$user->restore();
// Actualizar un contador directamente en la base de datos
Post::find(1)->increment('view_count');
Post::find(1)->increment('view_count', 5); // incrementar por 5
Product::find(1)->decrement('stock');
// Refrescar datos del modelo desde la base de datos
$user->refresh();
// Replicar un modelo (clonar sin ID)
$clonedUser = $user->replicate();
$clonedUser->save();
Métodos de comparación de modelos
<?php
// Comprobar si dos modelos son el mismo registro en base de datos
$user->is($anotherUser); // true si tienen el mismo ID y tabla
// Comprobar si un modelo ha cambiado desde su carga
$user->isDirty(); // true si hay cambios sin guardar
$user->isDirty('email'); // true si el email ha cambiado
// Comprobar si un modelo NO ha sido modificado
$user->isClean();
$user->isClean('name');
// Comprobar si un atributo específico ha cambiado
$user->wasChanged();
$user->wasChanged('email');
// Obtener atributos originales (antes de cambios)
$originalEmail = $user->getOriginal('email');
$allOriginal = $user->getOriginal(); // todos los valores originales

Eloquent permite definir relaciones entre modelos de forma elegante y expresiva.

Los principales tipos de relaciones en Laravel son:

Relación uno a uno
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* Obtiene el perfil asociado con el usuario.
*/
public function profile()
{
return $this->hasOne(Profile::class);
// Con claves personalizadas:
// return $this->hasOne(Profile::class, 'user_id', 'id');
}
}
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Profile extends Model
{
/**
* Obtiene el usuario al que pertenece este perfil.
*/
public function user()
{
return $this->belongsTo(User::class);
// Con claves personalizadas:
// return $this->belongsTo(User::class, 'user_id', 'id');
}
}
// Uso:
$profile = User::find(1)->profile;
$user = Profile::find(1)->user;
Relación uno a muchos
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Post extends Model
{
/**
* Obtiene los comentarios asociados con el post.
*/
public function comments()
{
return $this->hasMany(Comment::class);
}
}
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Comment extends Model
{
/**
* Obtiene el post al que pertenece el comentario.
*/
public function post()
{
return $this->belongsTo(Post::class);
}
}
// Uso:
$comments = Post::find(1)->comments; // Colección de comentarios
$post = Comment::find(1)->post;
// Contar relaciones
$postWithCommentCount = Post::withCount('comments')->get();
echo $postWithCommentCount[0]->comments_count;
Relación muchos a muchos
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* Los roles que pertenecen al usuario.
*/
public function roles()
{
return $this->belongsToMany(Role::class);
// Con tabla pivot personalizada:
// return $this->belongsToMany(Role::class, 'user_roles');
// Con claves personalizadas:
// return $this->belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id');
// Con columnas adicionales en la tabla pivot:
// return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
// Con timestamps en la tabla pivot:
// return $this->belongsToMany(Role::class)->withTimestamps();
}
}
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Role extends Model
{
/**
* Los usuarios que pertenecen al rol.
*/
public function users()
{
return $this->belongsToMany(User::class);
}
}
// Uso:
$roles = User::find(1)->roles;
$users = Role::find(1)->users;
// Adjuntar y separar relaciones
$user = User::find(1);
$user->roles()->attach(1); // Añadir un rol
$user->roles()->attach([1, 2, 3]); // Añadir varios roles
$user->roles()->attach(1, ['expires_at' => now()->addDays(30)]); // Con datos pivot
$user->roles()->detach(1); // Quitar un rol
$user->roles()->detach(); // Quitar todos los roles
$user->roles()->sync([1, 2]); // Sincronizar (solo mantiene los roles 1 y 2)
$user->roles()->syncWithoutDetaching([1, 2, 3]); // Añade solo los que faltan
// Acceso a la tabla pivot
$user = User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}

4. Uno a Muchos a Través (Has Many Through)

Section titled “4. Uno a Muchos a Través (Has Many Through)”
Relación has many through
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Country extends Model
{
/**
* Obtiene todos los post de los usuarios de este país.
*/
public function posts()
{
return $this->hasManyThrough(
Post::class, // Modelo final
User::class // Modelo intermedio
// 'user_id', // Clave externa en el modelo intermedio (opcional)
// 'country_id', // Clave externa en el modelo final (opcional)
// 'id', // Clave local en este modelo (opcional)
// 'id' // Clave local en el modelo intermedio (opcional)
);
}
}
// Uso:
$posts = Country::find('ES')->posts;
Relación polimórfica uno a uno
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Image extends Model
{
/**
* Obtiene el modelo al que pertenece esta imagen.
*/
public function imageable()
{
return $this->morphTo();
}
}
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* Obtiene la imagen del usuario.
*/
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Post extends Model
{
/**
* Obtiene la imagen del post.
*/
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
// Uso:
$imageOfUser = User::find(1)->image;
$ownerModel = Image::find(1)->imageable; // Puede ser User o Post

Los modelos de Laravel heredan una rica funcionalidad de clases base y traits.

Clase Base - Illuminate\Database\Eloquent\Model

Section titled “Clase Base - Illuminate\Database\Eloquent\Model”

Todos los modelos de Laravel extienden esta clase base, que proporciona:

  • Operaciones CRUD básicas
  • Construcción de consultas
  • Sistema de eventos
  • Conversión de atributos
  • Serialización

Proporciona el método factory() para crear instancias de prueba:

Trait HasFactory
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
use HasFactory;
}
// Uso en pruebas o en seeders:
User::factory()->count(50)->create();

Para implementar la eliminación suave (marcar como eliminados sin borrar):

Trait SoftDeletes
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentSoftDeletes;
class Post extends Model
{
use SoftDeletes;
// La tabla debe tener una columna deleted_at nullable
}
// Uso:
$post->delete(); // Marca como eliminado (actualiza deleted_at)
// Consulta sólo registros no eliminados (comportamiento por defecto)
$activePosts = Post::all();
// Incluir registros eliminados
$allPosts = Post::withTrashed()->get();
// Sólo registros eliminados
$deletedPosts = Post::onlyTrashed()->get();
// Restaurar registros eliminados
$post->restore();
Post::onlyTrashed()->restore(); // Restaurar todos
// Eliminar permanentemente
$post->forceDelete();
  • HasAttributes: Maneja conversiones de atributos y accesores/mutadores
  • Notifiable: Para enviar notificaciones a un modelo
  • Cacheable: Para implementar caché en las consultas de modelos
  • Auditable: Para registrar cambios en los modelos
  • Searchable: Para búsqueda de texto completo en Laravel Scout
🐝