PayFast Payment Gateway Integration in PHP

Do you want to integrate the PayFast payment gateway into your application? In this article, I will show you the integration of the PayFast payment gateway using PHP and MySQL. We’ll create 2 dummy products, perform payment using sandbox mode, and store transaction details in the database.

PayFast is an online payment processing service for South African merchants. It provides a variety of payment methods like credit/debit cards, Instant EFT, Masterpass, etc to accept payment from buyers.

We’ll write the code for PayFast integration and test it in sandbox mode. You have to grab the merchant id, merchant key, and passphrase of your PayFast account. One may get these details by registering on PayFast Sandbox.

To get started, you have to create the following PHP files in your project directory.

  • form.php : This creates the HTML form where users select the product and enter their details.
  • process_payment.php : It builds the actual payment form in the background providing PayFast standards and submits it programmatically.
  • notify.php : To this file, PayFast posted the Instant Transaction Notification(ITN).
  • return.php : The user will redirect to this page after completing a payment successfully.
  • cancel.php : If the user canceled the payment mid-way, they will redirect to this URL.
  • class-db.php : This file is responsible for performing database-related operations.
  • config.php : Store all required constants in this file.

Database Configuration

In order to see the payment flow in action, let’s create a products table using the below SQL. This table has fields – id, name, and price.

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `price` float(10,2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Upon creating a products table, add 2 dummy entries to it.

INSERT INTO `products` (`id`, `name`, `price`) VALUES
(1, 'Product 1', 100.00),
(2, 'Product 2', 150.00);

When payment is initiated or done, we should store the transaction details. For this, run the below SQL which creates a transactions table.

CREATE TABLE `transactions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `payment_id` varchar(255) DEFAULT NULL,
  `product_id` int(11) DEFAULT NULL,
  `first_name` varchar(255) DEFAULT NULL,
  `last_name` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `amount` float(10,2) DEFAULT NULL,
  `status` enum('pending','failed','canceled','completed') NOT NULL DEFAULT 'pending',
  `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

In our PHP applications, we’ll have to interact with these 2 database tables for fetching, inserting, and updating records. Let’s create a class-db.php file and add the following code to it.

<?php
class DB {
    private $dbHost     = "DB_HOST";
    private $dbUsername = "DB_USERNAME";
    private $dbPassword = "DB_PASSWORD";
    private $dbName     = "DB_NAME";
 
    public function __construct(){
        if(!isset($this->db)){
            // Connect to the database
            $conn = new mysqli($this->dbHost, $this->dbUsername, $this->dbPassword, $this->dbName);
            if($conn->connect_error){
                die("Failed to connect with MySQL: " . $conn->connect_error);
            }else{
                $this->db = $conn;
            }
        }
    }

    public function get_products() {
        $sql = $this->db->query("SELECT * FROM products");
        return $sql->fetch_all(MYSQLI_ASSOC);
    }
 
    public function get_product($id) {
        $sql = $this->db->query("SELECT * FROM products WHERE id = '$id'");
        return $sql->fetch_assoc();
    }

    public function get_transaction($payment_id) {
        $sql = $this->db->query("SELECT * FROM transactions WHERE payment_id = '$payment_id'");
        return $sql->fetch_assoc();
    }
 
    public function upsert_transaction($arr_data = array()) {
        $payment_id = $arr_data['payment_id'];
 
        // check if transaction exists
        $transaction = $this->get_transaction($payment_id);
 
        if(!$transaction) {
            // insert the transaction
            $product_id = $arr_data['product_id'];
            $first_name = $arr_data['first_name'];
            $last_name = $arr_data['last_name'];
            $email = $arr_data['email'];
            $amount = $arr_data['amount'];
            $this->db->query("INSERT INTO transactions(payment_id, product_id, first_name, last_name, email, amount) VALUES('$payment_id', '$product_id', '$first_name', '$last_name', '$email', '$amount')");
        } else {
            // update the transaction
            $status = $arr_data['status'];
            $this->db->query("UPDATE transactions SET status = '$status' WHERE payment_id = '$payment_id'");
        }
    }
}

PayFast Payment Gateway Configuration (config.php)

To integrate PayFast, we have to pass its credentials and a few URLs to handle the payment flow. I’ll define all necessary values as PHP constants.

<?php
session_start();

require_once "class-db.php";

define("PAYFAST_MERCHANT_ID", "");
define("PAYFAST_MERCHANT_KEY", "");
define("PAYFAST_PASSPHRASE", "");
define("PAYFAST_RETURN_URL", "DOMAIN_URL/return.php");
define("PAYFAST_CANCEL_URL", "DOMAIN_URL/cancel.php");
define("PAYFAST_NOTIFY_URL", "DOMAIN_URL/notify.php");
define("PAYFAST_SANDBOX_MODE", true); // set it to false to make it live

Along with defining constants, I also started a PHP session and included a database class file. This is because we’ll include this file wherever it’s needed to use session and database operations.

Initiate the PayFast Payment Process

We require the HTML form to start the payment flow. In this form, users will choose the product, fill in their basic details and submit it. For this purpose let’s use the form.php file.

<?php
require_once "config.php";

$db = new DB;
$arr_products = $db->get_products();
?>
<form action="process_payment.php" method="post">
    <p>
        <select name="product" required>
            <?php foreach( $arr_products as $product ) : ?>
                <option value="<?php echo $product['id']; ?>">
                    <?php echo $product['name'].' - R'.$product['price']; ?>
                </option>
            <?php endforeach; ?>
        </select>
    </p>
    <p>
        <input type="text" name="name_first" placeholder="First Name*" required />
    </p>
    <p>
        <input type="text" name="name_last" placeholder="Last Name*" required />
    </p>
    <p>
        <input type="email" name="email_address" placeholder="Email*" required />
    </p>
    <input type="submit" value="Pay Now" />
</form>

When this form is submitted, we store these ongoing transaction details in the database. While storing details, the payment status will be set to pending as the actual payment is yet to be received.

All these operations are handled inside the process_payment.php file which is set as a form action URL. This file automatically redirects the user to the PayFast website to complete the payment.

<?php
require_once "config.php";

extract($_POST);

$db = new DB;
$arr_product = $db->get_product($product);

if (empty($arr_product)) {
    die('Invalid product.');
}

function generateSignature($data, $passPhrase = null) {
    // Create parameter string
    $pfOutput = '';
    foreach( $data as $key => $val ) {
        if($val !== '') {
            $pfOutput .= $key .'='. urlencode( trim( $val ) ) .'&';
        }
    }
    // Remove last ampersand
    $getString = substr( $pfOutput, 0, -1 );
    if( $passPhrase !== null ) {
        $getString .= '&passphrase='. urlencode( trim( $passPhrase ) );
    }
    return md5( $getString );
}

// generate unique payment id
$payment_id = $_SESSION['payment_id'] = time().uniqid();

// insert initial transaction
$arr_data = array(
    'payment_id' => $payment_id,
    'product_id' => $product,
    'first_name' => $name_first,
    'last_name' => $name_last,
    'email' => $email_address,
    'amount' => $arr_product['price'],
);

$db = new DB;
$db->upsert_transaction($arr_data);

$data = array(
    // Merchant details
    'merchant_id' => PAYFAST_MERCHANT_ID,
    'merchant_key' => PAYFAST_MERCHANT_KEY,
    'return_url' => PAYFAST_RETURN_URL,
    'cancel_url' => PAYFAST_CANCEL_URL,
    'notify_url' => PAYFAST_NOTIFY_URL,
    // Buyer details
    'name_first' => $name_first,
    'name_last'  => $name_last,
    'email_address'=> $email_address,
    // Transaction details
    'm_payment_id' => $payment_id, //Unique payment ID to pass through to notify_url
    'amount' => number_format( sprintf( '%.2f', $arr_product['price'] ), 2, '.', '' ),
    'item_name' => $arr_product['name'],
);

$signature = generateSignature($data, PAYFAST_PASSPHRASE);
$data['signature'] = $signature;

$pfHost = PAYFAST_SANDBOX_MODE ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
$htmlForm = '<form action="https://'.$pfHost.'/eng/process" method="post" id="frmPayment">';
foreach($data as $name=> $value)
{
    $htmlForm .= '<input name="'.$name.'" type="hidden" value=\''.$value.'\' />';
}
$htmlForm .= '<input type="submit" value="Pay Now" style="display:none;" /></form>';
echo $htmlForm;
?>

<h3>Redirecting to a PayFast...</h3>

<script>
window.addEventListener('load', (event) => {
    document.getElementById("frmPayment").submit();
});
</script>

Confirm Payment Is Successful

Look at process_payment.php where we passed return_url and notify_url. Before the customer is redirected back to the return_url, PayFast sends payment notifications to the notify_url. In this file, we’ll verify the payment and mark it either completed or failed.

notify.php

<?php
// Tell PayFast that this page is reachable by triggering a header 200
header( 'HTTP/1.0 200 OK' );
flush();

require_once "config.php";

$pfHost = PAYFAST_SANDBOX_MODE ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
// Posted variables from ITN
$pfData = $_POST;

// Strip any slashes in data
foreach( $pfData as $key => $val ) {
    $pfData[$key] = stripslashes( $val );
}

// Convert posted variables to a string
$pfParamString = '';
foreach( $pfData as $key => $val ) {
    if( $key !== 'signature' ) {
        $pfParamString .= $key .'='. urlencode( $val ) .'&';
    } else {
        break;
    }
}

$pfParamString = substr( $pfParamString, 0, -1 );

function pfValidSignature( $pfData, $pfParamString, $pfPassphrase = null ) {
    // Calculate security signature
    if($pfPassphrase === null) {
        $tempParamString = $pfParamString;
    } else {
        $tempParamString = $pfParamString.'&passphrase='.urlencode( $pfPassphrase );
    }

    $signature = md5( $tempParamString );
    return ( $pfData['signature'] === $signature );
}

function pfValidIP() {
    // Variable initialization
    $validHosts = array(
        'www.payfast.co.za',
        'sandbox.payfast.co.za',
        'w1w.payfast.co.za',
        'w2w.payfast.co.za',
        );

    $validIps = [];

    foreach( $validHosts as $pfHostname ) {
        $ips = gethostbynamel( $pfHostname );

        if( $ips !== false )
            $validIps = array_merge( $validIps, $ips );
    }

    // Remove duplicates
    $validIps = array_unique( $validIps );
    $referrerIp = gethostbyname(parse_url($_SERVER['HTTP_REFERER'])['host']);
    if( in_array( $referrerIp, $validIps, true ) ) {
        return true;
    }
    return false;
}

function pfValidPaymentData( $cartTotal, $pfData ) {
    return !(abs((float)$cartTotal - (float)$pfData['amount_gross']) > 0.01);
}

function pfValidServerConfirmation( $pfParamString, $pfHost = 'sandbox.payfast.co.za', $pfProxy = null ) {
    // Use cURL (if available)
    if( in_array( 'curl', get_loaded_extensions(), true ) ) {
        // Variable initialization
        $url = 'https://'. $pfHost .'/eng/query/validate';

        // Create default cURL object
        $ch = curl_init();
    
        // Set cURL options - Use curl_setopt for greater PHP compatibility
        // Base settings
        curl_setopt( $ch, CURLOPT_USERAGENT, NULL );  // Set user agent
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );      // Return output as string rather than outputting it
        curl_setopt( $ch, CURLOPT_HEADER, false );             // Don't include header in output
        curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
        
        // Standard settings
        curl_setopt( $ch, CURLOPT_URL, $url );
        curl_setopt( $ch, CURLOPT_POST, true );
        curl_setopt( $ch, CURLOPT_POSTFIELDS, $pfParamString );
        if( !empty( $pfProxy ) )
            curl_setopt( $ch, CURLOPT_PROXY, $pfProxy );
    
        // Execute cURL
        $response = curl_exec( $ch );
        curl_close( $ch );
        if ($response === 'VALID') {
            return true;
        }
    }
    return false;
}

// get transaction stored in database
$db = new DB;
$transaction = $db->get_transaction($pfData['m_payment_id']);

$pfPassphrase = PAYFAST_PASSPHRASE;
$check1 = pfValidSignature($pfData, $pfParamString, $pfPassphrase);
$check2 = pfValidIP();
$cartTotal = $transaction['amount'];
$check3 = pfValidPaymentData($cartTotal, $pfData);
$check4 = pfValidServerConfirmation($pfParamString, $pfHost);

$arr_data = array('payment_id' => $pfData['m_payment_id']);

if($check1 && $check2 && $check3 && $check4) {
    // All checks have passed, the payment is successful
    $arr_data['status'] = 'completed';
    $db->upsert_transaction($arr_data);
} else {
    // Some checks have failed, check payment manually and log for investigation
    $arr_data['status'] = 'failed';
    $db->upsert_transaction($arr_data);
}

Success and Cancel Pages

If a customer has completed the payment, they will be redirected to the return_url where we should show them a success message and give them the transaction id.

return.php

<?php
require_once "config.php";

$payment_id = $_SESSION['payment_id'];

// check if transaction exists
$db = new DB;
$transaction = $db->get_transaction($payment_id);

if ('completed' == $transaction['status']) {
    echo "Payment is successful. Your Payment ID is $payment_id";
} elseif ('failed' == $transaction['status']) {
    echo "Payment is failed.";
}

For the canceled payment, the customer will redirect to the cancel_url. In this case, PayFast doesn’t send any notifications to the notify_url so we have to update the payment status in the cancel.php file.

cancel.php

<?php
require_once "config.php";

$arr_data = array(
    'payment_id' => $_SESSION['payment_id'],
    'status' => 'canceled',
);

$db = new DB;
$db->upsert_transaction($arr_data);

echo "Payment is canceled.";

We’re done with PayFast payment gateway integration in PHP. Now, to give it a trial run the form.php in the browser, complete the steps and you’ll see the working flow in action.

Finally, to make the payment integration live you just need to change the constant value for PAYFAST_SANDBOX_MODE.

define('PAYFAST_SANDBOX_MODE', false);

Related Articles

If you liked this article, then please subscribe to our YouTube Channel for video tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *