laravel-data-retention maintained by iamankitthapar
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
- Requirements
- Installation
- Your first retention rule
- Actions
- Conditions, exclusions, and grace periods
- Dashboard
- Scheduling
- Configuration
- Commands
- Reports and notifications
- Import and export
- Troubleshooting
- Development and support
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:
- Select Create policy.
- Enter a unique name and a full model class, such as
App\Models\ApiLog. - Choose an action, execution limit, and optional schedule.
- Enter conditions, exclusions, and action settings using the JSON editors.
- Save the policy and select Dry run to review the eligible count.
- 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.