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.

10 Answers 正确答案

up vote143down voteaccepted

I believe this is a perfect use-case for Eloquent events (http://laravel.com/docs/eloquent#model-events). You can use the "deleting" event to do the cleanup:


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

    // this is a recommended way to declare event handlers
    public static function boot() {
        parent::boot();

        static::deleting(function($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..

  • 4
    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 – Michel Ayres May 18 '15 at 12:25 
  • 4
    @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 8 '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(); }); – WatercaymanJan 26 '17 at 19:13
  • 3
    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 5 '17 at 13:45
  • 2
    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 at 8:08 

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');
  • 1
    This requires foreign keys in use, right? – Martti Laine Jan 6 '13 at 10:38
  • 44
    But not if you're using soft deletes, since the rows are not then really deleted. – tremby Feb 6 '14 at 0:43
  • 4
    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
  • 7
    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 9 '16 at 9:46
  • 1
    @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 

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.

  • 1
    You have to use: foreach($this->photos as $photo) ($this->photos instead of $this->photos()) Otherwise, good tip! – Barryvdh Jan 8 '13 at 10:24 
  • 18
    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 2 '13 at 12:54 
  • 5
    actually you can call: $this->photos()->delete(); no need for loop – ivanhoe – ivanhoe Nov 21 '13 at 2:38
  • 3
    @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
  • 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

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();

As of Laravel 5.2, the documentation states that these kinds of event handlers should 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.

  • 1
    Laravel 5.3 recommends putting them in separate classes called 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. – LeithOct 28 '16 at 21:37
  • 2
    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
  • @Leith The observe Method is also available in 5.1. – Tyler Reed Feb 13 '17 at 22:36

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');

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.

  • 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 5 at 14:13

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();

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();

yeah, but as @supersan stated upper in a comment, if you delete() on a QueryBuilder, the model event will not be fired, because we are not loading the model itself, then calling delete() on that model.

The events are fired only if we use the delete function on a Model Instance.

So, this beeing said:

if user->hasMany(post)
and if post->hasMany(tags)

in order to delete the post tags when deleting the user, we would have to iterate over $user->posts and calling $post->delete()

foreach($user->posts as $post) { $post->delete(); } -> this will fire the deleting event on Post

VS

$user->posts()->delete() -> this will not fire the deleting event on post because we do not actually load the Post Model (we only run a SQL like: DELETE * from posts where user_id = $user->id and thus, the Post model is not even loaded)

来自 https://stackoverflow.com/questions/14174070/automatically-deleting-related-rows-in-laravel-eloquent...