Search code examples
phplaravelapilaravel-passportlaravel-api

Laravel API DB transaction with too long code. How do I modularize or optimize this code? (I am new to Laravel)


I have very complex functions in laravel APIs ( called from Mobile App). The sample function is given below. If someone can help to improve the code structure. I wish to know the best practices in handling multiple transactions in a single API. Because if I move each table's entry to separate function - how do I handle errors of each one? Ex: when the quantity is not set in $request model

SUMMARY: Can someone please break it into smaller functions? .... Because I don't know how/where to break it into smaller ones.


    public function placeOrder(Request $request)
    {
        $this->validate($request, [
            'addressId' => 'required'
        ]);
        $userId = Auth::user()->id;
        $itemList = array();
        $deliveryCharge = Store::first()->delivery_charge;
        // $deliveryCharge = Store::findOrFail($request->header('StoreId'))->delivery_charge;
        $total = $deliveryCharge;
        $cod = $request->cod;
        $useBalanceFirst = $request->useBalanceFirst;
        $addressId = $request->addressId;
        $list = $request->list;
        $cost = 0;
        // fetch all items using id in request
        foreach ($list as $orderItemFromRequest) {


            if (!isset($orderItemFromRequest['id']))
                return $this->error("Invalid item selected");

            $itemFromDB = Item::find($orderItemFromRequest['id']);

            if (!isset($orderItemFromRequest['quantity']))
                return $this->error("Please enter quantity for {$itemFromDB->name}");

            $qty = $orderItemFromRequest['quantity'];
            if ($qty > 0.0) {

                if ($itemFromDB == null || $itemFromDB->available == false)
                    return $this->error("Invalid item selected");

                $itemFromDB->quantity = $qty;
                $total += ($itemFromDB->sell_rate * $itemFromDB->quantity) / $itemFromDB->rate_unit_multiple;
                $cost += ($qty * $itemFromDB->purchase_rate) / $itemFromDB->rate_unit_multiple;
                array_push($itemList, $itemFromDB);
            }
        }
        if (count($itemList) == 0) {
            return $this->error("At least 1 item is needed to place the order");
        }
        // check user balance 
        $user = User::findOrFail($userId);
        // if balance < total -> throw error    
        if ($cod != true &&  $user->balance < $total) return $this->error("Insufficient Balance. Please recharge your wallet or use C.O.D.", 403);



        $order = new Order();
        $order->user_id = $user->id;
        $order->total = $total;
        $order->delivery_charge = $deliveryCharge;
        $order->purchase_rate = $cost;

        // because in-case user changes his/her name in future - the order should have the historical name & number
        $order->customer_name = $user->name;
        $order->customer_contact = $user->contact_no;
        $order->address_id = $addressId;

        if ($cod == true) {
            if ($useBalanceFirst == true) {
                if ($user->balance < $order->total)
                    $order->amount_due = $order->total - $user->balance;
                else
                    $order->amount_due = 0;
            } else {
                $order->amount_due = $order->total;
            }
        } else
            $order->amount_due = 0;

        $ordertransaction = new OrderTimeLine();
        $ordertransaction->created_by = Auth::user()->id;



        DB::transaction(function () use ($order, $itemList, $user, $ordertransaction) {
            // insert 1 entry into orders
            $order->save();
            $order->refresh();
            $newOrderItemList = array();
            // insert N entries for Items 
            foreach ($itemList as $requestItem) {
                $item   = new OrderItem();
                $item->order_id = $order->id;
                $item->item_id = $requestItem->id;
                $item->item_name = $requestItem->name;
                $item->sell_rate = $requestItem->sell_rate;
                $item->purchase_rate = $requestItem->purchase_rate;
                $item->quantity = $requestItem->quantity;
                $item->quantity_unit = $requestItem->unit;
                $item->rate_unit_multiple = $requestItem->rate_unit_multiple;

                if ($requestItem->remaining_stocks <= 0) {
                    $item->purchase_rate_qty = 0;
                } else if ($requestItem->remaining_stocks < $requestItem->quantity) {
                    $item->purchase_rate_qty = $requestItem->remaining_stocks;
                } else
                    $item->purchase_rate_qty = $requestItem->quantity;

                $item->save();
                $item->refresh();

                $requestItem->remaining_stocks = $requestItem->remaining_stocks - $requestItem->quantity;
                // update remaining QTY and cost price
                unset($requestItem->quantity);
                if ($requestItem->remaining_stocks <= $requestItem->alert_stocks) {
                    // send notification
                    NotificationController::notifyAdminAboutStocks($requestItem);
                }

                $requestItem->save();
                array_push($newOrderItemList, $item);
            }
            $order->items = $newOrderItemList;
            // deduct balance from user table
            $user->balance = $user->balance - $order->total + $order->amount_due;
            $user->save();
            $balanceAffected = 0 - $order->total + $order->amount_due;
            // enter transaction in wallet_transaction table   
            if ($balanceAffected != 0) {
                $transaction =  new WalletTransaction();
                $transaction->user_id = $user->id;
                $transaction->amount = $balanceAffected;
                $transaction->type = WALLET_ORDER_PLACED;
                $transaction->order_id = $order->id;
                $transaction->save();
            }

            $ordertransaction->status = $order->status;
            $ordertransaction->order_id = $order->id;
            $ordertransaction->save();
        });
        NotificationController::notifyAdminAboutOrder($user, $order);
        return $this->success(["balance" => $user->balance, "order" => $order]);
    }


Solution

  • You are handling too many work in the controller function. First of all I suggest to make full use of Laravel Validation functionality. then use Laravel Collection to make your code more readable and compact. I have refactored your function as how I usually handle.

    public function placeOrder(Request $request)
    {
       $data = $request->validate([
          'addressId' => 'required',
          'cod' => 'required|boolean',
          'useBalanceFirst' => 'required|boolean',
          'list' => 'required|array',
          'list.*.quantity' => 'required|numeric',
          'list.*.id' => ['required', 'numeric', Rule::exists('items')->where(function ($query) {
              $query->where('available', true);
          })],
      ]);
      $user = Auth::user();
      $orderItems = collect($data['list'])->map(function($listItem){
          $item = Item::find($listItem['id']);
          $orderItem = new OrderItem([
              'item_id' => $item->id,
              'item_name' => $item->name,
              'sell_rate' => $item->sell_rate,
              'purchase_rate' => $item->purchase_rate,
              'quantity' => $listItem['quantity'],
              'quantity_unit' => $item->quantity_unit,
              'rate_unit_multiple' => $item->rate_unit_multiple,
          ]);
          if ($item->remaining_stocks <= 0) {
              $orderItem->purchase_rate_qty = 0;
          } else if ($item->remaining_stocks < $orderItem->quantity) {
              $orderItem->purchase_rate_qty = $item->remaining_stocks;
          } else
              $orderItem->purchase_rate_qty = $orderItem->quantity;
    
          return $orderItem;
      });
      $total =  $orderItems->sum(function($item){
          return ($item->sell_rate * $item->quantity) / $item->rate_unit_multiple;
      });
      $cost =  $orderItems->sum(function($item){
          return ($item->quantity * $item->purchase_rate) / $item->rate_unit_multiple;
      });
    
    $deliveryCharge = Store::first()->delivery_charge;
      $total += $deliveryCharge;
    
      // if balance < total -> throw error
      if (!$data['cod'] && $user->balance < $total)
      {
          return $this->error("Insufficient Balance. Please recharge your wallet or use C.O.D.", 403);
      }
    
    $order = new Order([
          'user_id' => $user->id,
          'total' => $total,
          'delivery_charge' => $deliveryCharge,
          'purchase_rate' => $cost,
          // because in-case user changes his/her name in future - the order should have the historical name & number
          'customer_name' => $user->name,
          'customer_contact' => $user->contact_no,
          'address_id' => $data['addressId'],
      ]);
      if ($data['cod']) {
          if ($data['useBalanceFirst']) {
              if ($user->balance < $order->total)
                  $order->amount_due = $order->total - $user->balance;
              else
                  $order->amount_due = 0;
          } else {
              $order->amount_due = $order->total;
          }
      } else
      {
          $order->amount_due = 0;
      }
      DB::transaction(function () use ($order, $orderItems, $user) {
          // insert 1 entry into orders
          $order->save();
          $order->items->saveMany($orderItems);
          // deduct balance from user table
          $user->balance = $user->balance - $order->total + $order->amount_due;
          $user->save();
    
        $ordertransaction = OrderTimeLine::create([
              'created_by' => $user->id,
              'status' => $order->status,
              'order_id' => $order->id,
          ]);
      });
    
      return $this->success(["balance" => $user->balance, "order" => $order]);
    }
    

    As you have noticed I have removed notification and user wallet transaction code as these should be updated asynchronously with no impact on your user experience. I suggest you take a look at Laravel Events.