Laravel: Implementing a Shopping Cart for Your Website
– Reading time: 3'
Introduction
In this article we will show how to implement a simple Store page and a Shopping Cart page for your Laravel E-commerce website.
The shopping cart will allow users to choose products to buy and make a payment via the PayPal payment gateway.
PayPal is the world’s most popular system for processing payments on e-commerce websites.
Creating Database Tables
Before reading this article, we recommend to refer to our previous post Laravel: Using Pagination, Sorting and Filtering with Your Tables where we described how to implement a simple product catalog. In this article we will use the Product and Category models from the previous post and we assume that you have several products and categories in your database.
Now let’s add two new database tables: orders and order_items. The orders table will contain orders the user makes in the Store. The order_items table will contain items (products) for the given order.
Create a migration file database/migrations/2021_05_29_151804_create_order_tables.php and put the following code into it:
<?php
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateOrderTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned();
$table->decimal('amount');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::create('order_items', function (Blueprint $table) {
$table->increments('id');
$table->bigInteger('order_id')->unsigned();
$table->bigInteger('product_id')->unsigned();
$table->tinyInteger('quantity');
$table->decimal('amount');
$table->timestamps();
$table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order_items');
Schema::dropIfExists('orders');
}
}
Next, apply the migration to your database:
php artisan migrate
Finally, create the Order
and OrderItem
model classes inside of your app directory:
<?php
// app/Order.php
namespace App;
use IlluminateDatabaseEloquentModel;
class Order extends Model
{
protected $table = 'orders';
}
<?php
// app/OrderItem.php
namespace App;
use IlluminateDatabaseEloquentModel;
class OrderItem extends Model
{
protected $table = 'order_items';
}
Implementing Simple Store
Now we are going to implement a simple Store page where the user will be able to choose products for purchase and add them into their Shopping Cart.
Create the StoreController
class inside of your app/Http/Controllers directory:
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use AppProduct;
class StoreController extends Controller
{
public function index()
{
$products = Product::get();
$cart = session()->get('cart');
if ($cart == null)
$cart = [];
return view('store.index')->with('products', $products)->with('cart', $cart);
}
public function addToCart(Request $request)
{
session()->put('cart', $request->post('cart'));
return response()->json([
'status' => 'added'
]);
}
}
The StoreController
class has two actions:
-
the
index
action method selects all products from the database. It also extracts the cart items from session (if the user already added some items into their Shopping Cart. - the
addToCart
action is an AJAX method which updates the Shopping Cart session variable when the user adds a product to the cart
Let’s now implement the view template for the index action. It is inspired by the Album example from Bootstrap 4 examples collection.
app/resources/views/cart/index.blade.php:
@extends('layout')
@push('head')
<script>
</script>
@endpush
@section('content')
<br>
<div class="float-right">
<a class="btn btn-success" href="{{ action('CartController@index') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cart" viewBox="0 0 16 16">
<path d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .491.592l-1.5 8A.5.5 0 0 1 13 12H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM3.102 4l1.313 7h8.17l1.313-7H3.102zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
<span id="items-in-cart">0</span> items in cart
</a>
</div>
<h1>Store</h1>
@if ($products->count() == 0)
<tr>
<td colspan="5">No products to display.</td>
</tr>
@endif
<?php $count = 0; ?>
@foreach ($products as $product)
@if ($count % 3 == 0)
<div class="row">
@endif
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" data-src="holder.js/100px225?theme=thumb&bg=55595c&fg=eceeef&text=Thumbnail" alt="Card image cap">
<div class="card-body">
<p class="card-text">{{ $product->category->name }} / {{$product->name}}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Only ${{$product->price}}</small>
<div class="btn-group">
<input type="number" value="1" min="1" max="100">
<button class="add-to-cart" type="button" class="btn btn-sm btn-outline-secondary"
data-id="{{$product->id}}" data-name="{{$product->name}}" data-price="{{$product->price}}">Add to Cart</button>
</div>
</div>
</div>
</div>
</div>
@if ($count % 3 == 2)
</div>
@endif
<?php $count++; ?>
@endforeach
@endsection
@section('footer-scripts')
<script>
$(document).ready(function() {
window.cart = <?php echo json_encode($cart) ?>;
updateCartButton();
$('.add-to-cart').on('click', function(event){
var cart = window.cart || [];
cart.push({'id':$(this)data('id'), 'name':$(this).data('name'), 'price':$(this).data('price'), 'qty':$(this).prev('input').val()});
window.cart = cart;
$.ajax('/store/add-to-cart', {
type: 'POST',
data: {"_token": "{{ csrf_token() }}", "cart":cart},
success: function (data, status, xhr) {
}
});
updateCartButton();
});
})
function updateCartButton() {
var count = 0;
window.cart.forEach(function (item, i) {
count += Number(item.qty);
});
$('#items-in-cart').html(count);
}
</script>
@endsection
In the view template above, we render the products we selected. We also implement some JavaScript event handlers, so when the user
clicks the Add to Cart button, the nadler updates the cart.
Next modify the layout template app/resources/views/layout.blade.php as follows:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="/docs/4.0/assets/img/favicons/favicon.ico">
<title>Store</title>
<!-- Bootstrap core CSS -->
<link href="/css/bootstrap.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4 class="text-white">About</h4>
<p class="text-muted">Add some information about the album below, the author, or any other background context. Make it a few sentences long so folks can pick up some informative tidbits. Then, link them off to some social networking sites or contact information.</p>
</div>
<div class="col-sm-4 offset-md-1 py-4">
<h4 class="text-white">Contact</h4>
<ul class="list-unstyled">
<li><a href="#" class="text-white">Follow on Twitter</a></li>
<li><a href="#" class="text-white">Like on Facebook</a></li>
<li><a href="#" class="text-white">Email me</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark box-shadow">
<div class="container d-flex justify-content-between">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
<strong>Store</strong>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
<main role="main" class="container">
@yield('content')
</main><!-- /.container -->
<footer class="text-muted">
<div class="container">
<p class="float-right">
<a href="#">Back to top</a>
</p>
</div>
</footer>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="/js/jquery.min.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.js"></script>
<script src="/js/holder.min.js"></script>
@yield('footer-scripts')
</body>
</html>
To make this layout work you need to download the jQuery and Bootstrap 4 CSS and JS files and put them into your public/css and public/js directories, respectively.
Finally add two routes into your app/routes/web.php file:
Route::get('store', 'StoreController@index');
Route::post('store/add-to-cart', 'StoreController@addToCart');
Now, if you open http://localhost/store URL in your web browser, you should be able to see the Store page:
Implementing Cart
Now lets implement the Shopping Cart page. Add the CartController
class into your app/Http/Controllers directory:
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
class CartController extends Controller
{
public function index()
{
$cart = session()->get('cart');
if ($cart == null)
$cart = [];
return view('cart.index')->with('cart', $cart);
}
}
Add the app/resources/views/cart/index.blade.php file for the view template:
@extends('layout')
@section('content')
<h1>Cart</h1>
<table class="table">
<thead class="thead-dark">
<tr>
<th scope="col">Product</th>
<th scope="col">Price</th>
<th scope="col">Qty</th>
</tr>
</thead>
<tbody>
<?php $total = 0; ?>
@foreach ($cart as $item)
<?php $total += $item['price'] * $item['qty']; ?>
<tr>
<td>{{ $item['name'] }}</td>
<td>${{ $item['price'] }}</td>
<td>{{ $item['qty'] }}</td>
</tr>
@endforeach
</tbody>
</table>
<p>
<strong>Total: ${{ $total}}</strong>
</p>
<p>
<a class="btn btn-primary btn-lg" href="/cart/pay-with-paypal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-wallet" viewBox="0 0 16 16">
<path d="M0 3a2 2 0 0 1 2-2h13.5a.5.5 0 0 1 0 1H15v2a1 1 0 0 1 1 1v8.5a1.5 1.5 0 0 1-1.5 1.5h-12A2.5 2.5 0 0 1 0 12.5V3zm1 1.732V12.5A1.5 1.5 0 0 0 2.5 14h12a.5.5 0 0 0 .5-.5V5H2a1.99 1.99 0 0 1-1-.268zM1 3a1 1 0 0 0 1 1h12V2H2a1 1 0 0 0-1 1z"/>
</svg>
Pay with PayPal</a>
</p>
@endsection
Add a route for the Cart page into your app/routes/web.php file:
Route::get('cart', 'CartController@index');
Now if you open the http://localhost/cart URL in your web browser, you should see the Shopping Cart page as follows:
PayPal Integration
To add PayPal support to your Shopping Cart, we will use a package called srmklive/paypal. Run the following command from your shell:
composer require srmklive/paypal ^1.0
Then add the provider to your config/app.php file:
'providers' => [
SrmklivePayPalProvidersPayPalServiceProvider::class
]
The following command will create config/paypal.php config file for you:
php artisan vendor:publish --provider "SrmklivePayPalProvidersPayPalServiceProvider"
The config file looks like below:
return [
'mode' => env('PAYPAL_MODE', 'sandbox')
'sandbox' => [
'username' => env('PAYPAL_SANDBOX_API_USERNAME', ''),
'password' => env('PAYPAL_SANDBOX_API_PASSWORD', ''),
'secret' => env('PAYPAL_SANDBOX_API_SECRET', ''),
'certificate' => env('PAYPAL_SANDBOX_API_CERTIFICATE', ''),
'app_id' => '<app_id>',
],
'live' => [
'username' => env('PAYPAL_LIVE_API_USERNAME', ''),
'password' => env('PAYPAL_LIVE_API_PASSWORD', ''),
'secret' => env('PAYPAL_LIVE_API_SECRET', ''),
'certificate' => env('PAYPAL_LIVE_API_CERTIFICATE', ''),
'app_id' => '',
],
'payment_action' => 'Sale',
'currency' => env('PAYPAL_CURRENCY', 'USD'),
'billing_type' => 'MerchantInitiatedBilling',
'notify_url' => '',
'locale' => '',
'validate_ssl' => false,
];
Add the following parameters to .env (you can take the API username, API password and API secret from your PayPal account settings page):
PAYPAL_MODE=sandbox
PAYPAL_SANDBOX_API_USERNAME=<your_username>
PAYPAL_SANDBOX_API_PASSWORD=<your_api_password>
PAYPAL_SANDBOX_API_SECRET=<your_api_secret>
PAYPAL_CURRENCY=USD
PAYPAL_SANDBOX_API_CERTIFICATE=
Then you need to modify the CartController
and add several actions to it:
// Add this line to the beginning
use SrmklivePayPalServicesExpressCheckout;
class CartController extends Controller
{
// ...
public function payWithPaypal()
{
$cart = session()->get('cart');
$totalAmount = 0;
foreach ($cart as $item) {
$totalAmount += $item['price'] * $item['qty'];
}
$order = new Order();
$order->user_id = Auth::user()->id;
$order->amount = $totalAmount;
$order->save();
$data = [];
foreach ($cart as $item) {
$data['items'] = [
[
'name' => $item['name'],
'price' => $item['price'],
'desc' => $item['name'],
'qty' => $item['qty'],
]
];
$orderItem = new OrderItem();
$orderItem->order_id = $order->id;
$orderItem->product_id = $item['id'];
$orderItem->quantity = $item['qty'];
$orderItem->amount = $item['price'];
$orderItem->save();
}
$data['invoice_id'] = $order->id;
$data['invoice_description'] = "Your Order #{$order->id}";
$data['return_url'] = route('paypal-success');
$data['cancel_url'] = route('paypal-cancel');
$data['total'] = $totalAmount;
$provider = new ExpressCheckout;
$response = $provider->setExpressCheckout($data);
$response = $provider->setExpressCheckout($data, true);
return redirect($response['paypal_link']);
}
public function paypalSuccess()
{
return 'Payment success!';
}
public function paypalCancel()
{
return 'Payment canceled!';
}
}
The action payWithPaypal
submits the order to PayPal. The paypalSuccess
and paypalCancel
actions
receive the payment status from PayPal.
Add the routes for the actions above:
Route::get('cart/pay-with-paypal', 'CartController@payWithPaypal');
Route::get('cart/paypal-success', 'CartController@paypalSuccess')->name('paypal-success');
Route::get('cart/paypal-cancel', 'CartController@paypalCancel')->name('paypal-cancel');
Now you can make payments via PayPal.
Conclusion
In this article, we’ve implemented a simple e-commerce Store page and a Shopping Cart page where the user can pay via the PayPal payment gateway. You can also consider using the module Laravel Cashier if you want to integrate with the Stripe payment gateway instead of PayPal.