Laravel 4 E-Commerce

A Comprehensive Tutorial

Christopher Pitt
Laravel 4 Tutorials

--

Introduction

One of the benchmarks of any framework is how well it fares in the creation of an e-commerce application. Laravel 4 is up to the challenge; and today we’re going to create an online shop.

While much effort has been spent in the pursuit of accuracy; there’s a good chance you could stumble across a curly quote in a code listing, or some other egregious errata. Please make a note and I will fix where needed.

I have also uploaded this code to Github. You need simply follow the configuration instructions in this tutorial, after downloading the source code, and the application should run fine.

This assumes, of course, that you know how to do that sort of thing. If not; this shouldn’t be the first place you learn about making PHP applications.

https://github.com/formativ/tutorial-laravel-4-e-commerce

If you spot differences between this tutorial and that source code, please raise it here or as a GitHub issue. Your help is greatly appreciated.

Note On Sanity

There is no way that an e-commerce platform, built in 30 minutes, can be production-ready. Please do not attempt to conduct any real business on the basis of this tutorial; without having taken the necessary precautions.

This tutorial is a guide, an introduction, a learning tool. It is not meant to be the last word in building e-commerce platforms. I do not want to hear about how all your customers want to sue you because you slapped your name and logo on the GitHub source-code and left all reason behind.

Getting Started

In this tutorial; we will create a number of database objects, which will later be made available through API endpoints. We’ll then use these, together with AngularJS, to create an online shop. We’ll finish things off with an overview of creating and emailing PDF documents.

You need to understand a bit of JavaScript for this tutorial. There’s too much functionality to also cover the basics, so you should learn them elsewhere.

Installing Laravel 4

Laravel 4 uses Composer to manage its dependencies. You can install Composer by following the instructions at http://getcomposer.org/doc/00-intro.md#installation-nix.

Once you have Composer working, make a new directory or navigation to an existing directory and install Laravel 4 with the following command:

composer create-project laravel/laravel ./ --prefer-dist

If you chose not to install Composer globally (though you really should), then the command you use should resemble the following:

php composer.phar create-project laravel/laravel ./ --prefer-dist

Both of these commands will start the process of installing Laravel 4. There are many dependencies to be sourced and downloaded; so this process may take some time to finish.

Installing Other Dependencies

Our application will do loads of things, so we’ll need to install a few dependencies to lighten the workload.

AngularJS

AngularJS is an open-source JavaScript framework, maintained by Google, that assists with running single-page applications. Its goal is to augment browser-based applications with model–view–controller capability, in an effort to make both development and testing easier.

Angular allows us to create a set of interconnected components for things like product listings, shopping carts and payment pages. That’s not all it can do; but that’s all we’re going to do with it (for now).

To get started, all we need to know is how to link the AngularJS library to our document:

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular.js"></script>

If this seems too easy to be enough, fret not. AngularJS includes no stylesheets or any other resources. It’s purely a JavaScript framework, so that script is all you need. If you prefer to keep scripts loading from your local machine, just download the contents of the file at the end of that src attribute.

Bootstrap

Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.

Bootstrap has become somewhat of a standard in modern applications. It’s often used as a CSS reset, a wire-framing tool and even as the baseline for all application CSS. We’re going to use it to neaten up our simple HTML.

It’s available for linking (as we did with AngularJS):

<link type="text/css" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css" />
<script type="text/javascript" src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>

You can also download it, and serve it directly from the public folder. As it contains CSS and fonts; be sure to update all paths to the relevant images and/or fonts that come bundled with it.

DOMPDF

At its heart, dompdf is (mostly) CSS 2.1 compliant HTML layout and rendering engine written in PHP. It is a style-driven renderer: it will download and read external stylesheets, inline style tags, and the style attributes of individual HTML elements. It also supports most presentational HTML attributes.

DOMPDF essentially takes HTML documents and converts them into PDF files. If you’ve ever had to produce PDF files programatically (at least in PHP) then this library should be like the singing of angels to your ears. It really is epic.

To install this guy; we need to add a composer dependency:

"require" : {
"dompdf/dompdf" : "dev-master"
}

This was extracted from composer.json.

Obviously you’re going to have other dependencies in your composer.json file (especially when working on Laravel 4 projects), so just make it play nice with the other dependencies already in there.

Stripe

Stripe is a simple, developer-friendly way to accept payments online. We believe that enabling transactions on the web is a problem rooted in code, not finance, and we want to help put more websites in business.

We’re going to look at accepting payments with Stripe. It’s superior to the complicated payment processes of other services, like PayPal.

Installation is similar to DOMPDF:

"require" : {
"stripe/stripe-php" : "dev-master"
}

This was extracted from composer.json.

Faker

Faker is a PHP library that generates fake data for you. Whether you need to bootstrap your database, create good-looking XML documents, fill-in your persistence to stress test it, or anonymize data taken from a production service, Faker is for you.

We’re going to use Faker for populating our database tables (through seeders) so we have a fresh set of data to use each time we migrate our database objects.

To install Faker; add another Composer dependency:

"require" : {
"fzaninotto/faker" : "dev-master"
}

This was extracted from composer.json.

Remember to make this dependency behave nicely with the others in composer.json. A simple composer update should take us the rest of the way to being able to use DOMPDF, Stripe and Faker in our application.

Creating Database Objects

For our online shop; we’re going to need categories for products to be sorted into, products and accounts. We’ll also need orders and order items, to track which items have been sold.

Creating Migrations

We listed five migrations, which we need to create. Fire up a terminal window and use the following command to create them:

php artisan migrate:make CreateCategoryTable

You can repeat this process five times, or you can use the first as a copy-and-paste template for the remaining four migrations.

After a bit of modification; I have the following migrations:

<?phpuse Illuminate\Database\Migrations\Migration;class CreateAccountTable
extends Migration
{
public function up()
{
Schema::create("account", function($table)
{
$table->engine = "InnoDB";

$table->increments("id");
$table->string("email");
$table->string("password");
$table->dateTime("created_at");
$table->dateTime("updated_at");
$table->dateTime("deleted_at");
});
}
public function down()
{
Schema::dropIfExists("account");
}
}

This file should be saved as app/database/migrations/nnnn_nn_nn
_nnnnnn_CreateAccountTable.php
.

If you’re wondering why I am creating the timestamp fields explicitly, instead of using $table->timestamps() and $table->softDeletes(); it’s because I prefer to know what the field names are. I would rather depend on these three statements than the two magic methods. Perhaps the field names will be configurable in future. Perhaps it will burn me. For now I’m declaring them.

<?phpuse Illuminate\Database\Migrations\Migration;class CreateCategoryTable
extends Migration
{
public function up()
{
Schema::create("category", function($table)
{
$table->engine = "InnoDB";

$table->increments("id");
$table->string("name");
$table->dateTime("created_at");
$table->dateTime("updated_at");
$table->dateTime("deleted_at");
});
}
public function down()
{
Schema::dropIfExists("category");
}
}

This file should be saved as app/database/migrations/nnnn_nn_nn
_nnnnnn_CreateCategoryTable.php
.

<?phpuse Illuminate\Database\Migrations\Migration;class CreateOrderItemTable
extends Migration
{
public function up()
{
Schema::create("order_item", function($table)
{
$table->engine = "InnoDB";

$table->increments("id");
$table->integer("order_id");
$table->integer("product_id");
$table->integer("quantity");
$table->float("price");
$table->dateTime("created_at");
$table->dateTime("updated_at");
$table->dateTime("deleted_at");
});
}
public function down()
{
Schema::dropIfExists("order_item");
}
}

This file should be saved as app/database/migrations/nnnn_nn_nn
_nnnnnn_CreateOrderItemTable.php
.

We add another price field for each order item because so that changes in product pricing don’t affect orders that have already been placed.

<?phpuse Illuminate\Database\Migrations\Migration;class CreateOrderTable
extends Migration
{
public function up()
{
Schema::create("order", function($table)
{
$table->engine = "InnoDB";

$table->increments("id");
$table->integer("account_id");
$table->dateTime("created_at");
$table->dateTime("updated_at");
$table->dateTime("deleted_at");
});
}
public function down()
{
Schema::dropIfExists("order");
}
}

This file should be saved as app/database/migrations/nnnn_nn_nn
_nnnnnn_CreateOrderTable.php
.

<?phpuse Illuminate\Database\Migrations\Migration;class CreateProductTable
extends Migration
{
public function up()
{
Schema::create("product", function($table)
{
$table->engine = "InnoDB";

$table->increments("id");
$table->string("name");
$table->integer("stock");
$table->float("price");
$table->integer("category_id");
$table->dateTime("created_at");
$table->dateTime("updated_at");
$table->dateTime("deleted_at");
});
}
public function down()
{
Schema::dropIfExists("product");
}
}

This file should be saved as app/database/migrations/nnnn_nn_nn
_nnnnnn_CreateProductTable.php
.

There’s nothing particularly special about these — we’ve create many of them before. What is important to note is that we’re calling the traditional user table account.

You can learn more about migrations at: http://laravel.com/docs/schema.

The relationships might not yet be apparent, but we’ll see them more clearly in the models…

Creating Models

We need to create the same amount of models. I’ve gone ahead and created them with table names matching those defined in the migrations. I’ve also added the relationship methods:

<?phpuse Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;
class Account
extends Eloquent
implements UserInterface, RemindableInterface
{
protected $table = "account";
protected $hidden = ["password"]; protected $guarded = ["id"]; protected $softDelete = true; public function getAuthIdentifier()
{
return $this->getKey();
}
public function getAuthPassword()
{
return $this->password;
}
public function getReminderEmail()
{
return $this->email;
}
public function orders()
{
return $this->hasMany("Order");
}
}

This file should be saved as app/models/Account.php.

<?phpclass Category
extends Eloquent
{
protected $table = "category";
protected $guarded = ["id"]; protected $softDelete = true; public function products()
{
return $this->hasMany("Product");
}
}

This file should be saved as app/models/Category.php.

<?phpclass Order
extends Eloquent
{
protected $table = "order";
protected $guarded = ["id"]; protected $softDelete = true; public function account()
{
return $this->belongsTo("Account");
}
public function orderItems()
{
return $this->hasMany("OrderItem");
}
public function products()
{
return $this->belongsToMany("Product", "order_item");
}
}

This file should be saved as app/models/Order.php.

<?phpclass OrderItem
extends Eloquent
{
protected $table = "order_item";
protected $guarded = ["id"]; protected $softDelete = true; public function product()
{
return $this->belongsTo("Product");
}
public function order()
{
return $this->belongsTo("Order");
}
}

This file should be saved as app/models/OrderItem.php.

<?phpclass Product
extends Eloquent
{
protected $table = "product";
protected $guarded = ["id"]; protected $softDelete = true; public function orders()
{
return $this->belongsToMany("Order", "order_item");
}
public function orderItems()
{
return $this->hasMany("OrderItem");
}
public function category()
{
return $this->belongsTo("Category");
}
}

This file should be saved as app/models/Product.php.

I’m using a combination of one-to-many relationships and many-to-many relationships (through the order_item) table. These relationships can be expressed as:

  1. Categories have many products.
  2. Accounts have many orders.
  3. Orders have many items (directly) and many products (indirectly).

We can now begin to populate these tables with fake data, and manipulate them with API endpoints.

You can learn more about models at: http://laravel.com/docs/eloquent.

Creating Seeders

Having installed Faker; we’re going to use it to populate the database tables with fake data. We do this for two reasons. Firstly, using fake data is safer than using production data.

Have you ever been writing a script that sends out emails and used some dummy copy while you’re building it? Ever used some cheeky words in that content? Ever accidentally sent that email out to 10,000 real customers email addresses? Ever been fired for losing a company north of £200,000?

I haven’t, but I know a guy that has. Don’t be that guy.

- Phil Sturgeon, Build APIs You Won’t Hate

Secondly, Faker provides random fake data so we get to see what our models look like with random variable data. This will show us the oft-overlooked field limits and formatting errors that we tend to miss while using the same set of pre-defined seed data.

Using Faker is easy:

<?phpclass DatabaseSeeder
extends Seeder
{
protected $faker;
public function getFaker()
{
if (empty($this->faker))
{
$this->faker = Faker\Factory::create();
}
return $this->faker;
}
public function run()
{
$this->call("AccountTableSeeder");
}
}

This file should be saved as app/database/seeds/DatabaseSeeder.php.

<?phpclass AccountTableSeeder
extends DatabaseSeeder
{
public function run()
{
$faker = $this->getFaker();
for ($i = 0; $i < 10; $i++)
{
$email = $faker->email;
$password = Hash::make("password");
Account::create([
"email" => $email,
"password" => $password
]);
}
}
}

This file should be saved as app/database/seeds/AccountTableSeeder.php.

The first step is to create an instance of the Faker\Generator class. We do this by calling the Faker\Factory::create() method and assigning it to a protected property.

Then, in AccountTableSeeder, we loop ten times; creating different accounts. Each account has a random email address, but all of them share the same hashed password. This is so that we will be able to log in with any of these accounts to interact with the rest of the application.

We can actually test this process, to see how the data is created, and how we can authenticate against it. Seed the database, using the following command:

php artisan migrate:refresh --seed

Depending on whether you have already migrated the schema; this may fail. If this happens, you can try the following commands:

php artisan migrate
php artisan db:seed

The refresh migration method actually reverses all migrations and re-migrates them. If you’ve not migrated before using it — there’s a change that the missing tables will cause problems.

You should see ten account records, each with a different email address and password hash. We can attempt to authenticate with one of these. To do this; we need to adjust the auth settings:

<?phpreturn [
"driver" => "eloquent",
"model" => "Account",
"table" => "account",
"reminder" => [
"email" => "email/request",
"table" => "token",
"expire" => 60
]
];

This file should be saved as app/config/auth.php.

Fire up a terminal window and try the following commands:

php artisan tinkerdd(Auth::attempt([
"email" => [one of the email addresses],
"password" => "password"
]));

You’ll need to type it in a single line. I have added the whitespace to make it more readable.

If you see bool(true) then the details you entered will allow a user to log in. Now, let’s repeat the process for the other models:

public function getFaker()
{
if (empty($this->faker))
{
$faker = Faker\Factory::create();
$faker->addProvider(new Faker\Provider\Base($faker));
$faker->addProvider(new Faker\Provider\Lorem($faker));
}
return $this->faker = $faker;
}

This was extracted from app/database/seeds/DatabaseSeeder.php.

We’ve modified the getFaker() method to add things called providers. Providers are like plugins for Faker; which extend the base array of properties/methods that you can query.

<?phpclass CategoryTableSeeder
extends DatabaseSeeder
{
public function run()
{
$faker = $this->getFaker();
for ($i = 0; $i < 10; $i++)
{
$name = ucwords($faker->word);

Category::create([
"name" => $name
]);
}
}
}

This file should be saved as app/database/seeds/CategoryTableSeeder.php.

<?phpclass ProductTableSeeder
extends DatabaseSeeder
{
public function run()
{
$faker = $this->getFaker();
$categories = Category::all(); foreach ($categories as $category)
{
for ($i = 0; $i < rand(-1, 10); $i++)
{
$name = ucwords($faker->word);
$stock = $faker->randomNumber(0, 100);
$price = $faker->randomFloat(2, 5, 100);
Product::create([
"name" => $name,
"stock" => $stock,
"price" => $price,
"category_id" => $category->id
]);
}
}
}
}

This file should be saved as app/database/seeds/ProductTableSeeder.php.

Here, we use the randomNumber() and randomFloat() methods. What’s actually happening, when you request a property value, is that Faker invokes a method of the same name (on one of the providers). We can just as easily use the $faker->word() means the same as $faker->word. Some of the methods (such as the random*() methods we’ve used here) take arguments, so we provide them in the method form.

<?phpclass OrderTableSeeder
extends DatabaseSeeder
{
public function run()
{
$faker = $this->getFaker();
$accounts = Account::all(); foreach ($accounts as $account)
{
for ($i = 0; $i < rand(-1, 5); $i++)
{
Order::create([
"account_id" => $account->id
]);
}
}
}
}

This file should be saved as app/database/seeds/OrderTableSeeder.php.

<?phpclass OrderItemTableSeeder
extends DatabaseSeeder
{
public function run()
{
$faker = $this->getFaker();
$orders = Order::all();
$products = Product::all()->toArray();
foreach ($orders as $order)
{
$used = [];
for ($i = 0; $i < rand(1, 5); $i++)
{
$product = $faker->randomElement($products);
if (!in_array($product["id"], $used))
{
$id = $product["id"];
$price = $product["price"];
$quantity = $faker->randomNumber(1, 3);

OrderItem::create([
"order_id" => $order->id,
"product_id" => $id,
"price" => $price,
"quantity" => $quantity
]);
$used[] = $product["id"];
}
}
}
}
}

This file should be saved as app/database/seeds/OrderItemTableSeeder.php.

public function run()
{
$this->call("AccountTableSeeder");
$this->call("CategoryTableSeeder");
$this->call("ProductTableSeeder");
$this->call("OrderTableSeeder");
$this->call("OrderItemTableSeeder");
}

This was extracted from app/database/seeds/DatabaseSeeder.php.

You can learn more about seeders at: http://laravel.com/docs/
migrations#database-seeding
and more about Faker at: https://github.com/fzaninotto/Faker.

The order in which we call the seeders is important. We can’t start populating orders and order items if we have no products or accounts in the database…

Creating API Endpoints

We don’t have time to cover all aspects of creating APIs with Laravel 4, so we’ll confine our efforts to creating endpoints for the basic interactions that need to happen for our interface to function.

Managing Categories And Products

The endpoints for categories and products are read-only in nature. We’re not concentrating on any sort of administration interface, so we don’t need to add or update them. We will need to adjust the quantity of available products, but we can do that from the OrderController, later on. For now, all we need is:

<?phpclass CategoryController
extends BaseController
{
public function indexAction()
{
return Category::with(["products"])->get();
}
}

This file should be saved as app/controllers/CategoryController.php.

<?phpclass ProductController
extends BaseController
{
public function indexAction()
{
$query = Product::with(["category"]);
$category = Input::get("category");
if ($category)
{
$query->where("category_id", $category);
}
return $query->get();
}
}

This file should be saved as app/controllers/ProductController.php.

The CategoryController has a single index() method which returns all categories, and the ProductController has a single index() method which returns all the products. If ?category=n is provided to the product/index route, the products will be filtered by that category.

We do, of course, still need to add these routes:

Route::any("category/index", [
"as" => "category/index",
"uses" => "CategoryController@indexAction"
]);
Route::any("product/index", [
"as" => "product/index",
"uses" => "ProductController@indexAction"
]);

This was extracted from app/routes.php.

Managing Accounts

For users to be able to buy products, they will need to log in. We’ve added some users to the database, via the AccountTableSeeder class, but we should create an authentication endpoint:

<?phpclass AccountController
extends BaseController
{
public function authenticateAction()
{
$credentials = [
"email" => Input::get("email"),
"password" => Input::get("password")
];
if (Auth::attempt($credentials))
{
return Response::json([
"status" => "ok",
"account" => Auth::user()->toArray()
]);
}
return Response::json([
"status" => "error"
]);
}
}

This file should be saved as app/controllers/AccountController.php.

We’ll also need to add a route for this:

Route::any("account/authenticate", [
"as" => "account/authenticate",
"uses" => "AccountController@authenticateAction"
]);

This was extracted from app/routes.php.

It should now be possible to determine whether login credentials are legitimate; through the browser:

/account/authenticate?email=x&password=y

This will return an object with a status value. If the details were valid then an account object will also be returned.

Managing Orders

We will need to be able to get all orders as well as orders by account:

<?phpclass OrderController
extends BaseController
{
public function indexAction()
{
$query = Order::with([
"account",
"orderItems",
"orderItems.product",
"orderItems.product.category"
]);
$account = Input::get("account"); if ($account)
{
$query->where("account_id", $account);
}
return $query->get();
}
}

This file should be saved as app/controllers/OrderController.php.

This looks similar to the indexAction() method, in ProductController. We’re also eager-loading the related “child” entities and querying by account (if that’s given).

For this; we will need to add a route:

Route::any("order/index", [
"as" => "order/index",
"uses" => "OrderController@indexAction"
]);

This was extracted from app/routes.php.

You can learn more about controllers at: http://laravel.com/docs/controllers and more about routes at: http://laravel.com/docs/routing.

We’ll deal with creating orders once we have the shopping and payment interfaces completed. Let’s not get ahead of ourselves…

Creating The Site With AngularJS

Will the API in place; we can begin the interface work. We’re using AngularJS, which creates rich interfaces from ordinary HTML.

Creating The Interface

AngularJS allows much of the functionality, we would previous have split into separate pages, to be in the same single-page application interface. It’s not a unique feature of AngularJS, but rather the preferred approach to interface structure.

Because of this; we only need a single view:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Laravel 4 E-Commerce</title>
<link
type="text/css"
rel="stylesheet"
href="{{ asset("css/bootstrap.3.0.3.min.css") }}"
/>
<link
type="text/css"
rel="stylesheet"
href="{{ asset("css/bootstrap.theme.3.0.3.min.css") }}"
/>
<link
type="text/css"
rel="stylesheet"
href="{{ asset("css/shared.css") }}"
/>
<script
type="text/javascript"
src="{{ asset("js/angularjs.1.2.4.min.js") }}"
></script>
<script
type="text/javascript"
src="{{ asset("js/angularjs.cookies.1.2.4.min.js") }}"
></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>
Laravel 4 E-Commerce
</h1>
</div>
</div>
<div class="row">
<div class="col-md-8">
<h2>
Products
</h2>
<div class="categories btn-group">
<button
type="button"
class="category btn btn-default active"
>
All
</button>
<button
type="button"
class="category btn btn-default"
>
Category 1
</button>
<button
type="button"
class="category btn btn-default"
>
Category 2
</button>
<button
type="button"
class="category btn btn-default"
>
Category 3
</button>
</div>
<div class="products">
<div class="product media">
<button
type="button"
class="pull-left btn btn-default"
>
Add to basket
</button>
<div class="media-body">
<h4 class="media-heading">Product 1</h4>
<p>
Price: 9.99, Stock: 10
</p>
</div>
</div>
<div class="product media">
<button
type="button"
class="pull-left btn btn-default"
>
Add to basket
</button>
<div class="media-body">
<h4 class="media-heading">Product 2</h4>
<p>
Price: 9.99, Stock: 10
</p>
</div>
</div>
<div class="product media">
<button
type="button"
class="pull-left btn btn-default"
>
Add to basket
</button>
<div class="media-body">
<h4 class="media-heading">Product 3</h4>
<p>
Price: 9.99, Stock: 10
</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<h2>
Basket
</h2>
<form class="basket">
<table class="table">
<tr class="product">
<td class="name">
Product 1
</td>
<td class="quantity">
<input
class="quantity form-control col-md-2"
type="number"
value="1"
/>
</td>
<td class="product">
9.99
</td>
<td class="product">
<a
class="remove glyphicon glyphicon-remove"
href="#"
></a>
</td>
</tr>
<tr class="product">
<td class="name">
Product 2
</td>
<td class="quantity">
<input
class="quantity form-control col-md-2"
type="number"
value="1"
/>
</td>
<td class="product">
9.99
</td>
<td class="product">
<a
class="remove glyphicon glyphicon-remove"
href="#"
></a>
</td>
</tr>
<tr class="product">
<td class="name">
Product 3
</td>
<td class="quantity">
<input
class="quantity form-control col-md-2"
type="number"
value="1"
/>
</td>
<td class="product">
9.99
</td>
<td class="product">
<a
class="remove glyphicon glyphicon-remove"
href="#"
></a>
</td>
</tr>
</table>
</form>
</div>
</div>
</div>
<script
type="text/javascript"
src="{{ asset("js/shared.js") }}"
></script>
</body>
</html>

This file should be saved as app/views/index.blade.php.

You’ll notice that we also reference a shared.css file:

.products {
margin-top: 20px;
}
.basket td {
vertical-align: middle !important;
}
.basket .quantity input {
width: 50px;
}

This file should be saved as public/css/shared.css.

These changes to the view coincide with a modified IndexController:

<?phpclass IndexController
extends BaseController
{
public function indexAction()
{
return View::make("index");
}
}

This file should be saved as app/controllers/IndexController.php.

Making The Interface Dynamic

So far; we’ve set up the API and static interface, for our application. It’s not going to be much use without the JavaScript to drive purchase functionality, and to interact with the API. Let’s dive into AngularJS!

I should mention that I am by no means an AngularJS expert. I learned all I know of it, while writing this tutorial, by following various guides. The point of this is not to teach AngularJS so much as it is to show AngularJS integration with Laravel 4.

AngularJS interfaces are just regular HTML and JavaScript. To wire the interface into the beginnings of an AngularJS application architecture; we have to add a script, and a few directives:

<body ng-controller="main">

This was extracted from app/views/index.blade.php.

<div class="col-md-8" ng-controller="products">

This was extracted from app/views/index.blade.php.

<div class="col-md-4" ng-controller="basket">

This was extracted from app/views/index.blade.php.

<script
type="text/javascript"
src="{{ asset("js/shared.js") }}"
></script>

This was extracted from app/views/index.blade.php.

This script element should be placed just before the </body> tag.

In addition to these modifications, we should also create the shared.js file:

var app = angular.module("app", ["ngCookies"]);app.controller("main", function($scope) {
console.log("main.init");
this.shared = "hello world"; $scope.main = this;
});
app.controller("products", function($scope) {
console.log("products.init:", $scope.main.shared);
$scope.products = this;
});
app.controller("basket", function($scope) {
console.log("basket.init:", $scope.main.shared);
$scope.basket = this;
});

This file should be saved as public/js/shared.js.

AngularJS implements the concept of modules — contains for modularising business and interface logic. We begin by creating a module (called app). This correlates with the ng-app="app" directive.

This demonstrates a powerful feature of AngularJS: the ability to have multiple applications on a single HTML page, and to make any element an application.

The remaining ng-controller directives define which controllers apply to which element. These match the names of the controllers which we have created. Controllers are nothing more than scoped functions. We assign the controller instances, and some shared data, to the $scope variable. This provides a consistent means of sharing data.

If you’ve linked everything correctly; you should see three console messages, letting you know that everything’s working.

Let’s popular the interface with real products. To achieve this; we need to request the products from the API, and render them (in a loop).

app.factory("CategoryService", function($http) {
return {
"getCategories": function() {
return $http.get("/category/index");
}
};
});
app.factory("ProductService", function($http) {
return {
"getProducts": function() {
return $http.get("/product/index");
}
};
});
app.controller("products", function(
$scope,
CategoryService,
ProductService
) {

var self = this;
var categories = CategoryService.getCategories();
categories.success(function(data) {
self.categories = data;
});
var products = ProductService.getProducts(); products.success(function(data) {
self.products = data;
});
$scope.products = this;});

This was extracted from public/js/shared.js.

There are some awesome things happening in this code. Firstly, we abstract the logic by which we get categories and products (from the API) into AngularJS’s implementation of services. We also have access to the $http interface; which is a wrapper for XMLHTTPRequest, and acts as a replacement for other libraries (think jQuery) which we would have used before.

The two services each have a method for returning the API data, for categories and products. These methods return things, called promises, which are references to future-completed data. We attach callbacks to these, within the ProductController, which essentially update the controller data.

So we have the API data, but how do we render it in the interface? We do so with directives and data-binding:

<div class="col-md-8" ng-controller="products">
<h2>
Products
</h2>
<div class="categories btn-group">
<button
type="button"
class="category btn btn-default active"
>
All
</button>
<button
type="button"
class="category btn btn-default"
ng-repeat="category in products.categories"
>
@{{ category.name }}
</button>
</div>
<div class="products">
<div
class="product media"
ng-repeat="product in products.products"
>
<button
type="button"
class="pull-left btn btn-default"
>
Add to basket
</button>
<div class="media-body">
<h4 class="media-heading">@{{ product.name }}</h4>
<p>
Price: @{{ product.price }}, Stock: @{{ product.stock }}
</p>
</div>
</div>
</div>
</div>

This was extracted from app/views/index.blade.php.

If you’re wondering how the interface is updated when the data is fetched asynchronously, but the good news is you don’t need to. AngularJS takes care of all interface updates; so you can focus on the actual application! Open up a browser and see it working…

Now that we have dynamic categories and products, we should implement a filter so that products are swapped out whenever a user selects a category of products.

  <button
type="button"
class="category btn btn-default active"
ng-click="products.setCategory(null)"
ng-class="{ 'active' : products.category == null }"
>
All
</button>
<button
type="button"
class="category btn btn-default"
ng-repeat="category in products.categories"
ng-click="products.setCategory(category)"
ng-class="{ 'active' : products.category.id == category.id }"
>
@{{ category.name }}
</button>
</div>
<div class="products">
<div
class="product media"
ng-repeat="product in products.products | filter : products.filterByCategory"
>

This was extracted from app/views/index.blade.php.

We’ve added three new concepts here:

  1. We’re filtering the ng-repeat directive with a call to products.filterByCategory(). We’ll create this in a moment, but it’s important to understand that filter allows functions to define how the items being looped are filtered.
  2. We’ve added ng-click directives. These directives allow the execution of logic when the element is clicked. We’re targeting another method we’re about to create; which will set the current category filter.
  3. We’ve added ng-class directives. These will set the defined class based on controller/scope logic. If the set category filter matches that which the button is being created from; the active class will be applied to the button.

These directives, in isolation, will only cause errors. We need to add the JavaScript logic to back them up:

app.controller("products", function(
$scope,
CategoryService,
ProductService
) {

var self = this;
// ... this.category = null; this.filterByCategory = function(product) { if (self.category !== null) {
return product.category.id === self.category.id;
}
return true; }; this.setCategory = function(category) {
self.category = category;
};
// ...});

This was extracted from public/js/shared.js.

Let’s move on to the shopping basket. We need to be able to add items to it, remove items from it and change quantity values.

app.factory("BasketService", function($cookies) {  var products = JSON.parse($cookies.products || "[]");  return {    "getProducts" : function() {
return products;
},
"add" : function(product) { products.push({
"id" : product.id,
"name" : product.name,
"price" : product.price,
"total" : product.price * 1,
"quantity" : 1
});
this.store(); }, "remove" : function(product) { for (var i = 0; i < products.length; i++) { var next = products[i]; if (next.id == product.id) {
products.splice(i, 1);
}
}

this.store();
}, "update": function() { for (var i = 0; i < products.length; i++) { var product = products[i];
var raw = product.quantity * product.price;
product.total = Math.round(raw * 100) / 100;

}
this.store(); }, "store" : function() {
$cookies.products = JSON.stringify(products);
},
"clear" : function() {
products.length = 0;
this.store();
}
};});

This was extracted from public/js/shared.js.

We’re grouping all the basket-related logic together in BasketService. You may have noticed the reference to ngCookies (when creating the app module) and the extra script file reference (in index.blade.php). These allow us to use AngularJS’s cookies module; for storing the basket items.

The getProducts() method returns the products. We need to store them as a serialised JSON array, so when we initially retrieve them; we parse them (with a default value of “[]”). The add() and remove() methods create and destroy special item objects. After each basket item operation; we need to persist the products array back to $cookies.

The update() method works out the total cost of each item; by taking into account the original price and the updated quantity. It also rounds this value to avoid floating-point calculation irregularities.

There’s also a store() method which persists the products to $cookies, and a clear() method which removes all products.

The HTML, compatible with all this, is:

<div class="col-md-4" ng-controller="basket">
<h2>
Basket
</h2>
<form class="basket">
<table class="table">
<tr
class="product"
ng-repeat="product in basket.products track by $index"
>
<td class="name">
@{{ product.name }}
</td>
<td class="quantity">
<input
class="quantity form-control col-md-2"
type="number"
ng-model="product.quantity"
ng-change="basket.update()"
min="1"
/>
</td>
<td class="product">
@{{ product.total }}
</td>
<td class="product">
<a
class="remove glyphicon glyphicon-remove"
ng-click="basket.remove(product)"
></a>
</td>
</tr>
</table>
</form>
</div>

This was extracted from app/views/index.blade.php.

We’ve change the numeric input element to use ng-model and ng-change directives. The first tells the input which dynamic (quantity) value to bind to, and the second tells the basket what to do if the input’s value has changed. We already know that this means re-calculating the total cost of that product, and storing the products back in $cookies.

We’ve also added an ng-click directive to the remove link; so that the product can be removed from the basket.

You may have noticed track by $index, in the hg-repeat directive. This is needed as ng-repeat will error when it tries to iterate over the parsed JSON value (which is stored in $cookies). I found out about this at: http://docs.angularjs.org/error/ngRepeat:dupes.

We need to be able to remove the basket items, also. Let’s modify the JavaScript/HTML to allow for this:

app.controller("products", function(
$scope,
CategoryService,
ProductService,
BasketService
) {
// ... this.addToBasket = function(product) {
BasketService.add(product);
};
// ...});

This was extracted from public/js/shared.js.

<button
type="button"
class="pull-left btn btn-default"
ng-click="products.addToBasket(product)"
>
Add to basket
</button>

This was extracted from app/views/index.blade.php.

Try it out in your browser. You should be able to add items into the basket, change their quantities and remove them. When you refresh, all should display correctly.

Completing Orders

To complete orders; we need to send the order item data to the server, and process a payment. We need to pass this endpoint an account ID (to link the orders to an account) which means we also need to add authentication…

app.factory("AccountService", function(
$http
) {
var account = null; return {
"authenticate": function(email, password) {
var request = $http.post("/account/authenticate", {
"email" : email,
"password" : password
});
request.success(function(data) {
if (data.status !== "error") {
account = data.account;
}
});
return request; },
"getAccount": function() {
return account;
}
};
});
app.factory("OrderService", function(
$http,
AccountService,
BasketService
) {
return {
"pay": function(number, expiry, security) {
var account = AccountService.getAccount();
var products = BasketService.getProducts();
var items = [];
for (var i = 0; i < products.length; i++) { var product = products[i]; items.push({
"product_id" : product.id,
"quantity" : product.quantity
});
} return $http.post("/order/add", {
"account" : account.id,
"items" : JSON.stringify(items),
"number" : number,
"expiry" : expiry,
"security" : security
});
}
};
});

This was extracted from public/js/shared.js.

The AccountService object has a method for authenticating (with a provided email and password) and it returns the result of a POST request to /account/authenticate. It also has a getAccount() method which just returns whatever’s in the account variable.

The OrderService object as a single method for sending order details to the server. I’ve bundled the payment particulars in with this method to save some time. The idea is that the order is created and paid for in a single process.

We need to amend the basket controller:

app.controller("basket", function(
$scope,
AccountService,
BasketService,
OrderService
) {
// ... this.state = "shopping";
this.email = "";
this.password = "";
this.number = "";
this.expiry = "";
this.secutiry = "";
this.authenticate = function() { var details = AccountService.authenticate(self.email, self.password); details.success(function(data) {
if (data.status == "ok") {
self.state = "paying";
}
});
} this.pay = function() { var details = OrderService.pay(
self.number,
self.expiry,
self.security
);
details.success(function(data) {
BasketService.clear();
self.state = "shopping";
});
} // ...});

This was extracted from public/js/shared.js.

We’ve added a state variable which tracks progress through checkout. We’re also keeping track of the account email address and password as well as the credit card details. In addition; there are two new methods which will be triggered by the interface:

<div class="col-md-4" ng-controller="basket">
<h2>
Basket
</h2>
<form class="basket">
<table class="table">
<tr
class="product"
ng-repeat="product in basket.products track by $index"
ng-class="{ 'hide' : basket.state != 'shopping' }"
>
<td class="name">
@{{ product.name }}
</td>
<td class="quantity">
<input
class="form-control"
type="number"
ng-model="product.quantity"
ng-change="basket.update()"
min="1"
/>
</td>
<td class="product">
@{{ product.total }}
</td>
<td class="product">
<a
class="remove glyphicon glyphicon-remove"
ng-click="basket.remove(product)"
></a>
</td>
</tr>
<tr>
<td
colspan="4"
ng-class="{ 'hide' : basket.state != 'shopping' }"
>
<input
type="text"
class="form-control"
placeholder="email"
ng-model="basket.email"
/>
</td>
</tr>
<tr>
<td
colspan="4"
ng-class="{ 'hide' : basket.state != 'shopping' }"
>
<input
type="password"
class="form-control"
placeholder="password"
ng-model="basket.password"
/>
</td>
</tr>
<tr>
<td
colspan="4"
ng-class="{ 'hide' : basket.state != 'shopping' }"
>
<button
type="button"
class="pull-left btn btn-default"
ng-click="basket.authenticate()"
>
Authenticate
</button>
</td>
</tr>
<tr>
<td
colspan="4"
ng-class="{ 'hide' : basket.state != 'paying' }"
>
<input
type="text"
class="form-control"
placeholder="card number"
ng-model="basket.number"
/>
</td>
</tr>
<tr>
<td
colspan="4"
ng-class="{ 'hide' : basket.state != 'paying' }"
>
<input
type="text"
class="form-control"
placeholder="expiry"
ng-model="basket.expiry"
/>
</td>
</tr>
<tr>
<td
colspan="4"
ng-class="{ 'hide' : basket.state != 'paying' }"
>
<input
type="text"
class="form-control"
placeholder="security number"
ng-model="basket.security"
/>
</td>
</tr>
<tr>
<td
colspan="4"
ng-class="{ 'hide' : basket.state != 'paying' }"
>
<button
type="button"
class="pull-left btn btn-default"
ng-click="basket.pay()"
>
Pay
</button>
</td>
</tr>
</table>
</form>
</div>

This was extracted from app/views/index.blade.php.

We’re using those ng-class directives to hide/show various table rows (in our basket). This lets us toggle the fields that users need to complete; and provides us with different buttons to dispatch the different methods in our basket controller.

You can learn more about AngularJS at: http://angularjs.org.

Finally; we need to tie this into our OrderController, where the orders are completed and the payments are processed…

Accepting Payments

We’re going to create a service provider to handle the payment side of things, and while we could go into great detail about how to do this; we don’t have the time. Read Taylor’s book, or my tutorial, or the docs.

Creating Orders

Before we start hitting Stripe up; we should create the endpoint for creating orders:

public function addAction()
{
$validator = Validator::make(Input::all(), [
"account" => "required|exists:account,id",
"items" => "required"
]);
if ($validator->passes())
{
$order = Order::create([
"account_id" => Input::get("account")
]);
try
{
$items = json_decode(Input::get("items"));
}
catch (Exception $e)
{
return Response::json([
"status" => "error",
"errors" => [
"items" => [
"Invalid items format."
]
]
]);
}
$total = 0; foreach ($items as $item)
{
$orderItem = OrderItem::create([
"order_id" => $order->id,
"product_id" => $item->product_id,
"quantity" => $item->quantity
]);
$product = $orderItem->product; $orderItem->price = $product->price;
$orderItem->save();
$product->stock -= $item->quantity;
$product->save();
$total += $orderItem->quantity * $orderItem->price;
}
$result = $this->gateway->pay(
Input::get("number"),
Input::get("expiry"),
$total,
"usd"
);
if (!$result)
{
return Response::json([
"status" => "error",
"errors" => [
"gateway" => [
"Payment error"
]
]
]);
}
$account = $order->account; $document = $this->document->create($order);
$this->messenger->send($order, $document);
return Response::json([
"status" => "ok",
"order" => $order->toArray()
]);
}
return Response::json([
"status" => "error",
"errors" => $validator->errors()->toArray()
]);
}

This was extracted from app/controllers/OrderController.php.

There are a few steps taking place here:

  1. We validate that the account and items details have been provided.
  2. We create an order item, and assign it to the prodded account.
  3. We json_decode() the provided items and return an error if an invalid format has been provided.
  4. We create individual order items for each provided item, and add the total value up.
  5. We pass this value, and the order to a GatewayInterface class (which we’ll create in a bit).
  6. If this pay() method returns true; we create a document (with the DocumentInterface we’re about to make) and send it (with the MessengerInterface we’re also about to make).
  7. Finally we return a status of ok.

The main purpose of this endpoint is to create the order (and order items) while passing the payment off to the service provider classes.

It would obviously be better to separate these tasks into their own classes/methods but there’s simply not time for that sort of thing. Feel free to do it in your own application!

Working The Service Provider

This leaves us with the service-provider part of things. I’ve gone through the motions to hook everything up (as you might have done following on from the tutorial which covered this); and here is a list of the changes:

"providers" => array(  // ...  "Formativ\Billing\BillingServiceProvider"),

This was extracted from app/config/app.php.

"autoload" : {  // ...  "psr-0": {
"Formativ\\Billing": "workbench/formativ/billing/src/"
}
}

This was extracted from composer.json.

<?phpnamespace Formativ\Billing;use App;
use Illuminate\Support\ServiceProvider;
class BillingServiceProvider
extends ServiceProvider
{
protected $defer = true;
public function register()
{
App::bind("billing.stripeGateway", function() {
return new StripeGateway();
});
App::bind("billing.pdfDocument", function() {
return new PDFDocument();
});
App::bind("billing.emailMessenger", function() {
return new EmailMessenger();
});
}
public function provides()
{
return [
"billing.stripeGateway",
"billing.pdfDocument",
"billing.emailMessenger"
];
}
}

This file should be saved as workbench/formativ/billing/src/Formativ/Billing/
BillingServiceProvider.php
.

<?phpnamespace Formativ\Billing;interface GatewayInterface
{
public function pay(
$number,
$expiry,
$amount,
$currency
);
}

This file should be saved as workbench/formativ/billing/src/Formativ/Billing/
GatewayInterface.php
.

<?phpnamespace Formativ\Billing;interface DocumentInterface
{
public function create($order);
}

This file should be saved as workbench/formativ/billing/src/Formativ/Billing/
DocumentInterface.php
.

<?phpnamespace Formativ\Billing;interface MessengerInterface
{
public function send(
$order,
$document
);
}

This file should be saved as workbench/formativ/billing/src/Formativ/Billing/
MessengerInterface.php
.

These are all the interfaces (what some might consider the scaffolding logic) that we need. Let’s make some payments!

You can learn more about service providers at: http://laravel.com/docs/packages#service-providers.

Making Payments

As I’ve mentioned; we’re going to receive payments through Stripe. You can create a new Stripe account at: https://manage.stripe.com/register.

You should already have the Stripe libraries installed, so let’s make a GatewayInterface implementation:

<?phpnamespace Formativ\Billing;use Stripe;
use Stripe_Charge;
class StripeGateway
implements GatewayInterface
{
public function pay(
$number,
$expiry,
$amount,
$currency
)
{
Stripe::setApiKey("...");
$expiry = explode("/", $expiry); try
{
$charge = Stripe_Charge::create([
"card" => [
"number" => $number,
"exp_month" => $expiry[0],
"exp_year" => $expiry[1]
],
"amount" => round($amount * 100),
"currency" => $currency
]);

return true;
}
catch (Exception $e)
{
return false;
}
}
}

This file should be saved as workbench/formativ/billing/src/Formativ/Billing/
StripeGateway.php
.

Using the document (found at: https://github.com/stripe/stripe-php); we’re able to create a test charge which goes through the Stripe payment gateway. You should be able to submit orders through the the interface we’ve created and actually see them on your Stripe dashboard.

You can learn more about Stripe at: https://stripe.com/docs.

Generating PDF Documents

The last thing left to do is generate and email the invoice. We’ll begin with the PDF generation, using DOMPDF and ordinary views:

public function getTotalAttribute()
{
return $this->quantity * $this->price;
}

This was extracted from app/models/OrderItem.php.

public function getTotalAttribute()
{
$total = 0;
foreach ($this->orderItems as $orderItem)
{
$total += $orderItem->price * $orderItem->quantity;
}
return $total;
}

This was extracted from app/models/Order.php.

These two additional model methods allow us to get the totals of orders and order items quickly. You can learn more about Eloquent attribute getters at: http://laravel.com/docs/eloquent#accessors-and-mutators.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Laravel 4 E-Commerce</title>
<style type="text/css">
body {
padding : 25px 0;
font-family : Helvetica;
}
td {
padding : 0 10px 0 0;
}
* {
float : none;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8">

</div>
<div class="col-md-4 well">
<table>
<tr>
<td class="pull-right">
<strong>Account</strong>
</td>
<td>
{{ $order->account->email }}
</td>
</tr>
<tr>
<td class="pull-right">
<strong>Date</strong>
</td>
<td>
{{ $order->created_at->format("F jS, Y"); }}
</td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h2>Invoice {{ $order->id }}</h2>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
@foreach ($order->orderItems as $orderItem)
<tr>
<td>
{{ $orderItem->product->name }}
</td>
<td>
{{ $orderItem->quantity }}
</td>
<td>
$ {{ number_format($orderItem->total, 2) }}
</td>
</tr>
@endforeach
<tr>
<td>&nbsp;</td>
<td>
<strong>Total</strong>
</td>
<td>
<strong>$ {{ number_format($order->total, 2) }}</strong>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>

This file should be saved as app/views/email/invoice.blade.php.

This view just displays information about the order, including items, totals and a grand total. I’ve avoided using Bootstrap since it seems to kill DOMDPF outright. The magic, however, is in how the PDF document is generated:

<?phpnamespace Formativ\Billing;class PDFDocument
implements DocumentInterface
{
public function create($order)
{
$view = View::make("email/invoice", [
"order" => $order
]);
define("DOMPDF_ENABLE_AUTOLOAD", false); require_once base_path() . "/vendor/dompdf/dompdf/dompdf_config.inc.php"; $dompdf = new DOMPDF();
$dompdf->load_html($view);
$dompdf->set_paper("a4", "portrait");
$dompdf->render();
$results = $dompdf->output();
$temp = storage_path() . "/order-" . $order->id . ".pdf";
file_put_contents($temp, $results);
return $temp;
}
}

This file should be saved as workbench/formativ/billing/src/Formativ/Billing/
PDFDocument.php
.

We generate a PDF invoice by rendering the invoice view; setting the page size (and orientation) and rendering the document. We’re also saving the PDF document to the app/storage/cache directory.

You can learn more about DOMPDF at: https://github.com/dompdf/dompdf.

Last thing to hook up is the MessengerInterface implementation:

Here's your invoice!

This file should be saved as app/views/email/wrapper.blade.php.

<?phpnamespace Formativ\Billing;use Mail;class EmailMessenger
implements MessengerInterface
{
public function send(
$order,
$document
)
{
Mail::send("email/wrapper", [], function($message) use ($order, $document)
{
$message->subject("Your invoice!");
$message->from("info@example.com", "Laravel 4 E-Commerce");
$message->to($order->account->email);
$message->attach($document, [
"as" => "Invoice " . $order->id,
"mime" => "application/pdf"
]);
});
}
}

This file should be saved as workbench/formativ/billing/src/Formativ/Billing/
EmailMessenger.php
.

The EmailMessenger class sends a simple email to the account, attaching the PDF invoice along the way.

You can learn more about sending email at: http://laravel.com/docs/mail.

Conclusion

If you enjoyed this tutorial; it would be helpful if you could click the Recommend button.

This tutorial comes from a book I’m writing. If you like it and want to support future tutorials; please consider buying it. Half of all sales go to Laravel.

--

--