Cleaning up old unused records and files in Laravel applications

Constantin Druc • December 18, 2020

Sometimes to improve the user's experience, it makes sense to store files and create records hoping they will eventually be used somehow.

We're doing it here with this tweet form. We let the user select and upload photos; we then store them on the server and create media records, but we'll only use them if the user continues posting their tweet.

Files are uploading while he user types in their tweet

It's just a nicer user experience because, when you think about it, the user is there to upload a photo and then add a comment to it. Not the other way around. So while the user types in the tweet, the files are uploading.

Then, when the tweet is posted, on the receiving end of our action, we'll find those media records and connect them to our tweet model.

// TweetsController.php
public function store(Request $request)
{
    # ... validation code

    $tweet = Tweet::create([
        'user_id' => $request->user()->id,
        'content' => $request->input('content')
    ]);

    # Find the media records that should be

    Media::find($request->mediaIds)
        ->each->update([
            'model_id' => $tweet->id,
            'model_type' => Tweet::class
        ]);

    return redirect()->back();
}

But if that doesn't happen, if the user closes the window without submitting the form, the media files and records will stay on the server and be of no use for anybody. They will just sit there, occupying valuable space.

Solution

It would be nice to have a way of cleaning up these old unused records and files. We can do that with the help of a custom artisan command that will loop through all the unused media records older than, let's say a day, and remove them and their files.

Let's create the command:

php artisan make:command DeleteUnusedMedia

Inside the handle() method, we need to query the media records that have a null model_id and that are older than one day, and then loop through them, remove the records and delete the file from storage.

One thing to be careful when creating queries that might return massive amounts of records is to not use get() or all(); use a cursor or chunk the results. Otherwise, there's a chance your script might go over the php memory limit.

class DeleteUnusedMedia extends Command
{
    protected $signature = 'media:delete-unused';
    protected $description = 'Delete old unused media';

    public function handle()
    {
        Media::whereNull('model_id')
            ->where('created_at', '<=', now()->subDays(1))
            ->chunk(100, function ($media) {
                $media->each(function ($item) {
                    Storage::disk('public')->delete($item->filepath);
                    $item->delete();
                });
            });
    }
}

Now, whenever we want to clean up our unused media records and files, we can just run the php artisan media:delete-unused command. But isn't there a better way of running this command? It would be nice to automatically run this every day so that we don't have to log into the server every time.

Scheduling artisan commands

Open your app/Console/Kernel.php file, navigate to the schedule(Schedule $schedule) method, and add the following:

protected function schedule(Schedule $schedule)
{
    $schedule->command('media:delete-unused')->daily();
}

The above will instruct Laravel to run the media:delete-unused command daily. However, you do need to have a cron entry running the scheduler; otherwise, it won't work.

So on your server, make sure you add the following cron entry:

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

The entry above runs the scheduler every minute. Then the scheduler checks if it has any commands it needs to run, and runs them.

That's it. That's how you automate the clean up of old unused records and files in your Laravel applications.

What's your e-mail?

Get a monthly digest of what I've been up to: screencasts, blogposts, and other goodies.