Looking to hire Laravel developers? Try LaraJobs

laravel-data-retention maintained by iamankitthapar

Description
A Laravel-first, policy-driven data retention engine.
Last update
2026/10/11 10:00 (dev-main)
License
Links
Downloads
1

Comments
comments powered by Disqus

Laravel Data Retention

Manage how long your Laravel application keeps data. Define a rule, preview the matching records, then delete, anonymize, or archive them in controlled batches.

Every feature is free under the MIT license. No subscription or license key is required.

Retention::rule('old-api-logs')
    ->model(ApiLog::class)
    ->olderThan('created_at', '6 months')
    ->action('delete')
    ->limit(100);

Contents

Features

  • Define rules in PHP or manage stored policies through the dashboard.
  • Permanently delete records, replace personal fields, or move records to an archive table.
  • Preview eligible records before running a rule.
  • Process large tables in chunks with a limit for each execution.
  • Exclude protected records and add a grace period.
  • Include or select only soft-deleted records.
  • Schedule rules with hourly, daily, weekly, monthly, or custom cron frequencies.
  • Select database connections for source records and archives.
  • Review execution history, optional per-record logs, and summary reports.
  • Receive email notifications and import or export stored policies as JSON.

Requirements

Laravel version Minimum PHP version
10, 11, 12 8.2
13 8.3

You also need a configured Laravel database connection. The dashboard uses your application's authentication system by default.

Installation

Run these commands from your Laravel application's root directory:

composer require iamankitthapar/laravel-data-retention
php artisan vendor:publish --tag=data-retention-config
php artisan vendor:publish --tag=data-retention-migrations
php artisan migrate

Laravel discovers the package automatically. Publishing creates config/data-retention.php and the migrations for these tables:

Table Purpose
retention_policies Policies managed through the dashboard.
retention_runs Execution history, counts, and results.
retention_run_items Optional results for individual records.

If package discovery is disabled, register Iamankitthapar\DataRetention\DataRetentionServiceProvider::class in your application's provider list.

Your first retention rule

This walkthrough uses API logs older than six months. Start with a limit of 10 records and preview the result before executing it.

1. Choose or create a model

You can use an existing Eloquent model and table. Replace ApiLog::class and created_at in the examples with your model and date column.

If you do not have an API log model, use the optional setup command:

php artisan retention:install-example
php artisan migrate

It creates:

File or table What it contains
app/Models/ApiLog.php An Eloquent model mapped to data_retention_api_logs.
A migration in database/migrations The API log table definition.
data_retention_api_logs id, method, path, status_code, timestamps, and an index on created_at.

The command creates files; migrate creates the table. It refuses to overwrite an existing ApiLog model or example migration. Laravel also applies your database connection's configured table prefix.

Your application must write its API logs. The example installer does not capture requests or add sample records.

2. Create a retention provider

php artisan make:provider RetentionServiceProvider

Replace app/Providers/RetentionServiceProvider.php with:

<?php

namespace App\Providers;

use App\Models\ApiLog;
use Iamankitthapar\DataRetention\Facades\Retention;
use Illuminate\Support\ServiceProvider;

class RetentionServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Retention::rule('old-api-logs')
            ->model(ApiLog::class)
            ->olderThan('created_at', '6 months')
            ->action('delete')
            ->limit(10);
    }
}

3. Register the provider

Add the following entry once to your application's provider list:

App\Providers\RetentionServiceProvider::class,
Laravel version Provider list location
10 The providers array in config/app.php.
11, 12, 13 The array returned by bootstrap/providers.php.

For example, on Laravel 11–13:

<?php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\RetentionServiceProvider::class,
];

4. Preview the matching records

php artisan retention:list
php artisan retention:preview old-api-logs

The preview shows the model, action, eligible-record count, and execution limit. It does not modify records or create a run log.

An empty example table returns zero eligible records. To try the walkthrough locally, open php artisan tinker and create one expired log:

$log = new App\Models\ApiLog;
$log->method = 'GET';
$log->path = '/example';
$log->status_code = 200;
$log->created_at = now()->subMonths(7);
$log->updated_at = $log->created_at;
$log->save();

Run the preview again. It should now include that record.

5. Execute and review

After reviewing the preview, run:

php artisan retention:run old-api-logs
php artisan retention:status

This rule permanently deletes up to 10 matching records. Increase the limit when you are ready to process more records. Each execution writes a summary to retention_runs.

Actions

Add the following examples to your retention provider's boot() method. Import the application model you use; models such as User and Order must already exist, and the referenced columns must match your schema.

Delete

Retention::rule('old-api-logs')
    ->model(ApiLog::class)
    ->olderThan('created_at', '6 months')
    ->action('delete')
    ->limit(100);

Deletion is permanent. For models using Laravel's SoftDeletes trait, this action calls forceDelete().

Anonymize

Keep a record while replacing selected fields:

Retention::rule('old-user-profiles')
    ->model(User::class)
    ->olderThan('created_at', '2 years')
    ->anonymize([
        'name' => 'Anonymous User',
        'email' => ['strategy' => 'email'],
    ])
    ->limit(100);

You can provide fixed values, strategy definitions, or callbacks:

->anonymize([
    'name' => 'Deleted user',
    'email' => fn (User $user) => "deleted-{$user->id}@example.invalid",
    'phone' => null,
])
Strategy Replacement
fixed The supplied value.
null null.
hash A SHA-256 hash of the current field value.
random A UUID.
email deleted-{record-id}@example.invalid.

Replacement values must satisfy the table's constraints, including unique indexes and non-null columns. Anonymization does not automatically remove a record from future matches; define conditions that exclude records already processed when needed.

Archive

Copy a record to an archive table, then permanently delete the source:

Retention::rule('old-orders')
    ->model(Order::class)
    ->olderThan('created_at', '7 years')
    ->archive(['table' => 'orders_archive'])
    ->limit(100);

Create the archive table first, with a schema compatible with the source model's raw attributes. To use another configured Laravel connection:

->archive([
    'connection' => 'archive',
    'table' => 'orders_archive',
])

On the same connection, copying and deleting run in one transaction. Separate connections cannot share an atomic transaction. If deletion fails after a cross-database copy, reconcile the two copies before retrying.

Conditions, exclusions, and grace periods

Filter records

->where('status', 'closed')
->olderThan('created_at', '1 year')

olderThan() calculates the cutoff when the query is built. You can also use retentionPeriod('1 year') for created_at, or retentionPeriod('closed_at', '1 year') for another date column.

Exclude protected records

->exclude(fn ($query) => $query->where('legal_hold', true))

Only use columns that exist in your table. An exclusion removes matching records from the eligible set.

Add a grace period

->olderThan('created_at', '1 year')
->gracePeriod('30 days')

Records become eligible after one year plus 30 days. Define the retention period before adding its grace period.

Select soft-deleted records

For models using SoftDeletes:

->includeSoftDeleted() // Match active and soft-deleted records.
->onlySoftDeleted()    // Match only soft-deleted records.

Choose one mode. By default, soft-deleted records are excluded. The delete and archive actions permanently remove the selected source records.

Select a source connection

->connection('mysql')

Use a connection name defined in your application's config/database.php. Without this setting, the rule uses the model's connection.

Dashboard

After installation, sign in to your application and visit:

/data-retention

The dashboard manages stored policies. Code-defined rules remain in your provider and are listed by retention:list.

To create a stored policy:

  1. Select Create policy.
  2. Enter a unique name and a full model class, such as App\Models\ApiLog.
  3. Choose an action, execution limit, and optional schedule.
  4. Enter conditions, exclusions, and action settings using the JSON editors.
  5. Save the policy and select Dry run to review the eligible count.
  6. Enable the policy and select Run when ready.

Conditions JSON

[
  { "column": "created_at", "operator": "<", "value": "2025-01-01" }
]

All conditions must match. Supported operators are =, !=, <>, <, <=, >, >=, LIKE, and NOT LIKE. Dates in stored conditions are fixed cutoffs; use PHP olderThan() rules for rolling periods such as “six months ago.”

Exclusions JSON

[
  { "column": "legal_hold", "operator": "=", "value": true }
]

Records matching any exclusion are left out. Use [] when no exclusions are needed.

Action configuration JSON

For anonymization:

{
  "fields": {
    "name": { "strategy": "fixed", "value": "Anonymous User" },
    "email": { "strategy": "email" }
  }
}

For archiving:

{ "connection": "archive", "table": "orders_archive" }

Delete policies do not need action settings; use {}.

The dashboard uses web and auth middleware by default. Add your application's administrator authorization middleware to restrict policy management. To disable the dashboard, set DATA_RETENTION_DASHBOARD_ENABLED=false in .env.

Scheduling

1. Give a rule a schedule

Add a frequency to the rule in your provider:

->schedule('daily')

Supported frequencies are hourly, daily, weekly, monthly, and cron:<expression>. A timezone is optional:

->schedule('cron:0 2 * * *', 'Asia/Kolkata')

Stored policies have equivalent Schedule and Timezone fields in the dashboard.

2. Register the schedules

Laravel 11–13: add these calls to routes/console.php:

use Iamankitthapar\DataRetention\Facades\Retention;

Retention::schedule();
Retention::scheduleStoredPolicies();

Laravel 10: add these calls to the existing schedule() method in app/Console/Kernel.php:

use Iamankitthapar\DataRetention\Facades\Retention;
use Illuminate\Console\Scheduling\Schedule;

protected function schedule(Schedule $schedule): void
{
    Retention::schedule($schedule);
    Retention::scheduleStoredPolicies($schedule);
}

Include scheduleStoredPolicies() only if you use scheduled stored policies, and run the package migrations first. These helpers register overlap prevention for each scheduled command.

3. Run Laravel's scheduler

Check the registered schedules:

php artisan schedule:list

For local development:

php artisan schedule:work

On a Linux server, configure a cron entry that invokes the scheduler every minute:

* * * * * cd /path/to/your-application && php artisan schedule:run >> /dev/null 2>&1

Configuration

Edit config/data-retention.php after publishing it.

Setting Default Purpose
chunk_size 1000 Records loaded in each database chunk.
max_records_per_run 1000 Default number of records attempted per rule execution.
continue_on_failure true Continue to the next record after an individual failure.
store_item_logs false Store per-record outcomes in retention_run_items.
run_log_retention_days null Default age used by retention:cleanup; null keeps logs until an explicit cleanup.
archive_connection null Default archive connection; otherwise use the source connection.
archive_table_suffix _archive Suffix used when no archive table is specified.
dashboard.enabled true Enable dashboard routes.
dashboard.path data-retention Dashboard URL path.
dashboard.middleware ['web', 'auth'] Middleware protecting dashboard routes.
allowed_connections [] Allowed connections for stored policies; empty permits configured connections.
notifications.recipients [] Email recipients.
notifications.notify_on_success false Also send notifications for successful runs.

A rule's limit() or a stored policy's execution limit overrides max_records_per_run. Limits apply to each rule execution, including failed attempts. chunk_size controls memory usage; it is not an execution limit. Both values must be positive.

Environment settings:

DATA_RETENTION_DASHBOARD_ENABLED=true
DATA_RETENTION_DASHBOARD_PATH=data-retention
DATA_RETENTION_NOTIFICATION_RECIPIENTS=operations@example.com,privacy@example.com

Commands

Run commands with php artisan:

Command Purpose
retention:install-example Create the optional API log model and migration.
retention:list List code-defined rules.
retention:preview {rule} Preview a code-defined rule without changing records.
retention:run {rule} Execute one code-defined rule.
retention:run Execute all registered code-defined rules.
retention:run --policy={id-or-name} Execute one enabled stored policy.
retention:dry-run {id-or-name} Preview a stored policy. Supports --format=json.
retention:status --limit=10 Show recent executions.
retention:report Summarize audit history. Supports --from, --to, and --format=json.
retention:policy:export --path=policies.json Export stored policies.
retention:policy:import policies.json --replace Import policies and update matching names.
retention:cleanup --days=90 Delete finished audit runs older than 90 days and their item logs.

retention:run without arguments does not execute stored policies. Preview and dry-run commands count matching records; they do not test writes, model events, or archive-table compatibility.

Reports and notifications

Each execution records the eligible count, attempted records, successful records, failures, timestamps, and final status. Enable store_item_logs for individual record identifiers, outcomes, and errors.

php artisan retention:status
php artisan retention:report --format=json
php artisan retention:report --from="2026-01-01 00:00:00" --to="2026-01-31 23:59:59"

Failures trigger email notifications when recipients are configured. Set notifications.notify_on_success to true to receive successful-run notifications too. Emails use your application's Laravel mail configuration. Delivery errors do not change the execution result.

To remove old audit history:

php artisan retention:cleanup --days=90

Setting run_log_retention_days supplies a default for that command; it does not schedule cleanup automatically. Audit records are execution logs, not backups or recovery snapshots.

Import and export

Move stored policies between environments:

php artisan retention:policy:export --path=retention-policies.json
php artisan retention:policy:import retention-policies.json --replace

Without --replace, imports create new policies and duplicate names fail. Imports are validated and saved in a transaction. Code-defined rules and execution history are not included in policy exports.

Imported policies retain their enabled state and schedules. Review them before importing into an environment with an active scheduler, and dry-run each policy before executing it.

Troubleshooting

Problem What to check
App\Models\ApiLog does not exist Run retention:install-example, or use an existing application model.
The installer says files already exist It preserves existing files. Check the model's table mapping and migration rather than rerunning it.
A table does not exist Publish the package migrations and run php artisan migrate. Run the example migration too if you installed it.
A rule is not listed Check that your provider is registered and defines the rule in boot().
Preview finds zero records Confirm the table contains records older than the cutoff and that exclusions or soft-delete scopes are not filtering them out.
The dashboard redirects to login Sign in through your application's authentication system.
A schedule does not run Check the rule frequency, schedule registration, and server scheduler. Use php artisan schedule:list.
Email notifications do not arrive Check recipients and the application's mail settings.

After changing cached application configuration, run php artisan optimize:clear and rebuild your deployment caches as needed.

Development and support

For contributors working in this package repository:

composer install
composer test

The CI matrix covers Laravel 10–13. See the requirements analysis for implementation details and scope.

Report bugs or request features through GitHub Issues. Include your Laravel and PHP versions, the relevant rule, and the error message.

This package manages Eloquent retention rules. Relationship-aware deletion, multi-tenancy, approval workflows, object-storage archives, and rollback or recovery are outside its current scope.

License: MIT.