How to Integrate PayPal with Symfony6?

Pranan Subba
6 min readOct 27, 2023

Here we are going to see the procedure for integrating PayPal with Symfony Framework.

As we have seen in the documentation there we can see the solution for NodeJs but not in PHP.

A couple of years ago I integrated PayPal in Symfony, but then had a PHP SDK which was installed with the help of Composer. Now the scenario is different. The PHP SDK is now depreciated and we cannot use the bundle. Below are the steps that you can follow.

Note: You must have a PayPal Account. Where you can create a PayPal Sandbox account.

Once you login to your PayPal account you must find the developer option. At current it is at the top right side of the menu bar. Clicking on the developer you will be redirected to the dashboard whose URL looks like https://developer.paypal.com/…. There you’ll see the sandbox account section among the various options.

There you have to create an app that will also create a Client ID and Secret which will be used while integrating PayPal with Symfony.

In Twig: Create a form with the required input fields

            {{ form_start(form,{'attr':{'novalidate': 'novalidate','id':'donate-form'}}) }}
<div class="row">
<div class="col-md-6">
<div class="form-outline mb-2">
<label class="form-label donation-form-label">Full name</label>
{{ form_widget(form.full_name,{'attr':{'class':'donation-form-input'}}) }}
<div class="text-danger donation-validation" id="error-full_name">
{{ form_errors(form.full_name)}}
</div>
</div>
<div class="form-outline mb-2">
<label class="form-label donation-form-label">Email</label>
{{ form_widget(form.email,{'attr':{'class':'donation-form-input'}}) }}
<div class="text-danger donation-validation" id="error-email">
{{ form_errors(form.email)}}
</div>
</div>
<div class="row mb-2">
<div class="col">
<label class="form-label donation-form-label">Phone No</label>
{{ form_widget(form.phone,{'attr':{'class':'donation-form-input'}}) }}
<div class="text-danger donation-validation" id="error-phone">
{{ form_errors(form.phone)}}
</div>
</div>
<div class="col">
<label class="form-label donation-form-label">Zip/Pin Code</label>
{{ form_widget(form.pin,{'attr':{'class':'donation-form-input'}}) }}
<div class="text-danger donation-validation" id="error-pin">
{{ form_errors(form.pin)}}
</div>
</div>
</div>
<div class="form-outline mb-2">
<label class="form-label donation-form-label">Address</label>
{{ form_widget(form.address,{'attr':{'class':'donation-form-input'}}) }}
<div class="text-danger donation-validation" id="error-address">
{{ form_errors(form.address)}}
</div>
</div>
</div>
<div class="col-md-6">
<div class="h-100">
<div class="mb-2">
<div class="input-group input-container">
<i class="fa-solid fa-dollar-sign input-icon"></i>
{{ form_widget(form.amount,{'attr':{'class':'donation-form-input'}}) }}
</div>
<div class="text-danger donation-validation" id="error-amount">
{{ form_errors(form.amount)}}
</div>
</div>
<div class="row mt-3 text-center price">
<div class="col-md-4 ">
<div class="bg-price-donation" id="tenDollar">$10</div>
</div>
<div class="col-md-4 ">
<div class="bg-price-donation" id="twentyDollar">$20</div>
</div>
<div class="col-md-4 ">
<div class="bg-price-donation" id="fiftyDollar">$50</div>
</div>
</div>
<div class="row mt-3 text-center text-white price">
<div class="col-md-4 ">
<div class="bg-price-donation" id="hundredDollar">$100</div>
</div>
<div class="col-md-4 ">
<div class="bg-price-donation" id="twoFiftyDollar">$250</div>
</div>
<div class="col-md-4 ">
<div class="bg-price-donation" id="customDollar">Custom</div>
</div>
</div>
<div id="paypal-button-container" class="mt-4"></div>
<p id="result-message"></p>
</div>
</div>
</div>
{{ form_end(form) }}

Instead of an HTML Button, you should use it as shown in the above code.

<div id=”paypal-button-container” class=”mt-4"></div>

JS SDK

<script src="https://www.paypal.com/sdk/js?client-id=AVgysO3goklzIaZbNvAnLRQz9LSDf1oNwl3tDwrtA_dV3c5eEXLXL3DvIjQP4Z_haUrZhDlytuX2UPLd&components=buttons&enable-funding=paylater,venmo,card" data-sdk-integration-source="integrationbuilder_sc"></script>

Js from the documentation

<script>
$(document).ready(function () {
let hasError = true;
window.paypal.Buttons({
style: {
shape: "rect",
layout: "vertical",
},
async createOrder() {
try {
const response = await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: $('#donation_amount').val(),
}),
});

const orderData = JSON.parse(await response.json());

if (orderData.id) {
return orderData.id;
} else {
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);

throw new Error(errorMessage);
}
} catch (error) {
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
},
async onApprove(onApprovedData, actions) {

var formData = {
'amount': $('#donation_amount').val(),
'full_name': $('#donation_full_name').val(),
'phone_no': $('#donation_phone').val(),
'email': $('#donation_email').val(),
'pin': $('#donation_pin').val(),
'address': $('#donation_address').val(),
};
try {
const response = await fetch(`/api/orders/${onApprovedData.orderID}/capture`, {
method: "POST",
body: JSON.stringify(formData),
headers: {
"Content-Type": "application/json",
},
});
const orderData = await response.json();
const errorDetail = orderData?.details?.[0];
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
return actions.restart();
} else if (errorDetail) {
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
} else if (!orderData.purchase_units) {
throw new Error(JSON.stringify(orderData));
} else {
const transaction =
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];

const route = `/orders/completed/${transaction.id}`;
window.location.href = route;
}
} catch (error) {
console.error(error);
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${error}`,
);
}
},
}).render("#paypal-button-container");

function resultMessage(message) {
const container = document.querySelector("#result-message");
container.innerHTML = message;
}
});

</script>

Symfony Controller

class Donations
{
const PAYPAL_CLIENT_ID => 'asdasd';
const PAYPAL_CLIENT_SECRET => 'asdasd';
}
<?php

namespace App\Controller;

use App\Form\DonationType;
use App\Entity\Donations;
use App\Service\ValidatorService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\KernelInterface;

class PaymentGatewayController extends AbstractController
{
private $em;
private $kernel;
public function __construct(EntityManagerInterface $em, KernelInterface $kernel)
{
$this->em = $em;
$this->kernel = $kernel;
}

#[Route(path: '/payment', name: 'app_payment_gateway')]
public function payment(Request $request, ValidatorService $validator): Response
{
$donation = new Donations();
$form = $this->createForm(DonationType::class, $donation);
$form->handleRequest($request);
return $this->render('payment_gateway/index.html.twig', [
'form' => $form,
]);
}

private function getAccessToken()
{
$cert = $this->kernel->getProjectDir().'/public/cert/cacert.pem';

if (!empty(Donations::PAYPAL_CLIENT_ID) && !empty(Donations::PAYPAL_CLIENT_SECRET)) {
$auth = base64_encode(Donations::PAYPAL_CLIENT_ID . ":" . Donations::PAYPAL_CLIENT_SECRET);

// $apiEndpoint = "https://api.paypal.com"; // For production

$apiEndpoint = "https://api-m.sandbox.paypal.com"; // ForDev
$url = $apiEndpoint . "/v1/oauth2/token";

$data = "grant_type=client_credentials";

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$headers = [
"Authorization: Basic $auth",
"Content-Type: application/x-www-form-urlencoded",
];

curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CAINFO, $cert);
$response = curl_exec($ch);

if (curl_errno($ch)) {
echo "Failed to generate Access Token: " . curl_error($ch);
} else {
$data = json_decode($response, true);

$access_token = $data['access_token'];
return $access_token;
}

curl_close($ch);
} else {
echo "MISSING_API_CREDENTIALS";
}
}

#[Route(path: '/api/orders', name: 'orders')]
public function orders(Request $request)
{
$data = json_decode($request->getContent(), true);
$url = Donations::BASE_URL.'/v2/checkout/orders';
$payload = [
'intent' => 'CAPTURE',
'purchase_units' => [
[
'amount' => [
'currency_code' => 'USD',
'value' => $data['amount']
]
]
]
];

$headers = [
"Content-Type: application/json",
"Authorization: Bearer " . $this->getAccessToken(),
];

$ch = curl_init($url);
$cert = $this->kernel->getProjectDir().'/public/cert/cacert.pem';
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $cert);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $cert);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));

$response = curl_exec($ch);

if (curl_errno($ch)) {
echo 'Error: ' . curl_error($ch);
}

curl_close($ch);

return new JsonResponse($response);
}

#[Route('/api/orders/{orderID}/capture', name:"capture_order")]
public function captureOrders($orderID, Request $request)
{
$donation = new Donations();
try {
$result = $this->captureOrder($orderID);
$data = json_decode($request->getContent(), true);

$jsonResponse = $result['jsonResponse'];
$httpStatusCode = $result['httpStatusCode'];

if(isset($jsonResponse['id']) && Donations::COMPLETED == $jsonResponse['status']) {
$donation->setAmount($data['amount']);
$donation->setFullName($data['full_name']);
$donation->setPhone($data['phone_no']);
$donation->setPin($data['pin']);
$donation->setEmail($data['email']);
$donation->setAddress($data['address']);
$donation->setPaypalOrderId($orderID);
$donation->setPaypalTransactionId($jsonResponse['purchase_units'][0]['payments']['captures'][0]['id']);

$this->em->persist($donation);
$this->em->flush();

return new JsonResponse($jsonResponse, $httpStatusCode);
}else{
dd($result);
}
return new JsonResponse($jsonResponse, $httpStatusCode);
} catch (Exception $error) {
error_log("Failed to create order: " . $error->getMessage());
return new JsonResponse(['error' => 'Failed to capture order.'], 500);
}
}

public function captureOrder($orderID)
{
$cert = $this->kernel->getProjectDir().'/public/cert/cacert.pem';
try {
$accessToken = $this->getAccessToken();
$url = Donations::BASE_URL . "/v2/checkout/orders/".$orderID."/capture";

$headers = [
'Content-Type: application/json',
"Authorization: Bearer $accessToken",
];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_CAINFO, $cert);
$response = curl_exec($ch);
if (curl_errno($ch)) {
error_log("Failed to capture order: " . curl_error($ch));
return new JsonResponse(['error' => 'Failed to capture order.'], 500);
}

return $this->handleResponse($response);

} catch (Exception $error) {
// error_log("Failed to capture order: " . $error->getMessage());
// return new JsonResponse(['error' => 'Failed to capture order.'], 500);
return $error;
}
}

public function handleResponse($response)
{
try {
$jsonResponse = json_decode($response, true);
return [
'jsonResponse' => $jsonResponse,
'httpStatusCode' => http_response_code(),
];
} catch (Exception $err) {
$errorMessage = $response;
throw new Exception($errorMessage);
}
}

#[Route(path: '/orders/completed/{transactionId}', name: 'orders_completed')]
public function orderCompleted($transactionId)
{
$order = $this->em->getRepository(Donations::class)->findOneBy(['paypal_transaction_id' => $transactionId]);

return $this->render('payment_gateway/res.html.twig',['order' => $order]);
}
}

This is it. Now your Symfony app is ready to receive the amount. For full code click here.

--

--

Pranan Subba
0 Followers

<?php echo “Hello World!”; ?>