How to soft delete related records when soft deleting a parent record in Laravel?

22,495

Solution 1

Eloquent doesn't provide automated deletion of related objects, therefore you'll need to write some code yourself. Luckily, it's pretty simple.

Eloquent models fire different events in different stages of model's life-cycle like creating, created, deleting, deleted etc. - you can read more about it here: http://laravel.com/docs/5.1/eloquent#events. What you need is a listener that will run when deleted event is fired - this listener should then delete all related objects.

You can register model listeners in your model's boot() method. The listener should iterate through all payments for the invoice being deleted and should delete them one by one. Bulk delete won't work here as it would execute SQL query directly bypassing model events.

This will do the trick:

class MyModel extends Model {
  protected static function boot() {
    parent::boot();

    static::deleted(function ($invoice) {
      $invoice->payments()->delete();
    });
  }
}

Solution 2

You can go one of 2 ways with this.

The simplest way would be to override Eloquents delete() method and include the related models as well e.g.:

public function delete()
{
    $this->payments()->delete();
    return parent::delete();
} 

The above method should work just find but it seems a little bit dirty and I'd say it's not the preferred method within the community.

The cleaner way (IMO) would be to tap into Eloquents events e.g.:

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

    static::deleting(function($invoice) { 
         $invoice->payments()->delete();

    });
}

Either (but not both) of the above methods would go in your Invoice model. Also, I'm assuming that you have your relationships set up in your model, however, I'm not sure if you allow multiple payments for one invoice. Either way you might need to change the payments() in the examples to whatever you've named the relationship in your invoice model.

Hope this helps!

Solution 3

I know you asked this question a long time ago but I found this package to be very simple and straightforward.

Or you can use this package it's useful too.

Remember to install the right version depending on your laravel version.

You must install it via composer:

 composer require askedio/laravel5-soft-cascade ^version

In second package:

 composer require iatstuti/laravel-cascade-soft-deletes

Register the service provider in your config/app.php.

you can read the docs on the GitHub page.

If you delete a record this package recognizes all of its children and soft-delete them as well.

If you have another relationship in your child model use its trait in that model as well. its so much easier than doing it manually.

The second package has the benefit of deleting grandchildren of the model. in some cases, I say its a better approach.

Solution 4

If the relationship of your database does not go any further than only one layer, then you could simply use Laravel events to handle your soft-deletes within the Model boot() method as follow:

<?php
//...

    protected static boot() {
        parent::boot();

        static::deleting(function($invoice) { 
             $invoice->payments()->delete();

        }); 
    }

If, however, your structure goes deeper than only one layer, you will have to tweak that piece of code.

Let's say for example you don't want to remove the payments of an invoice but rather the whole payment history of a given user.

<?php

// ...

class Invoice extends Model
{
    // ...

    /**
     * Holds the methods names of Eloquent Relations
     * to fall on delete cascade or on restoring
     * 
     * @var array
     */
    protected static $relations_to_cascade = ['payments']; 

    protected static boot()
    {
        parent::boot();

        static::deleting(function($resource) {
            foreach (static::$relations_to_cascade as $relation) {
                foreach ($resource->{$relation}()->get() as $item) {
                    $item->delete();
                }
            }
        });

        static::restoring(function($resource) {
            foreach (static::$relations_to_cascade as $relation) {
                foreach ($resource->{$relation}()->get() as $item) {
                    $item->withTrashed()->restore();
                }
            }
        });
    }

    public function payments()
    {
        return $this->hasMany(Payment::class);
    }
}

<?php

// ...

class User extends Model
{
    // ...

    /**
     * Holds the methods names of Eloquent Relations 
     * to fall on delete cascade or on restoring
     * 
     * @var array
     */
    protected static $relations_to_cascade = ['invoices']; 

    protected static boot()
    {
        parent::boot();

        static::deleting(function($resource) {
            foreach (static::$relations_to_cascade as $relation) {
                foreach ($resource->{$relation}()->get() as $item) {
                    $item->delete();
                }
            }
        });

        static::restoring(function($resource) {
            foreach (static::$relations_to_cascade as $relation) {
                foreach ($resource->{$relation}()->get() as $item) {
                    $item->withTrashed()->restore();
                }
            }
        });
    }

    public function invoices()
    {
        return $this->hasMany(Invoice::class);
    }
}

This paradigm ensures Laravel to follow the rabbit hole no matter how deep it goes.

Share:
22,495

Related videos on Youtube

user3407278
Author by

user3407278

Updated on January 01, 2020

Comments

  • user3407278
    user3407278 over 4 years

    I have this invoices table that which has the following structure

    id | name | amount | deleted_at
    2    iMac   1500   | NULL
    

    and a payments table with the following structure

    id | invoice_id | amount | deleted_at
    2    2            1000   | NULL
    

    Invoice Model

    class Invoice extends Model {
    
        use SoftDeletes;
    
    }
    

    here's the code to delete the invoice

    public function cance(Request $request,$id)
    {
        $record = Invoice::findOrFail($id);
        $record->delete();
        return response()->json([
            'success' => 'OK',
        ]);
    }
    

    Payments model

    class Payment extends Model {
    
        use SoftDeletes;
    
    }
    

    The softDelete on Invoice table works perfectly but its related records (payments) still exists.How do I delete them using softDelete?

  • jedrzej.kurylo
    jedrzej.kurylo over 8 years
    Calling delete() on payments relation directly will bypass the model and won't trigger the SoftDelete on related models. They will be removed from the database. In order to make thek soft-deleted you need to call delete() on each of related models.
  • jedrzej.kurylo
    jedrzej.kurylo over 8 years
    You are also missing the return statement in your overridden delete method - it should do "return parent::delete();", otherwise you lose the value that would be returned from delete() if you hadn't overwritten it.
  • user3407278
    user3407278 over 8 years
    Doesn't work! FatalErrorException in Invoice.php line 18: Cannot make static method Illuminate\Database\Eloquent\Model::boot() non static in class App\Models\Invoice
  • jedrzej.kurylo
    jedrzej.kurylo over 8 years
    Fixed, the function was missing the static modifier
  • user3407278
    user3407278 over 8 years
    That worked beautifully! Thanks a lot! Does this cause any performance issues when soft deleting say about 100 records?
  • jedrzej.kurylo
    jedrzej.kurylo over 8 years
    You have to fetch all of them and then save each of them, so it's additional 101 queries... In such case you could set the deleted_at manually for related models, this will be less clean but will run only one SQL query. I'll update the answer in a second
  • Rwd
    Rwd over 8 years
    @jedrzej.kurylo, I've just tested to make sure and YES you can use soft delete on a relationship!
  • jedrzej.kurylo
    jedrzej.kurylo over 8 years
    True, just checked the code. Good to know for the future :)
  • Tomonso Ejang
    Tomonso Ejang almost 6 years
    does it guarantee transaction @RossWilson
  • Rwd
    Rwd almost 6 years
    @TomonsoEjang What do you mean sorry? As in will it actually delete them? Or will it work if wrapped in a transaction?
  • Tomonso Ejang
    Tomonso Ejang almost 6 years
    @RossWilson on deleting the event of a model. Can it happen that the related record is deleted and the original model to be deleted failed to be deleted (here deleted means updated the deleted_at to some timestamp)
  • Rwd
    Rwd almost 6 years
    @ TomonsoEjang If you wrap the call in a transaction it should still behave the same way. There isn't any asynchronous behaviour so it will still start the transaction, run the code in the models and then end the transaction...if there is an issue then the transaction will be rolled back.
  • stetim94
    stetim94 over 4 years
    I know this is old, but i would really recommend to use of observers to achieve this.
  • hassanrazadev
    hassanrazadev about 4 years
    I used second package and it worked great for delete() but can I make it work for restore also?
  • Edinaldo Ribeiro
    Edinaldo Ribeiro over 3 years
    Worked like a charm!