Laravel Soft Deletes
While investigating the Laravel framework I ran into the following problem:
I have a model for which I enabled Soft Deletes. I want to allow the user to restore those models as well, using Soft Delete as a view-filtering option. Usually, the model should remain invisible from the overview and is only retained, because it links into other models that cannot be removed ever. The use case seems fine for Soft Delete.
I created a default Resource Controller
and routing the various routes using the Route::resource
shortcut. Unfortunately, this shortcut does not allow for restore
and forceDelete
routes, so I had to add those manually:
Route::delete("/model/{model}/restore",'ModelController@restore')
->middleware('can:restore,model')->name('model.restore');
Route::delete("/model/{model}/destroy",'ModelController@forceDelete')
->middleware('can:forceDelete,model')->name('model.forceDelete');
Route::resource('model', 'ModelController');
The additional routes I created as delete
methods as well, to mimic the standard delete route. A get
ought to be fine though as well.
This whole model is authorized using a relevant Policy
, which is initiated in the Controller
constructor:
public function __construct()
{
$this->authorizeResource(Model::class, 'model');
}
I added the relevant forceDelete
and restore
methods, much in the same way as the destroy method:
public function destroy(Model $model)
{
$fund->delete();
return redirect()->route('model.index')->with('success', __('The model was removed'));
}
public function forceDelete(Model $model)
{
$fund->forceDelete();
return redirect()->route('model.index')->with('success', __('The model was destroyed'));
}
public function restore(Model $model)
{
$fund->restore();
return redirect()->route('model.index')->with('success', __('The model was restored'));
}
This does not work, unfortunately. It returns a 404
on the restore
and forceDelete
requests, although the rest works fine.
It took me a few hours to find out why, but the cause is quite logical. The Soft Delete trait puts an additional where
clause on any query on the Model
. If you want to look in the soft deleted models collection, you need to apply the trashed()
query builder extension.
Now, when the Router
is binding the route parameter for the Model
, it will perform a basic find
query using the model identifier. However, because of the Soft Delete, it will only look into the non-deleted models. Ofcourse, for restore
and forceDelete
, we should be looking into the trashed models as well.
The workaround is to implement the resolveRouteBinding
method of the Model
class. This allows you to run a custom query to resolve the route identifier to a model instance, bypassing the trashed models.
Arguably, all Soft Deleted models should implement this, to avoid having user 1 delete a model that user 2 is currently editting or viewing. If user 2 then submits the model, he or she will get a nasty 404
on something they were able to view just a moment ago. If you are working with API calls under the hood, they will suddenly start returning errors although user 2 changed nothing.