Newbie jumping in at the deep end.. Paypal integration

I’ve been looking through all the examples and docs this week trying to drag my PHP skills from purely procedural to OO and MVC. Starting to get a feel for it and for me the best way to learn is actually do some coding.

I’m intending to use Yii for a project that will require users to choose an access level and pay for it via Paypal so I’ve decided to get this bit working first.

I have the Yii-User and Srbac modules installed and working so I may use those as a base.

My logic at the moment is:

Guests can access free parts of the site. Paid for parts require subscription.

To buy a subscription the user fills in a registration form (possibly Yii-User) and chooses an access level (say Silver, Gold, Platinium) that are defined roles (possibly using srbac).

Clicking the submit button takes them off to Paypal with the needed variables.

After paying they are returned to the site with the new access level (role). Paypal would also send an IPN notification to the site to stop people trying to bypass payment.

As I work through getting this set up I’ll update this thread. Anyone is welcome to comment.

First thing is to get the access levels that are available to buy showing in the registration form.

ok so far so good…

I’ve made the following changes and have an Access Level dropdown list:

Added




<div class="row">

<?php echo CHtml::activeLabelEx($form,'accessLevel'); ?>

<?php echo CHtml::activeDropDownList($form,'accesslevel',array('silver'=>'silver','gold'=>'gold','platinium'=>'platinium')); ?>

</div>



to the Registration View for the User module

Added




public $accesslevel;



to the User class

Added




'accessLevel' => UserModule::t("Access Level"),



to the attributeLabels method of the User class

Added




array('accesslevel', 'required'),



to the rules in the RegistrationFrom model

I’m wondering how I can have a set list of values for rules for accesslevel (ie silver, gold, platinium) to prevent people attemptng to choose others.

EDIT

Found rule for a set list of values:


array('accessLevel', 'in', 'range'=>array('silver','gold','platinium')),



ok, I had some cider and read the docs for forms properly and have written the first part of this again from scratch (using info I found in the docs, docs comments and the forums).

To make it super simple I decided to give the user a password after a successful transaction. This also avoids sending their password anywhere so a bit more secure.

My model looks like this:

SubscribeForm.php




<?php


/**

 * SubscribeForm class.

 * SubscribeForm is the data structure for intial subscriptions.

 * It is used by the 'subscribe' action of 'SiteController'.

 */

 

 

class SubscribeForm extends CFormModel

{

    public $username;

    public $email;	

    public $accessLevel;

	public $verifyCode;	

		

	public function rules()

    {

        return array(

            array('username, accessLevel, email, verifyCode','required'),

			array('username', 'length', 'max'=>20, 'min' => 3,'message' => ("Incorrect username (length between 3 and 20 characters).")),

			array('email', 'email'),

			array('username', 'unique', 'className'=>'User', 'message' =>("Name already exists.")),

			array('verifyCode', 'captcha', 'allowEmpty'=>!extension_loaded('gd')),

			array('username', 'match', 'pattern' => '/^[A-Za-z0-9\s,]+$/u','message' => ("Incorrect symbols. (A-z0-9)")),

			array('accessLevel', 'in', 'range'=>array('silver','gold','platinium')),

        );

    }

}

I added this to the SiteController class




	public function actionSubscribe()

	{

    $model=new SubscribeForm;

	$form = new CForm('application.views.site.subscribeForm', $model);

    if($form->submitted('subscribe') && $form->validate())

        $this->redirect(array('site/payment'));

    else

        $this->render('subscribe', array('form'=>$form));

	}

I added a file subscribeForm.php to views/site/




<?php

return array(

    'title'=>'Fill in form',

 

    'elements'=>array(

        'username'=>array(

            'type'=>'text',

            'maxlength'=>32,

        ),

        'email'=>array(

            'type'=>'text',

            'maxlength'=>32,

        ),

		'accessLevel'=>array(

             'type' => 'dropdownlist', 

             'items' => array('silver'=>'silver','gold'=>'gold','platinium'=>'platinium'),


		),

		Yii::app()->controller->widget("CCaptcha", array(), true),

		'verifyCode' => array(

			'label' => Yii::t('form', 'Please type the letters shown above'),

			'type' => 'text',

		)	     			

	),

 

    'buttons'=>array(

        'subscribe'=>array(

            'type'=>'submit',

            'label'=>'Subscribe',

        ),

    ),

);

?>

and… subscribe.php to views/site/




<h1>Subscribe</h1>

 

<div class="form">

<?php echo $form; ?>

</div>

Next up… posting this data off to Paypal with the correct data Paypal requires

discovered a problem with the set up above. Yii creates form fields like this name=“SubscribeForm[email]” so you can’t just go straight to paypal from a form.

I’ve set up a payment/confirm page that the user is taken to after completing the subscription form. Probably a better way anyway as this allows payment method choices to be presented.

So the subscribe action in the SiteController class now looks like this:




	public function actionSubscribe()

	{

        $model=new SubscribeForm;

	$form = new CForm('application.views.site.subscribeForm', $model);

        if($form->submitted('subscribe') && $form->validate()){

		$session=new CHttpSession;

		$session->open();

		$session['subUsername']=$_POST['SubscribeForm']['username'];

		$session['subEmail']=$_POST['SubscribeForm']['email'];

		$session['subAccessLevel']=$_POST['SubscribeForm']['accessLevel'];

      	$this->redirect(array('site/page&view=payment'));

	}

        else

                $this->render('subscribe', array('form'=>$form));

	}

and I have static page instead of a view. At the moment this just shows one paypal button, in the end it will show the correct button for the subscription level chosen.




<?php

$this->pageTitle=Yii::app()->name . ' - About';

$this->breadcrumbs=array(

	'Payment',

);

$session=new CHttpSession;

$session->open();

?>

<h1>Payment</h1>


<p>Choose to pay via cheque or paypal on this page</p>




<?php

echo "Chosen Username: ".$_SESSION['subUsername']."<br />";

echo "Your Email Address: ".$_SESSION['subEmail']."<br />";

echo "Chosen Subscription: ".$_SESSION['subAccessLevel']."<br />";

?>

sandbox:<br />


<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post">

<input type="hidden" name="cmd" value="_s-xclick">

<input type="hidden" name="custom" value="<?php echo $_SESSION['subUsername'];?>">

<input type="hidden" name="hosted_button_id" value="Q46ADGSUULD3L">

<input type="image" src="https://www.sandbox.paypal.com/en_US/GB/i/btn/btn_buynowCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online.">

<img alt="" border="0" src="https://www.sandbox.paypal.com/en_GB/i/scr/pixel.gif" width="1" height="1">

</form>

User is then taken to paypal for payment. Next up the IPN…

Paypal offer some code for ipn but it gave a parse error for me. I found a class that seemed well written so I’ve used that.

Class is here… http://www.geniegate.com/other/paypal/

I’m fairly new to writing OOP code and classes although I’ve worked with OS software that utilises them for years. The code below might not be that great. I would welcome pointers where you think the code could be improved or set up to work better with Yii.

At the moment this code is called by the IPN (/index.php?r=ipn/process) and writes data to an error file. I’m working with the sandbox at the moment.

(I renamed the class from the link above to ipn and put the file in vendors/geniegate/)

IpnController.php:




<?php


Yii::import('application.vendors.*');

require_once('geniegate/ipn.php');

	

class IpnController extends Controller {


	public function actionProcess()

	{

		$process = new ipn(); // Constructor

		$process->processIPN(); // Call processIPN() to actually deal with PayPal's IPN.		

		

		if ($process->status_completed()){ // ipn confirmed

			error_log('IPN confirmed');

			$this->checkItemPriceCurrency(); // check item is correct

			$this->checkTxUnique(); // check tx not in db

			$this->checkSellerEmail(); // make sure you are the one being paid

			error_log('Checks confirmed'); 

			// Finish a transaction.

			$this->createTxRecord(); // write transaction info to db

			// TODOs

			// match up session and pp username (from pp custom field)

			// write data to DB

			// send email to customer

			// display an ok screen

		}

	}

	

	public function checkItemPriceCurrency()

	{

		$itemPriceCurrency=array($_POST[item_name],$_POST[mc_gross],$_POST[mc_currency]);

		//three possible options

		$itemOption1=array(silver,5,GBP);

		$itemOption2=array(gold,10,GBP);

		$itemOption3=array(platinium,20,GBP);

		if ($itemPriceCurrency == $itemOption1 || $itemPriceCurrency == $itemOption2 || $itemPriceCurrency == $itemOption3){

			error_log('Item is ok - '.$_POST[item_name].$_POST[mc_gross].$_POST[mc_currency]);

		} else {

			error_log('Something up with item - '.$_POST[item_name].$_POST[mc_gross].$_POST[mc_currency]);

		}

	}


	public function checkTxUnique()

	{

		$model=new Payment;

		$findTx=Payment::model()->find('txn_id=:txn_id',array(':txn_id'=>$_POST[txn_id]));

		if (is_null($findTx)) {

			error_log('Tx Unique - '.$_POST[txn_id]);

		} else {

			error_log('Tx Not Unique - '.$_POST[txn_id]);

		}

	}	

		

	public function checkSellerEmail()

	{

		if ($_POST[receiver_email] == 'seller@paypalsandbox.com'){

			error_log('Email is ok');

		} else {

			error_log('Something up with email - '.$_POST[receiver_email]);

		}

	}

	

	public function createTxRecord()

	{

		$model=new Payment;

		$model->payer_id=$_POST[payer_id];

		$model->txn_id=$_POST[txn_id];

		$model->payment_status=$_POST[payment_status];

		$model->pending_reason=$_POST[pending_reason];

		$model->reason_code=$_POST[reason_code];

		$model->txn_type=$_POST[txn_type];

		$model->custom=$_POST[custom];

		$model->save();

		error_log('Payment record inserted to db');

	}


}

?>

I’ll revisit this when I have more done in other areas of the site.