For this tutorial we will go through the steps involved to integrate PayPal as your Payment Service Provider for a WordPress powered site. Working with Custom Post Types we will create a basic shopping cart and allow for payments to be taken via PayPal. Whilst the worked example is functional, you should take further steps to sanitise and store data when working with payments.
Introduction
We will utilise custom WP-Admin pages for viewing orders, custom post types for product creation (Jeffrey Way's CPT class) and custom page templates for viewing products, cart and processing. Our raw function will be created in PHP and we will apply some basic styling with CSS.
Within the code snippets below some markup may have been stripped for readability.
For those who opt-in here please advance to go and collect $200 or skip to stage 2 of this tutorial and benefit from sample data.
Step 1 WP-Admin and Page Structure
Let's publish the pages in WP-Admin we will use throughout the tutorial.
- Cart
- Products
- Thank you
We will re-visit these pages and allocate a custom template to each later.
Step 2 Child Theme Structure
Now we will create the directories and files required for our project.
We are making a child theme from Twenty Ten here, we only need to create the files we will be modifying or creating from scratch. Any others that are required e.g. footer.php WordPress will fill in the blanks for us.
Step 3 Jeffrey Way's Custom Post Types
We will work with Jeffrey Way's class (requires PHP 5.3) to create a product custom post type
which will allow creation of products, and not blog posts pretending to be products, via WP-Admin.
In the file /wppp/wppp/post_types.php add the following pieces of code.
First we will include the class.
include('jw_custom_posts.php');
Secondly, create a new Custom Post Type and identify which components of the write page to use.
$product = new JW_Post_type('Product', array( 'supports' => array('title', 'editor', 'excerpt', 'thumbnail', 'comments') ));
Thirdly we have one specific field that's better suited with a text field on its own. Price.
$product->add_meta_box('Product info', array( 'Price' => 'text' ));
When all together it will look like this.
include('jw_custom_posts.php'); $product = new JW_Post_type('Product', array( 'supports' => array('title', 'editor', 'excerpt', 'thumbnail', 'comments') )); $product->add_meta_box('Product info', array( 'Price' => 'text' ));
Step 5 Retrieving All Products
WordPress Custom Post Types are fantastic and with Jeffrey's class implementation can be fast. The custom data can be accessed very quickly, much like you would blog posts within "the loop".
Let's visit wppp/tpl-products.php file and retrieve the products.
// Template Name: Products
This is a convention WordPress requires of us to create a custom template. Provided the active theme holds this file we can assign it to any page.
Go ahead and assign this template to the products page previously published.
$products = new WP_Query(array('post_type' => 'product'));
Next we create a new instance of WP_Query
and look for
a post_type of "product".
Using WP_Query
we have access to many template tags existing within WordPress.
All that's required now is to loop over the products and output the data.
while($products->have_posts()) : $products->the_post(); the_title(); echo '<p><strong>Price: </strong>'; echo get_post_meta($post->ID, 'product_info_price', true); echo '</p>'; the_post_thumbnail('product'); the_excerpt(); endwhile;
get_post_meta();
will retrieve data stored within custom fields, and since a meta box was added using JW's class earlier this is what to use in order to retrieve its value.
Notice we use "product_info_price" as the second parameter for get_post_meta
. This is the name applied to our custom field when utilising JW's CPT class. The convention appears to be name-of-post-type_info_field
.
Step 6 Retrieving a Single Product
WordPress will serve single-custom-post-type.php if a custom post type exists and a file single-custom-post-name.php exists within the active theme. This is helpful when creating a new template for single products.
Much like retrieving multiples we could utilise WP_Query
(for custom queries) and the template tags WordPress provides. However when viewing a single item, technically we no longer need a loop or a custom WP_Query
.
the_post(); the_post_thumbnail('product'); the_title(); echo get_post_meta($post->ID, 'product_info_price', true); the_content();
One more addition to our single-product.php file, a form which will allow this item to be added to the shopping cart session.
<form action="<?php bloginfo('home'); ?>/cart" method="post"> <label>QTY:</label> <input type="text" name="wppp_qty" value="1" /> <input type="hidden" name="wppp_product_id" value="<?php echo $post->ID; ?>" /> <input type="hidden" name="wppp_action" value="add" /> <input type="submit" value="Add to cart" /> </form>
Two hidden fields have been added to this form, 1 which will store the post ID (or product ID) and the other will be used a little later. A default quantity of 1 is also set.
Step 7 Adding an Item to the Session
The "Add to cart" button resides on the single product page as illustrated in the previous step, after a user has chosen to add a product the form will be sent to the cart page.
Let's work with the wppp/tpl-cart.php file now.
/* TEMPLATE NAME: Cart */
tpl-cart.php is a custom template so we need to let WordPress know and assign the template to the cart page via WP-Admin.
if($_POST['wppp_product_id']) : $product_id = $_POST['wppp_product_id']; $qty = $_POST['wppp_qty']; $action = $_POST['wppp_action']; switch($action) { case "add" : $_SESSION['cart'][$product_id] = $_SESSION['cart'][$product_id] + $qty; break; case "empty" : unset($_SESSION['cart']); break; case "remove" : unset($_SESSION['cart'][$product_id]); break; } endif;
Now we check if suitable post data has been sent and if true we store data for convenience as variables.
Using a switch to determine the current action and process accordingly.
foreach($_SESSION['cart'] as $product => $qty) : $row = get_post($product); echo $row->post_name echo $row->post_title; echo get_post_meta($product, 'product_info_price', true); echo $qty; echo number_format(get_post_meta($product, 'product_info_price', true) * $qty, 2); endforeach;
To print the cart to the page a loop is used to iterate over the session data.
Whilst in this loop we query for human readable data instead of the numeric representation of each product / post stored within the session.
To do this get_post()
is perfect which allows for a quick way to query WordPress by passing a post ID. The data returned is a scaled down version of WP_Query
and it is stored within $row
.
$row
can now be printed to the page along with a running total showing the price of product multiplied by the quantity.
<form action="" method="post"> <input type="hidden" name="wppp_product_id" value="<?php echo $product; ?>" /> <input type="hidden" name="wppp_action" value="remove" /> <input type="submit" value="Remove" /> </form>
Within the loop a form is placed which, for convenience will allow a user to remove an item entirely from their cart.
Using the switch written earlier a check for the case of "remove" will allow for the item to be removed from the session.
Step 8 Preparing for PayPal
PayPal provides a number of ways to send and retrieve data, we'll be using Instant Payment Notification or IPN.
In order for PayPal to calculate and process any transactions, data can be sent by a form with fields matching the naming and expected data conventions as set out by PayPal.
The IPN guide can be found in the header or the footer menus of paypal.com/ipn.
Let's move on... within tpl-cart.php, underneath all a form is added with the bare essential PayPal requirements.
<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post" class="standard-form"> <?php $i = 1; ?> <?php foreach($_SESSION['basket'] as $product => $qty) : ?> <?php $row = get_post($product); ?> <input type="hidden" name="item_name_<?php echo $i; ?>" value="<?php echo $row->post_title; ?>" /> <input type="hidden" name="quantity_<?php echo $i; ?>" value="<?php echo $qty; ?>" /> <input type="hidden" name="amount_<?php echo $i; ?>" value="<?php echo get_post_meta($product, 'product_info_price', true); ?>" /> <?php $i++; ?> <?php endforeach; ?> <input type="hidden" name="cmd" value="_cart" /> <input type="hidden" name="upload" value="1" /> <input type="hidden" name="business" value="<?php echo get_otion('admin_email'); ?>" /> <input type="hidden" name="currency_code" value="GBP" /> <input type="hidden" name="lc" value="UK" /> <input type="hidden" name="rm" value="2" /> <input type="hidden" name="return" value="<?php echo bloginfo('home'); ?>/thankyou" /> <input type="hidden" name="cancel_return" value="<?php echo bloginfo('home'); ?>/cart" /> <input type="hidden" name="notify_url" value="<?php bloginfo('stylesheet_directory'); ?>/ipn" /> <input type="submit" class="submit-button" value="Proceed to PayPal" /> </form>
Check out developer.paypal.com for a sandbox and testing environment.
Once logged into your developer account you will be able to create test buyer and seller accounts and "Enter sandbox test site".
Sending the cart to "https://www.sandbox.paypal.com/cgi-bin/webscr
" will allow use of the test environment.
Should you decide to go live, the URL for the form action would simply change to "https://www.paypal.com/cgi-bin/webscr
".
developer.paypal.com
can be a buggy and a slow experience, have patience. Writing this tutorial I had to wait for PayPal to fix itself and return a couple of hours later.
<?php $i = 1; ?> <?php foreach($_SESSION['basket'] as $product => $qty) : ?> <?php $row = get_post($product); ?> <input type="hidden" name="item_name_<?php echo $i; ?>" value="<?php echo $row->post_title; ?>" /> <input type="hidden" name="quantity_<?php echo $i; ?>" value="<?php echo $qty; ?>" /> <input type="hidden" name="amount_<?php echo $i; ?>" value="<?php echo get_post_meta($product, 'product_info_price', true); ?>" /> <?php $i++; ?> <?php endforeach; ?>
Much like the previous session loop the data is retrieved and presented with a combination of raw PHP and a WordPress function.
Provided you send PayPal the correct type of data it will be processed via IPN.
In the form above product name, related quantities and prices for each product are all sent. PayPal will perform the calculation this time for multiples based on price per item and quantity.
<input type="hidden" name="cmd" value="_cart" /> <input type="hidden" name="upload" value="1" /> <input type="hidden" name="business" value="<?php echo get_otion('admin_email'); ?>" /> <input type="hidden" name="currency_code" value="GBP" /> <input type="hidden" name="lc" value="UK" /> <input type="hidden" name="rm" value="2" /> <input type="hidden" name="return" value="<?php echo bloginfo('home'); ?>/thankyou" /> <input type="hidden" name="cancel_return" value="<?php echo bloginfo('home'); ?>/cart" /> <input type="hidden" name="notify_url" value="<?php bloginfo('stylesheet_directory'); ?>/ipn.php" />
"Transaction and notification variables" as described in the IPN Guide have been implemented as hidden form fields much like the other variable types directed by PayPal.
Passing an email to the input with a name of "business" instructs PayPal which account is the seller. Here for convenience we use the current WordPress administrator's email.
business - Email address or account ID of the payment recipient (that is, the
merchant). Equivalent to the values of receiver_email (if payment is
sent to primary account) and business set in the Website Payment
HTML.
- IPN Guide -
The 3 URLs passed with the form (return
, cancel_return
and notify_url
) allow for links to be placed within the checkout process as a user visits paypal.com from the cart. The "cancel
" URL will be shown before and during the transaction, whilst "return
" is shown after the transaction.
You could say the most important field here is "notify_url
" which allows a developer to listen for PayPal instructions behind-the-scenes as the user processes their transaction.
When PayPal sends a response to the ipn.php file the transaction details can be stored within a database, emails can be sent and downloads presented. It is up to you to process the data using methods that reflect the product type for sale.
So let's create the database table in the ipn.php file and move onto retrieving orders.
Step 9 Database
For speed of implementation a longtext field for items_ordered
is created to store the items purchased with each order and the quantity as serialized data. It may be advisable with a live store to normalise any database tables behind your store to 4NF or consider using a Custom Post Type when storing orders.
Step 10 Testing
Now you should be able to publish new products, add a product(s) to the cart session, view the shopping cart session and proceed to PayPal.
After a customer has paid for goods at PayPal, what then? How can we identify if the transaction has been successful, which goods have been purchased and where should they be shipped?
In Step 8 buyer and seller accounts were highlighted for test purchases.
Also, previously "return_url
" was created as a hidden form field within tpl-cart.php, this file could be used if the user should chose to "Return to merchant site" after the transaction at PayPal.
Looping over post data will show what's going on.
foreach($_POST as $key => $value) : echo '<p><strong>Key: </strong>'.$key.'</p>'; echo '<p><strong>Value: </strong>'.$value.'</p>'; endforeach;
This loop will print any returned data from PayPal via post. You might decide to use this for storing data, it's really not practical to do so.
To arrive at the thank you page we are hoping the user will click "Return to merchant website" from PayPal. In the event a user decides to close the browser what then?
Because of this pitfall all that should be done via tpl-thankyou.php is to empty the cart and display the content as shown below.
/* TEMPLATE NAME: Page: Thank you */ session_destroy(); the_post(); the_title(); the_content();
We are then notified from PayPal no matter what the user decides to do after payment. This is where the "Notification" of Instant Payment Notification comes in.
When the form was initially sent to PayPal "notify_url
" had a value. This instructed PayPal that we would like to use the file http://yoursite.com/wp-content/themes/wppp/ipn.php for communication.
With this in mind we can now "listen" to PayPal (and not the user) for updates on the payment status and process. Let's create that final file and name it ipn.php.
$req = 'cmd=_notify-validate'; foreach($_POST as $key => $value) : $value = urlencode(stripslashes($value)); $req .= "&$key=$value"; endforeach; $header .= "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($req) . "\r\n\r\n"; $fp = fsockopen ('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30); if(!$fp) : // HTTP ERROR else : fputs ($fp, $header . $req); while(!feof($fp)) : $res = fgets ($fp, 1024); $fh = fopen('result.txt', 'w'); fwrite($fh, $res); fclose($fh); if (strcmp ($res, "VERIFIED") == 0) : // Make sure we have access to WP functions namely WPDB include_once($_SERVER['DOCUMENT_ROOT'].'/wp-load.php'); // You should validate against these values. $firstName = $_POST['first_name']; $lastName = $_POST['last_name']; $payerEmail = $_POST['payer_email']; $addressStreet = $_POST['address_street']; $addressZip = $_POST['address_zip']; $addressCity = $_POST['address_city']; $productsBought = $_POST['']; $txnID = $_POST['txn_id']; // Used to store quickly items bought $i = 1; foreach($_POST as $key => $value) : if($key == 'item_name'.$i) : $products_bought[] = $value; $i++; endif; endforeach; $products = serialize($products_bought); $wpdb->insert('customers', array('forename' => $firstName, 'surname' => $lastName, 'email' => $payerEmail, 'address_line_1' => $addressStreet, 'postcode' => $addressZip, 'town' => $addressCity, 'items_ordered' => $products, 'created' => current_time('mysql'), 'txn_id' => $txnID, 'user_ip' => $_SERVER['REMOTE_ADDR'] ), array('%s', // FORENAME '%s', // SURNAME '%s', // EMAIL '%s', // ADDRESS 1 '%s', // PCODE '%s', // TOWN '%s', // ORDERED '%s', // STATUS '%s', // CREATED '%s' // USER IP )); elseif(strcmp ($res, "INVALID") == 0) : // You may prefer to store the transaction even if failed for further investigation. endif; endwhile; fclose ($fp); endif;
The above code looks a little bit scary, you can see how it is pieced together by looking at the simplified code-sample.php over at PayPal.
Without explaining the example PayPal has given as a guide, we are listening for VALID or INVALID responses and processing accordingly. WPDB is used to store any required data returned by PayPal.
foreach($_POST as $key => $value) : if($key == 'item_name_'.$i) : $products_bought[] = $value; $i++; endif; endforeach;
This snippet loops over post data and checks if the current item is an item_name_x
which we know is our product's name. The data is then serialised and stored within an array.
The WPDB insert method is used later to send the serialized data along with other values to the customers table.
Step 12 WP-Admin Orders Page
Our final step involves creating a WP-Admin menu page and populating that page with the customers / orders data previously stored.
You may decide to create a more robust orders page to allow for pagination, marking each item for shipping, easy printing of shipping labels and anything else.
Let's follow the style conventions of WordPress and create a reasonably well presented long list of orders.
define(ADMIN_URL, admin_url()); // Helper function wppp_orders() { add_menu_page('Orders', 'Orders', 'administrator', __FILE__, 'wppp_orders_page', ADMIN_URL.'images/generic.png'); }
add_menu_page()
is executed with 6 parameters of a possible 7.
- Page title
- Menu title
- User role
- URL for our options page. Instead of competing for rank we use the file location and name
- Function to execute whilst accessing this page
- Icon for the menu
An optional parameter "menu position" could be passed but again let's not wrestle with other Authors.
function wppp_orders_page() { ?> <div class="wrap"> <div id="icon-users" class="icon32"></div> <h2>Orders</h2> <p>Below is a list of all orders.</p> <table class="widefat"> <thead> <th>#</th> <th>Forename</th> <th>Surname</th> <th>Email</th> <th>Address</th> <th>Products purchased</th> <th>User ip</th> </thead> <tbody> <tr> <td>ID</td> <td>Forename</td> <td>Surname</td> <td>Email</td> <td>Address</td> <td>Products purchased</td> <td>User ip</td> </tr> </tbody> </table> </div> <?php }
Above, a function is created, and within, some markup to display the orders. When adding the new menu page this function was also passed which instructs WordPress to execute this code when viewing the corresponding menu page.
Using wpdb
to output the orders will be the final stage.
function wppp_orders_page() { <div class="wrap"> <div id="icon-users" class="icon32"></div> <h2>Orders</h2> <p>Below is a list of all orders.</p> <table class="widefat"> <thead> <th>#</th> <th>Forename</th> <th>Surname</th> <th>Email</th> <th>Address</th> <th>Products purchased</th> <th>User ip</th> </thead> <tbody> <?php global $wpdb; ?> <?php $orders = $wpdb->get_results("SELECT * FROM customers"); ?> <?php if(orders) : ?> <?php foreach($orders as $order) : ?> <?php $products = unserialize($order->items_ordered); ?> <tr> <td><?php echo $order->id; ?></td> <td><?php echo $order->forename; ?></td> <td><?php echo $order->surname; ?></td> <td><?php echo $order->email; ?></td> <td><?php echo $order->address_line_1; ?>, <?php echo $order->postcode; ?>, <?php echo $order->town; ?></td> <td> <ul> <?php for($i = 0; $i <= count($products); $i++) : echo '<li>'.$products[$i].'</li>'; endfor; ?> </ul> </td> <td><?php echo $order->user_ip; ?></td> </tr> <?php endforeach; ?> <?php else : ?> <tr colspan="8"> <td>No orders yet.</td> </tr> <?php endif; ?> </tbody> </table> </div> }
When sending products and quantities to the database the data was serialized. It's now time to reverse that with unserialize
at each iteration.
A nested loop allows each line of unserialized data to be split and shown as list items.
add_action('admin_menu', 'wppp_orders');
Finally the functions created previously are executed using the add_action
function and the admin_menu
action specifically. For a full list of actions visit the Action reference.
Conclusion
In this tutorial a combination of best practices, hacks and techniques have been shown much of which will be open for debate. Some code and discussion has been omitted from the tutorial, namely additional.css, and functions.php.
additional.css is imported within the stylesheet for Twenty Ten (style.css) and applies some basic styles for the display throughout the example.
functions.php requires any files for custom posts and viewing orders within WP-Admin. A new image size is also set which crops the product thumbnail to match.
We make use of Twenty Ten's menu capability to show the top menu links for "Products" and "Cart".
Let us know in the comments what you think of this introduction to using PayPal with WordPress.
Comments