Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ianspangler's avatar

Cascading soft deletes

I am trying to devise a sensible strategy for cascading items that are soft deleted. I would imagine this is a common task, but with my application having many different tables and each having a number of foreign key constraints, it seems like a lot of manual effort (and some risk of error) in setting up events or triggers to update different tables when a record gets soft deleted. (ie: setting the deleted_at columns to be equal to the deleted_at column of the deleted item). Ideally, there would be some kind of migration command like:

$table->foreign('my_id')->references('foreign_id')->on('foreign_table')->onSoftDelete('cascade');

But I am pretty sure that does not exist in Laravel 5. So is there a standard, accepted practice with a large database that doesn't involve writing a lot of custom code? Appreciate any advice.

0 likes
7 replies
schir1964's avatar

What is the goal in achieving this?

Are wanting mark all child items as Soft Deleted also? What is the benefit in doing this?

If the child items are being referenced outside of the parent item, then I think it might make more sense to add a method to the model of the child item that returns the soft delete indicator of the parent which can used as needed.

michaeldyrynda's avatar

Cascading deletes are handled at the database-level, so when you set onDelete('cascade') in your migration, that translates to your database deleting any records attached by foreign key.

Soft deletes are handled by the application, so you'd either need to fire an event on the parent model and listen for it on the children or, in your parent model, bind the static::deleted() method in the boot method, and delete the relationships there.

I'm not sure if you could do something like:

public static function boot()
{
    parent::boot();

    static::deleted(function ($model) {
        // Probably lazy load these relationships to avoid lots of queries?
        $model->load([ 'relationshipOne', 'relationshipTwo', ]);

        $model->relationshipOne()->delete();
        $model->relationshipTwo()->delete();
    });
}

Or if you'd have to iterate over the related items:

static::deleted(function ($model) {
    $model->relationshipOne->each(function ($item) {
        $item->delete();
    });

    $model->relationshipTwo->each(function ($item) {
        $item->delete();
    });
});
1 like
ianspangler's avatar

Thanks for sharing this approach. Essentially, the soft deletes need to work exactly the same way as the hard deletes, which operate perfectly at the database-level. Ideally, there would be a streamline approach. Soft deleting seems like it offers the flexibility to restore data easily (or transfer data before a hard delete), so that's why I am looking to implement it. But maintaining the referential integrity of the foreign key constraints is very important in this application because leaving orphaned child records will literally break the display of information on certain pages of the application. To give a concrete example, there is a "movie credits" table that references an actor, a role, and a movie. Each dedicated movie credit page is dependent on those three parents being active in order to be valid. If, for instance, an actor is deleted from the actors table, any movie credits tied to that actor must be deleted as well.

TABLE: MOVIE_CREDITS
actor_id
role_id
movie_id
[other columns...]

TABLE: ACTORS
id
[other_columns...]

TABLE: ROLES
id
[other_columns...]

TABLE: MOVIES
id
[other_columns...]

Another scenario is with movies and directors. If a director gets soft-deleted, his movies should get soft-deleted as well. I was leaning toward writing some trigger SQL statements that set the "deleted_at" column of the corresponding child equal to the "deleted_at" column of the soft-deleted parent. It seems like, while maybe not ideal, this approach would involve less code complexity, no loading of relationships required, and at least all cascading (hard and soft) would be managed at the database level.

Any other recommendations based on the above?

michaeldyrynda's avatar

You may be missing the crucial point that soft deleting is managed entirely in code (forgive me if I'm wrong).

Whilst applying an onDelete('cascade') to a foreign key constraint will instruct the database to cascade the delete if the foreign relationship is removed, a soft delete is merely a column in the database that Laravel knows not to return by default. Using the SoftDeletes trait in your model will add a global scope that tacks a whereNull('deleted_at') onto each query for that model, unless you explicitly tell it to return the model withTrashed or onlyTrashed.

I suppose if you really did want to manage it at the database level, you could setup a trigger on the deleted_at column of the parent to then go and set the deleted_at on each child, but again, because soft deletes are managed at the application level, each time you add a new relationship, you have to alter the behaviour of the trigger to account for this. Add to this the fact that perhaps not all database engines support triggers, or the syntax for them is different, you're likely going to cause yourself more issues in the long run if you forget to update a trigger.

Keep in mind that in code, you're only writing the event listener once and only ever touch that if you need to add another delete relationship.

One last thought with your movie credits example; if you delete an actor from your database, they were still in that movie, regardless of whether or not you're still tracking them in your application as active.

Again, if the referential integrity is critical to your application and you don't want to address the issue in code, perhaps you could leave deletes at the database level with cascading and write to an audit log when something was deleted or delete from the pivot tables when you soft delete the parent record.

1 like
ianspangler's avatar

Thanks, I am going to try doing this in the application code as you suggested and see how it goes.

lesterm's avatar

I found this package to be very simple and straight forward

https://github.com/Askedio/laravel5-soft-cascade

Essentially: Install with composer: composer require askedio/laravel5-soft-cascade

Register the service provider in your config/app.php: Askedio\SoftCascade\Providers\GenericServiceProvider::class,

In your Model enable the trait and define $softCascade: use \Askedio\SoftCascade\Traits\SoftCascadeTrait; protected $softCascade = ['profiles'];

Usage: User::first()->delete(); User::withTrashed()->first()->restore();

Please or to participate in this conversation.