Sofia's avatar
Level 6

Can I have a model update itself?

Hello,

Let's say I have a model and I'd like to have a function on the model that updates several fields and calls to some other related models. Is there a best practice for grouping those things together that includes the model updating itself?

So a hypothetical example...

class Card extends Model 
{
   public function ban()
   {
	   $this->update([
            banned_at => Carbon::now();
	        ban_reason => "Hacked";
       ]);

       $this->user->notify("Your card has been banned");
	   
       //...other banning activity.
	}				
}

At first glance this looks pretty okay and I've seen it done this way before, but having the model update itself this way may inadvertently persist other unsaved changes. The $model->upate() method runs a save() under the hood.

I don't really want to go in the direction of having a dedicated class or "action" for this just for the sake of having someone else persist the update. Having it on the model seems like a simple and intuitive place at this stage.

Is there a way to get access to a model's query builder so that I can apply the update that way?

$this->query()->update([...]) would be nice, but that updates all rows (no primary key clause in the query).

Card::find($this->id)->query()->update([...]) is a bit weird...

What's the right way to do this? Or is having a model update itself a bad idea all around?

0 likes
2 replies
martinbean's avatar
Level 80

@Sofia Having “domain” methods on models like that is fine, and is something I do myself.

I think worrying about updating “dirty” fields is a non-issue, though. That’s literally how the Active Record pattern (which Eloquent is based on) works. Trying to avoid that behaviour would be removing the very fabric Eloquent is built on. It also doesn’t really make sense to “touch” attributes without saving them, and then call a method that does save the model and all of its dirty attributes:

// This code doesn't really make sense...
$card->foo = 'bar';
$card->bar = 'foo';
$card->qux = 'qux';

$card->ban();

I’ll also usually dispatch an event, rather than doing side effects in business logic methods like this:

class Card extends Model
{
    public function ban(): bool
    {
        $banned = $this->forceFill([
            'banned_at' => $this->freshTimestamp(),
            'banning_reason' => 'Hacked',
        ])->save();

        CardBanned::dispatchIf($banned, $this);

        return $banned;
    }
}

The CardBanned event can then have listeners that send notifications, do any clean-up actions, etc.

Sofia's avatar
Level 6

Thanks @martinbean!

That’s literally how the Active Record pattern (which Eloquent is based on) works. Trying to avoid that behaviour would be removing the very fabric Eloquent is built on.

Yes! But I also thought that having the model save itself was a no-go when using the Active Record pattern, probably for that reason. I will probably go for it anyway. You're right, the case of dirty fields being updated by accident would be unlikely.

I’ll also usually dispatch an event, rather than doing side effects in business logic methods

I agree with you on the event dispatching. In my case I actually have some operations that need to be atomic, so the model (self) update will be in a transaction with related model updates. But yes...the notification (like in my example) would be in a listener.

Please or to participate in this conversation.