SagePay PHP Class
Dec 20th, 2010, Categories: Development, Work Related
Tagged with E-Commerce, mySQL, PHP, PHP Developer Essex, SagePay
We all know that implementing a payment service into a e-commerce store can be a hellish task if you've never worked with that particular service before. If you have your own shop framework (as I do), there is no pre-written PHP for you to tweak the parameters of and hope for the best.
SagePay seems to be one of the most popular payment providers and once integrated, offers a professional and seamless user experience. The sample code that SagePay provide in their integration kit for their direct service is quite frankly, a mess. There are files everywhere and the code leaves somewhat to be desired.
So a while back I wrote a PHP class to handle SagePay transactions, complete with 3DSecure functionality. I've used it a fair few times and for my needs, it works very well.
The core code (mainly the cURL request) has been taken from the SagePay Direct PHP integration kit and refactored slightly. The rest of the class has been written to hopefully use as little code as possible and minimal integration with this class.
There are a few notes that need explaining. The first is that this class doesn't have anything in the way of validation of card details (ensuring the card number is numeric only and 16 digits long, the expiry date isn't in the past and the valid from date isn't in the future etc). I would recommend doing this in something like your basket class where you store the order information.
The second thing is that this class depends on a site constant 'ENV'. SagePay has two environments, live and test. Test uses dummy card information provided to you by SagePay to test with and doesn't handle real money. If the ENV constant is set to 'DEVELOPMENT', the billing/card information is set to one of the test SagePay cards (change this to your own test card data, it starts on line 277). Obviously, if ENV is 'LIVE', it assigns the card information you pass to it.
The final one is the implementation. Obviously there are a number of ways you can do this, but this is the way I've done it.
Once the user has entered their card and billing info, it's passed validation and they've confirmed they'd like to pay, they are redirected to a page. When this page is called, the following code is run:
index.php
<?php
// pass the card and billing data to a static method in the
// sagepay class to be formatted and returned.
$data = SagePay::formatRawData($arr);
// instantiate the SagePay object, passing it this formatted data.
$payment = new SagePay($data);
// execute the payment request
$payment->execute();
if($payment->status == '3dAuth') {
// SagePay has returned a request for 3DSecure authentication
// returned by SagePay on request for 3DSecure authentication
$_SESSION['payment']['acsurl'] = $payment->acsurl;
// returned by SagePay on request for 3DSecure authentication
$_SESSION['payment']['pareq'] = $payment->pareq;
// Store the transaction code that you set for passing to 3DSecure
$_SESSION['payment']['vendorTxCode'] = $payment->vendorTxCode;
// returned by SagePay on request for 3DSecure authentication
$_SESSION['payment']['md'] = $payment->md;
// set a flag so your code knows to load the 3D Secure page.
$secure_auth = true;
} else if($payment->status == 'success') {
// Transaction successful. Redirect to your complete page
} else {
$_SESSION['error'] = $payment->error;
// redirect to basket overview page and explain error to user
}
?>
As explained above, if there is an error, redirect the user to the basket page or similar and inform them of the error (stored to a session variable in the example above). If no 3D Secure authentication was requested, redirect them to your success page. If 3D Secure authentication was requested, we need to load our 3D Secure page. Obviously how your application is structured is going to depict how you load this page, but regardless of how you load it, you're going to need a HTML page with an iFrame in it, which loads our 3D Secure page. The code below is the code for the page that is loaded into the iFrame.
3dSecure.php
<html><head><title>3D Secure Verification</title></head>
<body>
<form name="form" action="<?php echo $_SESSION['payment']['acsurl']; ?>" method="POST">
<input type="hidden" name="PaReq" value="<?php echo $_SESSION['payment']['pareq']; ?>" />
<input type="hidden" name="TermUrl" value="<?php echo SECURE_SITE . '/checkout/payment/3dCallback.php?VendorTxCode=' . $_SESSION['payment']['vendorTxCode']; ?>" />
<input type="hidden" name="MD" value="<?php echo $_SESSION['payment']['md']; ?>" />
<input type="submit" value="Proceed to 3D secure authentication" />
</form>
<script type="text/javascript">document.form.submit();</script>
</body>
</html>
As you can see, it's just a form with some pre-populated values from the information we stored in our session array earlier. The piece of JavaScript at the bottom auto-submits the form to the 3DSecure service, which in turn, loads the page that the user sees, requesting their 3DSecure password. It's important to note that the SECURE_SITE constant contains the URL of the site behind it's SSL (i.e. https://mysitename.com/checkout/).
So the users fills in the resulting form from the 3DSecure service and providing they don't get their password wrong (which 3DSecure will handle if they do), it will return some parameters that inform SagePay that they have successfully identified themselves to 3D Secure and we can then continue with the transaction.
Notice one of the parameters we passed to 3DSecure above was named 'TermUrl'. This is the URL that the user is returned to, along with some information about whether their transaction was approved or not. So in the example above, we're targeting a file called 3dCallback.php. Let's take a look at that file:
3dCallback.php
<?php
$data = array();
$data['MD'] = $_POST['MD'];
$data['PAReq'] = $_POST['PaRes'];
$data['PaRes'] = $_POST['PaRes'];
$data['VendorTxCode'] = $_GET['VendorTxCode'];
$_SESSION['mdx'] = $_POST['MDX'];
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<link rel="stylesheet" type="text/css" href="images/directKitStyle.css">
<title>3D-Secure Redirect</title>
<script language="Javascript"> function OnLoadEvent() { document.form.submit(); } </script>
</head>
<body OnLoad="OnLoadEvent();">
<form name="form" action="./3dComplete.php" method="POST"/>
<input type="hidden" name="PARes" value="<?php echo $data['PaRes']; ?>"/>
<input type="hidden" name="MD" value="<?php echo $data['MD']; ?>"/>
<noscript>
<center><p>Please click button below to Authorise your card</p><input type="submit" value="Go"/></p></center>
</noscript>
</form>
</body>
</html>
Ok, so what's going on here? So first up we're storing some POST and GET data. The GET data is the VendorTXCode (we could have equally have just grabbed this from the session), the rest of the data is data returned by 3DSecure that we need to pass to SagePay. So in the page, we have another form that automatically submits itself. In the form are the PaRes and MD values from 3DSecure. We're also storing the 'mdx' parameter to the session for future reference. The form submits so 3dComplete.php - which really only requires the following code:
3dComplete.php
<?php
$data['MD'] = $_POST['MD'];
$data['PaRes'] = $_POST['PARes'];
$secure = new SecureAuth($data);
if($secure->status == 'success') {
// transaction successful
} else {
// an error has occurred. Process accordingly
}
?>
We could have stored this information to a session and redirected using header() if we wanted to. So the POST values 'MD' and 'PaRes' are passed to a new instance of the SecureAuth class - in a nutshell, this basically throws this information, along with the vendorTxCode to SagePay. It takes that vendorTxCode to reference the transaction that we tried very first off, checks that the authentication strings from 3DSecure are also present and correct in the cURL data and then attempts to authenticate the payment.
You'll get one of the following responses back:
- OK - The transaction was successful and payment has been accepted. In this case, store the order, send any confirmation emails out and give your user a pat on the back.
- REJECTED - The payment was not authorised by the bank, usually due to insufficient funds.
- NOT AUTHED - As above.
- INVALID - The user's card details where invalid in some way (an incorrect expiry date or something). Prompt the user to re-enter their payment information.
- FAIL - There is a problem somewhere along the line, perhaps SagePay is having issues, or your account has become inactive or something. We really don't want to see this error!
Handle the response accordingly. If the response was 'OK' give your user and yourself a pat on the back. You've successfully implemented SagePay!
I've added the class below for viewing. If you wish to use this for development, you can download it here.
In terms of future development, I'd like to tidy up the interaction with 3DSecure slightly. It might be an idea to have the 3dAuth class extend the SagePay class in one way or another rather than writing two very similar cURL requests.
I hope this has helped - if you've got any questions, comments or ideas for future development, please leave a comment!
sagepay.class.php
<?php
/**
* SagePay class
* Handles the formatting of requests to SagePay,
* the actual request and response of the request
*
* @package Payment
* @author Pete Robinson <work@pete-robinson.co.uk>
* @version 1.0
* @license http://www.gnu.org/copyleft/gpl.html GPL License 2
**/
class SagePay
{
public
$status = '', // status returned from the cURL request
$error = '', // stores any errors
$vendorTxCode = '', // vendor transaction code. must be unqiue
$acsurl = '', // used to store data for 3D Secure
$pareq = '', // used to store data for 3D Secure
$md = ''; // used to store data for 3D Secure
private
$env = '', // environment, set according to 'ENV' site constant
$url = '', // the URL to post the cURL request to (set further down)
$data = array(), // the data to post
$price = 0, // transaction amount
$standardFields = array(), // holds standard SagePay info (currency etc)
$response = array(), // response from SagePay cURL request
$description = 'New order from your online store', // Description of the order sent to SagePay
$curl_str = ''; // the url encoded string derrived from the $this->data array
const
TYPE = 'PAYMENT', // Transaction type
PROTOCOL_VERSION = '2.22', // SagePay protocol vers no
VENDOR = 'my_vendor_name', // Your SagePay vendor name
CURRENCY = 'gbp'; // Currency transaction is to be in. For multi-currency sites, you need to change this from a constant to a property
/**
* Constructor method
* Sets the $this->env property, assigns the necessary urls,
* sets the price, sets and formats the data to pass to SagePay
* @return void
* @param arr $data - the data provided by the user (billing, price and card)
**/
public function __construct($data)
{
$this->env = ENV;
// sets the url to post to based on ENV
$this->setUrls();
$this->setPrice($data['Amount']);
// adds all of the config fields to the data array
$this->setData($data);
// converts $this->data from an array into a query string
$this->formatData();
}
/**
* setData method
* Assigns the user data and SagePay config data to $this->data
* @return void
* @param arr $data - billing and card info from user
**/
private function setData($data)
{
// Set the billing, card and purchase details as provided by the user
$this->data = $data;
// Format the StartDate field
if($data['StartDateMonth']){
// If so, add start date to data array to be appended to POST
$this->data['StartDate'] = $data['StartDateMonth'] . $data['StartDateYear'];
}
// Format the ExpiryDate field
$this->data['ExpiryDate'] = $data['ExpiryDateMonth'] . $data['ExpiryDateYear'];
// set the vendorTxCode
$this->vendorTxCode = $data['VendorTxCode'];
// set the required fields to pass to SagePay
$this->standardFields = array(
'VPSProtocol' => self::PROTOCOL_VERSION,
'TxType' => self::TYPE,
'Vendor' => self::VENDOR,
'VendorTxCode' => $this->vendorTxCode,
'Amount' => $this->price,
'Currency' => self::CURRENCY,
'Description' => $this->description
);
// Add Payment Type
$this->data['PaymentType'] = self::TYPE;
// Add currency details
$this->data['Currency'] = self::CURRENCY;
// Add vendor and transaction details
$this->data['VendorTxCode'] = $this->vendorTxCode;
$this->data['Description'] = $this->description;
$this->data['Vendor'] = self::VENDOR;
}
/**
* setUrls method
* Selects which SagePay url to use (live or test)
* based on the $this->env property
* @return void
**/
private function setUrls()
{
$this->url = ($this->env == 'DEVELOPMENT') ? 'https://test.sagepay.com/gateway/service/vspdirect-register.vsp' : 'https://live.sagepay.com/gateway/service/vspdirect-register.vsp';
}
/**
* setPrice method
*
* @return void
**/
private function setPrice($price)
{
$this->price = $price;
}
/**
* setVendorTxCode method
*
* @return void
**/
private function setVendorTxCode($code)
{
$this->vendorTxCode = $code;
}
/**
* formatData method
* Takes $this->data and converts it to
* a url encoded query string
* @return void
**/
private function formatData()
{
$arr = array();
// loop through $this->data
foreach($this->data as $key => $value){
// assign as an item of $arr (field=value)
$arr[] = $key . '='. urlencode($value);
}
// Implode the array using & as the glue and store the data
$this->curl_str = implode('&', $arr);
}
/**
* execute method
* Executes the cURL request to SagePay and formats the result
*
* @return void
**/
public function execute()
{
// Max exec time of 1 minute.
set_time_limit(60);
// Open cURL request
$curlSession = curl_init();
// Set the url to post request to
curl_setopt ($curlSession, CURLOPT_URL, $this->url);
// cURL params
curl_setopt ($curlSession, CURLOPT_HEADER, 0);
curl_setopt ($curlSession, CURLOPT_POST, 1);
// Pass it the query string we created from $this->data earlier
curl_setopt ($curlSession, CURLOPT_POSTFIELDS, $this->curl_str);
// Return the result instead of print
curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, 1);
// Set a cURL timeout of 30 seconds
curl_setopt($curlSession, CURLOPT_TIMEOUT,30);
curl_setopt($curlSession, CURLOPT_SSL_VERIFYPEER, FALSE);
// Send the request and convert the return value to an array
$response = preg_split('/$\R?^/m',curl_exec($curlSession));
// Check that it actually reached the SagePay server
// If it didn't, set the status as FAIL and the error as the cURL error
if (curl_error($curlSession)){
$this->status = 'FAIL';
$this->error = curl_error($curlSession);
}
// Close the cURL session
curl_close ($curlSession);
// Turn the reponse into an associative array
for ($i=0; $i < count($response); $i++){
// Find position of first "=" character
$splitAt = strpos($response[$i], "=");
// Create an associative array
$this->response[trim(substr($response[$i], 0, $splitAt))] = trim(substr($response[$i], ($splitAt+1)));
}
// Return values. Assign stuff based on the return 'Status' value from SagePay
switch($this->response['Status']) {
case 'OK':
// Transactino made succssfully
$this->status = 'success';
$_SESSION['transaction']['VPSTxId'] = $this->response['VPSTxId']; // assign the VPSTxId to a session variable for storing if need be
$_SESSION['transaction']['TxAuthNo'] = $this->response['TxAuthNo']; // assign the TxAuthNo to a session variable for storing if need be
break;
case '3DAUTH':
// Transaction required 3D Secure authentication
// The request will return two parameters that need to be passed with the 3D Secure
$this->acsurl = $this->response['ACSURL']; // the url to request for 3D Secure
$this->pareq = $this->response['PAReq']; // param to pass to 3D Secure
$this->md = $this->response['MD']; // param to pass to 3D Secure
$this->status = '3dAuth'; // set $this->status to '3dAuth' so your controller knows how to handle it
break;
case 'REJECTED':
// errors for if the card is declined
$this->status = 'declined';
$this->error = 'Your payment was not authorised by your bank or your card details where incorrect.';
break;
case 'NOTAUTHED':
// errors for if their card doesn't authenticate
$this->status = 'notauthed';
$this->error = 'Your payment was not authorised by your bank or your card details where incorrect.';
break;
case 'INVALID':
// errors for if the user provides incorrect card data
$this->status = 'invalid';
$this->error = 'One or more of your card details where invalid. Please try again.';
break;
case 'FAIL':
// errors for if the transaction fails for any reason
$this->status = 'fail';
$this->error = 'An unexpected error has occurred. Please try again.';
break;
default:
// default error if none of the above conditions are met
$this->status = 'error';
$this->error = 'An error has occurred. Please try again.';
break;
}
}
/**
* formatRawData static method
* Takes the array from the form the user fills out
* and returns an array with the correct array keys assigned to each item
*
* @param array $arr - the array of user data to process
* @return array
**/
public static function formatRawData($arr)
{
$data = array();
// this is test card details for SagePay, if we are testing, we don't want to use live card details
if(ENV == 'DEVELOPMENT') {
// this is where the VendorTxCode is set. Once it's set here, don't set it anywhere else, use this one
$data['VendorTxCode'] = 'prefix_' . time() . rand(0, 9999);
$data['CardHolder'] = 'DELTA';
$data['CardNumber'] = '4462000000000003';
$data['StartDateMonth'] = '01';
$data['StartDateYear'] = '05';
$data['ExpiryDateMonth'] = '01';
$data['ExpiryDateYear'] = '12';
$data['CardType'] = 'VISA';
$data['IssueNumber'] = '';
$data['CV2'] = '123';
$data['BillingFirstnames'] = 'Tester';
$data['BillingSurname'] = 'Testing';
$data['BillingAddress1'] = '88';
$data['BillingAddress2'] = '432 Testing Road';
$data['BillingCity'] = 'Test Town';
$data['BillingCountry'] = 'GB';
$data['BillingPostCode'] = '412';
$data['Amount'] = $arr['total'];
} else {
// this is where the VendorTxCode is set. Once it's set here, don't set it anywhere else, use this one
$data['VendorTxCode'] = 'prefix_' . time() . rand(0, 9999);
// If you're using different names for your input fields for this data (card and billing address), use this section
// to map the data to the array keys that SagePay expects. I've used the same keys so this piece of code is pretty much redundant
$data['CardHolder'] = $arr['CardHolder'];
$data['CardNumber'] = $arr['CardNumber'];
$data['StartDateMonth'] = $arr['StartDateMonth'];
$data['StartDateYear'] = $arr['StartDateYear'];
$data['ExpiryDateMonth'] = $arr['ExpiryDateMonth'];
$data['ExpiryDateYear'] = $arr['ExpiryDateYear'];
$data['CardType'] = $arr['CardType'];
$data['IssueNumber'] = $arr['IssueNumber'];
$data['CV2'] = $arr['CV2'];
$data['BillingFirstnames'] = $arr['BillingFirstnames'];
$data['BillingSurname'] = $arr['BillingSurname'];
$data['BillingAddress1'] = $arr['BillingAddress1'];
$data['BillingAddress2'] = $arr['BillingAddress2'];
$data['BillingCity'] = $arr['BillingCity'];
$data['BillingCountry'] = $arr['BillingCountry'];
$data['BillingPostCode'] = $arr['BillingPostCode'];
$data['Amount'] = $arr['Amount'];
}
return $data;
}
}
/**
* SecureAuth class
* Handles the integration with 3dSecure
*
* @package Payment
* @author Pete Robinson <work@pete-robinson.co.uk>
* @version 1.0
* @license http://www.gnu.org/copyleft/gpl.html GPL License 2
**/
class SecureAuth
{
public
$vendorTxCode = '', // vendor transaction code. must be unqiue
$status = '', // status returned from the cURL request
$error = ''; // stores any errors
private
$md = '', // param received from SagePay to pass with the 3D Secure request
$pareq = '', // param received from SagePay to pass with the 3D Secure request
$data = array(), // the data to post to the 3D Secure server
$response = '', // the response from the server
$url = '', // the url to pos the cURL request to
$env = '', // the environment, set according to 'ENV' site constant
$curl_str = ''; // the url encoded string derrived from the $this->data array
/**
* Constructor method
* Sets the $this->env property, assigns the necessary urls,
* sets and formats the data to pass to 3D Secure
* @return void
* @param arr $data - the data provided by the user (billing, price and card)
**/
public function __construct($data)
{
$this->data = $data;
$this->env = ENV;
$this->setUrls();
$this->formatData();
$this->execute();
}
/**
* setUrls method
* Selects which SagePay url to use (live or test)
* based on the $this->env property
* @return void
**/
private function setUrls()
{
$this->url = ($this->env == 'DEVELOPMENT') ? 'https://test.sagepay.com/gateway/service/direct3dcallback.vsp' : 'https://live.sagepay.com/gateway/service/direct3dcallback.vsp';
}
/**
* formatData method
* Takes $this->data and converts it to
* a url encoded query string
* @return void
**/
private function formatData()
{
// Initialise arr variable
$str = array();
// Step through the fields
foreach($this->data as $key => $value){
// Stick them together as key=value pairs (url encoded)
$str[] = $key . '=' . urlencode($value);
}
// Implode the arry using & as the glue and store the data
$this->curl_str = implode('&', $str);
}
/**
* execute method
* Executes the cURL request to SagePay and formats the result
*
* @return void
**/
private function execute()
{
// Max exec time of 1 minute.
set_time_limit(60);
// Open cURL request
$curlSession = curl_init();
// Set the url to post request to
curl_setopt ($curlSession, CURLOPT_URL, $this->url);
// cURL params
curl_setopt ($curlSession, CURLOPT_HEADER, 0);
curl_setopt ($curlSession, CURLOPT_POST, 1);
// Pass it the query string we created from $this->data earlier
curl_setopt ($curlSession, CURLOPT_POSTFIELDS, $this->curl_str);
// Return the result instead of print
curl_setopt($curlSession, CURLOPT_RETURNTRANSFER,1);
// Set a cURL timeout of 30 seconds
curl_setopt($curlSession, CURLOPT_TIMEOUT,30);
curl_setopt($curlSession, CURLOPT_SSL_VERIFYPEER, FALSE);
// Send the request and convert the return value to an array
$response = preg_split('/$\R?^/m',curl_exec($curlSession));
// Check that it actually reached the SagePay server
// If it didn't, set the status as FAIL and the error as the cURL error
if (curl_error($curlSession)){
$this->status = 'FAIL';
$this->error = curl_error($curlSession);
}
// Close the cURL session
curl_close ($curlSession);
// Turn the response into an associative array
for ($i=0; $i < count($response); $i++) {
// Find position of first "=" character
$splitAt = strpos($response[$i], '=');
// Create an associative array
$this->response[trim(substr($response[$i], 0, $splitAt))] = trim(substr($response[$i], ($splitAt+1)));
}
// Return values. Assign stuff based on the return 'Status' value from SagePay
switch($this->response['Status']) {
case 'OK':
// Transactino made succssfully
$this->status = 'success';
$_SESSION['transaction']['VPSTxId'] = $this->response['VPSTxId']; // assign the VPSTxId to a session variable for storing if need be
$_SESSION['transaction']['TxAuthNo'] = $this->response['TxAuthNo']; // assign the TxAuthNo to a session variable for storing if need be
break;
case '3DAUTH':
// Transaction required 3D Secure authentication
// The request will return two parameters that need to be passed with the 3D Secure
$this->acsurl = $this->response['ACSURL']; // the url to request for 3D Secure
$this->pareq = $this->response['PAReq']; // param to pass to 3D Secure
$this->md = $this->response['MD']; // param to pass to 3D Secure
$this->status = '3dAuth'; // set $this->status to '3dAuth' so your controller knows how to handle it
break;
case 'REJECTED':
// errors for if the card is declined
$this->status = 'declined';
$this->error = 'Your payment was not authorised by your bank or your card details where incorrect.';
break;
case 'NOTAUTHED':
// errors for if their card doesn't authenticate
$this->status = 'notauthed';
$this->error = 'Your payment was not authorised by your bank or your card details where incorrect.';
break;
case 'INVALID':
// errors for if the user provides incorrect card data
$this->status = 'invalid';
$this->error = 'One or more of your card details where invalid. Please try again.';
break;
case 'FAIL':
// errors for if the transaction fails for any reason
$this->status = 'fail';
$this->error = 'An unexpected error has occurred. Please try again.';
break;
default:
// default error if none of the above conditions are met
$this->status = 'error';
$this->error = 'An error has occurred. Please try again.';
break;
}
// set error sessions if the request failed or was declined to be handled by controller
if($this->status != 'success') {
$_SESSION['error']['status'] = $this->status;
$_SESSION['error']['description'] = $this->error;
}
}
}
?>










Glyn Jones
#1 18 days ago
Hi Pete,
I'm wondering where it would be possible to modify the code to handle simulator accounts?
I think this would make your class even better.
Cheers,
Glyn