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

pmusa's avatar
Level 2

restoring() and restored() Model events

I want to restore comments (model is Comment) attached to a post (model is Post) after the post is restored.

This fails :

public function restored(Post $post)
{
    $post->comments()
        ->onlyTrashed()->where('deleted_at', '>=', $post->deleted_at)
        ->get()
        ->each(function ($comment) {
            $comment->restore();
        });
}

This works:

public function restoring(Post $post)
{
    $post->comments()
        ->onlyTrashed()->where('deleted_at', '>=', $post->deleted_at)
        ->get()
        ->each(function ($comment) {
            $comment->restore();
        });
}

Difference is: restoring() instead of restored().

The following condition : where('deleted_at', '>=', $post->deleted_at) is here because I do not want to restore comments that were soft-deleted before the post was deleted. In other words, it's because I do not want to restore comments that were soft-deleted by moderators. I want to restore the comments that were soft-deleted the very moment I soft-deleted the post.

Reason why it fails: I believe it fails with restored() because $post->deleted_at becomes null so I cannot use it in my where() condition anymore.

Question: how do I hold the previous value of $post->deleted_at, right before it was restored? I tried playing with getDirty() and getChanges() but these did not help, they hold no track of previous value in the observer.

Thank you for your help.

0 likes
10 replies
Nakov's avatar
Nakov
Best Answer
Level 73

@pmusa I believe you can save a temp variable yourself using the restoring event first and then use the same one in the restored even though I don't know what is wrong by using the restoring if it already works?

So in your observer add this:

private $deletedAt = null;

public function restoring(Post $post)
{
    $this->deletedAt = $post->deleted_at;

    ... 
}

// and use it here:

public function restored(Post $post)
{
    $post->comments()
        ->onlyTrashed()->where('deleted_at', '>=', $this->deletedAt)
        ->get()
        ->each(function ($comment) {
            $comment->restore();
        });    
}

I tried the same using this bit of code in the model and it worked fine for me:

private static $cache = null;

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

    static::restoring(function ($model) {
        static::$cache = $model->deleted_at;
    });

    static::restored(function ($model) {
        dd(static::$cache);
    });
}
pmusa's avatar
Level 2

hehe, I tried that already. Without any success. It worked for you because you only restore a single row. Try with a collection of 3 comments to restore and you will see it fails. You will notice only your first row will be restored. 2 others will remain. And it will throw an error, because once the method finishes restoring your first row (first comment), your $this->deletedAt now equals null, so all other models (the 2 other comments) in your collection won't be restored.

Nakov's avatar

@pmusa I don't get it. This observer is on the Post not on the Comment so it gets there once. And no matter how many I try to delete, as long as I have access to the value within the restored method then I can use it within it as many times I want. It does not call the restored listener each time a comment has been deleted. But each time a Post has been deleted. So you cannot restore a Post twice, it gets restored once.

Otherwise that means that the date is wrong, which means not all 3 comments have the exact date of when the Post was deleted.

You can debug how many comments this returns in the restored method:

dd($post->comments()
        ->onlyTrashed()->where('deleted_at', '>=', $this->deletedAt)
        ->get());

Then you'll know if your query works.

pmusa's avatar
Level 2

Your query returns an error because $this->deletedAt is null.

Here are some tests :

    public function restoring(Post $post)
    {
        $this->deletedAt = $post->deleted_at;
        dd($this->deletedAt);

    }

This will return an instance of Carbon with a good date (not null). Great so far!

And then :

public function restored(Post $post)
{
    dd($this->deletedAt);
    /*dd($post->comments()
        ->onlyTrashed()//->where('deleted_at', '>=', $this->deletedAt)
        ->get());*/
}

this will return null. Why though? Also, if I run dd($post); I can see all the references to the column deleted_at are null. Since $this->deletedAt references object of $post I believe it also becomes null at some point.

Here is my test btw :

    public function test_it_can_restore_its_comments_when_restored_post()
    {
        $post = app(PostFactory::class)
            ->withComments(3)
            ->create();
        $this->assertEquals(1, Post::count());
        $this->assertEquals(3, Comment::count());

        $post->delete();
        $this->assertEquals(0, Post::count());
        $this->assertEquals(1, Post::onlyTrashed()->count());
        $this->assertEquals(0, Comment::count());
        $this->assertEquals(3, Comment::onlyTrashed()->count());

        $post->restore();
        $this->assertEquals(1, Post::count());
        $this->assertEquals(3, Comment::count());

        $this->assertEquals(0, Post::onlyTrashed()->count());
        $this->assertEquals(0, Comment::onlyTrashed()->count());
    }

it fails near the end at this line : $this->assertEquals(3, Comment::count()); with InvalidArgumentException: Illegal operator and value combination because you can't perform >= on a nullhere : ->where('deleted_at', '>=', $this->deletedAt)

Nakov's avatar

It returns null because it is invoked on a new instance of the observer. If you don't mind just try out the code that I've showed you above, overriding the boot method on the Post model instead and see if that works, as it did for me. So you can take next steps from there.

pmusa's avatar
Level 2

calling a static variable did the trick indeed, plus your explanation was on point about restoring() and restored() being obviously called on 2 separate instances, which explained the empty var. I don't feel very comfortable calling a static var though. Is there any downside to this? Will it cause me headaches later on? Most importantly: What if I happen to restore 2 posts at the same time?

Nakov's avatar

@pmusa No it won't hurt even later on, plus two posts will be two separate instances of a Post model it won't be called on one. Having static variable must be because the boot method is static, and you cannot call a reference variable from within a static method.

pmusa's avatar
Level 2

"Having static variable must be because the boot method is static, and you cannot call a reference variable from within a static method." Right, but I'm using an observer. Like this :

private static $deleted_at = null;

public function deleting(Post $post)
{
    $post->comments
        ->each->delete();
}

public function restoring(Post $post)
{
    static::$deleted_at = $post->deleted_at;
}

public function restored(Post $post)
{
    $post->comments()
        ->onlyTrashed()->where('deleted_at', '>=', static::$deleted_at)
        ->get()
        ->each->restore();
}

Let's say I run a foreach() loop that is supposed to restore 2 posts. Isn't there any risk, since the timing is very close (milliseconds) that the deleted_at static var of first post is swapped with deleted_at static var of the second post? which would result in restoring comments with inaccurate time in here : ->where('deleted_at', '>=', static::$deleted_at)

so far that seems to work (all tests green!) but i need to make sure before i go forward.

Nakov's avatar

@pmusa The events are fired from within an instance of the model my friend so again it will be a different observer. But to make sure 💯 of course is to make a test for it.

So for the next Post it will again go through the same process, first the restoring is called which will set the variable to the new date, then restored will use the new state.

And it cannot get mixed as this is procedural way, it cannot get mixed in the middle of processing.

pmusa's avatar
Level 2

Got it. I'll sure write some tests for that particular scenario, just in case. Thank you very much @nakov , you have been of great help! all tests green, i can have a sleep now.

Please or to participate in this conversation.