webrek/laravel-data-retention

Declarative data retention for Laravel: keep records for a window, then delete or anonymize them automatically — with a compliance audit log.

Maintainers

Package info

github.com/webrek/laravel-data-retention

pkg:composer/webrek/laravel-data-retention

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-29 18:44 UTC

This package is auto-updated.

Last update: 2026-06-29 20:45:45 UTC


README

Última versión en Packagist Descargas totales Pruebas Versión de PHP Licencia

Declara cuánto tiempo se conservan las filas de un modelo y qué pasa cuando caducan. Después, un comando programado purga o anonimiza las filas que ya rebasaron su ventana, y registra cada una de las que toca en una bitácora de auditoría.

Conservar datos personales más tiempo del necesario es un riesgo bajo la LFPDPPP, el GDPR y la mayoría de los regímenes de privacidad. Este paquete convierte "borrar clientes inactivos después de un año" o "anonimizar tickets cerrados después de 90 días" de una tarea manual recurrente a una declaración que vive junto al modelo y se ejecuta sola.

use Illuminate\Database\Eloquent\Model;
use Webrek\DataRetention\Concerns\HasRetention;
use Webrek\DataRetention\RetentionPolicy;

class Customer extends Model
{
    use HasRetention;

    public function retentionPolicy(RetentionPolicy $policy): RetentionPolicy
    {
        return $policy
            ->since('last_seen_at')          // mide la antigüedad desde esta columna
            ->keepFor(365)                   // conserva un año, luego…
            ->where(fn ($q) => $q->where('legal_hold', false))
            ->anonymize([                    // …limpia la PII, conserva la fila
                'name'  => '[redacted]',
                'email' => fn (Customer $c) => "anon+{$c->id}@example.test",
                'phone' => null,
            ], markColumn: 'anonymized_at');
    }
}

Instalación

composer require webrek/laravel-data-retention

Publica y ejecuta la migración para la bitácora de auditoría:

php artisan vendor:publish --tag=data-retention-migrations
php artisan migrate

Opcionalmente publica la configuración:

php artisan vendor:publish --tag=data-retention-config

Declarar una política

Agrega el trait HasRetention a un modelo, implementa retentionPolicy() y lista el modelo bajo data-retention.models:

// config/data-retention.php
'models' => [
    App\Models\Customer::class,
    App\Models\EventLog::class,
],

Una política son dos decisiones: cuánto tiempo conservar una fila y qué hacer cuando caduca.

Cuánto tiempo

$policy
    ->since('created_at')   // la columna ancla; por defecto created_at
    ->keepFor(30);          // un entero son días…
use Carbon\CarbonInterval;

$policy->keepFor(CarbonInterval::months(18)); // …o cualquier CarbonInterval

Las filas cuya columna ancla es null nunca son elegibles: los datos que el paquete no puede fechar son datos que no tocará.

Qué pasa

Acción Efecto
->delete() Elimina la fila. Los modelos con soft delete se marcan como eliminados (soft-deleted); todo lo demás se borra de forma definitiva (hard delete). Los eventos del modelo se disparan, así que se ejecutan los observers y las cascadas.
->forceDelete() Elimina la fila de forma permanente, ignorando el soft delete.
->anonymize([...]) Conserva la fila pero sobrescribe las columnas indicadas.

anonymize() recibe un mapa de columna => valor. Cada valor es un literal o un closure que recibe el modelo:

$policy->anonymize([
    'name'   => '[redacted]',
    'email'  => fn ($model) => 'anon+' . $model->id . '@example.test',
    'ip'     => null,
], markColumn: 'anonymized_at');

Pasa una markColumn (un timestamp nullable) y el runner la sella, después omite las filas ya anonimizadas en ejecuciones posteriores, de modo que el job se mantiene barato e idempotente. Sin ella, la anonimización simplemente vuelve a aplicar los mismos valores en cada ejecución.

Legal holds y acotamiento

where() agrega restricciones a la consulta de elegibilidad. Úsalo para eximir registros bajo un legal hold por litigio, o para acotar una política a una parte de la tabla:

$policy
    ->keepFor(365)
    ->where(fn ($q) => $q->where('legal_hold', false))
    ->where(fn ($q) => $q->where('region', 'MX'))
    ->delete();

Purgar filas con soft delete

Una necesidad común es limpiar de forma permanente los registros un tiempo después de haberlos enviado a la papelera. Ancla en deleted_at, incluye las filas en la papelera y haz force delete:

$policy
    ->since('deleted_at')
    ->keepFor(90)
    ->includeTrashed()
    ->forceDelete();

Modelos que no puedes editar

Para un modelo de un vendor o del framework al que no puedes agregar el trait, registra una política desde un service provider:

use Webrek\DataRetention\Facades\DataRetention;

DataRetention::register(\Spatie\Activitylog\Models\Activity::class, fn ($policy) =>
    $policy->keepFor(90)->delete()
);

Ejecutarlo

php artisan retention:run                 # corre todas las políticas configuradas
php artisan retention:run --dry-run       # reporta qué cambiaría, sin cambiar nada
php artisan retention:run --model="App\Models\Customer"
php artisan retention:list                # muestra las políticas configuradas

Prográmalo como programes el resto de tu mantenimiento. Lo típico es a diario, fuera de las horas pico:

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('retention:run')->dailyAt('03:00');

El runner pagina las filas elegibles por llave primaria, así que una ejecución interrumpida simplemente continúa en la siguiente pasada en lugar de empezar de cero u omitir filas.

La bitácora de auditoría

Cada fila que toca una política se escribe en data_retention_log: el nombre de la política, la acción, el modelo y la llave, las columnas afectadas (para la anonimización) y cuándo ocurrió. Esa es la evidencia que espera una revisión de protección de datos: la prueba de que las reglas de retención se ejecutaron y de qué hicieron.

Cada ejecución de una política también dispara un evento Webrek\DataRetention\Events\RecordsRetained que lleva un RetentionResult, para que puedas reenviar los resultados a tus propias métricas o alertas.

Desactiva la bitácora en la configuración si guardas esa evidencia en otro lado:

'logging' => ['enabled' => false],

Configuración

return [
    'connection' => env('DATA_RETENTION_CONNECTION'), // conexión de la bitácora de auditoría
    'models'     => [/* modelos con una política HasRetention */],
    'chunk'      => 500,                              // filas por lote
    'logging'    => [
        'enabled'    => true,
        'table'      => 'data_retention_log',
        'connection' => null,
    ],
];

Pruebas

composer test

Contribuir

Consulta CONTRIBUTING.md. Ejecuta make check antes de abrir un PR.

Seguridad

Por favor reporta las vulnerabilidades a través del formulario de aviso de seguridad, no como issues públicos. Consulta SECURITY.md.

Licencia

La Licencia MIT (MIT). Consulta LICENSE.