216

When I delete a row using this syntax:

$user->delete();

Is there a way to attach a callback of sorts, so that it would e.g. do this automatically:

$this->photo()->delete();

Preferably inside the model-class.

kjones
  • 1,339
  • 1
  • 13
  • 28
Martti Laine
  • 12,655
  • 22
  • 68
  • 102

18 Answers18

299

I believe this is a perfect use-case for Eloquent events. You can use the "deleting" event to do the cleanup:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    public function photos()
    {
        return $this->has_many('Photo');
    }
    
    // this is a recommended way to declare event handlers
    protected static function booted () {
        static::deleting(function(User $user) { // before delete() method call this
             $user->photos()->delete();
             // do the rest of the cleanup...
        });
    }
}

You should probably also put the whole thing inside a transaction, to ensure the referential integrity..

miken32
  • 42,008
  • 16
  • 111
  • 154
ivanhoe
  • 4,593
  • 1
  • 23
  • 22
  • 9
    Note: I spend some time until I got this working. I needed to add `first()` into the query so I could access the *model-event* e.g. `User::where('id', '=', $id)->first()->delete();` [Source](http://laravel.io/forum/11-08-2014-eloquent-deleting-model-event-doesnt-fire#reply-16665) – Michel Ayres May 18 '15 at 12:25
  • 7
    @MichelAyres: yes, you need to call delete() on a model instance, not on Query Builder. Builder has it's own delete() method which basically just runs a DELETE sql query, so I presume it doesn't know anything about orm events... – ivanhoe Jun 08 '15 at 11:43
  • 3
    This is the way to go for soft-deletes. I believe the new / preferred Laravel way is to stick all of these in the AppServiceProvider's boot() method in this way: \App\User::deleting(function ($u) { $u->photos()->delete(); }); – Watercayman Jan 26 '17 at 19:13
  • 4
    Almost worked in Laravel 5.5, I had to add a `foreach($user->photos as $photo)`, then `$photo->delete()` to make sure each child had its children removed on all levels, instead of only one as it was happening for some reason. – George Oct 05 '17 at 13:45
  • I have a worry here. If user has no photos, it means `$user->photos()->delete();` will eventually lead to an error right cos it will be calling delete on a null value. Please, can someone help throw more light on this if am wrong and if am correct, it means a check; `if($user->photos() { $user->photos()->delete(); }` be done before chaining with the delete() method right? – FONGOH MARTIN Oct 06 '17 at 04:15
  • @FONGOHMARTIN There's no need to check if there are photos in this case. When you call it with parentheses like `$user->photos()` it doesn't do any action on DB right away, it just returns a QueryBuilder instance. Equivalent of $user->photos()->delete() is this SQL query: `DELETE FROM photos WHERE user_id = ?` and this will delete user's photos, if there are any. If not, it will just do nothing and return 0. – ivanhoe Oct 15 '17 at 18:42
  • @FONGOHMARTIN Additionally if you wish to check if there are records in the DB table you can't do it the way you tried. As I explained in the previous comment $user->photos() will return just the relation object, not the result. It is object and will always be true. You need to test it like `if ($user->photos()->exists())` or `if ($user->photos->isEmpty())`. Note the difference between photos() and photos in this 2 examples, first returns query builder's relation for further preparation,while the other runs the query and returns the result as a collection. – ivanhoe Oct 15 '17 at 18:47
  • That's nasty. That creates a hard coded dependency from photo to user. – Henry Jan 18 '18 at 22:02
  • Is it possible to return an success or error message? For instance, if the transaction was rolled back or something? – Adam Jan 24 '18 at 09:54
  • @Adam The above event handler code will be run by Eloquent on delete, so you can't return anything from there AFAIK, but if you put `$user->delete()` call in a try/catch block it will catch any errors in this event handler as well, because it's running as a part of that call. Then you can wrap that line in a transaction so that you can roll the whole delete thing back, and from there (it's usually in a controller or a repository) you can also return the status message as you wish. – ivanhoe Jan 26 '18 at 06:49
  • 21
    This doesn't cascade it further though. For example if `Photos` has `tags` and you do the same in `Photos` model (i.e. on `deleting` method: `$photo->tags()->delete();`) it never gets trigger. But if I make it a `for` loop and do something like `for($user->photos as $photo) { $photo->delete(); }` then the `tags` also get deleted! just FYI – supersan Jun 22 '18 at 08:08
  • 1
    Should `return $this->has_many('Photo');` instead be `return $this->hasMany('Photo');` (*hasMany* instead of *has_many*)? Laravel v.5.7 – Genki Mar 14 '19 at 12:16
  • Can you please provide a way how to "put the whole thing inside a transaction"? – Yevgeniy Afanasyev Sep 08 '20 at 06:41
  • 1
    @YevgeniyAfanasyev I believe DB events are run in a blocking mode, so it's enough when calling the delete() on model, to do it inside a transaction, say `DB::transaction(function () { $user->delete(); })`. – ivanhoe Sep 09 '20 at 12:32
254

You can actually set this up in your migrations:

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

Source: http://laravel.com/docs/5.1/migrations#foreign-key-constraints

You may also specify the desired action for the "on delete" and "on update" properties of the constraint:

$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
Chris Schmitz
  • 8,097
  • 6
  • 31
  • 41
  • 1
    Yeah, I guess I should have clarified that dependency. – Chris Schmitz Jan 06 '13 at 15:20
  • 85
    But not if you're using soft deletes, since the rows are not then really deleted. – tremby Feb 06 '14 at 00:43
  • @tremby is right, and often you will be using soft deletes. This is not a great approach IMO since it's not universal. – Trevor Gehman Sep 16 '15 at 19:54
  • 12
    Also - this will delete the record in the DB, but will not run your delete method, so if you are doing extra work on delete (for example - delete files), it will not run – amosmos Dec 22 '15 at 13:57
  • @amosmos, could you clarify what you'll mean by this statement. I'm just looking for the best approach to cascade delete related properties in a related table, do you mean if I delete in the controller thus: $users->delete(); it wouldn't delete the related roles in the other table? – ken4ward Dec 22 '15 at 14:43
  • Does this approach serves better? http://laravel-tricks.com/tricks/cascading-deletes-with-model-events – ken4ward Dec 22 '15 at 14:43
  • So, onDelete('cascade') will delete the photo's connected to the user when a record from table users is being deleted? – Joshua - Pendo Dec 22 '15 at 18:40
  • 13
    This approach relies on DB to do the cascade delete, but not all DBs support this, so extra care is required. For instance MySQL with MyISAM engine doesn't, nor any NoSQL DBs, SQLite in the default setup, etc. Additional problem is that artisan will not warn you about this when you run migrations, it will just not create foreign keys on MyISAM tables and when you later delete a record no cascade will happen. I had this problem once and believe me it's very hard to debug. – ivanhoe Sep 09 '16 at 09:46
  • 2
    @kehinde The approach shown by you does NOT invoke deletion events on the relations to-be-deleted. You should iterate over the relation and call delete individually. – Tom Dec 10 '16 at 10:22
  • This seems to be the cleanest option. If you get errors after adding the foreign key with the onDelete constraint, just add $table->engine = 'InnoDB'; to all migrations. – Jeffrey May 06 '17 at 21:57
  • FYI: This solution doesn't work if you're using softDelete in your models. – Thalles Portilho Aug 11 '23 at 17:11
64

Note: This answer was written for Laravel 3. Thus might or might not works well in more recent version of Laravel.

You can delete all related photos before actually deleting the user.

<?php

class User extends Eloquent
{

    public function photos()
    {
        return $this->has_many('Photo');
    }

    public function delete()
    {
        // delete all related photos 
        $this->photos()->delete();
        // as suggested by Dirk in comment,
        // it's an uglier alternative, but faster
        // Photo::where("user_id", $this->id)->delete()

        // delete the user
        return parent::delete();
    }
}

Hope it helps.

akhy
  • 5,760
  • 6
  • 39
  • 60
  • 1
    You have to use: foreach($this->photos as $photo) ($this->photos instead of $this->photos()) Otherwise, good tip! – Barryvdh Jan 08 '13 at 10:24
  • 22
    To make it more efficient, use one query: Photo::where("user_id", $this->id)->delete(); Not the nicest way, but only 1 query, *way* better performance if a user has 1.000.000 photo's. – Dirk Jun 02 '13 at 12:54
  • @Dirk: i think your way is a lot cleaner and also faster :D – akhy Jun 03 '13 at 08:17
  • 5
    actually you can call: $this->photos()->delete(); no need for loop – ivanhoe – ivanhoe Nov 21 '13 at 02:38
  • 5
    @ivanhoe I noticed that the deleting event will not fire in photo if you delete the collection, however, iterating through as akhyar suggests will cause the deleting event to fire. Is this a bug? – adamkrell Dec 11 '13 at 19:20
  • @Drinian Are you on Laravel 4? How about `$this->photos->delete()`? Have not tried it, but it might work – akhy Dec 14 '13 at 10:43
  • 1
    Yes, I'm on L4. Doesn't work. The deleting event doesn't fire. I tried it a couple of times. I haven't had time to comb through Laravel's code to find out why. – adamkrell Dec 14 '13 at 16:51
  • 1
    @akhyar Almost, you can do it with `$this->photos()->delete()`. The `photos()` returns the query builder object. – Sven van Zoelen Jan 30 '14 at 10:38
  • 1
    @adamkrell it happens in Laravel 5.4 as well. I believe the deleting event doesn't work with Eloquent Collections, so if you happen to have a deleting event in photo model, it wont be called, unless you iterate the collection. – Fermin Arellano Apr 01 '18 at 06:12
45

Relation in User model:

public function photos()
{
    return $this->hasMany('Photo');
}

Delete record and related:

$user = User::find($id);

// delete related   
$user->photos()->delete();

$user->delete();
Calin Blaga
  • 1,375
  • 12
  • 19
  • 6
    This works, but use care to use $user()->relation()->detach() if there is a piviot table involved (in case of hasMany / belongsToMany relations), or else you will delete the reference, not the relation. – James Bailey Jan 24 '19 at 14:19
  • This works for me laravel 6. @Calin can you explain more pls? – Arman H Feb 23 '20 at 06:54
35

There are 3 approaches to solving this:

1. Using Eloquent Events On Model Boot (ref: https://laravel.com/docs/5.7/eloquent#events)

class User extends Eloquent
{
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->delete();
        });
    }
}

2. Using Eloquent Event Observers (ref: https://laravel.com/docs/5.7/eloquent#observers)

In your AppServiceProvider, register the observer like so:

public function boot()
{
    User::observe(UserObserver::class);
}

Next, add an Observer class like so:

class UserObserver
{
    public function deleting(User $user)
    {
         $user->photos()->delete();
    }
}

3. Using Foreign Key Constraints (ref: https://laravel.com/docs/5.7/migrations#foreign-key-constraints)

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Paras
  • 9,258
  • 31
  • 55
  • 2
    I think that the 3 options is the most elegant since is building the constraint into the database itself. I test it and works just fine. – redeemefy Dec 08 '18 at 17:50
  • 1
    You better mention what's the differences, the first and second are great for only one change at a time, not batching and also when you using databases that don't support the cascade the third one work on multiple rows but not on all of the databases. – Steve Moretz Nov 19 '21 at 14:08
15

As of Laravel 5.2, the documentation states that these kinds of event handlers can be registered in the AppServiceProvider:

<?php
class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        User::deleting(function ($user) {
            $user->photos()->delete();
        });
    }

I even suppose to move them to separate classes instead of closures for better application structure.

miken32
  • 42,008
  • 16
  • 111
  • 154
Attila Fulop
  • 6,861
  • 2
  • 44
  • 50
  • 2
    Laravel 5.3 recommends putting them in separate classes called [Observers](https://laravel.com/docs/5.3/eloquent#observers) - while it's only documented in 5.3 though, the `Eloquent::observe()` method is available in 5.2 as well and can be used from the AppServiceProvider. – Leith Oct 28 '16 at 21:37
  • 3
    If you have any 'hasMany' relations _from_ your `photos()`, you'll also need to be careful - this process will *not* delete grandchildren because you're not loading models. You'll need to loop over `photos` (note, not `photos()`) and fire the `delete()` method on them as models in order to fire the delete-related events. – Leith Nov 11 '16 at 23:07
  • 2
    @Leith The observe Method is also available in 5.1. – Tyler Reed Feb 13 '17 at 22:36
13

It is better if you override the delete method for this. That way, you can incorporate DB transactions within the delete method itself. If you use the event way, you will have to cover your call of delete method with a DB transaction every time you call it.

In your User model.

public function delete()
{
    \DB::beginTransaction();

     $this
        ->photo()
        ->delete()
    ;

    $result = parent::delete();

    \DB::commit();

    return $result;
}
budhajeewa
  • 2,268
  • 1
  • 17
  • 19
Ranga Lakshitha
  • 286
  • 2
  • 14
9

Using Constrained()

After Laravel 7, new foreignId() and constrained() methods are available for defining relationship constraint in database. OnDelete() method can be used on these methods to automatically delete related records.

Old style

$table->unsignedBigInterer('user_id');

$table->foreign('user_id')
    ->references('id')
    ->on('users')
    ->onDelete('cascade');

New style

$table->foreignId('user_id')
      ->constrained()
      ->onDelete('cascade');
SEYED BABAK ASHRAFI
  • 4,093
  • 4
  • 22
  • 32
  • This is assuming that photo and user has a 1:1 relationship. Not applicable to 0:1 relationship. – Siwei Feb 16 '23 at 22:11
6

To elaborate on the selected answer, if your relationships also have child relationships that must be deleted, you have to retrieve all child relationship records first, then call the delete() method so their delete events are fired properly as well.

You can do this easily with higher order messages.

class User extends Eloquent
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->get()->each->delete();
        });
    }
}

You can also improve performance by querying only the relationships ID column:

class User extends Eloquent
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->get(['id'])->each->delete();
        });
    }
}
Steve Bauman
  • 8,165
  • 7
  • 40
  • 56
4
  1. Add delete function on model that you want to delete

  2. Define relations of models

for example in this instance:

/**
 * @return bool|null
 */
public function delete(): ?bool
{
    $this->profile()->delete();
    $this->userInterests()->delete();
    $this->userActivities()->delete();
    $this->lastLocation()->delete();

    return parent::delete();
}

And relations in user model are:

public function profile()
{
    return $this->hasOne(Profile::class, 'user_id', 'id');
}

public function userInterests()
{
    return $this->hasMany(userInterest::class, 'user_id', 'id');
}

public function userActivities()
{
    return $this->hasMany(userActivity::class, 'user_id', 'id');
}

public function lastLocation()
{
    return $this->hasOne(LastLocation::class, 'user_id', 'id');
}
jinglebird
  • 558
  • 1
  • 5
  • 15
3

I would iterate through the collection detaching everything before deleting the object itself.

here's an example:

try {
        $user = User::findOrFail($id);
        if ($user->has('photos')) {
            foreach ($user->photos as $photo) {

                $user->photos()->detach($photo);
            }
        }
        $user->delete();
        return 'User deleted';
    } catch (Exception $e) {
        dd($e);
    }

I know it is not automatic but it is very simple.

Another simple approach would be to provide the model with a method. Like this:

public function detach(){
       try {
            
            if ($this->has('photos')) {
                foreach ($this->photos as $photo) {
    
                    $this->photos()->detach($photo);
                }
            }
           
        } catch (Exception $e) {
            dd($e);
        }
}

Then you can simply call this where you need:

$user->detach();
$user->delete();
agm1984
  • 15,500
  • 6
  • 89
  • 113
2

This way worked for me on Laravel 8:

public static function boot() {

    parent::boot();
    
    static::deleted(function($item){
        $item->deleted_by = \Auth::id(); // to know who delete item, you can delete this row
        $item->save();  // to know who delete item, you can delete this row
        foreach ($item->photos as $photo){
            $photo->delete();
        }
    });
}

public function photos()
{
    return $this->hasMany('App\Models\Photos');
}

Note: deleting in this syntax $user->photos()->delete(); not worked for me...

AnasSafi
  • 5,353
  • 1
  • 35
  • 38
0

In my case it was pretty simple because my database tables are InnoDB with foreign keys with Cascade on Delete.

So in this case if your photos table contains a foreign key reference for the user than all you have to do is to delete the hotel and the cleanup will be done by the Data Base, the data base will delete all the photos records from the data base.

Alex
  • 91
  • 1
  • 9
  • As has been noted in other Answers, cascading deletions at the database layer will not work when using Soft Deletes. Buyer beware. :) – Ben Johnson Sep 05 '18 at 14:13
0

It’s better to use onDelete cascade when defining your model’s migration. This takes care of deleting the model’s relations for you:

e.g.

 $table->foreign(’user_id’)
  ->references(’id’)->on(’users’)
  ->onDelete(’cascade’);

If you happen to find yourself thinking about how to delete a model and its relations to a level greater than 3 or 4 nested relations, then you should consider redefining your model's relationships.

Leena Patel
  • 2,423
  • 1
  • 14
  • 28
0

Here are the perfect solutions.

# model

public function order_item_properties()
{
    return $this->hasMany(OrderItemProperty::class, 'order_id', 'id');
}

public function order_variations()
{
    return $this->hasMany(OrderItemVariation::class, 'order_id', 'id');
}

# controller

$order_item = OrderItem::find($request->order_id);

$order_item->order_item_properties()->delete();
$order_item->order_variations()->delete();

$order_item->delete();

return response()->json([
    'message' => 'Deleted',
]);
Pri Nce
  • 576
  • 6
  • 18
0
$table->foreignId('user_id')->constrained('user')->cascadeOnDelete();

or

$table->foreignId('user_id')->constrained()->cascadeOnDelete();
Rajib Bin Alam
  • 353
  • 1
  • 4
  • 16
-1

Or you can do this if you wanted, just another option:

try {
    DB::connection()->pdo->beginTransaction();

    $photos = Photo::where('user_id', '=', $user_id)->delete(); // Delete all photos for user
    $user = Geofence::where('id', '=', $user_id)->delete(); // Delete users

    DB::connection()->pdo->commit();

}catch(\Laravel\Database\Exception $e) {
    DB::connection()->pdo->rollBack();
    Log::exception($e);
}

Note if you are not using the default laravel db connection then you need to do the following:

DB::connection('connection_name')->pdo->beginTransaction();
DB::connection('connection_name')->pdo->commit();
DB::connection('connection_name')->pdo->rollBack();
-1

You can use this method as an alternative.

What will happen is that we take all the tables associated with the users table and delete the related data using looping

$tables = DB::select("
    SELECT
        TABLE_NAME,
        COLUMN_NAME,
        CONSTRAINT_NAME,
        REFERENCED_TABLE_NAME,
        REFERENCED_COLUMN_NAME
    FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
    WHERE REFERENCED_TABLE_NAME = 'users'
");

foreach($tables as $table){
    $table_name =  $table->TABLE_NAME;
    $column_name = $table->COLUMN_NAME;

    DB::delete("delete from $table_name where $column_name = ?", [$id]);
}
Daanzel
  • 987
  • 7
  • 6
  • I don't think all these queries are necessary since eloquent orm can handle this if you specify it clearly. –  May 09 '19 at 08:52