php[architect] logo

Want to check out an issue? Sign up to receive a special offer.

How To: Soft Delete Data in Laravel 11

Posted by on December 5, 2024

The terrible thing about deleting data from a database is that it’s gone forever. We can’t even look at the data to see if we need it because it’s gone. If we need the data back, our only solution is to restore a backup, cross our fingers, and hope we have the backup just before it was deleted so the loss is minimized.

But what if there was a way to keep the data around and just mark it as deleted so we can still see it and quickly restore it?

Thankfully, Laravel provides a built-in feature that allows us to flag database rows as deleted without actually deleting them from the database. This article discusses how to use Soft Deletes in Laravel.

Why Should We Use Soft Deletes?

When we DELETE a row from the database it’s gone forever without going through a potentially painful process of doing a full database restore. Soft deleting the data flags the row as deleted but keeps it inside our database which allows us to easily view and restore the data with minimal work and can be a huge time saver when data is accidentally deleted.

It also allows us to keep rows that may be associated with this deleted data we may need for other purposes. For example, we may have data created by, and associated with, a specific user, and if their account is deleted that data may be inaccessible.

Laravel provides support for soft deleting using the Illuminate\Database\Eloquent\SoftDeletes trait.

I like to think of it like an “active” column you might normally add to a model. With the active column set to 1, we can see the data and with an active column set to 0, we won’t see it. That doesn’t prevent us from pulling reports based on its existing in the past, but it can prevent new data from being associated with it. The SoftDeletes trait just abstracts this away.

Migrations

The first part of the process we need to tackle is setting up our database tables to have the SoftDeletes column.

Before we go much further, we should note that this article was made with Laravel 11.27.2 which was the most current version as of the recording/writing but most of this article will work with minor changes using the last 3 or 4 previous version and most likely the future versions.

Adding the Soft Delete Columns to a New Table

Let’s start by creating a new model to track a Task in a task management application. We’ll create the model, resource controller, and migration all in one step.

% php artisan make:model -mcr Task           

   INFO  Model [app/Models/Task.php] created successfully. 

   INFO  Migration [database/migrations/2024_10_14_003040_create_tasks_table.php] created successfully. 

   INFO  Controller [app/Http/Controllers/TaskController.php] created successfully.  

Open the newly created migration and add the following lines to the up() function. The $table->softDeletes(); function call is what sets up the table to allow for the SoftDeletes trait to work. It adds a deleted_at column that will hold the datetime of when the row was deleted or null if it hasn’t been deleted. We’ll also add a name to make our examples a little easier to follow.

$table->string("name");
$table->softDeletes();

Now we can run the migration to add the table.

% ./vendor/bin/sail artisan migrate

   INFO  Running migrations. 

  2024_10_14_003040_create_tasks_table ................................................................................................ 16.95ms DONE

We can use the same $table->softDeletes(); call to add the deleted_at column to an existing table.

Setting Up the Model to Use Soft Deletes

Now that we have our database tables set up we can start working with soft deleted models in our code. The first step is adding the SoftDeletes trait to the models. Below is our Task model where we have set it up to use the SoftDeletes trait.

It’s important to note that even though we added the SoftDeletes column to our table Laravel doesn’t automatically use it until we add the trait so we will still irreversibly delete data without it.

{% highlight php %}
<?php

namespace App\Models;

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

class Task extends Model
{
use HasFactory;
use SoftDeletes;
}


## Deleting a Model
Now that everything is set up let's test it we'll start by using Tinker to create a new `Task`.

$test = \App\Models\Task::create([“name”=>”Test Task”]);
= App\Models\Task {#7242
name: “Test Task”,
updated_at: “2024-10-14 00:52:51”,
created_at: “2024-10-14 00:52:51”,
id: 1,
}


When we check the database we can see that it's been persisted to the database and the `deleted_at` column is set to `null` indicating that it hasn't been deleted.

mysql> select * from tasks;
+—-+———————+———————+————–+————+
| id | created_at | updated_at | name | deleted_at |
+—-+———————+———————+————–+————+
| 1 | 2024-10-14 00:52:51 | 2024-10-14 00:52:51 | Test Task | NULL |
+—-+———————+———————+————–+————+
1 row in set (0.00 sec)


Now we'll delete the model.

$test->delete();
=> true


And back in MySQL, we can see `deleted_at` is no longer `null` which indicates it has been deleted.

mysql> select * from tasks;
+—-+———————+———————+————–+———————+
| id | created_at | updated_at | name | deleted_at |
+—-+———————+———————+————–+———————+
| 1 | 2024-10-14 00:52:51 | 2024-10-14 00:53:24 | Test Task | 2024-10-14 00:53:24 |
+—-+———————+———————+————–+———————+
1 row in set (0.00 sec)


### Restoring a Model
To restore the task we can use the `restore()` function.

$test->restore()
=> true


mysql> select * from tasks;
+—-+———————+———————+————–+————+
| id | created_at | updated_at | name | deleted_at |
+—-+———————+———————+————–+————+
| 1 | 2024-10-14 00:52:51 | 2024-10-14 00:56:38 | Test Task | NULL |
+—-+———————+———————+————–+————+
1 row in set (0.00 sec)


### Finding a Deleted Model
When we're using the `SoftDeletes` trait Eloquent will automatically filter out soft-deleted rows from all queries but what if we need that data? For example, when we attempt to use `findOrFail()` we'll receive a `ModelNotFoundException` because the `SoftDeletes` trait is filtering them out.

$found = \App\Models\Task::findOrFail(1);

Illuminate\Database\Eloquent\ModelNotFoundException No query results for model [App\Models\Task] 2.


To get around this we need to call the `withTrashed()` function before we call `findOrFail()`.

$found = \App\Models\Task::withTrashed()->findOrFail(1);
= App\Models\Task {#7309
id: 1,
created_at: “2024-10-14 00:58:24”,
updated_at: “2024-10-14 00:59:44”,
name: “Test Task”,
deleted_at: “2024-10-14 00:59:44”,
}



### Force Deleting a Model
There will be cases where we need to completely delete the record from the database and not just mark it as deleted. `SoftDeletes` provides the `forceDelete()` function that will do just that.

$test->forceDelete();
=> true


mysql> select * from tasks where id = 1;
Empty set (0.00 sec)



### Finding a Deleted Model In a Relationship
A part of our logic we need to be on the lookout for is when we have a model that defines an Eloquent relationship with the `SoftDeletes` models.

Normally, we would define our relationship like so.

class Task extends Model
{
use SoftDeletes;

public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}

}


If the `User` associated with this `Task` is soft-deleted and we attempt to access it through this relationship the function will return `null`.

$found->user;
=> null


This may be the correct logic for your application but what if we want to display the user's information still?

The solution to this is to use the `withTrashed()` function to have it return a result.

class Task extends Model
{
use SoftDeletes;

public function user()
{
    return $$this->belongsTo(User::class)->withTrashed();
}

}


## Viewing Trashed Models From a Controller
If a user attempts to access a page for a deleted model using the default routing they'll receive a 404 error. This makes sense but what if we want to display a message informing them it was deleted instead?

To do this we can add a call to `withTrashed()` to our route definition and then the page will route as expected.

Route::get(“/tasks/{task}”, [TaskController::class, “show”])
->name(“tasks.show”)
->withTrashed();


## Adding Additional Deleted Logic
Because we interact with the delete logic through the `delete()` function we can add additional logic to what happens when we delete the model. Because we lose functionality like cascading deletes with this we can add our own version of this inside our delete function.

public function delete(): void
{
$this->logs()->delete();
$this->save();
}



One of the things I like to do is also track **who** deleted the model. This is helpful because then not only can we tell people when an entity was deleted but can also tell them who did it. It makes it so much easier to track down why it was deleted it if wasn't supposed to be and there's a joy I always feel when the person asking me who deleted the model is the person who did the deleting. 

I do this by adding a `deleted_by` column and then creating logic inside the `delete()` function that sets both the `deleted_at` and `deleted_by` columns to the correct values and saves the results.

public function delete(): void
{
$this->deleted_at = now();
$this->deleted_by = Auth::user()->id
$this->save();
}


## Pruning Old Data
We're not going to want to have this data live in our database forever because it can impact performance as the tables get larger and larger. To help with this Laravel provides the `Illuminate\Database\Eloquent\Prunable` trait which provides a helpful solution to automatically prune data as it ages out. This is not limited to being used with soft-deleted tables so feel free to use it everywhere.

To do this we'll add the `Prunable` trait to our model and then define a `prunable()` function. This is where we'll define the logic for which rows should be pruned.

class Task extends Model
{
use HasFactory;
use SoftDeletes;
use Prunable;

public function prunable()
{
    return static::withTrashed()
      ->whereNotNull("deleted_at");
}

}


Then we can call `php artisan model:prune` to prune the data.

% ./vendor/bin/sail artisan model:prune

INFO Pruning [App\Models\Task] records.

App\Models\Task ………………………………………………………………………………………………………… 2 records


## What You Need To Know
- Soft deletes allow us to keep data in the database
- Makes it easier to restore and report on data
- Use the `Prunable` trait to delete data after a set time

Tags:
 

Leave a comment

Use the form below to leave a comment: