Laravel Logo

The annoying 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 needed it because it’s gone. If we need the data back, our only solution will be to restore a backup and hope we have the backup just before it was deleted so the loss is minimized.

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/video discusses how to use Soft Deletes in Laravel.

Why Should We Use Soft Deletes?

As we’ve discussed in “Stop Deleting Data”, 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 allows us to easily view and restore the data with minimal work and can be a huge time saver when data is accidentally deleted.

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

Migrations

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

Adding the Soft Delete Columns to a New Table

Let’s start by creating a new model to track a Project in a project management application. We’ll create the model and migration in one step so we can be as lazy as possible.

$ php artisan make:model -m Project
Model created successfully.
Created Migration: 2020_05_02_012758_create_projects_table

When we open the newly created migration we’ll add the following lines to our up() function.

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

It looks like this.

class CreateProjectsTable extends Migration
{
    public function up()
    {
        Schema::create('projects', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('name');
            $table->softDeletes();
        });
    }

    public function down()
    {
        Schema::dropIfExists('projects');
    }
}

The $table->softDeletes(); function call is what sets up the table to allow for the SoftDeletes trait to work. Without it, we’ll get query errors.

Now we’ll run the migration.

$ php artisan migrate
Migrating: 2020_05_02_012758_create_projects_table
Migrated:  2020_05_02_012758_create_projects_table (0.01 seconds)

Let’s look at what the table looks like inside MySQL.

mysql> show columns from projects;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | timestamp           | YES  |     | NULL    |                |
| updated_at | timestamp           | YES  |     | NULL    |                |
| name       | varchar(255)        | NO   |     | NULL    |                |
| deleted_at | timestamp           | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

As we can see there’s a deleted_at column in the table definition. This is what the $table->softDeletes(); call added to the table and what the SoftDeletes trait will use to track if the row has been deleted.

Adding the Soft Delete Columns to an Existing Table

Let’s look at how we can add the soft delete columns to an existing table. We’re going to create a new migration using the make:migration command to add the columns to the users table because out of the box Laravel doesn’t have this column on the users table.

$ php artisan make:migration add_soft_delete_columns_to_users
Created Migration: 2020_05_02_012405_add_soft_delete_columns_to_users

Next we’re going to alter the migration so it both adds the columns in the up() function ($table->softDeletes();) and removes them in the down() function ($table->dropSoftDeletes();).

class AddSoftDeleteColumnsToUsers extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->softDeletes();
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
}

Again, we’ll run the migration.

$ php artisan migrate
Migrating: 2020_05_02_012405_add_soft_delete_columns_to_users
Migrated:  2020_05_02_012405_add_soft_delete_columns_to_users (0.06 seconds)

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 Illuminate\Database\Eloquent\SoftDeletes trait to the models. Below is an example model where we have set it up to use the SoftDeletes logic.

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

<?php
namespace App;

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

class Project extends Model
{
    use SoftDeletes;
}

Deleting a Model

Now that everything is set up let’s test it to see what happens.

First we’ll use tinker to create a new Project.

php artisan tinker
Psy Shell v0.10.3 (PHP 7.3.17-1+ubuntu16.04.1+deb.sury.org+1  cli) by Justin Hileman
>>> $test = new \App\Project();
=> App\Project {#3083}
>>> $test->name = "Test Project";
=> "Test Project"
>>> $test->save();
=> true

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 projects;
+----+---------------------+---------------------+--------------+------------+
| id | created_at          | updated_at          | name         | deleted_at |
+----+---------------------+---------------------+--------------+------------+
|  1 | 2020-05-02 01:34:15 | 2020-05-02 01:35:19 | Test Project | 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 projects;
+----+---------------------+---------------------+--------------+---------------------+
| id | created_at          | updated_at          | name         | deleted_at          |
+----+---------------------+---------------------+--------------+---------------------+
|  1 | 2020-05-02 01:34:15 | 2020-05-02 01:34:36 | Test Project | 2020-05-02 01:34:36 |
+----+---------------------+---------------------+--------------+---------------------+
1 row in set (0.00 sec)

Restoring a Model

If we accidentally delete the model Laravel makes it easy to restore the record using the restore() function.

>>> $test->restore()
=> true
mysql> select * from projects;
+----+---------------------+---------------------+--------------+------------+
| id | created_at          | updated_at          | name         | deleted_at |
+----+---------------------+---------------------+--------------+------------+
|  1 | 2020-05-02 01:34:15 | 2020-05-02 01:35:19 | Test Project | NULL       |
+----+---------------------+---------------------+--------------+------------+
1 row in set (0.00 sec)

Deleting a Model

Let’s say you have a case where someone accidentally entered information and we need to 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 projects where id = 1;
Empty set (0.00 sec)

Finding a Deleted Model

What happens when we delete a model and need to find it later?

First, let’s set up a new Project and soft delete it.

>>> $test = new \App\Project();
=> App\Project {#3100}
>>> $test->name = "Test Project 2";
=> "Test Project 2"
>>> $test->save();
=> true
>>> $test->id
=> 2
>>> $test->delete();
=> true

When we attempt to use findOrFail() we’ll receive a ModelNotFoundException because the SoftDeletes trait is filtering them out.

>>> $found = \App\Project::findOrFail(2);
Illuminate/Database/Eloquent/ModelNotFoundException with message 'No query results for model [App/Project] 2'

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

>>> $found = \App\Project::withTrashed()->findOrFail(2);
=> App\Project {#3120
     id: 2,
     created_at: "2020-05-02 01:39:19",
     updated_at: "2020-05-02 01:39:23",
     name: "Test Project 2",
     deleted_at: "2020-05-02 01:39:23",
   }

Finding a Deleted Model In a Relationship

The other part of our code 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 Project extends Model
{
    use SoftDeletes;

    public function user()
    {
        return $this->hasOne('App\User', 'id', 'user_id');
    }
}

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

>>> $found->user;
=> null

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

class Project extends Model
{
    use SoftDeletes;

    public function user()
    {
        return $this->hasOne('App\User', 'id', 'user_id')->withTrashed();
    }
}
>>> $found->user;
=> App\User {#3106
     id: 2,
     name: "Jared Roberts I",
     email: "elisa.lang@example.org",
     email_verified_at: "2020-04-28 00:28:06",
     created_at: "2020-04-28 00:28:06",
     updated_at: "2020-04-28 00:28:06",
     deleted_at: "2020-05-02 01:44:23",
   }

Viewing Trashed Models From a Controller

One of the annoying parts about the soft delete trait is that if a user attempts to access a page for a deleted resource they’ll receive a 404 error and not something helpful like a message informing them the item was deleted. To get around this we can add a call to withTrashed() to our route definition.

Route::get('/users/{user', 'UserController@show')
    ->name('users.show')
    ->withTrashed();

Adding deleted_by

As a small aside, one of the things we 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.

We do this by adding a deleted_by column and then we create our own 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();
}

We generally add this to our own SoftDeletes trait or class.

What You Need To Know

  • Soft deletes allow us to keep data in the database
  • Makes it easier to restore
  • Makes it easier to see when it was deleted
Looking for a new Laravel Developer Job? You can find one with one click using Jooble.