Yii 1.1: PayPal Integration And IPN (Instant Payment Notification)

18 followers

Introduction

Had to implement PayPal payments for a client and would like to share part of my code with you. Because the tutorial will become too long, I'll leave some code to be done by you, e.g. creating models, controllers and db tables for products, orders.

Preparation

Sign up a developer account

  1. Head to https://developer.paypal.com/ and sign up a developer account.
  2. Grap a cool PHP IPN Listener from https://github.com/Quixotix/PHP-PayPal-IPN and put the file IpnListener.php in protected/components/ directory. On my host the default setting for using CURL did not work, so I had to change the property: public $use_curl = false; in that class to use fsockopen();
  3. Log in and create test accounts. One for merchant and one for client.

Configure Yii

Edit protected/config/main.php and add blocks of code for LIVE and DEV environment.

// Define LIVE constant as true if 'localhost' is not present in the host name. Configure the detecting of environment as necessary of course.
defined('LIVE') || define('LIVE', strpos($_SERVER['HTTP_HOST'],'localhost')===false ? true : false);
if (LIVE) {
  define('PAYPAL_SANDBOX',false);
  define('PAYPAL_HOST', 'ipnpb.paypal.com');
  define('PAYPAL_URL', 'https://ipnpb.paypal.com/cgi-bin/webscr');
  define('PAYPAL_EMAIL',''); // live email of merchant
}else{
  define('PAYPAL_HOST', 'www.sandbox.paypal.com');
  define('PAYPAL_URL', 'https://www.sandbox.paypal.com/uk/cgi-bin/webscr');
  define('PAYPAL_EMAIL', ''); // dev email of merchant
  define('PAYPAL_SANDBOX',true);
}

Implementation of payments

Assuming we're going to use the HTML forms method, in your view script enter:

<div class="form">
<?php
    $form=$this->beginWidget('CActiveForm', array(
        'id'=>'orderForm',
        'htmlOptions'=>array('onsubmit'=>'return false;')
    ));
    // paypal fields
    echo CHtml::hiddenField('cmd','_cart');
    echo CHtml::hiddenField('upload','1');
    echo CHtml::hiddenField('currency_code','EUR'); // enter currency
    echo CHtml::hiddenField('business',PAYPAL_EMAIL);
// set up path to successful order
    echo CHtml::hiddenField('return',Yii::app()->getRequest()->getBaseUrl(true).'order/success');
// set up url to cancel order
    echo CHtml::hiddenField('cancel_return',Yii::app()->getRequest()->getBaseUrl(true).'order/canceled');
// set up path to paypal IPN listener
    echo CHtml::hiddenField('notify_url',Yii::app()->getRequest()->getBaseUrl(true).'order/paypalNotify');
    echo CHtml::hiddenField('item_name_1',$productLang->title); // product title goes here
    echo CHtml::hiddenField('quantity_1','',array('id'=>'paypalQty'));
    echo CHtml::hiddenField('amount_1','',array('id'=>'paypalPrice'));
    echo CHtml::hiddenField('custom','',array('id'=>'paypalOrderId')); // here we will set order id after we create the order via ajax
    echo CHtml::hiddenField('charset','utf-8');
    // order fields
    echo CHtml::hiddenField('currencyCode','EUR'); // currency code
    echo CHtml::submitButton('',array('style'=>'display:none;'));
?>
    <div class="note">Fields with asterisk are required<span class="required">*</span></div>
    <?php echo $form->errorSummary($model); ?>
 
    <div class="row indent-bot5">
        <?php echo $form->labelEx($model,'qty'); ?>
        <?php echo $form->textField($model,'qty',array('size'=>4)); ?>
        <?php echo $form->error($model,'qty'); ?>
    </div>
 
    <div class="row indent-bot5">
        <h2 class="strong">Price: <span id="singlePrice"><?php echo $product->price // model product with property price ?> EURO</span></h2>
    </div>
 
    <div class="row indent-bot5">
        <h2 class="strong">Total: <span id="totalPriceTxt"><?php echo $product->price?></span> EURO</h2>
    </div>
 
    <div class="row indent-bot5">
        <?php // set path to paypal button image
 echo CHtml::imageButton(bu('images/paypalButton.png'),array('id'=>'paypalBtn','name'=>'paypalBtn',
            'onclick'=>'createOrder(this);'))?>
    </div>
 
<?php $this->endWidget(); ?>
</div>
<script type="text/javascript">
    $(function(){
        setTotalPrice();
        $('#Order_qty').keyup(function(){
            setTotalPrice();
        });
    });
 
    function setTotalPrice(){
        var qty=parseInt($('#Order_qty').val(),10);
        qty = isNaN(qty) ? 0 : qty;
        $('#paypalQty').val(qty);
        var totalPrice = qty * parseFloat($('#singlePrice').html());
        $('#paypalPrice').val(totalPrice);
        $('#totalPriceTxt').html(totalPrice);
        $('#Order_total').val(totalPrice);
    }
 
    // create db record in tbl order, update paypalPrice field and submit the form
    function createOrder(btn,totalPrice,action){
        var requiredFields = ['qty']; // enter more required fields (field with id="Order_qty" will be checked for value)
        var error = false;
        $.each(requiredFields, function(key, field) {
            if($('#Order_'+field).val()===''){
                error = true;
                alert('Please fill out all required fields.');
                return false;
            }
        });
        if (error)
            return false;
        if($('#Order_qty').val() > <?php echo $product->qty?>){
            alert('<?php echo Exceeding available quantity. We are sorry for the inconvenience.'); // assuming we have property qty in model $product
            return;
        }
 
// OrderController needs to create a record in tbl order and return response in JSON format (in this case) use json_encode for example if your PHP version supports this function
        $.post(Yii::app()->getRequest()->getBaseUrl().'order/create'; ?>',$('#orderForm').serializeArray(),function(orderResp) {
            if(orderResp.error === undefined){
                var action;
                $('#paypalOrderId').val(orderResp.id);
                $('#orderForm').attr({action:'<?php echo PAYPAL_URL?>',onsubmit:true}).submit();
            }else{
                alert(orderResp.error);
            }
        },'json');
    }
</script>

Note: You will have to login in https://developer.paypal.com/ in advance, before making a test payment.

IPN (Instant Payment Notification) Listener Script

The listener script is there to accept the request from PayPal about the status of payments. Remember that we're going to use a ready IPN class and we set the notify URL to be order/paypalNotify? Here's a sample: OrderController::actionPaypalNotify()

public function actionPaypalNotify(){
    $paypal = new PayPal();
    $paypal->notify();
}

This assumes we have a PayPal.php file and PayPal class in protected/components dir.

class PayPal {
 
    public function notify(){
        $logCat = 'paypal';
        $listener = new IpnListener();
        $listener->use_sandbox = PAYPAL_SANDBOX;
        try {
            $listener->requirePostMethod();
            if ($listener->processIpn() && $_POST['payment_status']==='Completed') {
                $order = Order::model()->findByPk($_POST['custom']); // we set custom as our order id on sending the request to paypal
                if ($order === null) {
                    Yii::log('Cannot find order with id ' . $custom, CLogger::LEVEL_ERROR, $logCat);
                    Yii::app()->end(); // note that die; will not execute Yii::log() so we have to use Yii::app()->end();
                }
                $order->setAttributes(array(
                    'payDate'=>date('Y-m-d H:m:i'), // payDate field in model Order
                    'statusId'=>Order::STATUS_PAID // statusId field in model Order
                ));
                $order->save();
                Product::deductQty($order); // deduct quantity for this product
                Product::sendSuccessEmails($order); // send success emails to merchant and buyer
            }else{
                Yii::log('invalid ipn', CLogger::LEVEL_ERROR, $logCat);
            }
        } catch (Exception $e) {
            Yii::log($e->getMessage(), CLogger::LEVEL_ERROR, $logCat);
        }
    }
 
}

And here's part of Order model just to show some constants and statuses:

class Order extends CActiveRecord
{
    const STATUS_INITIATED = 1;
    const STATUS_CANCELED = 2;
    const STATUS_EXPIRED = 3;
    const STATUS_PAID = 4;
    public $statuses = array(
        self::STATUS_INITIATED => 'Initiated',
        self::STATUS_CANCELED => 'Canceled',
        self::STATUS_EXPIRED => 'Expired',
        self::STATUS_PAID => 'Paid',
    );
// more code of Order model

That's about it folks

Hope the tutorial is clear enough. Will update it if needed. Help me improve it by comments and opinions. Please use the forum if you have any questions. Thank you.

Total 3 comments

#11148 report it
got 2 doodle at 2012/12/19 01:56pm
Make sure your listener outputs html

I spent a very long time before I figured out that the listener (even though nobody is expected to go directly there) MUST output some html or paypal will claim that the url is invalid and will not flag the transaction as complete.

My code went something like this.

$pp = new PayPal();
 $pp->clear();
 $verified = $pp->handler();
  /* worth a try we will render a blank view */
 $this->render('ipn_blank'); // an empty view
/* update the database send emails and stuff */

doodle

#11102 report it
yasen at 2012/12/17 02:18am
Re: A few questions

@cgabbanini good question. In this case one may use CFormModel instead of CActiveForm. The form however submits to order/create and a record is created in table order. Also I had other fields for user's data if they choose to checkout as a guest and they are inserted in table order too. If an error is returned from the model, it will be displayed in the JavaScript alert window.

I looked at Yii login.php view script that is created in the basic application with the "yiic webapp {name of application}" command. They also use CActiveForm in the view though the login form can be done with CFormModel and isn't a typical CRUD operation. To be honest I don't understand in depth when what should be used and could not find much info too.

About your second question. I'm not sure I understand it. I do not use other methods that those mentioned in the article...

PS If you want to take the discussion further, please post in the forums. Thanks.

#11078 report it
cgabbanini at 2012/12/14 02:54pm
A few questions

Hi, thanks for your helpful contribution. I'm developing a small app store and paypal interactions (expecially for subscriptions) is quite annoying. Luckily for me I found the following module which helped me a lot: http://www.yiiframework.com/extension/ppext/

Anyway I would like to ask you a few questions. First, why did you choose not to you use a CFormModel to manage Paypal purchase form? And second, did you use different methods to handle PDT and IPN notifications? Thank you for your time

Leave a comment

Please to leave your comment.

Write new article