Книга: Laravel 4 Cookbook
Назад: Multisites
Дальше: Embedded Systems

E-Commerce

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.

The code for this chapter can be found at: https://github.com/formativ/tutorial-laravel-4-e-commerce.

Note on Sanity

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.

Getting Started

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.

You need to understand a bit of JavaScript for this chapter. 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:

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.

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:

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.

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):

 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.

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:

1 "require" : { 2   "dompdf/dompdf" : "dev-master" 3 } 

This was extracted from composer.json.

Obviously you’re going to have other dependencies in your composer.js 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:

1 "require" : { 2   "stripe/stripe-php" : "dev-master" 3 } 

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:

1 "require" : { 2   "fzaninotto/faker" : "dev-master" 3 } 

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:

1 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:

 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 } 

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.

 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 } 

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

 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 } 

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.

 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 } 

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

 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 } 

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 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 } 

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

 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 } 

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

 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 } 

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

 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 } 

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

 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 } 

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 being 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.

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 } 

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

 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 } 

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

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 

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:

 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 ]; 

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

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 ])); 

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:

 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 } 

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.

 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 } 

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

 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 } 

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.

 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 } 

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

 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 } 

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

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 } 

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:

 1 <?php  2   3 class CategoryController  4 extends BaseController  5 {  6   public function indexAction()  7   {  8     return Category::with(["products"])->get();  9   } 10 } 

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

 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 } 

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:

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 ]); 

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 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 } 

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

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

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

This was extracted from app/routes.php.

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.

Managing Orders

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 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:

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

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:

  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> 

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

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 } 

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

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 } 

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 chapter, 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:

1 <body ng-controller="main"> 

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

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

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

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

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

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

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

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

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 }); 

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).

 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 }); 

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:

 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> 

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.

 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   > 

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:

 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 }); 

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.

 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 }); 

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:

 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> 

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:

 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 }); 

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

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> 

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…

 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 }); 

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:

 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 }); 

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:

  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> 

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 mime, or the docs.

Creating Orders

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 } 

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 chapter which covered this); and here is a list of the changes:

1 "providers" => array( 2  3   // ... 4  5   "Formativ\Billing\BillingServiceProvider" 6  7 ), 

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

1 "autoload" : { 2  3   // ... 4  5   "psr-0": { 6     "Formativ\\Billing": "workbench/formativ/billing/src/" 7   } 8 } 

This was extracted from composer.json.

 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 } 

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

 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 } 

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

1 <?php 2  3 namespace Formativ\Billing; 4  5 interface DocumentInterface 6 { 7   public function create($order); 8 } 

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

 1 <?php  2   3 namespace Formativ\Billing;  4   5 interface MessengerInterface  6 {  7   public function send(  8     $order,  9     $document 10   ); 11 } 

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:

 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 } 

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:

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

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

 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 } 

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.

 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>&nbsp;</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 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:

 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 } 

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:

1 Here's your invoice! 

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

 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 } 

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.

Назад: Multisites
Дальше: Embedded Systems