26

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?

user3407278
  • 1,233
  • 5
  • 16
  • 32

5 Answers5

25

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();
    });
  }
}
Abhishek Sachan
  • 1,976
  • 2
  • 18
  • 33
jedrzej.kurylo
  • 39,591
  • 9
  • 98
  • 107
  • 1
    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 – user3407278 Aug 23 '15 at 07:07
  • Fixed, the function was missing the static modifier – jedrzej.kurylo Aug 23 '15 at 07:10
  • That worked beautifully! Thanks a lot! Does this cause any performance issues when soft deleting say about 100 records? – user3407278 Aug 23 '15 at 07:18
  • 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 – jedrzej.kurylo Aug 23 '15 at 07:20
  • I know this is old, but i would really recommend to use of observers to achieve this. – stetim94 Nov 11 '19 at 14:01
  • Worked like a charm! – LeMajstor Nov 27 '20 at 01:27
12

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!

Rwd
  • 34,180
  • 6
  • 64
  • 78
  • 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 Aug 23 '15 at 06:38
  • 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. – jedrzej.kurylo Aug 23 '15 at 06:54
  • 1
    @jedrzej.kurylo, I've just tested to make sure and YES you can use soft delete on a relationship! – Rwd Aug 23 '15 at 07:30
  • True, just checked the code. Good to know for the future :) – jedrzej.kurylo Aug 23 '15 at 07:43
  • does it guarantee transaction @RossWilson – Tomonso Ejang Jul 20 '18 at 08:07
  • @TomonsoEjang What do you mean sorry? As in will it actually delete them? Or will it work if wrapped in a transaction? – Rwd Jul 20 '18 at 08:54
  • @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) – Tomonso Ejang Jul 20 '18 at 11:46
  • @ 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. – Rwd Jul 20 '18 at 14:06
7

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.

Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
Salar Bahador
  • 1,433
  • 1
  • 14
  • 26
2

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.

Waiyl Karim
  • 2,810
  • 21
  • 25
0

You can use Model Observers as well:

php artisan make:Observer InvoiceOberser --model=Invoice

It will create a new file in /app/Observers/InvoiceObserver.php with the following methods:

  • created
  • updated
  • deleted
  • restored
  • forceDeleted

You just need to update the deleted method to this:

public function deleted(Invoice $invoice)
{
    $invoice->payments()->delete();
}

And finally in /app/Providers/EventServiceProvider.php add this lines:

// On the top
use App\Models\Invoice;
use App\Observers\InvoiceObserver;

// On boot method
Invoice::observe(InvoiceObserver::class);