Automatically Delete Last 7 Days Records in Laravel Like a Pro - Techvblogs

Automatically Delete Last 7 Days Records in Laravel Like a Pro

Learn how to automatically delete last 7 days records in Laravel with clean, scheduled commands.


Suresh Ramani - Author - Techvblogs
Suresh Ramani
 

2 days ago

TechvBlogs - Google News

Managing database storage becomes critical as your Laravel application grows. Old records can slow down queries, consume valuable disk space, and create maintenance headaches. Learning how to automatically delete records older than 7 days keeps your database lean and your application running smoothly.

This comprehensive guide shows you exactly how to implement automatic record deletion in Laravel using task scheduling, custom commands, and best practices that protect your data. Whether you’re dealing with logs, temporary files, or user-generated content, you’ll master the techniques that Laravel professionals use in production environments.

Introduction to Automatic Record Deletion in Laravel

Why You Might Want to Delete Old Records

Database bloat is one of the silent killers of application performance. As your Laravel app accumulates data over months and years, tables grow exponentially, making queries slower and backups larger. Automatic deletion solves this problem by removing outdated records before they impact your system.

Storage costs money, especially in cloud environments where you pay for every gigabyte. Removing unnecessary data reduces hosting expenses and keeps your database size manageable. More importantly, smaller tables mean faster queries and better user experiences.

Common Use Cases for Time-Based Deletions in Laravel

Log Management: Application logs, error reports, and debug information typically become useless after a week. Automatically purging these records prevents log tables from consuming gigabytes of storage.

Session Cleanup: User sessions, temporary tokens, and cache entries often have built-in expiration dates but remain in the database until manually removed.

Audit Trail Maintenance: While audit logs are important for compliance, keeping years of detailed records may not be necessary. Regular cleanup maintains compliance while managing storage.

User Activity Data: Page views, click tracking, and analytics data can accumulate rapidly. Deleting older activity records keeps your analytics tables performant.

File Upload Cleanup: Temporary uploads, draft content, and abandoned form data create clutter that automatic deletion can address.

Understanding the Risks and Precautions

Automatic deletion is powerful but dangerous if implemented incorrectly. Once data is deleted, recovery becomes difficult or impossible without proper backups. Always test deletion logic thoroughly in development environments before deploying to production.

Consider the business impact of deleted data. Some records might seem old but could be referenced by active features or required for regulatory compliance. Collaborate with stakeholders to define appropriate retention policies before implementing automatic cleanup.

Getting Started with Laravel Task Scheduling

What is Laravel Scheduler and How Does it Work?

Laravel’s task scheduler provides an elegant solution for running recurring commands without managing multiple cron jobs. Instead of creating dozens of cron entries, you define all scheduled tasks within your Laravel application and set up a single cron job that runs every minute.

The scheduler evaluates your defined tasks and executes those due to run. This approach centralizes schedule management within your codebase, making it version-controlled and environment-specific.

Here’s how Laravel scheduling works:

  1. Define scheduled tasks in app/Console/Kernel.php
  2. Set up a single cron job to run php artisan schedule:run every minute
  3. Laravel checks which tasks are due and executes them automatically

Setting Up Your Cron Job for schedule:run

First, add the Laravel scheduler to your server’s crontab. Open your crontab editor:

crontab -e

Add this line to run the Laravel scheduler every minute:

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

For production environments, consider logging scheduler output:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /var/log/laravel-scheduler.log 2>&1

Verify your cron job is working by checking the scheduler output:

php artisan schedule:list

Creating Your First Scheduled Command

Generate a new Artisan command for record deletion:

php artisan make:command DeleteOldRecords

This creates app/Console/Commands/DeleteOldRecords.php. Here’s the basic structure:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Carbon\Carbon;

class DeleteOldRecords extends Command
{
    protected $signature = 'records:delete-old {--days=7 : Number of days to keep}';
    protected $description = 'Delete records older than specified days';

    public function handle()
    {
        $days = $this->option('days');
        $this->info("Deleting records older than {$days} days...");
        
        // Deletion logic will go here
        
        return Command::SUCCESS;
    }
}

The command signature includes an optional --days parameter, making it flexible for different retention periods.

Writing the Logic to Delete Records Older Than 7 Days

Using Eloquent to Filter and Delete Old Records

Laravel’s Eloquent ORM makes it straightforward to filter and delete old records. Use Carbon for date manipulation and Eloquent’s where clauses to target specific timeframes.

Here’s a basic example for deleting old log entries:

use App\Models\ActivityLog;
use Carbon\Carbon;

public function handle()
{
    $cutoffDate = Carbon::now()->subDays($this->option('days'));
    
    $deletedCount = ActivityLog::where('created_at', '<', $cutoffDate)->delete();
    
    $this->info("Deleted {$deletedCount} activity log records");
    
    return Command::SUCCESS;
}

For more complex scenarios, you might need to delete from multiple tables:

public function handle()
{
    $days = $this->option('days');
    $cutoffDate = Carbon::now()->subDays($days);
    
    // Delete old logs
    $logCount = ActivityLog::where('created_at', '<', $cutoffDate)->delete();
    
    // Delete old sessions
    $sessionCount = UserSession::where('last_activity', '<', $cutoffDate->timestamp)->delete();
    
    // Delete old temporary files
    $fileCount = TemporaryFile::where('created_at', '<', $cutoffDate)->delete();
    
    $this->info("Cleanup complete:");
    $this->info("- Logs: {$logCount}");
    $this->info("- Sessions: {$sessionCount}");
    $this->info("- Files: {$fileCount}");
    
    return Command::SUCCESS;
}

Creating a Custom Artisan Command for Deletion

Expand your deletion command to handle multiple record types safely:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Carbon\Carbon;
use App\Models\ActivityLog;
use App\Models\UserSession;
use App\Models\TempUpload;
use Illuminate\Support\Facades\Log;

class DeleteOldRecords extends Command
{
    protected $signature = 'records:delete-old 
                            {--days=7 : Number of days to keep}
                            {--model= : Specific model to clean up}
                            {--dry-run : Show what would be deleted without actually deleting}';
    
    protected $description = 'Delete records older than specified days';

    protected $models = [
        'activity_logs' => ActivityLog::class,
        'user_sessions' => UserSession::class,
        'temp_uploads' => TempUpload::class,
    ];

    public function handle()
    {
        $days = $this->option('days');
        $specificModel = $this->option('model');
        $dryRun = $this->option('dry-run');
        
        $cutoffDate = Carbon::now()->subDays($days);
        
        $this->info("Cleaning up records older than {$cutoffDate->format('Y-m-d H:i:s')}");
        
        if ($dryRun) {
            $this->warn("DRY RUN MODE - No records will be deleted");
        }
        
        $totalDeleted = 0;
        
        foreach ($this->models as $name => $modelClass) {
            if ($specificModel && $specificModel !== $name) {
                continue;
            }
            
            $count = $this->cleanupModel($modelClass, $cutoffDate, $dryRun);
            $totalDeleted += $count;
            
            $this->info("- {$name}: {$count} records " . ($dryRun ? 'would be deleted' : 'deleted'));
        }
        
        Log::info("Cleanup command completed", [
            'total_deleted' => $totalDeleted,
            'cutoff_date' => $cutoffDate,
            'dry_run' => $dryRun
        ]);
        
        return Command::SUCCESS;
    }
    
    protected function cleanupModel($modelClass, $cutoffDate, $dryRun = false)
    {
        $query = $modelClass::where('created_at', '<', $cutoffDate);
        
        if ($dryRun) {
            return $query->count();
        }
        
        return $query->delete();
    }
}

This enhanced command includes:

  • Dry-run mode for testing
  • Model-specific cleanup options
  • Comprehensive logging
  • Safe deletion with confirmation

Testing Your Deletion Logic Safely

Always test deletion commands in development before production deployment. Create test records and verify the logic works correctly:

// Create test records in tinker
php artisan tinker

// Generate old records for testing
ActivityLog::factory()->create(['created_at' => now()->subDays(10)]);
ActivityLog::factory()->create(['created_at' => now()->subDays(5)]);
ActivityLog::factory()->create(['created_at' => now()->subDays(1)]);

// Test with dry-run
php artisan records:delete-old --days=7 --dry-run

// Test actual deletion
php artisan records:delete-old --days=7

For more thorough testing, create a dedicated test:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\ActivityLog;
use Carbon\Carbon;

class DeleteOldRecordsTest extends TestCase
{
    public function test_deletes_old_records_correctly()
    {
        // Create old record (should be deleted)
        $oldRecord = ActivityLog::factory()->create([
            'created_at' => Carbon::now()->subDays(10)
        ]);
        
        // Create recent record (should be kept)
        $recentRecord = ActivityLog::factory()->create([
            'created_at' => Carbon::now()->subDays(3)
        ]);
        
        $this->artisan('records:delete-old --days=7');
        
        $this->assertDatabaseMissing('activity_logs', ['id' => $oldRecord->id]);
        $this->assertDatabaseHas('activity_logs', ['id' => $recentRecord->id]);
    }
}

Integrating the Deletion Command with Laravel Scheduler

Registering the Command in Kernel.php

Add your deletion command to Laravel’s task scheduler in app/Console/Kernel.php:

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected $commands = [
        Commands\DeleteOldRecords::class,
    ];

    protected function schedule(Schedule $schedule)
    {
        // Run cleanup daily at 2 AM
        $schedule->command('records:delete-old --days=7')
                 ->dailyAt('02:00')
                 ->withoutOverlapping()
                 ->runInBackground();
                 
        // Alternative: Run weekly on Sundays
        $schedule->command('records:delete-old --days=7')
                 ->weekly()
                 ->sundays()
                 ->at('02:00');
                 
        // Run with different retention for different models
        $schedule->command('records:delete-old --days=30 --model=activity_logs')
                 ->dailyAt('03:00');
    }
}

Key scheduling options explained:

  • withoutOverlapping(): Prevents multiple instances from running simultaneously
  • runInBackground(): Allows other scheduled tasks to run concurrently
  • onFailure(): Defines actions when the command fails
  • onSuccess(): Defines actions when the command succeeds

Setting the Right Frequency for Automatic Cleanup

Choose scheduling frequency based on your data volume and business needs:

Daily Cleanup (Recommended for high-traffic apps):

$schedule->command('records:delete-old --days=7')->daily();

Weekly Cleanup (Good for moderate traffic):

$schedule->command('records:delete-old --days=7')->weekly();

Hourly Cleanup (For very high-volume applications):

$schedule->command('records:delete-old --days=7')->hourly();

Custom Frequency:

// Every 6 hours
$schedule->command('records:delete-old --days=7')->cron('0 */6 * * *');

// Only on weekdays at 3 AM
$schedule->command('records:delete-old --days=7')
         ->weekdays()
         ->at('03:00');

Logging and Notifications After Deletion Runs

Implement comprehensive logging and notifications to monitor your cleanup operations:

protected function schedule(Schedule $schedule)
{
    $schedule->command('records:delete-old --days=7')
             ->dailyAt('02:00')
             ->withoutOverlapping()
             ->appendOutputTo(storage_path('logs/cleanup.log'))
             ->emailOutputOnFailure('[email protected]')
             ->onSuccess(function () {
                 // Log successful cleanup
                 Log::info('Daily cleanup completed successfully');
             })
             ->onFailure(function () {
                 // Send alert for failed cleanup
                 Log::error('Daily cleanup failed');
                 // Send notification to monitoring service
             });
}

For more advanced monitoring, integrate with notification channels:

use Illuminate\Support\Facades\Notification;
use App\Notifications\CleanupCompleted;

$schedule->command('records:delete-old --days=7')
         ->dailyAt('02:00')
         ->onSuccess(function () use ($schedule) {
             Notification::route('mail', '[email protected]')
                        ->route('slack', 'your-slack-webhook-url')
                        ->notify(new CleanupCompleted());
         });

Example of Pruneable Trait Implementation

Laravel 8.37+ introduced the Pruneable trait, which provides an elegant way to automatically delete old model records. This approach is cleaner and more Laravel-like than custom commands for simple use cases.

Setting Up Pruneable Models

First, add the Pruneable trait to your model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
use Carbon\Carbon;

class ActivityLog extends Model
{
    use Prunable;
    
    protected $fillable = ['action', 'user_id', 'ip_address', 'user_agent'];
    
    /**
     * Get the prunable model query.
     */
    public function prunable()
    {
        return static::where('created_at', '<=', Carbon::now()->subDays(7));
    }
    
    /**
     * Prepare the model for pruning (optional).
     */
    protected function pruning()
    {
        // Perform any cleanup before the model is deleted
        // For example, delete related files
        if ($this->file_path) {
            Storage::delete($this->file_path);
        }
    }
}

Scheduling Prunable Models

Add the pruning command to your scheduler:

protected function schedule(Schedule $schedule)
{
    // Prune all models that use the Prunable trait
    $schedule->command('model:prune')->daily();
    
    // Prune specific models only
    $schedule->command('model:prune --model="App\Models\ActivityLog"')->daily();
    
    // Prune with chunking for large datasets
    $schedule->command('model:prune --chunk=1000')->daily();
}

Advanced Pruneable Configuration

For more complex pruning logic, you can customize the prunable query:

class UserSession extends Model
{
    use Prunable;
    
    public function prunable()
    {
        // Delete sessions older than 30 days OR inactive for 7 days
        return static::where(function ($query) {
            $query->where('created_at', '<=', Carbon::now()->subDays(30))
                  ->orWhere('last_activity', '<=', Carbon::now()->subDays(7));
        });
    }
    
    protected function pruning()
    {
        // Log session cleanup
        Log::info('Pruning user session', ['session_id' => $this->id]);
        
        // Update related user statistics
        $this->user->decrement('active_sessions_count');
    }
}

Mass Assignment with Pruneable

For models that need conditional pruning based on relationships:

class TempUpload extends Model
{
    use Prunable;
    
    public function prunable()
    {
        // Only prune uploads that aren't associated with published content
        return static::where('created_at', '<=', Carbon::now()->subDays(7))
                    ->whereDoesntHave('posts', function ($query) {
                        $query->where('status', 'published');
                    });
    }
    
    protected function pruning()
    {
        // Delete the actual file before removing the record
        if (Storage::exists($this->file_path)) {
            Storage::delete($this->file_path);
        }
        
        // Clear any cache entries
        Cache::forget("upload.{$this->id}");
    }
}

Best Practices and Tips for Safe Automation

Using Soft Deletes vs Hard Deletes for Flexibility

Soft deletes provide a safety net by marking records as deleted without actually removing them from the database. This approach allows data recovery and maintains referential integrity.

Enable soft deletes in your model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class ActivityLog extends Model
{
    use SoftDeletes;
    
    protected $dates = ['deleted_at'];
}

Add the deleted_at column to your migration:

public function up()
{
    Schema::table('activity_logs', function (Blueprint $table) {
        $table->softDeletes();
    });
}

Modify your cleanup command to use soft deletes:

public function handle()
{
    $cutoffDate = Carbon::now()->subDays($this->option('days'));
    
    // Soft delete old records
    $deletedCount = ActivityLog::where('created_at', '<', $cutoffDate)->delete();
    
    // Permanently delete very old soft-deleted records
    $purgedCount = ActivityLog::onlyTrashed()
        ->where('deleted_at', '<', Carbon::now()->subDays(30))
        ->forceDelete();
    
    $this->info("Soft deleted: {$deletedCount}, Permanently deleted: {$purgedCount}");
}

Backing Up Data Before Deleting

Implement automatic backups before running deletion operations:

public function handle()
{
    if ($this->shouldBackup()) {
        $this->createBackup();
    }
    
    // Proceed with deletion
    $this->performCleanup();
}

protected function shouldBackup()
{
    // Create backups only for production or when explicitly requested
    return app()->environment('production') || $this->option('backup');
}

protected function createBackup()
{
    $timestamp = now()->format('Y-m-d_H-i-s');
    $backupFile = storage_path("backups/cleanup_backup_{$timestamp}.sql");
    
    // Export data before deletion
    $this->info("Creating backup: {$backupFile}");
    
    // Example using mysqldump
    $command = sprintf(
        'mysqldump -u%s -p%s %s activity_logs > %s',
        config('database.connections.mysql.username'),
        config('database.connections.mysql.password'),
        config('database.connections.mysql.database'),
        $backupFile
    );
    
    exec($command);
}

Adding Authorization and Role Checks for Safety

Protect your deletion commands with proper authorization:

public function handle()
{
    // Check if the command is running in a safe environment
    if (!$this->isSafeToRun()) {
        $this->error('This command cannot run in the current environment');
        return Command::FAILURE;
    }
    
    // Require confirmation for production
    if (app()->environment('production') && !$this->option('force')) {
        if (!$this->confirm('Are you sure you want to delete old records in production?')) {
            $this->info('Operation cancelled');
            return Command::SUCCESS;
        }
    }
    
    $this->performCleanup();
}

protected function isSafeToRun()
{
    // Add your safety checks here
    $maintenanceMode = app()->isDownForMaintenance();
    $hasAdminPermission = $this->checkAdminPermission();
    
    return !$maintenanceMode && $hasAdminPermission;
}

protected function checkAdminPermission()
{
    // Implement your authorization logic
    // This could check environment variables, config files, or external services
    return config('app.cleanup_enabled', false);
}

Monitoring and Debugging Scheduled Jobs

Laravel provides several ways to monitor scheduled job execution:

Check Job History:

php artisan schedule:list
php artisan schedule:test

Monitor with Horizon (if using Redis):

composer require laravel/horizon
php artisan horizon:install
php artisan horizon

Custom Monitoring Integration:

protected function schedule(Schedule $schedule)
{
    $schedule->command('records:delete-old --days=7')
             ->dailyAt('02:00')
             ->before(function () {
                 Log::info('Starting daily cleanup');
                 // Send start notification to monitoring service
                 Http::post('https://monitoring-service.com/job/start', [
                     'job' => 'daily-cleanup',
                     'timestamp' => now()
                 ]);
             })
             ->after(function () {
                 Log::info('Daily cleanup completed');
                 // Send completion notification
                 Http::post('https://monitoring-service.com/job/complete', [
                     'job' => 'daily-cleanup',
                     'timestamp' => now()
                 ]);
             });
}

Health Check Endpoints:

Create a health check route to monitor scheduler status:

// routes/web.php
Route::get('/health/scheduler', function () {
    $lastRun = Cache::get('last_cleanup_run');
    $isHealthy = $lastRun && $lastRun->diffInHours(now()) < 25; // Should run daily
    
    return response()->json([
        'status' => $isHealthy ? 'healthy' : 'unhealthy',
        'last_run' => $lastRun,
        'next_due' => now()->addDay()->startOfDay()->addHours(2)
    ]);
});

Update your cleanup command to register successful runs:

public function handle()
{
    // Perform cleanup
    $result = $this->performCleanup();
    
    // Register successful completion
    if ($result === Command::SUCCESS) {
        Cache::put('last_cleanup_run', now(), now()->addDays(2));
    }
    
    return $result;
}

Conclusion and Next Steps

Implementing automatic record deletion in Laravel requires careful planning, thorough testing, and proper monitoring. You’ve learned how to use Laravel’s task scheduler, create custom Artisan commands, implement the Pruneable trait, and follow best practices that protect your data while keeping your database optimized.

The techniques covered in this guide provide a solid foundation for database maintenance automation. Your Laravel applications now have the tools to handle data lifecycle management without manual intervention, reducing storage costs and maintaining optimal performance.

Recap of Key Steps to Automate Deletion in Laravel

  1. Command Creation: Built custom Artisan commands with flexible parameters and safety features
  2. Scheduler Integration: Configured Laravel’s task scheduler for automated execution
  3. Pruneable Implementation: Used Laravel’s modern Pruneable trait for elegant model cleanup
  4. Safety Measures: Implemented soft deletes, backups, and authorization checks
  5. Monitoring Setup: Added logging, notifications, and health checks for production reliability

Advanced Ideas: Queue-Based Deletions, Notifications

Queue-Based Cleanup for Large Datasets:

For applications with millions of records, consider using queued jobs to prevent timeouts:

# Create a queued job for cleanup
php artisan make:job CleanupOldRecords
// In the job class
class CleanupOldRecords implements ShouldQueue
{
    public function handle()
    {
        ActivityLog::where('created_at', '<', Carbon::now()->subDays(7))
                  ->chunk(1000, function ($records) {
                      $records->each->delete();
                  });
    }
}

// Dispatch from your scheduled command
$schedule->call(function () {
    CleanupOldRecords::dispatch();
})->daily();

Advanced Notification System:

# Create notification for cleanup reports
php artisan make:notification CleanupReport
class CleanupReport extends Notification
{
    public function via($notifiable)
    {
        return ['mail', 'slack', 'database'];
    }
    
    public function toSlack($notifiable)
    {
        return (new SlackMessage)
            ->success()
            ->content('Daily cleanup completed')
            ->attachment(function ($attachment) {
                $attachment->title('Cleanup Statistics')
                          ->fields([
                              'Records Deleted' => $this->deletedCount,
                              'Storage Freed' => $this->formatBytes($this->bytesFreed),
                              'Duration' => $this->duration . ' seconds'
                          ]);
            });
    }
}

Where to Go From Here: Cleanup Strategies Beyond 7 Days

Database Archiving Strategy:

Instead of deleting old records, consider archiving them to separate tables or databases:

class ArchiveOldRecords extends Command
{
    public function handle()
    {
        DB::transaction(function () {
            // Move old records to archive table
            DB::statement('INSERT INTO archived_activity_logs SELECT * FROM activity_logs WHERE created_at < ?', [
                Carbon::now()->subMonths(6)
            ]);
            
            // Delete from main table
            ActivityLog::where('created_at', '<', Carbon::now()->subMonths(6))->delete();
        });
    }
}

Conditional Cleanup Based on Storage:

public function handle()
{
    $databaseSize = $this->getDatabaseSize();
    $threshold = 5 * 1024 * 1024 * 1024; // 5GB
    
    if ($databaseSize > $threshold) {
        // Aggressive cleanup
        $this->performCleanup(30); // Keep only 30 days
    } else {
        // Standard cleanup
        $this->performCleanup(90); // Keep 90 days
    }
}

Integration with External Storage:

// Archive to S3 before deletion
use Illuminate\Support\Facades\Storage;

protected function archiveToS3($records)
{
    $data = $records->toJson();
    $filename = 'archives/' . now()->format('Y/m/d') . '/activity_logs.json';
    
    Storage::disk('s3')->put($filename, $data);
    
    return $filename;
}

Your Laravel applications now have professional-grade data lifecycle management. As your needs evolve, you can expand these patterns to handle more complex scenarios like cross-database cleanup, compliance-driven retention policies, and integration with data warehousing systems.

The foundation you’ve built today scales from small applications to enterprise systems processing millions of records. Focus on monitoring your cleanup operations and adjusting retention policies based on actual usage patterns and business requirements.

Comments (0)

Comment


Note: All Input Fields are required.