How to Implement a Shopping Cart for Guest Users

Implement a Shopping Cart for Guest Users

So you want to implement a shopping cart for guest users? If you have ever developed an application that includes a “shopping cart” feature, you have surely been faced with the question: How should I handle shopping carts for users who are not logged in to accounts. This can be a puzzling question. It is not efficient to store these carts in the database, as each guest user would create a new entry, quickly leading to a huge number of records. Instead, storing guest users’ carts within their browser session presents a viable and secure alternative. I will demonstrate the concept in the context of a Laravel ecommerce application, though the principles can be applied to any framework.

How it Works

Whenever a visitor to a site makes a request to the server, one thing that gets sent along with the request is the session cookie. This cookie is an identifier that corresponds to a set of session data stored on the server. The data in the session is defined by the programmer, and can contain information about the user, previous actions taken, or, in this case, information about what is in the user’s cart.

Step 1: Creating the Session Cart

To start, we need some sort of clean interface between the session data, and the logic of our application. This can take the form of a class called SessionCart. This class will need to be string-serializable (in order to be stored in the session), it will need to have a way to store and distinguish between different products, and we will need a way to get the total price of all products in the cart. With that in mind, here is our SessionCart implementation:

class SessionCart implements \Serializable{
    public $items;
    private $next_id = 1;

    //create the session cart
    public function __construct($_items){
        $this->items = collect($_items);
    }
    
    //add an item to the session cart
    public function add_item($item){
        $item->id = $this->next_id;
        $this->items->push($item);
        $this->next_id += 1;
    }

    //get the price of all items in the session cart
    public function price(){
        return $this->items->map(function($item){
            return $item->price();
        })->sum();
    }

    //serialize the cart for storage in the session
    public function serialize(){
        return serialize([
            $this->items,
            $this->next_id
        ]);
    }

    //deserialize the cart for use on server
    public function unserialize($data){
        list(
            $this->items,
            $this->next_id
        ) = unserialize($data);
    }
}

Step 2: Add Item Route Handler

Looks simple enough, right? To fully understand this class, we need to understand how it will be passed around by our application. Essentially, we begin in one of our route handlers by creating an empty session cart class. There will be nothing in the $items array at the beginning, and the empty session cart will be serialized into the user’s session. Then, let’s say the user adds a product to their cart. Along with the data submitted by the user back to our web application about the item added to the cart, we will receive the user’s session again. Then, in our route handler, all we need to do is deserialize it, call add_item on it while passing in an instance of the ORM model (IMPORTANT: the model must also be serializable, or it will not work!), reserialize it, and overwrite the session cart in the session with the updated instance. Below is a route handler that uses the SessionCart class above to store a new item in the cart for a guest user.

public function add_to_cart(Request $request, $product_id){

        //validate input, check if the product exists
        $product = Product::findOrFail($product_id);

        //validate user input
        $this->validate($request, [
            'quantity' => 'required|integer|min:1|max:1000',
            'media_url' => 'required|string|max:255',
        ]);



        //create a order-item model instance
        $order_item = new OrderItem([
            'quantity' => $request->input('quantity'),
            'order_id' => NULL,
            'product_id' => $product_id,
            'cart_id' => NULL,
            'media_url' => $request->input('media_url')
        ]);

        //get the cart from the session (automatic unserialize call)
        //second arg is default value if no session cart is found in the session
        $cart = session()->get('cart', new SessionCart([]));

        //add the item to the cart
        $cart->add_item($order_item);
        session()->put('cart', $cart);
        Session::save();
        
        
        //flash success
        session()->flash('message', 'Item added to cart!');

        //redirect user back where they came from or to cart
        $redirect_route = "cart";
        if ($request->input('redirect') != NULL){
            $redirect_route = $request->input('redirect');
        }
        return redirect()->route($redirect_route);
    }

Step 3: Remove Item Route Handler

Similar logic can be implemented for removing an item from the cart. The order of things is the same… first, unserialize the cart to an instance of the SessionCart class. Then, pop the instance of the model you want to remove from the SessionCart array holding cart items. Then, reserialize the cart, and insert it back into the session, to overwrite the old data in the session cart. A route handler for doing that can be seen below.

public function remove_from_cart(Request $request, $order_item_id){


        //get the cart from the session (unserialize happens automatically)
        //second arg is default value
        $cart = session()->get('cart', new SessionCart([]));  

        //delete the item from the session cart array
        $cart->items = $cart->items->where('id', '<>', $order_item_id);

        //put the updated SessionCart instance back into the session
        session()->put('cart', $cart);
        Session::save();
        
        //notify user of success and redirect
        session()->flash('message', 'Item deleted successfully!');
        return redirect()->route('cart');
    }

Step 4: Converting the Cart to an Order

So, we have a way to add items to and remove them from the guest’s cart. Now, we just need to convert them into an order for our online shop. We will want to do this after the guest makes a payment, and the process is pretty simple. The steps we will need to take are to first create an order in our database, then loop over our guest’s cart’s items, and attach them to the order by setting their foreign key, “order_id”. Then, it is a simple matter of actually saving the items from the guest cart to the database, since they are now officially a part of an order, and not simply being stored in the session anymore. Take a look at my method of processing a guest checkout below:

public function process_checkout(Request $request){

        //validate input
        $this->validate($request, [
            'name' => 'required|string|max:255',
            'email' => 'required|email|max:255',
            'phone' => 'nullable|string|regex:/^\d{3}-\d{3}-\d{4}$/',
            'street_address' => 'required|string|max:255',
            'apt' => 'nullable|string|max:255',
            'city' => 'required|string|max:255',
            'state' => 'required|min:2|max:2',
            'zip_code' => 'required|string|regex:/^\d{5}$/'
        ]);

        //get cart
        //if they aren't logged in, show the session cart
        $cart = session()->get('cart', new SessionCart([]));    
        //^second arg is default value}

        //if cart is empty, cannot check out!
        if ($cart->price() < 1){
            session()->flash('message', 'Must buy at least one item!');
            return redirect()->route('shop');
        }

        //create a new order
        $order = new Order([
            'status' => Order::INITIATED,
            'shipping_address' => implode(' ', [
                $request->input('street_address'),
                $request->input('apt'),
                $request->input('city'),
                $request->input('state'),
                $request->input('zip_code'),
            ]),
            'billing_address' => implode(' ', [
                $request->input('street_address'),
                $request->input('apt'),
                $request->input('city'),
                $request->input('state'),
                $request->input('zip_code'),
            ]),
            'shipping_method' => 'default',
        ]);
        
        //attach logged in user to the order
        if (auth()->check()){
            $order->user_id = auth()->id();
        }

        //save order
        $order->save();

        //attach order items to the new order
        foreach($cart->items as $item){
            $item->order_id = $order->id;
            if (!auth()->check()){
                $item->id = null;
            }
            $item->save();
        }

        //charge the user
        //PAYMENT LOGIC WOULD GO HERE

        //advance the step of the order upon payment success
        $order->status = Order::CUSTOMER_MADE_PAYMENT;
        $order->paid_for = true;
        $order->save();

        //create payment record
        $record = new PaymentRecord();
        $record->price = $cart->price();
        $record->user_id = $request->user()->id;
        $record->billing_address = implode(' ', [
            $request->input('street_address'),
            $request->input('apt'),
            $request->input('city'),
            $request->input('state'),
            $request->input('zip_code'),
        ]);
        $record->order_id = $order->id;
        $record->save();

        //clear the user's cart
        if (auth()->check()){
            foreach ($cart->items as $item){
                $item->cart_id = null;
                $item->save();
            }
        }else{
            //if they aren't logged in, delete from session cart instead
            $cart = session()->get('cart', new SessionCart([]));

            //delete the item
            $cart->items = collect([]);    //empty the cart
            session()->put('cart', $cart);
            Session::save();    
            //save the session so that changes are reflected
             
        }
        
        //flash success and redirect
        session()->flash('message', 'Order placed!');
        return redirect()->route('cart');
    }

Notes

First of all, the add_to_cart and remove_from_cart functions are methods of a Laravel controller called CartController in my project. They are attached to route handlers like so:

//cart routes
Route::get('/add-to-cart/{product_id}', [CartController::class, 'add_to_cart'])->name('add_to_cart');

Route::get('/remove-from-cart/{item_id}', [CartController::class, 'remove_from_cart'])->name('remove_from_cart');

Likewise, the process_checkout function is a method of my CheckoutController, which handles checkout logic. The route handler attachment is below:

Route::post('/checkout', [CheckoutController::class, 'process_checkout'])->name('handle_checkout');

The important thing is to implement the core logic I demonstrated, regardless of whether you decide to use Laravel controllers or not.

It is critical that any objects you add to the SessionCart instance’s $items array be serializable to the format you are serializing you SessionCart instance to. This is because, when the SessionCart instance is serialized, it also automatically serializes each object contained within that SessionCart. So, if your object you are using to represent your cart items is not serializable, you will experience data loss between hitting different routes that interact with the SessionCart instance. In my project, I use Laravel Eloquent ORM model instances, which are serializable by default, which makes this easy for me.

Conclusion

So, we have seen a viable method for storing the carts for non-authenticate users on our web apps. Of course, I used Laravel to implement my app, and thus my code is tailored to Laravel, but the core principle of

  1. Create a serializable record of a cart item.
  2. Add the record to a serializable collection.
  3. Serialize the collection and store the collection in the session.
  4. Get the collection from the session and modify or read from it in other route handlers to modify its contents or create an order.

can be implemented in any server-side web framework with support for sessions.

I hope you have found this little demonstration helpful. If you have any questions, feel free to contact me using my contact page. Thank you, and happy coding!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *