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, in this chapter, we’re going to create an online shop.
There is no way that an e-commerce platform, built in 40 minutes, can be production-ready. Please do not attempt to conduct any real business on the basis of this chapter; without having taken the necessary precautions.
This chapter 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.
In this chapter; 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 PDF documents dynamically.
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:
1
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:
1
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.
Our application will do loads of things, so we’ll need to install a few dependencies to lighten the workload.
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:
1
<script
2
type=
"text/javascript"
3
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular.js"
4
></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.
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):
1
<link
2
type=
"text/css"
3
rel=
"stylesheet"
4
href=
"//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"
5
/>
6
<link
7
type=
"text/css"
8
rel=
"stylesheet"
9
href=
"//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css"
10
/>
11
<script
12
type=
"text/javascript"
13
src=
"//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"
14
></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.
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:
1
"require"
:
{
2
"dompdf/dompdf"
:
"dev-master"
3
}
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:
1
"require"
:
{
2
"stripe/stripe-php"
:
"dev-master"
3
}
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:
1
"require"
:
{
2
"fzaninotto/faker"
:
"dev-master"
3
}
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.
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.
We listed five migrations, which we need to create. Fire up a terminal window and use the following command to create them:
1
php artisan migrate:make CreateCategoryTable
After a bit of modification; I have the following migrations:
1
<?
php
2
3
use
Illuminate\Database\Migrations\Migration
;
4
5
class
CreateAccountTable
6
extends
Migration
7
{
8
public
function
up
()
9
{
10
Schema
::
create
(
"account"
,
function
(
$table
)
11
{
12
$table
->
engine
=
"InnoDB"
;
13
14
$table
->
increments
(
"id"
);
15
$table
->
string
(
"email"
);
16
$table
->
string
(
"password"
);
17
$table
->
dateTime
(
"created_at"
);
18
$table
->
dateTime
(
"updated_at"
);
19
$table
->
dateTime
(
"deleted_at"
);
20
});
21
}
22
23
public
function
down
()
24
{
25
Schema
::
dropIfExists
(
"account"
);
26
}
27
}
1
<?
php
2
3
use
Illuminate\Database\Migrations\Migration
;
4
5
class
CreateCategoryTable
6
extends
Migration
7
{
8
public
function
up
()
9
{
10
Schema
::
create
(
"category"
,
function
(
$table
)
11
{
12
$table
->
engine
=
"InnoDB"
;
13
14
$table
->
increments
(
"id"
);
15
$table
->
string
(
"name"
);
16
$table
->
dateTime
(
"created_at"
);
17
$table
->
dateTime
(
"updated_at"
);
18
$table
->
dateTime
(
"deleted_at"
);
19
});
20
}
21
22
public
function
down
()
23
{
24
Schema
::
dropIfExists
(
"category"
);
25
}
26
}
1
<?
php
2
3
use
Illuminate\Database\Migrations\Migration
;
4
5
class
CreateOrderItemTable
6
extends
Migration
7
{
8
public
function
up
()
9
{
10
Schema
::
create
(
"order_item"
,
function
(
$table
)
11
{
12
$table
->
engine
=
"InnoDB"
;
13
14
$table
->
increments
(
"id"
);
15
$table
->
integer
(
"order_id"
);
16
$table
->
integer
(
"product_id"
);
17
$table
->
integer
(
"quantity"
);
18
$table
->
float
(
"price"
);
19
$table
->
dateTime
(
"created_at"
);
20
$table
->
dateTime
(
"updated_at"
);
21
$table
->
dateTime
(
"deleted_at"
);
22
});
23
}
24
25
public
function
down
()
26
{
27
Schema
::
dropIfExists
(
"order_item"
);
28
}
29
}
1
<?
php
2
3
use
Illuminate\Database\Migrations\Migration
;
4
5
class
CreateOrderTable
6
extends
Migration
7
{
8
public
function
up
()
9
{
10
Schema
::
create
(
"order"
,
function
(
$table
)
11
{
12
$table
->
engine
=
"InnoDB"
;
13
14
$table
->
increments
(
"id"
);
15
$table
->
integer
(
"account_id"
);
16
$table
->
dateTime
(
"created_at"
);
17
$table
->
dateTime
(
"updated_at"
);
18
$table
->
dateTime
(
"deleted_at"
);
19
});
20
}
21
22
public
function
down
()
23
{
24
Schema
::
dropIfExists
(
"order"
);
25
}
26
}
1
<?
php
2
3
use
Illuminate\Database\Migrations\Migration
;
4
5
class
CreateProductTable
6
extends
Migration
7
{
8
public
function
up
()
9
{
10
Schema
::
create
(
"product"
,
function
(
$table
)
11
{
12
$table
->
engine
=
"InnoDB"
;
13
14
$table
->
increments
(
"id"
);
15
$table
->
string
(
"name"
);
16
$table
->
integer
(
"stock"
);
17
$table
->
float
(
"price"
);
18
$table
->
dateTime
(
"created_at"
);
19
$table
->
dateTime
(
"updated_at"
);
20
$table
->
dateTime
(
"deleted_at"
);
21
});
22
}
23
24
public
function
down
()
25
{
26
Schema
::
dropIfExists
(
"product"
);
27
}
28
}
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.
The relationships might not yet be apparent, but we’ll see them more clearly in the models…
We need to create the same amount of models. I’ve gone ahead and created them with table names matching those defined int he migrations. I’ve also added the relationship methods:
1
<?
php
2
3
use
Illuminate\Auth\UserInterface
;
4
use
Illuminate\Auth\Reminders\RemindableInterface
;
5
6
class
Account
7
extends
Eloquent
8
implements
UserInterface
,
RemindableInterface
9
{
10
protected
$table
=
"account"
;
11
12
protected
$hidden
=
[
"password"
];
13
14
protected
$guarded
=
[
"id"
];
15
16
protected
$softDelete
=
true
;
17
18
public
function
getAuthIdentifier
()
19
{
20
return
$this
->
getKey
();
21
}
22
23
public
function
getAuthPassword
()
24
{
25
return
$this
->
password
;
26
}
27
28
public
function
getReminderEmail
()
29
{
30
return
$this
->
email
;
31
}
32
33
public
function
orders
()
34
{
35
return
$this
->
hasMany
(
"Order"
);
36
}
37
}
1
<?
php
2
3
class
Category
4
extends
Eloquent
5
{
6
protected
$table
=
"category"
;
7
8
protected
$guarded
=
[
"id"
];
9
10
protected
$softDelete
=
true
;
11
12
public
function
products
()
13
{
14
return
$this
->
hasMany
(
"Product"
);
15
}
16
}
1
<?
php
2
3
class
Order
4
extends
Eloquent
5
{
6
protected
$table
=
"order"
;
7
8
protected
$guarded
=
[
"id"
];
9
10
protected
$softDelete
=
true
;
11
12
public
function
account
()
13
{
14
return
$this
->
belongsTo
(
"Account"
);
15
}
16
17
public
function
orderItems
()
18
{
19
return
$this
->
hasMany
(
"OrderItem"
);
20
}
21
22
public
function
products
()
23
{
24
return
$this
->
belongsToMany
(
"Product"
,
"order_item"
);
25
}
26
}
1
<?
php
2
3
class
OrderItem
4
extends
Eloquent
5
{
6
protected
$table
=
"order_item"
;
7
8
protected
$guarded
=
[
"id"
];
9
10
protected
$softDelete
=
true
;
11
12
public
function
product
()
13
{
14
return
$this
->
belongsTo
(
"Product"
);
15
}
16
17
public
function
order
()
18
{
19
return
$this
->
belongsTo
(
"Order"
);
20
}
21
}
1
<?
php
2
3
class
Product
4
extends
Eloquent
5
{
6
protected
$table
=
"product"
;
7
8
protected
$guarded
=
[
"id"
];
9
10
protected
$softDelete
=
true
;
11
12
public
function
orders
()
13
{
14
return
$this
->
belongsToMany
(
"Order"
,
"order_item"
);
15
}
16
17
public
function
orderItems
()
18
{
19
return
$this
->
hasMany
(
"OrderItem"
);
20
}
21
22
public
function
category
()
23
{
24
return
$this
->
belongsTo
(
"Category"
);
25
}
26
}
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:
We can now being to populate these tables with fake data, and manipulate them with API endpoints.
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.
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:
1
<?
php
2
3
class
DatabaseSeeder
4
extends
Seeder
5
{
6
protected
$faker
;
7
8
public
function
getFaker
()
9
{
10
if
(
empty
(
$this
->
faker
))
11
{
12
$this
->
faker
=
Faker\Factory
::
create
();
13
}
14
15
return
$this
->
faker
;
16
}
17
18
public
function
run
()
19
{
20
$this
->
call
(
"AccountTableSeeder"
);
21
}
22
}
1
<?
php
2
3
class
AccountTableSeeder
4
extends
DatabaseSeeder
5
{
6
public
function
run
()
7
{
8
$faker
=
$this
->
getFaker
();
9
10
for
(
$i
=
0
;
$i
<
10
;
$i
++
)
11
{
12
$email
=
$faker
->
email
;
13
$password
=
Hash
::
make
(
"password"
);
14
15
Account
::
create
([
16
"email"
=>
$email
,
17
"password"
=>
$password
18
]);
19
}
20
}
21
}
The first step is to create an instance of the FakerGenerator class. We do this by calling the FakerFactory::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:
1
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:
1
php artisan migrate 2
php artisan db:seed
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:
1
<?
php
2
3
return
[
4
"driver"
=>
"eloquent"
,
5
"model"
=>
"Account"
,
6
"table"
=>
"account"
,
7
"reminder"
=>
[
8
"email"
=>
"email/request"
,
9
"table"
=>
"token"
,
10
"expire"
=>
60
11
]
12
];
Fire up a terminal window and try the following commands:
1
php artisan tinker
1
dd
(
Auth
::
attempt
([
2
"email"
=>
[
one
of
the
email
addresses
],
3
"password"
=>
"password"
4
]));
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:
1
public
function
getFaker
()
2
{
3
if
(
empty
(
$this
->
faker
))
4
{
5
$faker
=
Faker\Factory
::
create
();
6
$faker
->
addProvider
(
new
Faker\Provider\Base
(
$faker
));
7
$faker
->
addProvider
(
new
Faker\Provider\Lorem
(
$faker
));
8
}
9
10
return
$this
->
faker
=
$faker
;
11
}
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.
1
<?
php
2
3
class
CategoryTableSeeder
4
extends
DatabaseSeeder
5
{
6
public
function
run
()
7
{
8
$faker
=
$this
->
getFaker
();
9
10
for
(
$i
=
0
;
$i
<
10
;
$i
++
)
11
{
12
$name
=
ucwords
(
$faker
->
word
);
13
14
Category
::
create
([
15
"name"
=>
$name
16
]);
17
}
18
}
19
}
1
<?
php
2
3
class
ProductTableSeeder
4
extends
DatabaseSeeder
5
{
6
public
function
run
()
7
{
8
$faker
=
$this
->
getFaker
();
9
10
$categories
=
Category
::
all
();
11
12
foreach
(
$categories
as
$category
)
13
{
14
for
(
$i
=
0
;
$i
<
rand
(
-
1
,
10
);
$i
++
)
15
{
16
$name
=
ucwords
(
$faker
->
word
);
17
$stock
=
$faker
->
randomNumber
(
0
,
100
);
18
$price
=
$faker
->
randomFloat
(
2
,
5
,
100
);
19
20
Product
::
create
([
21
"name"
=>
$name
,
22
"stock"
=>
$stock
,
23
"price"
=>
$price
,
24
"category_id"
=>
$category
->
id
25
]);
26
}
27
}
28
}
29
}
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.
1
<?
php
2
3
class
OrderTableSeeder
4
extends
DatabaseSeeder
5
{
6
public
function
run
()
7
{
8
$faker
=
$this
->
getFaker
();
9
10
$accounts
=
Account
::
all
();
11
12
foreach
(
$accounts
as
$account
)
13
{
14
for
(
$i
=
0
;
$i
<
rand
(
-
1
,
5
);
$i
++
)
15
{
16
Order
::
create
([
17
"account_id"
=>
$account
->
id
18
]);
19
}
20
}
21
}
22
}
1
<?
php
2
3
class
OrderItemTableSeeder
4
extends
DatabaseSeeder
5
{
6
public
function
run
()
7
{
8
$faker
=
$this
->
getFaker
();
9
10
$orders
=
Order
::
all
();
11
$products
=
Product
::
all
()
->
toArray
();
12
13
foreach
(
$orders
as
$order
)
14
{
15
$used
=
[];
16
17
for
(
$i
=
0
;
$i
<
rand
(
1
,
5
);
$i
++
)
18
{
19
$product
=
$faker
->
randomElement
(
$products
);
20
21
if
(
!
in_array
(
$product
[
"id"
],
$used
))
22
{
23
$id
=
$product
[
"id"
];
24
$price
=
$product
[
"price"
];
25
$quantity
=
$faker
->
randomNumber
(
1
,
3
);
26
27
OrderItem
::
create
([
28
"order_id"
=>
$order
->
id
,
29
"product_id"
=>
$id
,
30
"price"
=>
$price
,
31
"quantity"
=>
$quantity
32
]);
33
34
$used
[]
=
$product
[
"id"
];
35
}
36
}
37
}
38
}
39
}
1
public
function
run
()
2
{
3
$this
->
call
(
"AccountTableSeeder"
);
4
$this
->
call
(
"CategoryTableSeeder"
);
5
$this
->
call
(
"ProductTableSeeder"
);
6
$this
->
call
(
"OrderTableSeeder"
);
7
$this
->
call
(
"OrderItemTableSeeder"
);
8
}
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…
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.
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:
1
<?
php
2
3
class
CategoryController
4
extends
BaseController
5
{
6
public
function
indexAction
()
7
{
8
return
Category
::
with
([
"products"
])
->
get
();
9
}
10
}
1
<?
php
2
3
class
ProductController
4
extends
BaseController
5
{
6
public
function
indexAction
()
7
{
8
$query
=
Product
::
with
([
"category"
]);
9
$category
=
Input
::
get
(
"category"
);
10
11
if
(
$category
)
12
{
13
$query
->
where
(
"category_id"
,
$category
);
14
}
15
16
return
$query
->
get
();
17
}
18
}
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:
1
Route
::
any
(
"category/index"
,
[
2
"as"
=>
"category/index"
,
3
"uses"
=>
"CategoryController@indexAction"
4
]);
5
6
Route
::
any
(
"product/index"
,
[
7
"as"
=>
"product/index"
,
8
"uses"
=>
"ProductController@indexAction"
9
]);
For users to be able to buy products, they will need to log in. We’ve added some users to the database, via the UserTableSeeder class, but we should create an authentication endpoint:
1
<?
php
2
3
class
AccountController
4
extends
BaseController
5
{
6
public
function
authenticateAction
()
7
{
8
$credentials
=
[
9
"email"
=>
Input
::
get
(
"email"
),
10
"password"
=>
Input
::
get
(
"password"
)
11
];
12
13
if
(
Auth
::
attempt
(
$credentials
))
14
{
15
return
Response
::
json
([
16
"status"
=>
"ok"
,
17
"account"
=>
Auth
::
user
()
->
toArray
()
18
]);
19
}
20
21
return
Response
::
json
([
22
"status"
=>
"error"
23
]);
24
}
25
}
We’ll also need to add a route for this:
1
Route
::
any
(
"account/authenticate"
,
[
2
"as"
=>
"account/authenticate"
,
3
"uses"
=>
"AccountController@authenticateAction"
4
]);
It should now be possible to determine whether login credentials are legitimate; through the browser:
1
/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.
Orders are slightly more complicated. We will need to be able to get all orders as well as orders by account. We’ll also need to create new orders.
Let’s begin by getting the orders:
1
<?
php
2
3
class
OrderController
4
extends
BaseController
5
{
6
public
function
indexAction
()
7
{
8
$query
=
Order
::
with
([
9
"account"
,
10
"orderItems"
,
11
"orderItems.product"
,
12
"orderItems.product.category"
13
]);
14
15
$account
=
Input
::
get
(
"account"
);
16
17
if
(
$account
)
18
{
19
$query
->
where
(
"account_id"
,
$account
);
20
}
21
22
return
$query
->
get
();
23
}
24
}
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:
1
Route
::
any
(
"order/index"
,
[
2
"as"
=>
"order/index"
,
3
"uses"
=>
"OrderController@indexAction"
4
]);
We’ll deal with creating orders once we have the shopping and payment interfaces completed. Let’s not get ahead of ourselves…
Will the API in place; we can begin the interface work. We’re using AngularJS, which creates rich interfaces from ordinary HTML.
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:
1
<!doctype html>
2
<html
lang=
"en"
>
3
<head>
4
<meta
charset=
"utf-8"
/>
5
<title>
Laravel 4 E-Commerce</title>
6
<link
7
type=
"text/css"
8
rel=
"stylesheet"
9
href=
"{{ asset("
css
/
bootstrap
.
3
.
0
.
3
.
min
.
css
")
}}"
10
/>
11
<link
12
type=
"text/css"
13
rel=
"stylesheet"
14
href=
"{{ asset("
css
/
bootstrap
.
theme
.
3
.
0
.
3
.
min
.
css
")
}}"
15
/>
16
<link
17
type=
"text/css"
18
rel=
"stylesheet"
19
href=
"{{ asset("
css
/
shared
.
css
")
}}"
20
/>
21
<script
22
type=
"text/javascript"
23
src=
"{{ asset("
js
/
angularjs
.
1
.
2
.
4
.
min
.
js
")
}}"
24
></script>
25
<script
26
type=
"text/javascript"
27
src=
"{{ asset("
js
/
angularjs
.
cookies
.
1
.
2
.
4
.
min
.
js
")
}}"
28
></script>
29
</head>
30
<body>
31
<div
class=
"container"
>
32
<div
class=
"row"
>
33
<div
class=
"col-md-12"
>
34
<h1>
35
Laravel 4 E-Commerce 36
</h1>
37
</div>
38
</div>
39
<div
class=
"row"
>
40
<div
class=
"col-md-8"
>
41
<h2>
42
Products 43
</h2>
44
<div
class=
"categories btn-group"
>
45
<button
46
type=
"button"
47
class=
"category btn btn-default active"
48
>
49
All 50
</button>
51
<button
52
type=
"button"
53
class=
"category btn btn-default"
54
>
55
Category 1 56
</button>
57
<button
58
type=
"button"
59
class=
"category btn btn-default"
60
>
61
Category 2 62
</button>
63
<button
64
type=
"button"
65
class=
"category btn btn-default"
66
>
67
Category 3 68
</button>
69
</div>
70
<div
class=
"products"
>
71
<div
class=
"product media"
>
72
<button
73
type=
"button"
74
class=
"pull-left btn btn-default"
75
>
76
Add to basket 77
</button>
78
<div
class=
"media-body"
>
79
<h4
class=
"media-heading"
>
Product 1</h4>
80
<p>
81
Price: 9.99, Stock: 10 82
</p>
83
</div>
84
</div>
85
<div
class=
"product media"
>
86
<button
87
type=
"button"
88
class=
"pull-left btn btn-default"
89
>
90
Add to basket 91
</button>
92
<div
class=
"media-body"
>
93
<h4
class=
"media-heading"
>
Product 2</h4>
94
<p>
95
Price: 9.99, Stock: 10 96
</p>
97
</div>
98
</div>
99
<div
class=
"product media"
>
100
<button
101
type=
"button"
102
class=
"pull-left btn btn-default"
103
>
104
Add to basket 105
</button>
106
<div
class=
"media-body"
>
107
<h4
class=
"media-heading"
>
Product 3</h4>
108
<p>
109
Price: 9.99, Stock: 10 110
</p>
111
</div>
112
</div>
113
</div>
114
</div>
115
<div
class=
"col-md-4"
>
116
<h2>
117
Basket 118
</h2>
119
<form
class=
"basket"
>
120
<table
class=
"table"
>
121
<tr
class=
"product"
>
122
<td
class=
"name"
>
123
Product 1 124
</td>
125
<td
class=
"quantity"
>
126
<input
127
class=
"quantity form-control col-md-2"
128
type=
"number"
129
value=
"1"
130
/>
131
</td>
132
<td
class=
"product"
>
133
9.99 134
</td>
135
<td
class=
"product"
>
136
<a
137
class=
"remove glyphicon glyphicon-remove"
138
href=
"#"
139
></a>
140
</td>
141
</tr>
142
<tr
class=
"product"
>
143
<td
class=
"name"
>
144
Product 2 145
</td>
146
<td
class=
"quantity"
>
147
<input
148
class=
"quantity form-control col-md-2"
149
type=
"number"
150
value=
"1"
151
/>
152
</td>
153
<td
class=
"product"
>
154
9.99 155
</td>
156
<td
class=
"product"
>
157
<a
158
class=
"remove glyphicon glyphicon-remove"
159
href=
"#"
160
></a>
161
</td>
162
</tr>
163
<tr
class=
"product"
>
164
<td
class=
"name"
>
165
Product 3 166
</td>
167
<td
class=
"quantity"
>
168
<input
169
class=
"quantity form-control col-md-2"
170
type=
"number"
171
value=
"1"
172
/>
173
</td>
174
<td
class=
"product"
>
175
9.99 176
</td>
177
<td
class=
"product"
>
178
<a
179
class=
"remove glyphicon glyphicon-remove"
180
href=
"#"
181
></a>
182
</td>
183
</tr>
184
</table>
185
</form>
186
</div>
187
</div>
188
</div>
189
<script
190
type=
"text/javascript"
191
src=
"{{ asset("
js
/
shared
.
js
")
}}"
192
></script>
193
</body>
194
</html>
You’ll notice that we also reference a shared.css file:
1
.products
{
2
margin-top
:
20px
;
3
}
4
.basket
td
{
5
vertical-align
:
middle
!important
;
6
}
7
.basket
.quantity
input
{
8
width
:
50px
;
9
}
These changes to the view coincide with a modified IndexController:
1
<?
php
2
3
class
IndexController
4
extends
BaseController
5
{
6
public
function
indexAction
()
7
{
8
return
View
::
make
(
"index"
);
9
}
10
}
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!
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:
1
<body
ng-controller=
"main"
>
1
<div
class=
"col-md-8"
ng-controller=
"products"
>
1
<div
class=
"col-md-4"
ng-controller=
"basket"
>
1
<script
2
type=
"text/javascript"
3
src=
"{{ asset("
js
/
shared
.
js
")
}}"
4
></script>
In addition to these modifications, we should also create the shared.js file:
1
var
app
=
angular
.
module
(
"app"
,
[
"ngCookies"
]);
2
3
app
.
controller
(
"main"
,
function
(
$scope
)
{
4
console
.
log
(
"main.init"
);
5
6
this
.
shared
=
"hello world"
;
7
8
$scope
.
main
=
this
;
9
});
10
11
app
.
controller
(
"products"
,
function
(
$scope
)
{
12
console
.
log
(
"products.init:"
,
$scope
.
main
.
shared
);
13
14
$scope
.
products
=
this
;
15
});
16
17
app
.
controller
(
"basket"
,
function
(
$scope
)
{
18
console
.
log
(
"basket.init:"
,
$scope
.
main
.
shared
);
19
20
$scope
.
basket
=
this
;
21
});
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.
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.
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).
1
app
.
factory
(
"CategoryService"
,
function
(
$http
)
{
2
return
{
3
"getCategories"
:
function
()
{
4
return
$http
.
get
(
"/category/index"
);
5
}
6
};
7
});
8
9
app
.
factory
(
"ProductService"
,
function
(
$http
)
{
10
return
{
11
"getProducts"
:
function
()
{
12
return
$http
.
get
(
"/product/index"
);
13
}
14
};
15
});
16
17
app
.
controller
(
"products"
,
function
(
18
$scope
,
19
CategoryService
,
20
ProductService
21
)
{
22
23
var
self
=
this
;
24
var
categories
=
CategoryService
.
getCategories
();
25
26
categories
.
success
(
function
(
data
)
{
27
self
.
categories
=
data
;
28
});
29
30
var
products
=
ProductService
.
getProducts
();
31
32
products
.
success
(
function
(
data
)
{
33
self
.
products
=
data
;
34
});
35
36
$scope
.
products
=
this
;
37
38
});
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:
1
<div
class=
"col-md-8"
ng-controller=
"products"
>
2
<h2>
3
Products 4
</h2>
5
<div
class=
"categories btn-group"
>
6
<button
7
type=
"button"
8
class=
"category btn btn-default active"
9
>
10
All 11
</button>
12
<button
13
type=
"button"
14
class=
"category btn btn-default"
15
ng-repeat=
"category in products.categories"
16
>
17
@{{ category.name }} 18
</button>
19
</div>
20
<div
class=
"products"
>
21
<div
22
class=
"product media"
23
ng-repeat=
"product in products.products"
24
>
25
<button
26
type=
"button"
27
class=
"pull-left btn btn-default"
28
>
29
Add to basket 30
</button>
31
<div
class=
"media-body"
>
32
<h4
class=
"media-heading"
>
@{{ product.name }}</h4>
33
<p>
34
Price: @{{ product.price }}, Stock: @{{ product.stock }} 35
</p>
36
</div>
37
</div>
38
</div>
39
</div>
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.
1
<button
2
type=
"button"
3
class=
"category btn btn-default active"
4
ng-click=
"products.setCategory(null)"
5
ng-class=
"{ 'active' : products.category == null }"
6
>
7
All 8
</button>
9
<button
10
type=
"button"
11
class=
"category btn btn-default"
12
ng-repeat=
"category in products.categories"
13
ng-click=
"products.setCategory(category)"
14
ng-class=
"{ 'active' : products.category.id == category.id }"
15
>
16
@{{ category.name }} 17
</button>
18
</div>
19
<div
class=
"products"
>
20
<div
21
class=
"product media"
22
ng-repeat=
"product in products.products | filter : products.filterByCategory"
23
>
We’ve added three new concepts here:
These directives, in isolation, will only cause errors. We need to add the JavaScript logic to back them up:
1
app
.
controller
(
"products"
,
function
(
2
$scope
,
3
CategoryService
,
4
ProductService
5
)
{
6
7
var
self
=
this
;
8
9
// ...
10
11
this
.
category
=
null
;
12
13
this
.
filterByCategory
=
function
(
product
)
{
14
15
if
(
self
.
category
!==
null
)
{
16
return
product
.
category
.
id
===
self
.
category
.
id
;
17
}
18
19
return
true
;
20
21
};
22
23
this
.
setCategory
=
function
(
category
)
{
24
self
.
category
=
category
;
25
};
26
27
// ...
28
29
});
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.
1
app
.
factory
(
"BasketService"
,
function
(
$cookies
)
{
2
3
var
products
=
JSON
.
parse
(
$cookies
.
products
||
"[]"
);
4
5
return
{
6
7
"getProducts"
:
function
()
{
8
return
products
;
9
},
10
11
"add"
:
function
(
product
)
{
12
13
products
.
push
({
14
"id"
:
product
.
id
,
15
"name"
:
product
.
name
,
16
"price"
:
product
.
price
,
17
"total"
:
product
.
price
*
1
,
18
"quantity"
:
1
19
});
20
21
this
.
store
();
22
23
},
24
25
"remove"
:
function
(
product
)
{
26
27
for
(
var
i
=
0
;
i
<
products
.
length
;
i
++
)
{
28
29
var
next
=
products
[
i
];
30
31
if
(
next
.
id
==
product
.
id
)
{
32
products
.
splice
(
i
,
1
);
33
}
34
35
}
36
37
this
.
store
();
38
39
},
40
41
"update"
:
function
()
{
42
43
for
(
var
i
=
0
;
i
<
products
.
length
;
i
++
)
{
44
45
var
product
=
products
[
i
];
46
var
raw
=
product
.
quantity
*
product
.
price
;
47
48
product
.
total
=
Math
.
round
(
raw
*
100
)
/
100
;
49
50
}
51
52
this
.
store
();
53
54
},
55
56
"store"
:
function
()
{
57
$cookies
.
products
=
JSON
.
stringify
(
products
);
58
},
59
60
"clear"
:
function
()
{
61
products
.
length
=
0
;
62
this
.
store
();
63
}
64
65
};
66
67
});
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:
1
<div
class=
"col-md-4"
ng-controller=
"basket"
>
2
<h2>
3
Basket 4
</h2>
5
<form
class=
"basket"
>
6
<table
class=
"table"
>
7
<tr
8
class=
"product"
9
ng-repeat=
"product in basket.products track by $index"
10
>
11
<td
class=
"name"
>
12
@{{ product.name }} 13
</td>
14
<td
class=
"quantity"
>
15
<input
16
class=
"quantity form-control col-md-2"
17
type=
"number"
18
ng-model=
"product.quantity"
19
ng-change=
"basket.update()"
20
min=
"1"
21
/>
22
</td>
23
<td
class=
"product"
>
24
@{{ product.total }} 25
</td>
26
<td
class=
"product"
>
27
<a
28
class=
"remove glyphicon glyphicon-remove"
29
ng-click=
"basket.remove(product)"
30
></a>
31
</td>
32
</tr>
33
</table>
34
</form>
35
</div>
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.
We need to be able to remove the basket items, also. Let’s modify the JavaScript/HTML to allow for this:
1
app
.
controller
(
"products"
,
function
(
2
$scope
,
3
CategoryService
,
4
ProductService
,
5
BasketService
6
)
{
7
8
// ...
9
10
this
.
addToBasket
=
function
(
product
)
{
11
BasketService
.
add
(
product
);
12
};
13
14
// ...
15
16
});
1
<button
2
type=
"button"
3
class=
"pull-left btn btn-default"
4
ng-click=
"products.addToBasket(product)"
5
>
6
Add to basket 7
</button>
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.
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…
1
app
.
factory
(
"AccountService"
,
function
(
$http
)
{
2
3
var
account
=
null
;
4
5
return
{
6
"authenticate"
:
function
(
email
,
password
)
{
7
8
var
request
=
$http
.
post
(
"/account/authenticate"
,
{
9
"email"
:
email
,
10
"password"
:
password
11
});
12
13
request
.
success
(
function
(
data
)
{
14
if
(
data
.
status
!==
"error"
)
{
15
account
=
data
.
account
;
16
}
17
});
18
19
return
request
;
20
21
},
22
"getAccount"
:
function
()
{
23
return
account
;
24
}
25
};
26
});
27
28
app
.
factory
(
"OrderService"
,
function
(
29
$http
,
30
AccountService
,
31
BasketService
32
)
{
33
return
{
34
"pay"
:
function
(
number
,
expiry
,
security
)
{
35
36
var
account
=
AccountService
.
getAccount
();
37
var
products
=
BasketService
.
getProducts
();
38
var
items
=
[];
39
40
for
(
var
i
=
0
;
i
<
products
.
length
;
i
++
)
{
41
42
var
product
=
products
[
i
];
43
44
items
.
push
({
45
"product_id"
:
product
.
id
,
46
"quantity"
:
product
.
quantity
47
});
48
49
}
50
51
return
$http
.
post
(
"/order/add"
,
{
52
"account"
:
account
.
id
,
53
"items"
:
JSON
.
stringify
(
items
),
54
"number"
:
number
,
55
"expiry"
:
expiry
,
56
"security"
:
security
57
});
58
}
59
};
60
});
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:
1
app
.
controller
(
"basket"
,
function
(
2
$scope
,
3
AccountService
,
4
BasketService
,
5
OrderService
6
)
{
7
8
// ...
9
10
this
.
state
=
"shopping"
;
11
this
.
email
=
""
;
12
this
.
password
=
""
;
13
this
.
number
=
""
;
14
this
.
expiry
=
""
;
15
this
.
secutiry
=
""
;
16
17
this
.
authenticate
=
function
()
{
18
19
var
details
=
AccountService
.
authenticate
(
self
.
email
,
self
.
password
);
20
21
details
.
success
(
function
(
data
)
{
22
if
(
data
.
status
==
"ok"
)
{
23
self
.
state
=
"paying"
;
24
}
25
});
26
27
}
28
29
this
.
pay
=
function
()
{
30
31
var
details
=
OrderService
.
pay
(
32
self
.
number
,
33
self
.
expiry
,
34
self
.
security
35
);
36
37
details
.
success
(
function
(
data
)
{
38
BasketService
.
clear
();
39
self
.
state
=
"shopping"
;
40
});
41
42
}
43
44
// ...
45
46
});
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:
1
<div
class=
"col-md-4"
ng-controller=
"basket"
>
2
<h2>
3
Basket 4
</h2>
5
<form
class=
"basket"
>
6
<table
class=
"table"
>
7
<tr
8
class=
"product"
9
ng-repeat=
"product in basket.products track by $index"
10
ng-class=
"{ 'hide' : basket.state != 'shopping' }"
11
>
12
<td
class=
"name"
>
13
@{{ product.name }} 14
</td>
15
<td
class=
"quantity"
>
16
<input
17
class=
"form-control"
18
type=
"number"
19
ng-model=
"product.quantity"
20
ng-change=
"basket.update()"
21
min=
"1"
22
/>
23
</td>
24
<td
class=
"product"
>
25
@{{ product.total }} 26
</td>
27
<td
class=
"product"
>
28
<a
29
class=
"remove glyphicon glyphicon-remove"
30
ng-click=
"basket.remove(product)"
31
></a>
32
</td>
33
</tr>
34
<tr>
35
<td
36
colspan=
"4"
37
ng-class=
"{ 'hide' : basket.state != 'shopping' }"
38
>
39
<input
40
type=
"text"
41
class=
"form-control"
42
placeholder=
"email"
43
ng-model=
"basket.email"
44
/>
45
</td>
46
</tr>
47
<tr>
48
<td
49
colspan=
"4"
50
ng-class=
"{ 'hide' : basket.state != 'shopping' }"
51
>
52
<input
53
type=
"password"
54
class=
"form-control"
55
placeholder=
"password"
56
ng-model=
"basket.password"
57
/>
58
</td>
59
</tr>
60
<tr>
61
<td
62
colspan=
"4"
63
ng-class=
"{ 'hide' : basket.state != 'shopping' }"
64
>
65
<button
66
type=
"button"
67
class=
"pull-left btn btn-default"
68
ng-click=
"basket.authenticate()"
69
>
70
Authenticate 71
</button>
72
</td>
73
</tr>
74
<tr>
75
<td
76
colspan=
"4"
77
ng-class=
"{ 'hide' : basket.state != 'paying' }"
78
>
79
<input
80
type=
"text"
81
class=
"form-control"
82
placeholder=
"card number"
83
ng-model=
"basket.number"
84
/>
85
</td>
86
</tr>
87
<tr>
88
<td
89
colspan=
"4"
90
ng-class=
"{ 'hide' : basket.state != 'paying' }"
91
>
92
<input
93
type=
"text"
94
class=
"form-control"
95
placeholder=
"expiry"
96
ng-model=
"basket.expiry"
97
/>
98
</td>
99
</tr>
100
<tr>
101
<td
102
colspan=
"4"
103
ng-class=
"{ 'hide' : basket.state != 'paying' }"
104
>
105
<input
106
type=
"text"
107
class=
"form-control"
108
placeholder=
"security number"
109
ng-model=
"basket.security"
110
/>
111
</td>
112
</tr>
113
<tr>
114
<td
115
colspan=
"4"
116
ng-class=
"{ 'hide' : basket.state != 'paying' }"
117
>
118
<button
119
type=
"button"
120
class=
"pull-left btn btn-default"
121
ng-click=
"basket.pay()"
122
>
123
Pay 124
</button>
125
</td>
126
</tr>
127
</table>
128
</form>
129
</div>
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.
Finally; we need to tie this into our OrderController, where the orders are completed and the payments are processed…
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 mime, or the docs.
Before we start hitting Stripe up; we should create the endpoint for creating orders:
1
public
function
addAction
()
2
{
3
$validator
=
Validator
::
make
(
Input
::
all
(),
[
4
"account"
=>
"required|exists:account,id"
,
5
"items"
=>
"required"
6
]);
7
8
if
(
$validator
->
passes
())
9
{
10
$order
=
Order
::
create
([
11
"account_id"
=>
Input
::
get
(
"account"
)
12
]);
13
14
try
15
{
16
$items
=
json_decode
(
Input
::
get
(
"items"
));
17
}
18
catch
(
Exception
$e
)
19
{
20
return
Response
::
json
([
21
"status"
=>
"error"
,
22
"errors"
=>
[
23
"items"
=>
[
24
"Invalid items format."
25
]
26
]
27
]);
28
}
29
30
$total
=
0
;
31
32
foreach
(
$items
as
$item
)
33
{
34
$orderItem
=
OrderItem
::
create
([
35
"order_id"
=>
$order
->
id
,
36
"product_id"
=>
$item
->
product_id
,
37
"quantity"
=>
$item
->
quantity
38
]);
39
40
$product
=
$orderItem
->
product
;
41
42
$orderItem
->
price
=
$product
->
price
;
43
$orderItem
->
save
();
44
45
$product
->
stock
-=
$item
->
quantity
;
46
$product
->
save
();
47
48
$total
+=
$orderItem
->
quantity
*
$orderItem
->
price
;
49
}
50
51
$result
=
$this
->
gateway
->
pay
(
52
Input
::
get
(
"number"
),
53
Input
::
get
(
"expiry"
),
54
$total
,
55
"usd"
56
);
57
58
if
(
!
$result
)
59
{
60
return
Response
::
json
([
61
"status"
=>
"error"
,
62
"errors"
=>
[
63
"gateway"
=>
[
64
"Payment error"
65
]
66
]
67
]);
68
}
69
70
$account
=
$order
->
account
;
71
72
$document
=
$this
->
document
->
create
(
$order
);
73
$this
->
messenger
->
send
(
$order
,
$document
);
74
75
return
Response
::
json
([
76
"status"
=>
"ok"
,
77
"order"
=>
$order
->
toArray
()
78
]);
79
}
80
81
return
Response
::
json
([
82
"status"
=>
"error"
,
83
"errors"
=>
$validator
->
errors
()
->
toArray
()
84
]);
85
}
There are a few steps taking place here:
The main purpose of this endpoint is to create the order (and order items) while passing the payment off to the service provider classes.
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 chapter which covered this); and here is a list of the changes:
1
"providers"
=>
array
(
2
3
// ...
4
5
"Formativ\Billing\BillingServiceProvider"
6
7
),
1
"autoload"
:
{
2
3
// ...
4
5
"psr-0"
:
{
6
"Formativ\\Billing"
:
"workbench/formativ/billing/src/"
7
}
8
}
1
<?
php
2
3
namespace
Formativ\Billing
;
4
5
use
App
;
6
use
Illuminate\Support\ServiceProvider
;
7
8
class
BillingServiceProvider
9
extends
ServiceProvider
10
{
11
protected
$defer
=
true
;
12
13
public
function
register
()
14
{
15
App
::
bind
(
"billing.stripeGateway"
,
function
()
{
16
return
new
StripeGateway
();
17
});
18
19
App
::
bind
(
"billing.pdfDocument"
,
function
()
{
20
return
new
PDFDocument
();
21
});
22
23
App
::
bind
(
"billing.emailMessenger"
,
function
()
{
24
return
new
EmailMessenger
();
25
});
26
}
27
28
public
function
provides
()
29
{
30
return
[
31
"billing.stripeGateway"
,
32
"billing.pdfDocument"
,
33
"billing.emailMessenger"
34
];
35
}
36
}
1
<?
php
2
3
namespace
Formativ\Billing
;
4
5
interface
GatewayInterface
6
{
7
public
function
pay
(
8
$number
,
9
$expiry
,
10
$amount
,
11
$currency
12
);
13
}
1
<?
php
2
3
namespace
Formativ\Billing
;
4
5
interface
DocumentInterface
6
{
7
public
function
create
(
$order
);
8
}
1
<?
php
2
3
namespace
Formativ\Billing
;
4
5
interface
MessengerInterface
6
{
7
public
function
send
(
8
$order
,
9
$document
10
);
11
}
These are all the interfaces (what some might consider the scaffolding logic) that we need. Let’s make some 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:
1
<?
php
2
3
namespace
Formativ\Billing
;
4
5
use
Stripe
;
6
use
Stripe_Charge
;
7
8
class
StripeGateway
9
implements
GatewayInterface
10
{
11
public
function
pay
(
12
$number
,
13
$expiry
,
14
$amount
,
15
$currency
16
)
17
{
18
Stripe
::
setApiKey
(
"..."
);
19
20
$expiry
=
explode
(
"/"
,
$expiry
);
21
22
try
23
{
24
$charge
=
Stripe_Charge
::
create
([
25
"card"
=>
[
26
"number"
=>
$number
,
27
"exp_month"
=>
$expiry
[
0
],
28
"exp_year"
=>
$expiry
[
1
]
29
],
30
"amount"
=>
round
(
$amount
*
100
),
31
"currency"
=>
$currency
32
]);
33
34
return
true
;
35
}
36
catch
(
Exception
$e
)
37
{
38
return
false
;
39
}
40
}
41
}
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.
The last thing left to do is generate and email the invoice. We’ll begin with the PDF generation, using DOMPDF and ordinary views:
1
public
function
getTotalAttribute
()
2
{
3
return
$this
->
quantity
*
$this
->
price
;
4
}
1
public
function
getTotalAttribute
()
2
{
3
$total
=
0
;
4
5
foreach
(
$this
->
orderItems
as
$orderItem
)
6
{
7
$total
+=
$orderItem
->
price
*
$orderItem
->
quantity
;
8
}
9
10
return
$total
;
11
}
1
<!doctype html>
2
<html
lang=
"en"
>
3
<head>
4
<meta
charset=
"utf-8"
/>
5
<title>
Laravel 4 E-Commerce</title>
6
<style
type=
"text/css"
>
7
8
body
{
9
padding
:
25px
0
;
10
font-family
:
Helvetica
;
11
}
12
13
td
{
14
padding
:
0
10px
0
0
;
15
}
16
17
*
{
18
float
:
none
;
19
}
20
21
</style>
22
</head>
23
<body>
24
<div
class=
"container"
>
25
<div
class=
"row"
>
26
<div
class=
"col-md-8"
>
27
28
</div>
29
<div
class=
"col-md-4 well"
>
30
<table>
31
<tr>
32
<td
class=
"pull-right"
>
33
<strong>
Account</strong>
34
</td>
35
<td>
36
{{ $order->account->email }} 37
</td>
38
</tr>
39
<tr>
40
<td
class=
"pull-right"
>
41
<strong>
Date</strong>
42
</td>
43
<td>
44
{{ $order->created_at->format("F jS, Y"); }} 45
</td>
46
</tr>
47
</table>
48
</div>
49
</div>
50
<div
class=
"row"
>
51
<div
class=
"col-md-12"
>
52
<h2>
Invoice {{ $order->id }}</h2>
53
</div>
54
</div>
55
<div
class=
"row"
>
56
<div
class=
"col-md-12"
>
57
<table
class=
"table table-striped"
>
58
<thead>
59
<tr>
60
<th>
Product</th>
61
<th>
Quantity</th>
62
<th>
Amount</th>
63
</tr>
64
</thead>
65
<tbody>
66
@foreach ($order->orderItems as $orderItem) 67
<tr>
68
<td>
69
{{ $orderItem->product->name }} 70
</td>
71
<td>
72
{{ $orderItem->quantity }} 73
</td>
74
<td>
75
$ {{ number_format($orderItem->total, 2) }} 76
</td>
77
</tr>
78
@endforeach 79
<tr>
80
<td>
</td>
81
<td>
82
<strong>
Total</strong>
83
</td>
84
<td>
85
<strong>
$ {{ number_format($order->total, 2) }}</strong>
86
</td>
87
</tr>
88
</tbody>
89
</table>
90
</div>
91
</div>
92
</div>
93
</body>
94
</html>
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:
1
<?
php
2
3
namespace
Formativ\Billing
;
4
5
class
PDFDocument
6
implements
DocumentInterface
7
{
8
public
function
create
(
$order
)
9
{
10
$view
=
View
::
make
(
"email/invoice"
,
[
11
"order"
=>
$order
12
]);
13
14
define
(
"DOMPDF_ENABLE_AUTOLOAD"
,
false
);
15
16
require_once
base_path
()
.
"/vendor/dompdf/dompdf/dompdf_config.inc.php"
;
17
18
$dompdf
=
new
DOMPDF
();
19
$dompdf
->
load_html
(
$view
);
20
$dompdf
->
set_paper
(
"a4"
,
"portrait"
);
21
22
$dompdf
->
render
();
23
$results
=
$dompdf
->
output
();
24
25
$temp
=
storage_path
()
.
"/order-"
.
$order
->
id
.
".pdf"
;
26
file_put_contents
(
$temp
,
$results
);
27
28
return
$temp
;
29
}
30
}
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.
Last thing to hook up is the MessengerInterface implementation:
1
Here's your invoice!
1
<?
php
2
3
namespace
Formativ\Billing
;
4
5
use
Mail
;
6
7
class
EmailMessenger
8
implements
MessengerInterface
9
{
10
public
function
send
(
11
$order
,
12
$document
13
)
14
{
15
Mail
::
send
(
"email/wrapper"
,
[],
function
(
$message
)
use
(
$order
,
$document
)
16
{
17
$message
->
subject
(
"Your invoice!"
);
18
$message
->
from
(
"[email protected]"
,
"Laravel 4 E-Commerce"
);
19
$message
->
to
(
$order
->
account
->
email
);
20
21
$message
->
attach
(
$document
,
[
22
"as"
=>
"Invoice "
.
$order
->
id
,
23
"mime"
=>
"application/pdf"
24
]);
25
});
26
}
27
}
The EmailMessenger class sends a simple email to the account, attaching the PDF invoice along the way.