WordPress and PayPal: An Introduction

WordPress and PayPal: An Introduction

Tutorial Details
  • Program: WordPress
  • Version: 3.2.1+
  • Difficulty: Beginner
  • Estimated Completion Time: 1 hour

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.

If you would like to skip over the content creation stages e.g. pages, and products you can download and import this sample-data.xml file with the WordPress importer and run this sql in which orders will be stored.

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

Pages

Let’s publish the pages in WP-Admin we will use throughout the tutorial.

  1. Cart
  2. Products
  3. Thank you

We will re-visit these pages and allocate a custom template to each later.


Step 2 Child Theme Structure

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.

Custom Post Type - Products

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

Product list

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.

Structure
$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

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'); ?&gt/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

Cart

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

PayPal Developer Sandbox

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

Orders

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.

  1. Page title
  2. Menu title
  3. User role
  4. URL for our options page. Instead of competing for rank we use the file location and name
  5. Function to execute whilst accessing this page
  6. 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.

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://webdesignergeeks.com Ajay Patel

    Really great post, help me a lot… Thanks Alistair Rossini

  • http://www.wpfix.org Wpfix

    Very useful tutorial.

  • http://www.kreativtheme.com Kreativ Theme

    nice tut … useful for me as I’m working on something secret … ;)

  • http://www.customicondesign.com custom icon design

    I looked around the post, same it will work for paypal shopping cart. I think if you can provide the demo it’s better.

    Many thanks.

  • http://prop-14.com Randy

    This is one of the best articles yet on this WP Tuts. I just recently used the PayPal IPN in a WP site and it worked great. The sandbox is a great feature, though PayPal’s docs can be a bit “all over the place” so anything tutsplus puts out about PayPal will certainly be a life saver. I might only add that PayPal also has a curl example.

    I wish I had known that all you needed to do was include “wp-load.php” to have access to the wpdb class. I actually created a custom bare bones page for my listener “page-ipn”.

    I suppose extending the WP_List_Table to list the customer info would have been outside of the scope of this tutorial but that would have been the icing on the cake!

    Nice job!

  • Pingback: WordPress and PayPal: An Introduction | Qtiva

  • Pingback: WordPress and PayPal: An Introduction | Shadowtek Hosting and Design Solutions

  • http://wp.envato.com/ Japh Thomson
    Staff

    Hey guys, there are now demo and source download links at the top of the article! Enjoy :)

    • http://webdesignergeeks.com Ajay Patel

      That is really cool Japh, Thanks !

  • Pingback: Wordpresss - Parts | Pearltrees

  • http://www.vaultstudios.co.uk Martin Harvey

    Just when a client presents a problem for me, Wptuts comes to the rescue once again. Thanks Alistair, great post.

  • Pingback: Host Nine Weekly Round-up: April 2-6 | HostNine Company Blog

  • http://www.highergroundstudio.com Kyle King

    Why would you do it this way rather than using a e-commerce solution like woocommerce or wp e-commerce?

    • Alistair
      Author

      WP eCommerce and WooCommerce are biggish attempts to bring eCommerce to WordPress supplying lots of features and theming all of that can be exhaustive.

      If it’s only a couple of products or you fancy rolling your own eCommerce in WordPress these are some steps you could take.

  • http://www.dkla.net Dexter

    Pretty awesome tutorial! A cool addition would be some sort of wish list or saved items feature integrated as well. Thanks for the awesome work!

    • Alistair
      Author

      Yeah I think so, allowing users to submit content types and distinguish between saved links and future links.

      Nice

  • http://pckilkenny.com Kieran Ryan

    Nice tutorial and a cleaner version than the one on Paypal

  • Pingback: 15 Wordpress Articles From This Week You Don’t Want to Miss

  • http://photoshopix.com Hugh Madison

    Really helpful tutorial on time as usual, thanks to Alistair for great article and Japh for the links

    • Alistair
      Author

      Thanks Hugh, that’s a nice comment.

  • http://www.customicondesign.com custom icon design

    I think there are some errors in the demo page. I have tested the product to add to cart. but the view card in some times it’s empty, I dont know why.

  • Pingback: Tweet-Parade (no.14 Apr 2012) | gonzoblog.nl

  • Pingback: Alex Barber | Digital Artist | WordPress with PayPal: overview at wptuts

  • sinan

    This is very helpfull thank you very much.

  • http://LEGITNERD.com JHouse

    Yes, this tutorial is awesome and definitely needed. I’ve been using Custom Post Types with Paypal, but not to this extent. Basically, the cart was only viewable on Paypal, as most of the transaction data. I want to, however, keep the customer on the client’s site for the entire transaction, thus not going to Paypal for the Check Out process. This is possible with Paypal Payments Advanced. I assume your code above can be tweaked to work with PPA? It seems plausible.

    Thanks again for the quality tutorial. Much much much appreciated.

    • Alistair
      Author

      Hi JHouse, glad you have found this useful.

      IPN is the extent of my knowledge which does require to buyer to hit the Paypal website in the transaction.

      You could possibly wrap the final form (IPN) within an iFrame on your site and process this way, however I’ve yet to require / test this approach.

      Custom posts are excellent and once you have done the heavy lifting for a specific content type this could be your go-to for future projects that require… much like a PHP Class or jQuery plugin.

      “Tweaked” maybe doesn’t do the developments involved justice. There is a large amount of remaining coding required to have a robust and real World shopping cart, although this tutorial should help with creating a foundation for that.

      Thanks all for positive feedback.

  • Pingback: Ett fullmatat inlägg med otroligt spännande länkar och smarta tjänster! | Helen Alfvegren

  • http://www.hackbits.com Wylie Hobbs

    Very useful tutorial, I’m sure it will come in handy soon! Thanks for posting.

  • Takeshi Goda

    I’ve been waiting for something like this for years. You’ve made my dream come true. Brilliant.

  • Lana Phillips

    I’m having some issues on step 5. After giving the products page the template of Products, you show two bits of code for me to copy. The second one that creates the loop for the products… what file does it go into?

    • Alistair
      Author

      Hi Lana,

      This is a custom template which needs to be created. It could have any name, however for this tutorial it was named with “tpl-products.php” and assigned to the products page via wp-admin.

  • http://tinothepro.com tinothepro

    Interesting tutorial, haven’t had a chance to play with it yet. The demo version though needs some tweaking. You have no way to return to the shopping section once you add to the cart. When you select products and choose something else, you click “view cart” and the cart is empty. Good concept but a few fixes are in order! :) Thanks for this!

  • http://www.svengiesen.de Sven Giesen

    Thanks for this !!

    I needed exactly this for an actual project and everything is set up and runs now with some tweaks.

    The IPN Sandbox Tester works perfectly, but with your configuration no transaction in sandbox AND live got listened via IPN.

    I figured out that the hidden field “ipn_notification_url” (according to the DOC) had to be changed to “notify_url”. I dont know why this is mentioned differently in the docs, but it works since then.

    As an addition i skipped the part with $wpdb and stored the results as a custom post type called “orders”.

    Attention!! to get this to work make the ipn.php to a WP Page Template via the comment “Template Name: IPN Listener” at the beginning of the document. No need for get_header() etc. Create a new page with this template and use the URL of it in the “notify_url” field. Otherwise the WordPress related function for creating the orders wont work.

    So, if you have registered the post type and the custom field listed in the snippet below, you can just replace the part;

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

    by this:

    if (strcmp ($res, “VERIFIED”) == 0) :

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

    $orderdetails = array(
    ‘post_title’ => $firstName.’ from ‘.$addressCity.’ (‘.$txnID.’)',
    ‘post_type’ => ‘orders’,
    ‘post_content’ => $_POST, // or whatever you think is important.. like the buyers additional notes etc.
    ‘post_status’ => ‘pending’
    );

    $post_id = wp_insert_post($orderdetails);

    do_action(‘wp_insert_post’, ‘wp_insert_post’);

    update_post_meta($post_id, ‘_unipro_orders_firstname’, $firstName, true );
    update_post_meta($post_id, ‘_unipro_orders_lastname’, $lastName, true );
    update_post_meta($post_id, ‘_unipro_orders_mail’, $payerEmail, true );
    update_post_meta($post_id, ‘_unipro_orders_txn’, $txnID, true );
    update_post_meta($post_id, ‘_unipro_orders_ip’, $_SERVER['REMOTE_ADDR'], true );
    update_post_meta($post_id, ‘_unipro_orders_items_ordered’, implode(unserialize($products)), true );
    update_post_meta($post_id, ‘_unipro_orders_street’,$addressStreet, true );
    update_post_meta($post_id, ‘_unipro_orders_zip’,$addressZip, true );
    update_post_meta($post_id, ‘_unipro_orders_city’,$addressCity, true );

    elseif(strcmp ($res, “INVALID”) == 0) :

    Now your orders are listed like usual articles with the status pending. e.g. You have sent the ordered products to the customer, you just set the status to published and its done. Works like a to do list.

    Attention again: you should set your order post type in a way up that your orders arent accessible from the front end for every one. There are many ways to do this. Its up to you.

    • Alistair
      Author

      Dude that’s awesome, thank you for sharing with myself and others. The orders as a custom post type hidden from the World is a fantastic way to handle that side of things. Being able to produce a downloadable .xml file amongst other things now almost immediately available. Not to mention the benefits if you create a user -> order relationship. Users can sign in and view previous orders this way.

      The notify_url very ipn_notification_url is confusing you are right, it looks like when PayPal aquired X.com and did some updates to sandbox they changed this but not the documentation.

      I was bringing in an old snippet for the form in which ipn_notification_url worked fine but no longer does. This also took some time to figure out through trial and error. As you can see it’s still documented Above, I’ll need to ask Japh to update this here.

      Thanks again, wonderful comment. All of this makes me want to write some more.

    • Katmassive

      I attempted this but nothing shows up on the Orders page can you include how you registered the post type and custom fields?

      P.S. In sandbox everything works but the orders page with the original code or this modification. :/

  • Jim

    Hi, Alistair! when do you update this tutorial?

    • Alistair
      Author

      Hi Jim, I think that’s all for this one.

  • Pingback: Wordpress İçin Yararlı Makaleler | Web Araçları

  • http://jai.no Jaidev Kristiansen

    Hi,

    great tut!

    I am just curious about the SESSION['basket'] variable that suddenly is there when preparing for PayPal

    $qty) : ?>

    I cannot see that we populate the “basket” anywhere. What am I missing?

    regards,
    Jaidev

    • Alistair
      Author

      Hi Jaidev, apologies for late reply here. It’s been a while now…

      SESSION->basket (or cart) is used as an array to store any products associated with the session. It’s used later in the tutorial to check if there are items and if so which items are stored within that array.

      Not sure if there is a typo in the tutorial here but from a technical stand-point that’s all that’s going on.

  • http://www.jsxtech.com Jaspal Singh

    Excellent Article on wordpress theme customization and integration with paypal.

    Thanks for sharing.

    • Alistair
      Author

      No problems man! Thanks for the positive words!

  • https://twitter.com/paillao coke

    Hello Alistair can you Upload or send me this files?

    sample-data.xml
    customer.sql

  • ashique mahamud

    please watch out below line:
    <form action="/cart” method=”post”>

    when i click on “ADD TO CART” button i get fatal error “call to undefined function get_header()”
    any help will be apprehensive…

    i typed my error again fully below:

    Fatal error: Call to undefined function get_header() in /home/ashprogr/public_html/cms/wptest/wp-content/themes/twentyten/tpl-cart.php on line 25

    • Alistair
      Author

      Really not sure what you have done here Ashique.

      get_header();

      Should be one of the first lines in the tpl.cart.php file. Certainly not after the form

  • Drumknott

    I found probably the result of an error in the cart.

    Returns an incorrect result. in the tpl-cart page.
    Please check this.

  • Drumknott

    Sorry, my mistake, the calculations are correct. :)

  • nub

    the demo is not working

  • http://www.superdesigngirl.com Katherine

    the demo link is not working, I would very much like to see the demo, I am working on a similar project and this would be very useful. Thanks!

  • Alistair
    Author

    @drumknott – Thanks for clarification.

    @nub – Yeah, sorry about that hosting is currently unavailable.

    @katherine – All the files are still available for download, it’s a little less convenient but shouldn’t take too long too install the theme and sample content.

  • http://www.php-sri-lanka.com/ Upeksha Wisidagama

    This tut was a great starting point for PayPal integration for one of my projects. I will next follow the resources pointed out in this tut. Thanks Rossini for this tut!

    • agrc
      Author

      You’re most welcome

  • Nur Hossain

    I think it’s a great article. But I can’t download the sql file. Can anybody help me download it? Thanks!

    • http://wp.tutsplus.com/ Japh

      Hi @nurhossain:disqus, unfortunately this article is almost a year old, and it seems the domain the author hosted the SQL file on has expired. Sorry for the inconvenience.

    • agrc
      Author

      Apologies Nur

  • Nur Hossain

    I have got a problem with step 3. I have just downloaded the files, uploaded to my server. Problem is I can’t see post options, I mean price, as shown on step-3 image. Can anybody tell me why? I am using PHP 5.3+.

    Thanks!