How to Integrate PayHere Payment Gateway with Symfony PHP Framework

Supul kalhara
4 min readJun 8, 2024

--

Register with PayHere and Get your Credentials

Register for a PayHere Sandbox Account

payhere sandbox mode

Before integrating PayHere, you need to register for a sandbox account to test the payment gateway.

  1. Go to the PayHere Sandbox Registration page.
  2. Fill in the registration form and submit it.
  3. Once registered, log in to your sandbox account.

Create a Merchant Account and Obtain Credentials

  1. After logging in, go to the “Merchant Settings” section.
  2. Here, you will need to create a new merchant by specifying your domain. This will generate your Merchant ID and Merchant Secret.
  3. Note down these credentials as you will need them for the integration.

Add Domain for Testing

  1. In the “Merchant Settings,” add your local development domain (e.g., http://localhost:8000 or any other domain you use for development).
  2. Ensure that the domain is correctly added and configured.

Configure PayHere in Symfony

Add your PayHere Merchant ID and Secret Key to your Symfony environment variables.

In your .env file, add:

PAYHERE_MERCHANT_ID=your_merchant_id
PAYHERE_SECRET_KEY=your_secret_key

Replace your_merchant_id and your_secret_key with the credentials you obtained from the PayHere sandbox.

Create Payment Controller

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;

class PaymentController extends AbstractController
{
/**
* @Route("/pay", name="pay")
*/
public function pay(Request $request): Response
{
$amount = $request->request->get('amount');
$order_id = uniqid();
$merchant_id = $_ENV['PAYHERE_MERCHANT_ID'];
$secret_key = $_ENV['PAYHERE_SECRET_KEY'];

// Here, you should save the order details to your database, setting the status to 'pending'.
// Example:
// $order = new Order();
// $order->setId($order_id);
// $order->setAmount($amount);
// $order->setStatus('pending');
// $entityManager = $this->getDoctrine()->getManager();
// $entityManager->persist($order);
// $entityManager->flush();

// Prepare PayHere form data
$form_data = [
'merchant_id' => $merchant_id,
'return_url' => $this->generateUrl('pay_return', [], true),
'cancel_url' => $this->generateUrl('pay_cancel', [], true),
'notify_url' => $this->generateUrl('pay_notify', [], true),
'order_id' => $order_id,
'items' => 'Test Item',
'currency' => 'LKR',
'amount' => $amount,
'first_name' => 'John',
'last_name' => 'Doe',
'email' => 'john.doe@example.com',
'phone' => '0771234567',
'address' => 'No.1, Galle Road',
'city' => 'Colombo',
'country' => 'Sri Lanka',
];

return $this->render('payment/pay.html.twig', [
'form_data' => $form_data,
]);
}

/**
* @Route("/pay/notify", name="pay_notify", methods={"POST"})
*/
public function notify(Request $request): Response
{
$merchant_id = $request->request->get('merchant_id');
$order_id = $request->request->get('order_id');
$payhere_amount = $request->request->get('payhere_amount');
$payhere_currency = $request->request->get('payhere_currency');
$status_code = $request->request->get('status_code');
$md5sig = $request->request->get('md5sig');
$secret = $_ENV['PAYHERE_SECRET_KEY'];

$local_md5sig = strtoupper(md5($merchant_id . $order_id . $payhere_amount . $payhere_currency . $status_code . strtoupper(md5($secret))));

if ($md5sig === $local_md5sig) {
// Payment status
$entityManager = $this->getDoctrine()->getManager();
$order = $entityManager->getRepository(Order::class)->find($order_id);

if ($status_code == '2') {
// Payment success, update order status
$order->setStatus('completed');
} else {
// Payment failed or cancelled, update order status
$order->setStatus('failed');
}

$entityManager->persist($order);
$entityManager->flush();
}

return new Response('OK');
}

/**
* @Route("/pay/return", name="pay_return")
*/
public function return(Request $request): Response
{
$order_id = $request->query->get('order_id');
$status_code = $request->query->get('status_code');

if ($status_code == '2') {
// Update order status to completed
$entityManager = $this->getDoctrine()->getManager();
$order = $entityManager->getRepository(Order::class)->find($order_id);
$order->setStatus('completed');
$entityManager->persist($order);
$entityManager->flush();

return new Response('Payment Successful!');
} else {
return new Response('Payment Failed.');
}
}

/**
* @Route("/pay/cancel", name="pay_cancel")
*/
public function cancel(Request $request): Response
{
$order_id = $request->query->get('order_id');

// Update order status to cancelled
$entityManager = $this->getDoctrine()->getManager();
$order = $entityManager->getRepository(Order::class)->find($order_id);
$order->setStatus('cancelled');
$entityManager->persist($order);
$entityManager->flush();

return new Response('Payment Cancelled.');
}
}

Make notify_url publically accessible (can’t use “localhost”)

If your application uses authentication, you need to exclude the notify_url from authentication or allow it to be accessed anonymously.

Exclude notify_url from Authentication

In your security.yaml configuration file, you can define the path to be publicly accessible:

security:
# other configurations
firewalls:
# other firewalls
dev:
pattern: ^/pay/notify$
security: false
# other firewalls
access_control:
- { path: ^/pay/notify$, roles: IS_AUTHENTICATED_ANONYMOUSLY }

Allow Anonymous Access

Alternatively, you can configure the notify_url route to allow anonymous access:

security:
# other configurations
firewalls:
main:
anonymous: true
# other configurations
access_control:
- { path: ^/pay/notify$, roles: IS_AUTHENTICATED_ANONYMOUSLY }

Create Payment Form View

Create a Twig template to render the PayHere payment form.

After the file is rendered, automatically open the PayHere payment gateway popup.

<!DOCTYPE html>
<html>
<head>
<title>PayHere Payment</title>
</head>
<body>
<h1>Make a Payment</h1>
<form id="payhere" method="post" action="https://sandbox.payhere.lk/pay/checkout">
<input type="hidden" name="merchant_id" value="{{ form_data.merchant_id }}">
<input type="hidden" name="return_url" value="{{ form_data.return_url }}">
<input type="hidden" name="cancel_url" value="{{ form_data.cancel_url }}">
<input type="hidden" name="notify_url" value="{{ form_data.notify_url }}">
<input type="hidden" name="order_id" value="{{ form_data.order_id }}">
<input type="hidden" name="items" value="{{ form_data.items }}">
<input type="hidden" name="currency" value="{{ form_data.currency }}">
<input type="hidden" name="amount" value="{{ form_data.amount }}">
<input type="hidden" name="first_name" value="{{ form_data.first_name }}">
<input type="hidden" name="last_name" value="{{ form_data.last_name }}">
<input type="hidden" name="email" value="{{ form_data.email }}">
<input type="hidden" name="phone" value="{{ form_data.phone }}">
<input type="hidden" name="address" value="{{ form_data.address }}">
<input type="hidden" name="city" value="{{ form_data.city }}">
<input type="hidden" name="country" value="{{ form_data.country }}">
<input type="submit" value="Buy Now" style="display: none;">
</form>
</body>
</html>

<script>
$(document).ready(function () {
setTimeout(() => {
$('form#payhere').submit();
}, 500);
});
</script>

You have now successfully integrated the PayHere payment gateway with your Symfony application. This guide provides a basic integration. Depending on your requirements, you might need to handle additional scenarios such as different currencies, recurring payments, and more comprehensive error handling. Be sure to consult the PayHere API documentation for more details.

Good Luck With Your PayHere Integration!

--

--

Supul kalhara

Passionate Software Engineer Eager to Uncover the Building Blocks of Technology