64x64 icon dark hosting
Get a Tuts+ subscription for just $45! Deploy New Relic now to claim.
Advertisement

How to Sell Digital Goods with CodeIgniter

by

In today's tutorial, you'll learn how to create a small web app to sell digital items (eg. eBooks) securely and accept payments from PayPal.
Please note that this tutorial requires PHP 5. Please upgrade your PHP installation if you're still running version 4.


Getting Started

Download the latest CodeIgniter release to your server, and set up your MySQL database (I named it digitalgoods) with the following SQL queries:

The first query creates CodeIgniter's default user sessions table. We then create a table to log file downloads, one to store the items and another to store purchase details. Finally, we insert a couple of items into the table.

We've inserted two items into the database, so we have to create those files on the server. In the root directory for your application (the same folder as CodeIgniter's system directory), create a new directory named files:


In that directory, create two text files, named UNIX and CHMOD.txt and Intro to 8086 Programming.txt. The capital letters are important on most web servers. These are the file names we set in the database for our two items. Enter some content in the files so we can be sure the files are being downloaded correctly.

CodeIgniter Configuration

In the system/application/config/database.php file, set your database settings in the fields provided:

In config/autoload.php set the following libraries and helpers:

In config/config.php set the base_url:

In the same file, paste the following to create our own configuration settings:

Set each of the new config options to your desired settings, but keep 'Download Limit' disabled for now.

Finally, under config/routes.php set the default controller:


View All Items

Now to create the main controller for the site, create a file in the controllers directory named items.php, and inside enter:

In the constructor we've loaded in the 'items_model' model (which we've named 'Item') which we'll create next, and placed the 'site_name' configuration setting into a variable which we can access in the views. For the index method, we've just set a simple 'Hello, World!' message for now.

Also, create a new model by creating new file in the models directory named items_model.php and inside enter:

If you take a look at the site in your browser now, you should see a 'Hello, World!' message which is being served from the index() method in the controller.

Now that we have no errors, let's get all the items from the database and display them.

In your Items controller, edit the index() function to use the following lines:

On the first line, we set a the title for the page to 'All Items'. As usual, everything in the $data array will be extracted when it's passed to the view - so $data['page_title'] can be accessed simply as $page_title.

Following that, we call a get_all() method from the Item model (which we haven't yet created) and place the result in what will become the $items variable.

On the three following lines, we load three view files.

The View

Inside the application/system/views/ directory, create the following file/folder structure: (header.php, footer.php, and an items/ directory with index.php inside).


header.php and footer.php are universal in our app and will be used on every page, and for each controller we will create a separate views folder (items/) and each method will have a view file named after it (index.php).

Inside header.php enter the following HTML5:

On line 5 we're including a CSS file we'll create later which will be located at the root of our site's file structure.

On line 6 we use both the $page_title and $site_title variables to create a relevant title for the page.

Finally, on line 13 we use CodeIgniter's anchor() method (from the URL Helper) to create a link to the site's homepage.

Inside footer.php enter:

Line 4 sets the dates for the copyright notice. If the current year is 2010 then only the year 2010 will display in the notice. If the current year is later than 2010, say 2013, the date in the copyright notice would display as "2010–2013".

Retrieving and Displaying Items

In the controller, you will recall we called a get_all() method from the Items model. So inside models/items_model.php type:

This one line of code will retrieve all entries from the 'items' table in the database as PHP objects and return them in an array.

Now, to display the items, enter the following in views/items/index.php:

First, we check whether we retrieved any items from the database and display a message saying so if no items were found.

If items were retrieved from the database, we display a title and open an unordered list. We then loop through each item with foreach().

CodeIgniter returns each entry from the database as an object, so you can access an item's details with $item->name, $item->id, $item->price etc. for each field in the database.

We're going to give each item a SEO-friendly URL, with the item's name in it. For example: http://example.com/item/unix-and-chmod/1/ (the name being in the second segment and the ID in the third).

At line 10 we set the $segments variable to an array containing data which will be converted into a URL (each item in the array will be a segment of the URL). The first segment is simply 'item', then we use the CodeIgniter's url_title() function to make the item's name URL-friendly (removing capital letters and replacing spaces with dashes). And the third segment is the item's ID.

On the next line we create a new list item and pass the $segments to anchor() to create a URL. We also display the item's price. The loop is then closed off, the list closed and the 'if' statement terminated.

Refresh the page in your browser and you should see the two items from the database:



View Single Items

You may be wondering why the URL segments for a single item are item/unix-and-chmod/1 as in CodeIgniter that means we're pointing at a controller named 'item' with a method of 'unix-and-chmod' which doesn't quite make sense as our controller is named 'items' and having a separate method for each item is madness. Well, you're right. We're going to use CodeIgniter's 'Routes' to forward all 'item' requests to a 'details' method in our 'items' controller to help keep our URLs short.

Inside system/application/config/routes.php, after the 'default_controller' and 'scaffolding_trigger' options add:

What this piece of code does is internally forward all requests to item/ (an 'item' controller) to items/details ('items' controller - 'details' method).

So inside controllers/items.php add the following method:

When writing methods which are the result of a route, as this one is, I like to include a comment describing the route's path.

Click the link for either item on the main page and you should see the infamous 'Hello, World!' message.

Now we're sure routes are working fine, replace the 'Hello, World!' echo statement with the following:

On the first line we grab the ID from the third segment of the URL, then use it to get the item with that ID (we'll create the get() method in the model after).

If the item can't be found in the database, we set an 'Item not found' error message in the user's session and redirect them back to the main page.

We set the page title to the item's name, make the item accessible in the view and load the relevant view files.

Model – Get a Single Item

Inside models/items_model.php add the following method:

On the first line we query the database for an entry with the provided ID in the 'items' table. We get the result and store it in variable $r.

If an entry was found, we return the first array item. Otherwise, we return false.

Displaying Errors

In our controller we set an error message in the user's session using set_flashdata() if an item isn't found. To display that error in the browser, add the following to the bottom of views/header.php:

This simply displays a 'success' or 'error' message if either exist in the user's session.

Note: Anything stored in 'flashdata' (as our success and error messages are) are displayed only on the next page the user loads - CodeIgniter clears them on the next page load so they only display once.

Single Item View

Create a file named views/items/details.php and type the following inside:

Here, we're simply displaying the item's name and price in the header, we use the nl2br() function to convert SQL-style linebreaks in the item's description into HTML-style <br /> tags. We then create a SEO-friendly link to a purchase page (for example http://example.com/purchase/unix-and-chmod/1).



Purchase Page

First we need to add a new route to direct requests for a 'purchase' link to 'items/purchase' instead - just as we did with the single item pages. Add the following to config/routes.php:

Now, inside controllers/items.php add the following 'purchase' method:

It's basically the same as the single item view, just with 'Purchase “ ... ” ' in the page title and we're loading the items/purchase view instead.

The View

Create a file at views/items/purchase.php with the following inside:

Here we use CodeIgniter's form helper to create the opening tag for the form which directs back to the current page. In the form we collect the user's email address so we can send them their download link after once we receive confirmation of their purchase from PayPal.


Interfacing with PayPal

To communicate with PayPal, we're going to use a version of the PayPal Lib CodeIgniter library by Ran Aroussi which I modified to include the recommended changes from the Wiki page and add easy support for PayPal's Sandbox for development purposes.

PayPal Lib

Create a new file under application/config/ named paypallib_config.php with the following inside:

And inside application/libraries/ create a file named Paypal_Lib.php (the capital letters are important) with the following inside:

Send Details to PayPal

When a user enters their email address into the form on the 'Purchase' page, we want to:

  1. Verify the user has entered an email address;
  2. Add their email address to our database, along with a random key which we'll use to reference their purchase;
  3. Send the item's details, along with the user's random key, to PayPal to process payment.

Inside controllers/items.php, add the following before the $data['page_title'] = ... line inside the purchase() method:

On the first line we check that the submitted 'email' form field is a valid email address. Everything inside the if loop on line 3 will run if the 'email' field validated.

On line 6 we generate a unique key for the current purchase by hashing the item's ID, the current time, the user's email address and a random number using a MD5 hash.

Line 7 adds the item ID, user's email and the random key to the database via the setup_payment() method we have yet to create.

The rest of the code loads the 'Paypal_Lib' library and adds details about the item and site using the PayPal library's functions. We also pass the random key we generated to PayPal using the 'custom' field. PayPal will send this key back to us upon payment confirmation so we can activate the user's download.

Finally, we redirect the user to the PayPal payment page.

Now we need to create the setup_payment() function in the model. So inside models/items_model.php add the following:

This is pretty simple: we create an array containing the provided item ID, user's email address and random key. We also set 'active' to 0 (this gets set to '1' upon payment confirmation). The array is then inserted into the 'purchases' table.

PayPal's Process

With the system we're using to interface with PayPal, once a user has completed payment, PayPal will send the user to a 'success' URL which we provide. This page will simply say "Your payment has been received. We're currently processing it".

Behind-the-scenes, PayPal will send our 'IPN listener' confirmation of the payment and some details about it and the listener sends these details back to PayPal to confirm the message is genuine. It's after our IPN listener receives this second confirmation that we process the purchase and activate the user's download.

We're going to create a new controller to handle these requests from PayPal. So inside controllers/ create a file named paypal.php and enter the following inside:

That's the 'success' and 'cancel' pages (cancel is used with a user doesn't continue with the payment and clicks the cancel button on PayPal instead).

PayPal Developer Tools

PayPal provide a 'Sandbox' for developers to test their code with. You can create your own sandbox PayPal addresses to send pretend payments. Sign up for a free developer account at https://developer.paypal.com/ then go to 'Create a preconfigured buyer or seller account':


Fill out the form to create a new buyer account, enter a balance and click on through. On the 'Test Accounts' page you will find the email address for your new Sandbox buyer email address:


Now go back to the site we're creating and click through to purchase an item. Notice that when you get to PayPal, the address is https://www.sandbox.paypal.com/..... Login with the 'buyer' account you created on the right:


Continue through the payment process and click the 'Return to Merchant' button upon completion. You should be directed back to your homepage, with the "Your payment is being processed now. Your download link will be emailed to your shortly." message below the header.


The main interface of the site is now complete. We just need to add in our IPN listener and email the item to the buyer.


IPN Listener

As mentioned above, once PayPal has confirmed payment, it will send data to our IPN listener, once we validate the data with PayPal (to prevent fraudulent data), we can use the data to activate the buyer's purchase.

The IPN function is a little big, so I'll break it up. Inside the PayPal controller, add the following function:

Right at the start we validate the data sent to the listener with PayPal – the library takes care of all this. If the data is valid, we grab a some details (the item name, price, currency, the payer's PayPal email address, the transaction ID and the unique key we sent to PayPal when the payment process began).

We can then use the key to confirm the payment (by setting the 'active' field to '1') and add the payer's PayPal email and transaction ID to the database for future reference.

Using the key we can get the purchase details from the database, along with the item purchased. Before continuing with the IPN function, let's create the confirm_payment() and get_purchase_by_key() model methods.

So inside the model, add the following:

The functions should be pretty self explanatory by now, so now we need to email the customer their download link. This is handled in the IPN listender, so back in the controller add the following to the end of the ipn() function:

Here we're using CodeIgniter's Email class to send the email. We start by setting up 'To', 'From', 'Name' and 'Subject' variables with the relevant data.

We then write a short message for the body with a link to the file they purchased, followed by their download link (which will be in the format of: http://example.com/download/{key}). Finally we add the variables into the Email class methods and send it.

The final thing we need in the IPN listener is to send the site's admin an email with the transaction details. Add the following to the end of the ipn() function:

IMPORTANT! The IPN listener won't work if you're running on a local server ('localhost'). Clearly if PayPal attempted to visit http://localhost/paypal/ipn/ they're not going to arrive at your system. Upload your files to a remote server accessible by a domain name, or external IP address, for this to work.


File Downloads

The final step to getting the site fully working is to get the download links to work. When a customer goes to the download link we email them (eg. http://example.com/download/{key}), we use their key to look up the download. If the purchase associated with the key is set to active (payment fulfilled) and the file exists on the server, the download will start.

First thing we need to do is add another route to set /download/ requests to go to items/download. Add the following to your config/routes.php file:

Now, inside your items controller, add the following download() method:

In the first couple lines we lookup the purchase using the key in the URL. On line 6, if we can't find a purchase record with that key, we set an error message that the key is invalid and redirect the user to the homepage. Similarly, on line 10, if the purchase was not fulfilled (active is '0'), we display an error.

We then retrieve the actual item from the database, and retrieve the file name. We use the read_file() method from CodeIgniter's File helper to get the contents of the file. If the file can't be found (or is empty), we display an error. Otherwise, we use the force_download() method to initiate a download.

Item Theft

We do have one problem—it's possible to guess the name of a file and download it directly (ie. by visiting http://example.com/files/UNIX and CHMOD.txt). To fix this, simply create a file named .htaccess inside the files/ directory with the following line inside:

This tells the server to deny any requests for files in this directory – but the server itself can still access the files so buyers can still download using their own unique link.


Stylish

At the root of your app (same location as the files and system directories), create a new directory named css, inside it create a file named style.css and add the following inside to spice things up a little:



BONUS: Download Limits

You may want to limit how often a user may download their purchase within a certain time period (perhaps to stop them sharing the download link around). Implementing this feature doesn't take much work, so let's add it in now.

We already set up a database table to log file downloads–'downloads'–which we haven't used yet. We also have the 'Download Limit' setting in the config/config.php file, so go ahead and 'enable' it now:

The default setting is to allow up to four file downloads in a seven day period. If, for example, the buyer tries to download five times in seven days, we'll forward them back to the home page and display an error explaining why we can't serve up their download right now.

The first thing we need to do is keep a log every time a download is initiated. To do this, add the following directly before the force_download(...) statement at the end of the download() function in the Items controller:

Here we're sending the item id, purchase id and the user's IP address and user agent to a log_download() method in the Items model which we'll create next.

Add the following method to your Items_model:

This simply adds the data we provided, and the current time, to the 'downloads' table.

We'll also need a method to get the downloads of a purchase, so add the following to the model:

Now, to actually add in the download limit, find the following piece of code in the download() method in the Items controller:

Directly after this, add the following:

On the third line we check whether the 'Download Limit' functionality is enabled in the config file. If it is, we retrieve the downloads of the current purchase (limited to how many file downloads is permitted).

At line 6, we calculate the furthest time away downloads are limited to (eg. if we have a limit of 4 downloads in 7 days, we find the time 7 days ago) by multiplying the number of days by 86400 (the number of seconds in a day) and subtracting it from the current time.

The loop starting at line 7 checks each download logged to see if it was downloaded within the time limit (eg. 7 days). If it is, we increase $count, otherwise, we break out of the loop as we know if this logged download is older than the limit, all subsequent logs will be to.

At line 15, if the $count is greater than the number of downloads allowed, we display an error message. Otherwise, the rest of the code will be executed and the download will be logged and initiated.


We're done. Try it out!

Note: To disable the PayPal 'sandbox' mode so you can recieve real payments, change the $config['paypal_lib_sandbox_mode'] option in config/paypallib_config.php to false.

Advertisement