posts/Jtopwo1YLKbuVdzT4kALShFS17qeXpvha8iIqs3N.png

Timestamps, Userstamps y Soft Deletes en Laravel: guía completa con patrones, paquetes y buenas prácticas

Laravel, timestamps, userstamps, soft deletes, auditoría, Eloquent, Livewire, Blade, PHP, buenas prácticas

Introducción

En aplicaciones empresariales, “guardar datos” no es suficiente. También necesitas saber cuándo se creó o modificó un registro, quién lo hizo y si puede recuperarse tras una eliminación lógica. En Laravel, esto se resuelve con tres capacidades complementarias:

  • Timestamps: marcan los momentos de creación/actualización.

  • Userstamps: registran el usuario responsable de crear/editar/eliminar.

  • Soft Deletes: permiten “eliminar” sin borrar físicamente, habilitando restauración y auditoría posterior.

Combinadas, estas técnicas ofrecen una trazabilidad completa con un costo de implementación bajo y gran impacto en cumplimiento, auditoría forense y soporte operativo. La idea central coincide con el enfoque del artículo original de Ilyas Kazi sobre el “quién, cuándo y por qué” de los cambios en Eloquent.

1. Timestamps en Eloquent (created_at y updated_at)

¿Qué son?

Laravel añade automáticamente

created_at
y
updated_at
en modelos Eloquent cuando la tabla tiene esos campos tipo
timestamp
o
datetime
. Esta característica viene habilitada por defecto y cubre el cuándo. Documentación y artículos del ecosistema lo enfatizan como base de auditoría.

Migración mínima

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body')->nullable();
    $table->timestamps(); // created_at, updated_at
});

Deshabilitar o personalizar

class Post extends Model
{
    public $timestamps = false; // deshabilita timestamps

    // Si necesitas nombres personalizados:
    const CREATED_AT = 'creado_en';
    const UPDATED_AT = 'actualizado_en';
}

Buenas prácticas

  • Define índices según tus consultas (por ejemplo,

    created_at
    para reportes cronológicos).

  • Evita mutarlos manualmente; si lo haces (importaciones o fixtures), centraliza la lógica.

2. Userstamps (created_by, updated_by, deleted_by)

¿Qué son?

Los userstamps guardan el quién realizó una acción sobre el modelo (creación, actualización y, si usas SoftDeletes, eliminación lógica). No forman parte del núcleo de Laravel, pero existen paquetes y enfoques consolidados.

Opción A: Paquete listo para usar

Un paquete popular es wildside/userstamps (a veces referenciado como Laravel Userstamps). Mantiene

created_by
,
updated_by
y
deleted_by
automáticamente usando el usuario autenticado. Requiere Laravel 9+ y PHP 8.2+.

Instalación

composer require wildside/userstamps

Migración típica

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('created_by')->nullable()->constrained('users');
    $table->foreignId('updated_by')->nullable()->constrained('users');
    $table->foreignId('deleted_by')->nullable()->constrained('users');
});

Uso en el modelo

use Wildside\Userstamps\Userstamps;
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use Userstamps, SoftDeletes;
    // El trait completará created_by, updated_by y, con SoftDeletes, deleted_by.
}

Alternativas: Existen otros paquetes con la misma idea (p. ej.,

sqits/laravel-userstamps
,
tobidsn/laravel-userstamps
). Evalúa mantenimiento, compatibilidad de versión y comunidad.

Opción B: Implementación manual con Observers

Si prefieres cero dependencias, puedes usar Model Observers para setear los campos con

auth()->id()
:

class PostObserver
{
    public function creating(Post $post): void
    {
        if (auth()->check()) {
            $post->created_by = auth()->id();
            $post->updated_by = auth()->id();
        }
    }

    public function updating(Post $post): void
    {
        if (auth()->check()) {
            $post->updated_by = auth()->id();
        }
    }

    public function deleting(Post $post): void
    {
        if (in_array(SoftDeletes::class, class_uses_recursive($post)) && auth()->check()) {
            $post->deleted_by = auth()->id();
            // No llames save() aquí para no disparar más eventos;
            // rely on SoftDeletes para persistir.
        }
    }
}

Registro del observer

// AppServiceProvider::boot()
Post::observe(PostObserver::class);

Buenas prácticas

  • Usa

    foreignId
    y llaves foráneas con
    onDelete('set null')
    para no bloquear borrados de usuarios.

  • Añade índices a

    created_by
    ,
    updated_by
    ,
    deleted_by
    si generarás reportes por usuario.

3. Soft Deletes (deleted_at) y el “por qué” de restaurar

¿Qué son?

Soft Deletes marcan un registro como eliminado al establecer

deleted_at
sin borrarlo físicamente. Esto habilita recuperación, auditoría y compliance. La comunidad y recursos oficiales recomiendan su uso cuando el negocio exige trazabilidad y reversibilidad. Laravel News+2Medium+2

Migración

Schema::table('posts', function (Blueprint $table) {
    $table->softDeletes(); // agrega deleted_at
});

Modelo

use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes;
}

Consultas frecuentes

Post::withTrashed()->find($id);   // incluir eliminados
Post::onlyTrashed()->get();       // solo eliminados
Post::find($id)->restore();       // restaurar
Post::find($id)->forceDelete();   // eliminación física

Estos métodos (

withTrashed
,
onlyTrashed
,
restore
) forman parte del flujo estándar de SoftDeletes en artículos técnicos reconocidos. Medium

Buenas prácticas

  • Evita usar booleanos (“deleted”) para emular soft delete; Laravel espera

    deleted_at
    , aunque es posible redefinirlo con trabajo adicional. Stack Overflow

  • Considera políticas de retención: cuándo permitir

    restore()
    y cuándo exigir
    forceDelete()
    por normativa.

4. Integración conjunta: timestamps + userstamps + soft deletes

Patrón de auditoría mínima viable (MVP)

  • timestamps()
    en todas las tablas transaccionales.

  • softDeletes()
    en recursos críticos (clientes, pedidos, facturas, catálogos con histórico).

  • created_by
    ,
    updated_by
    ,
    deleted_by
    con paquete o observers.

class Post extends Model
{
    use SoftDeletes;
    // Si usas paquete:
    use \Wildside\Userstamps\Userstamps;

    protected $fillable = ['title', 'body'];
}

Consultas tipo “quién cambió qué y cuándo”

// Últimos cambios por usuario
Post::query()
    ->whereNotNull('updated_by')
    ->with(['updater:id,name']) // relación belongsTo('updated_by')
    ->orderByDesc('updated_at')
    ->limit(20)
    ->get();

Relaciones sugeridas

class Post extends Model
{
    public function creator() { return $this->belongsTo(User::class, 'created_by'); }
    public function updater() { return $this->belongsTo(User::class, 'updated_by'); }
    public function deleter() { return $this->belongsTo(User::class, 'deleted_by'); }
}

5. Seguridad y cumplimiento

  • Impersonación: si usas impersonate, registra también el usuario real en un campo adicional o en el contexto de logs.

  • Privacidad: protege columnas de auditoría en serializaciones públicas (

    hidden
    en el modelo) para APIs expuestas.

  • Políticas (Policies): controla quién puede

    restore
    o
    forceDelete
    .

  • Migraciones: aplica

    nullable()
    a
    deleted_by
    para evitar inconsistencias cuando se elimina un usuario.

  • Integridad: añade índices compuestos para consultas frecuentes (p. ej.,

    (deleted_at, updated_at)
    ).

6. Rendimiento y escalabilidad

  • Índices:

    deleted_at
    debe estar indexado si usas
    whereNull('deleted_at')
    con frecuencia.

  • Carga diferida: evita

    withTrashed()
    indiscriminado.

  • Batching: en limpiezas periódicas, usa

    chunkById()
    con
    forceDelete()
    para no bloquear.

7. Testing de auditoría

Prueba de creación

public function test_creates_with_userstamps(): void
{
    $user = User::factory()->create();
    $this->be($user);

    $post = Post::create(['title' => 'T', 'body' => 'B']);

    $this->assertNotNull($post->created_at);
    $this->assertEquals($user->id, $post->created_by);
}

Prueba de soft delete y restore

public function test_soft_delete_and_restore(): void
{
    $user = User::factory()->create();
    $this->be($user);

    $post = Post::factory()->create();
    $post->delete();

    $this->assertNotNull($post->fresh()->deleted_at);
    $this->assertEquals($user->id, $post->deleted_by);

    $post->restore();
    $this->assertNull($post->fresh()->deleted_at);
}

8. Paquetes y referencias

  • Artículo original sobre el enfoque “quién/cuándo/por qué” con timestamps, userstamps y soft deletes. Medium

  • Paquete Laravel Userstamps (wildside): instalación y uso. GitHub+1

  • Notas y ejemplos de userstamps en el ecosistema Laravel News. Laravel News

  • Soft Deletes en la práctica y métodos

    withTrashed
    ,
    onlyTrashed
    ,
    restore
    . Medium

  • Consideraciones y guía general de Soft Deletes en el ecosistema Laravel. Laravel News+1

Conclusión

Para una auditoría robusta en Laravel, aplica timestamps como estándar, añade soft deletes donde la recuperación sea requisito funcional y habilita userstamps con un paquete maduro o con observers si necesitas control total. Este trinomio te aporta trazabilidad clara del cuándo, quién y por qué, sin comprometer el rendimiento ni la mantenibilidad.

Comparte esta publicación

0 comentarios

Únete a la conversación y comparte tu experiencia.

Dejar un comentario

Comparte dudas, propuestas o mejoras para la comunidad.