Laravel Logo

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 we can get started using them.

Why Should I 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 you to easily restore the data with minimal work and can be a huge time saver when a user accidentally deletes some data.

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 our 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

Now when we open the newly created migration we’ll add the following to 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)

Eloquent

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 setup 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 you 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. 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",
   }

Good Luck!

Let us know in the comments if this has been helpful and if you’re finding soft deletes useful.