t0berius's avatar

laravel observer not udpating value

Updated my application yesterday to laravel8 now facing this problem:

public function updating(User $user){

    //check if EXP amount was updated
    if($user->isDirty('exp')){

        //increased
        if($user->exp > $user->getOriginal('exp')){
            $nextLevel = $user->currentLevel->nextLevel();

            while(!is_null($nextLevel) && $user->exp >= $nextLevel->experience_required) {
                $user->current_level = $nextLevel->id;

                $nextLevelID = $nextLevel->id + 1;
                $nextLevel = UserLevel::where('id', $nextLevelID)->first();
            }
        }

}

Users can earn EXP and gain access to new levels. In the old laravel7 version I've used a trait to make sure the increment() method will trigger the observer. As stated in the new laravel8 release the increment() function will trigger the model events too.

The event itself is triggered correctly, but for some reason it seems like the line

                $user->current_level = $nextLevel->id;

will not be written into the database. Any idea how to fix this? I'm a bit clueless, because the code itself is working fine, using a simple Log::info() shows the correct level is assigned too.

0 likes
18 replies
Snapey's avatar

is nextlevel to be written to a different model?

I dont see anywhere that you persist something to the database?

t0berius's avatar

I don't understand the question in detail. Using

$user->current_level = $nextLevel->id;

should set the updated value and because of I've used the updating event, the manipulated value should be written to database?!

nextLevelis just queried to make sure the user isn't in need of to be assigned a new level (for example when he earns that much EXP he would go up 2 levels, for this I use the while loop).

Snapey's avatar

if the update is to the current model, then you should use $this

$this->current_level = $nextLevel->id;

if its a different model then apply the new value and call save()

t0berius's avatar

@snapey

says exp not defined.

public function updating(User $user){
    Log::info($this->exp);
}

As stated above, using $user->exp works, but I cannot update the attributes like for example using:

$user->current_level = 5;

inside the updating observer doesn't save the new value to the database.

Snapey's avatar

what model is this observer bound to?

t0berius's avatar

Bump. @snapey to the user model. exp and current_level are attributes of.

Using:

public function updating(User $user){

    $user->current_level = 5;

    dd($user);

shows the updated model too, but it's not written into database.

Snapey's avatar

looks ok to me. Sorry, cannot see anything wrong.

t0berius's avatar

@snapey

Any idea how to debug, when I use my old trait it works fine.

use App\Traits\IncrementDecrement;

<?php

namespace App\Traits;

trait IncrementDecrement
{

    /*
        custom trait to overrite the increment / decrement methods to make sure they trigger the "updated" event (so using an observer is still possible)
        currently used on user model for controlling user EXP updates
    */

    public function increment($column, $amount = 1, array $extra = [])
    {
        $this->$column = $this->$column + $amount;

        $this->save();
    }

    public function decrement($column, $amount = 1, array $extra = [])
    {
        $this->$column = $this->$column - $amount;

        $this->save();
    }
}

When I remove the trait from the User model and update the exp field the observer is triggered, but current_level is not updated.

My UserObserver <?php

namespace App\Observers;

use App\User;
use App\UserLevel;
use DB;

class UserObserver
{
    public function updating(User $user){

        //check if EXP amount was updated
        if($user->isDirty('exp')){
            //increased
            if($user->exp > $user->getOriginal('exp')){
                $nextLevel = $user->currentLevel->nextLevel();

                while(!is_null($nextLevel) && $user->exp >= $nextLevel->experience_required) {
                    $user->current_level = $nextLevel->id;
                    $nextLevel = UserLevel::whereId($nextLevel->id + 1)->first();
                }
            }
        }
    }

}

Update is triggered simply by

    $user = User::find(2)->increment('exp', 5000);
Snapey's avatar

I'm not sure if we are supposed to change model content during the updating event, but changing to saving will work ok for you I think.

I did some tests and changes to the model during updating were ignored.

t0berius's avatar

@snapey

Any idea why it's working when I incude the custom trait?

After reading the upgrade notices for laravel8 I was sure I can exclude my own trait and events are being triggered.

Snapey's avatar

because you are directly modifying the model

use saving instead of updating. Im sure it will work ok

t0berius's avatar

@snapey

Sorry for my late reply. Updated from observer event from updating to saving the event will never be triggered (used Log::info() to verify), when using:

    $user = User::find(2);
    $user->increment('exp', 5000);

Any idea how to get the automatic level upgrade done? Seems like using increment() will not trigger the saving event, manipulating the value from inside the updating event (which is triggered by increment() is not possible too!?

Snapey's avatar

What I found

increment() does call the updating event, but not the saving event

Changes to the model during the updating event are not affecting the stored data (as though the updating function is only receiving a copy of the model)

I cannot find any articles or forum responses relating to changing the content of the model during the updating event, which seems to be a bit counter-intuitive to what would be expected.

See https://github.com/laravel/framework/issues/32567

The 'special case' around increment and decrement is that they are supposed to be atomic so that the column can be read and incremented by the database in a single transaction. As such, its not like a typical model change.

1 like
t0berius's avatar

@snapey

thanks! Any suggestion how you would proceed to make sure levels are updated accordingly when updating the EXP? Using the custom trait was one way.

Snapey's avatar

I would probably put it all in a model function. Event listeners are good for side effects but this seems to be core to the business logic behind increasing points?

Please or to participate in this conversation.