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

SimonAngatia's avatar

Model::decrement() and Model::increment() not working efficiently

Hey guys, I am getting data from a request and I have created a model observer to do some computations on the data after it has been saved. The computations are like incrementing the user balance based on the amount that came in the request. The shocking thing however is that this is working for some instances and not others, even though the amount is received and saved in the database. What could be happening?

public function created(XpgTransaction $xpgTransaction)
    {   $userId = $xpgTransaction->user_id;
        $user = User::where('id', $userId)->first();
        $userBalance = $user->casino_bakiye;
        if($xpgTransaction->type =="debit"){
            $debitBalance = $xpgTransaction->amount;
            $user->decrement('casino_bakiye', $debitBalance);
        }
        if($xpgTransaction->type =="credit"){
            $creditBalance = $xpgTransaction->amount;
            $user->increment('casino_bakiye', $creditBalance);
        }

    }
0 likes
45 replies
Sinnbeck's avatar

Can you show the code you are using for storing the amounts as well? If you store it in multiple places, please show them all.

laracoft's avatar
laracoft
Best Answer
Level 27

You probably have what is known as a race condition. Consider hiring. :) I also can't tell if you are using database transactions which is crucial when it comes to movement of monetary amounts.

SimonAngatia's avatar
public function Debit(){
      $data = json_decode(file_get_contents('php://input'), true);
      $username = $data['Login'];
      $user = User::where('username', $username)->first();
      if(!$user){
        $rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
        return response()->json($rivalaoResponse);
      }

      $userId = $user->id;
      $userBalance = $user->casino_bakiye;
      $operator = $data['OperatorId'];
      $session = $data['Session'];
      $gameId = $data['GameId'];
      $roundId = $data['RoundId'];
      $sequence = $data['Sequence'];
      $amount = $data['Amount'];
      $debitDetails =$data['DebitDetails'];
      if($sequence){
        $game = XpgTransaction::where('gameId', $gameId)
                              ->where('roundId', $roundId)
                              ->where('sequence', $sequence)
                              ->first();
        if(!$game){
          
          $trxn = new XpgTransaction;
          $trxn->user_id = $userId;
          $trxn->operatorId = $operator;
          $trxn->session = $session;
          $trxn->gameId =$gameId;
          $trxn->roundId =$roundId;
          $trxn->sequence = $sequence;
          $trxn->type = "debit";
          $trxn->debitDetails = $debitDetails;
          $trxn ->request=json_encode($data);
          $trxn->amount = $amount;
          $trxn->save();
            $updatedBalance = $user->fresh()->casino_bakiye;
            if($updatedBalance !=null){
              $rivalaoResponse = ["d"=>["Data"=>[(float)$updatedBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
              return response()->json($rivalaoResponse);
            } 
            $rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"Invalid balance returned"]];
            return response()->json($rivalaoResponse); 
        
        }
        return response()->json($game);
        $rivalaoResponse = ["d"=>["ErrorCode"=>-20,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
        return response()->json($rivalaoResponse);
      }

SimonAngatia's avatar

What I am doing is that after the amount being saved in the database, it should increment or decrement the user balance in the user's model. Something that is working for some but not others

laracoft's avatar

Can add headers to your table please? My brain can only store 10 variables at a time. :D

laracoft's avatar

Why is it $user->casino_bakiye and not $user->casino_balance? Can do add a $user->refresh()?

...
$user->refresh();
$userId = $user->id;
$userBalance = $user->casino_bakiye;
$operator = $data['OperatorId'];
$session = $data['Session'];
$gameId = $data['GameId'];
$roundId = $data['RoundId'];
$sequence = $data['Sequence'];
$amount = $data['Amount'];
SimonAngatia's avatar

casino_bakiye is on users table, casino_balance on xpgtransactions table

SimonAngatia's avatar
 public function Debit(){
      $data = json_decode(file_get_contents('php://input'), true);
      $username = $data['Login'];
      $user = User::where('username', $username)->first();
      if(!$user){
        $rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
        return response()->json($rivalaoResponse);
      }
      $type="debit";
      $userId = $user->id;
      $amount = $data['Amount'];
      $gameId = $data['GameId'];
      $roundId = $data['RoundId'];
      $sequence = $data['Sequence'];
      if($sequence){
        $game = XpgTransaction::where('gameId', $gameId)
                              ->where('roundId', $roundId)
                              ->where('sequence', $sequence)
                              ->first();
        if(!$game){
          $trxn = new XpgTransaction;
          $trxn->user_id = $userId;
          $trxn ->request=json_encode($data);
          $trxn->type =$type;
          $trxn->casino_balance = (float)$user->casino_bakiye;


          $currentBalance = decrement_user_balance($user->id, $amount);
          $rivalaoResponse = ["d"=>["Data"=>[$currentBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
          
          try {
            return  response()->json($rivalaoResponse);
             } finally {
            $trxn->save();
        }
         
        }
        $rivalaoResponse = ["d"=>["ErrorCode"=>-20,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
        return response()->json($rivalaoResponse);
      }
      
      

    }

SimonAngatia's avatar
<?php
use App\User;

if (! function_exists('decrement_user_balance')) {
    function decrement_user_balance($userId, $amount)
    {
       $user = User::find($userId);
       $user->decrement('casino_bakiye', $amount);
       
       return $user->casino_bakiye;
    }
}

The above is the helpers.php code

laracoft's avatar

@simonangatia where does decrement_user_balance() come from?

I think it is easier that you understand the concept of the problem before we go any further

You are trying to move money, which is:

  1. Decrease from source
  2. Increase at destination

The BIG question is, is 1 and 2 guaranteed to be atomic, i.e. 1 single operation? Please google if you don't understand atomic.

Are you using database transactions (please google also) to guarantee nothing else will happen between 1 and 2?

Try and figure out what happens below:

  1. Source is 10
  2. X reads source and intend to minus 1 to it, i.e. 9
  3. Y reads source and intend to minus 1 to it, i.e. 9 //Problem, Y should not read until X has written
  4. X writes 9 to source
  5. Y writes 9 to source

At step 5, is source value 9 correct? No. After X and Y, it should be 8. But it is 9 because 3 happened before 4. And you are left wondering why Y did not decrement. That is the problem you are facing, also known as a race condition (Once again, please google).

SimonAngatia's avatar

Thank you for helping but allow me to explain more. The problem is not the balance being accidentally decreased or increased. The decreasing and increasing is working efficiently. I have two tables in question:

  1. Users table which contains user's balance column(casino_bakiye) 2.Xpgtransactions table: Where I save transactions coming from the API.

I also have a route /debit where debit requests are sent. In the request, there is amount and username among the data sent.

So what I am required to do is

  1. Get the user from the users table with the username that comes with the request
  2. Get that user's balance 3.decrement that balance

4.return a response with that user's current balance. 5.save the transaction to the Xpgtransactions table

For example, if the current user's balance is 1000 , then the request comes with amount as 100, I am to decrement that 1000 by 100 and return the resulting amount in the response. Meaning that 1000 - 100 = 900 So I have to return 900, thats after decrementing that user's balance by 100. Then save the transaction amount to xpgtransactions table.

THE PROBLEM:

Now the problem is that with a few transactions, the decrement logic doesn't happen. It just but saves the transaction to the Xpgtransactions table. So I wonder how it skips decrementing the amount and saves the data to xpgtransactions table.

So in this case, if the current user amount is 1000 and the request comes with 100, I need to decrement 1000 by 100: 1000 - 100 = 900 now in this case it returns 1000 instead of 900, it skips decrementing logic.

So that is mu concern

laracoft's avatar

@simonangatia look at my earlier example steps 1 to 5, can you match Y to your problem and see that it skips the decrementing logic?

SimonAngatia's avatar

Now the question is, what do I do to make it work in sequence? So that data shouldn't be saved in the database before the balance is decremented? I have try if-else statements but didn't work

SimonAngatia's avatar

Thank you for suggesting db transactions. Let me read about it and see if that will help

SimonAngatia's avatar

Brother, I have tried the transactions but the same error happens. I am even shocked. Though I am rolling back the requests but the API doesn't required that. So I must get the all the data saved. No provision for rolling back

public function Debit(){
      $data = json_decode(file_get_contents('php://input'), true);
      $username = $data['Login'];
      $user = User::where('username', $username)->first();
      if(!$user){
        $rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
        return response()->json($rivalaoResponse);
      }
      $type="debit";
      $userId = $user->id;
      $amount = $data['Amount'];
      $gameId = $data['GameId'];
      $roundId = $data['RoundId'];
      $sequence = $data['Sequence'];
      
        $game = XpgTransaction::where('gameId', $gameId)
                              ->where('roundId', $roundId)
                              ->where('sequence', $sequence)
                              ->first();

      DB::beginTransaction();
        try {
          if(!$game){
            $trxn = new XpgTransaction;
            $trxn->user_id = $userId;
            $trxn ->request=json_encode($data);
            $trxn->type =$type;
            $trxn->save();
           if($user->decrement('casino_bakiye', $amount)){
            $trxn->casino_balance = $user->refresh()->casino_bakiye;
            $trxn->save();

            DB::commit();
            $currentBalance = $user->refresh()->casino_bakiye;
            $rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
            return  response()->json($rivalaoResponse); 
           }else{
            
            $currentBalance = $user->refresh()->casino_bakiye;
            $rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>-1,"HasErrors"=>true,"Message"=>"Unknown error"]];
            return  response()->json($rivalaoResponse);
            DB::rollBack();
           }
           
          }else{
          $rivalaoResponse = ["d"=>["ErrorCode"=>-20,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
          return response()->json($rivalaoResponse);
          }

        } catch (\Exception $e) {
          $currentBalance = $user->refresh()->casino_bakiye;
          $rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>-1,"HasErrors"=>true,"Message"=>"Unknown error"]];
          return  response()->json($rivalaoResponse); 
            DB::rollBack();
        }


laracoft's avatar

@simonangatia as mentioned right at my first post, race condition is not an easy issue, there are quite a number of ways to handle them, it is better you understand the issue at hand and formulate the logic yourself.

I will share pseudo code with you.

function Debit()
{
    // 1. Source is 10
    DB::beginTransaction();

    try {
        // 2. X reads source and intend to minus 1 to it, i.e. 9
        // 4. X writes 9 to source

        // all good
        DB::commit();
    } catch (\Exception $e) {
        // something went wrong
        DB::rollback();
        // you must write logic to perform 2 and 4 again, maybe use a for loop or something
        // or if the API allows for it, return an error ask it to call our Debit() again later
    }
}

This way, if Y comes along and tries to call Debit() before X has commit(), an exception will be thrown.

SimonAngatia's avatar

I figured out and tried a while loop, It is working well on my side but the integration team is telling me that it is returning duplicate data

public function Debit(){
      $data = json_decode(file_get_contents('php://input'), true);
      $username = $data['Login'];
      $user = User::where('username', $username)->first();
      if(!$user){
        $rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
        return response()->json($rivalaoResponse);
      }
      $type="debit";
      $userId = $user->id;
      $amount = $data['Amount'];
      $gameId = $data['GameId'];
      $roundId = $data['RoundId'];
      $sequence = $data['Sequence'];

      $initialBalance = $user->casino_bakiye;
      
        $game = XpgTransaction::where('gameId', $gameId)
                              ->where('roundId', $roundId)
                              ->where('sequence', $sequence)
                              ->first();

      DB::beginTransaction();
        try {
          if(!$game){
            $trxn = new XpgTransaction;
            $trxn->user_id = $userId;
            $trxn ->request=json_encode($data);
            $trxn->type =$type;
            $trxn->save();

            do {
              $user->decrement('casino_bakiye', $amount);
              $trxn->casino_balance = $user->refresh()->casino_bakiye;
              $trxn->save();
  
              DB::commit();
            } while ($user->refresh()->casino_bakiye ==$initialBalance);

            $currentBalance = $user->refresh()->casino_bakiye;
            $rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
            return  response()->json($rivalaoResponse); 
           
          }else{
          $rivalaoResponse = ["d"=>["ErrorCode"=>-21,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
          return response()->json($rivalaoResponse);
          }

        } catch (\Exception $e) {
          $rivalaoResponse = ["d"=>["ErrorCode"=>-1,"HasErrors"=>true,"Message"=>"Unknown error"]];
          return  response()->json($rivalaoResponse); 
        }



    }
SimonAngatia's avatar

Everything is working well on my side and all the data is saved and balance decremented. But they are telling me it is returning duplicate data

Snapey's avatar

As per your other question. Transactions are not relevant here. You need to lock the table from update by other requests whilst you do what you need to do.

Transactions are for rolling back to a known state if an error occurs whilst updating multiple tables.

SimonAngatia's avatar

I have googled but haven't gotten good explanation for laravel

Next

Please or to participate in this conversation.