Digging into Laravel SoftDeletes

Saurabh Mahajan
4 min readDec 16, 2020
Laravel SoftDeletes

It is often said that Digging into Laravel Codebase is one of the best method to understand how the Laravel works under the hood. It also helps you to pick up some of the best practices being used in the Framework. So I decided to dig and understand how Laravel implements Soft Deletes Functionality.

For those of you who have not used Soft Deletes Feature, it can be implemented simply by including a SoftDeletes Trait on your Model. Eloquent will then make sure that when Model are Deleted, they are not actually removed from the Database. Instead it will set the deleted_at column equal to timestamp when the Model was deleted. This column has a default value of Null indicating that they are not Deleted. I was interested in knowing how the following Features are implemented.

1. How does Laravel ensures then only Non-Deleted Records are returned on Query.

2. How is deleted_at column updated when Delete method is called.

3. How does method like withTrashed, onlyTrahsed etc. works.

So I went to SoftDeletes Trait which is located at Illuminate\Database\Eloquent. Here we have 2 methods which are of concern to us. bootSoftDeletes() and initializeSoftDeletes(). Naming convention of these methods are important. They must begin with keyword boot and initialize followed by the name of the Trait, which in this case is SoftDeletes. This way they are being automatically called during the boot method of the Model from bootTraits.

The initializeSoftDeletes() just makes sure that the deleted_at column is casts as type datetime. This way Laravel will automatically convert this field into a instance of Carbon.

The second method bootSoftDeletes() is the one where real magic happens. This actually adds a Global Scope SoftDeletingScope. In case you are not aware Global Scope allows you to add constraint to all queries on Model. This constraint needs to be added on the apply method of the scope. This will automatically be called by Laravel on every query. And if we check the apply method in SoftDeleingScope

public function apply(Builder $builder, Model $model)
{
$builder->whereNull($model->getQualifiedDeletedAtColumn());
}

$model->getQualifiedDeletedAtColumn() returns the deleted_at column and then we add the Null Clause to this column. So this way this clause is added to every Query on Model and this is how SoftDeletes Trait make sure that only Non-Deleted Records are returned.

As a side note, I also figured out that Laravel allows you to use any other column instead of deleted_at column. You simply need to describe the const DELETED_AT in your Model like below:

const DELETED_AT = ‘your_column_name’;

So now the next thing was to figure out how the deleted_at column is updated when the delete() is called on Model. Well this turned out to be simple. Trait just overrides the performDeleteOnModel(). This Method is actually called from within the Model::delete() and this is where the actual Delete happens after the various checks have been performed. So instead of deleting the actual record, we simple call the method runSoftDelete() where we set the deleted_at column to the current timestamp.

SoftDeletes trait also allows you to delete the record from the Model using forceDelete(). This also happens in the Trait method performDeleteOnModel(). We just do a check at the beginning of the method and the rest of the code is similar to the Model::performDeleteOnModel().

Similarly, SoftDeletes trait also has a restore(). This allows you to Un-Delete a Record. This method simply sets the deleted_at column back to null.

In addition, SoftDeletes Trait also registers the Model Events restoring, restored and forceDeleted. You can hook into these events and perform any check or operations at these stages.

Now the last remaining thing was to find out how the methods like withTrashed, onlyTrashed etc are implemented. This involves use of Macros. Macros provide a way to add Functions to Classes. So the functions like withTrashed, onlyTrashed etc. are added to the Builder Class using Macros. Here is how withTrashed Macro is added, it involved removing the Global Scope so that all records are returned.

protected function addWithTrashed(Builder $builder)
{
$builder->macro(‘withTrashed’, function (Builder $builder, $withTrashed = true) {
if (! $withTrashed) {
return $builder->withoutTrashed();
}
return $builder->withoutGlobalScope($this);
});
}

If you look at the SoftDeletingScope it also has Macro for Restore and it also overrides onDelete Method. These methods were also present in the Trait. So why these methods were added in Scope if they are already present in the Trait. Well the method in Trait corresponds to Model instance and will only affect 1 row. Whereas the one in the Scope are for the Builder and they may effect more than 1 row.

//This will call the Delete Method in Trait
Post::find(2)->delete()
//This will call the Delete Method in Scope.
Post::withoutTrashed()->delete()

So creating the SoftDelete Functionality involves just a Trait File and Scope File. Hope you have enjoyed this Article and are intrigued as to how easy Laravel it is to extend Laravel and implement such functionality.

--

--