Tips for a PayPal implementation with Symfony in 2022 🪙
We recently had to implement a PayPal payment. It was my first payment implementation ever and I obviously wanted to do it right. The thing is, our client wanted a specific behavior, when we were more worried about the security. All the PayPal PHP libraries we found were deprecated, and the only thing we could rely on was the PayPal documentation, which is okay, but doesn’t offer many options and mainly leads to using the JavaScript SDK. So here are a few tips and workarounds for the PayPal implementation with Symfony in 2022!
In order for you to easily follow along and see more complete and functional code, I’ve set a sandbox project up on my GitHub, feel free to clone it!
Section intitulée the-tools-at-your-disposalThe tools at your disposal
PayPal for developers comes with a bunch of useful tools. You can connect to the platform with your usual personal account to get a lot of fake accounts and sandbox app ids. You can create single or bulk accounts, make them personal or business or even manage the funds on these accounts in order to test as many cases as you like.
We strongly recommend writing down the email and password of some of your test accounts since the PayPal website will ask you to reconnect, each time using 2FA, every ten minutes or so, which can be quite annoying. Also, don’t forget to write those down in a developer documentation, so if you were using your personal developer account and if you were to leave that project, other developers wouldn’t have to set up new test accounts. 🙂
Section intitulée bundlesBundles?
Of course, as Symfony developers, our first reflex was to look for a bundle. Unfortunately, none is currently maintained. The official SDKs are archived on GitHub too.
Section intitulée using-the-js-sdkUsing the Js SDK
Section intitulée first-stepsFirst steps
The standard way to implement the PayPal payment is by using the JavaScript SDK. Basically, you just have to include their script in your HTML, set a div with a specific id to welcome the buttons, less than ten JavaScript lines and voilà, you have the two nice default buttons!
To obtain this result:
your code will look something like:
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID&locale=en_US"></script>
<script>
paypal.Buttons({
// All your options here !
}).render('#paypal-button-container');
</script>
Click on any of these buttons and a popup payment window will show up. In case you’re wondering, no, changing the amount of the transaction in the HTML or in the JS iframe won’t change the amount displayed in the PayPal payment window, which you can verify in the API call history in the Dashboard. Of course, with this example, you haven’t set any amount to pay (the default amount is 0.01 USD), you’ll have to add a few options.
Section intitulée add-some-callbacksAdd some callbacks
Many callbacks are available depending on the user’s action and how the payment went. The three main callbacks we used are onApprove
, onCancel
and onError
, whose names are pretty straightforward. You’ll often see something called the intent: it defines whether you want to only authorize a payment or actually capture it.
paypal.Buttons({
// All your options here !
createOrder: (data, actions) => {
return actions.order.create({
intent: 'capture', // capture or authorize
purchase_units: [{
amount: {
value: 50.25
},
description: 'Magical unicorn',
invoice_id: '1234567890',
soft_descriptor: 'unicorn-2345678'
}],
application_context: {
brand_name: 'My amazing brand',
shipping_preference: 'NO_SHIPPING' // if you handle shipping
}
});
},
// Finalizes the transaction after payer approval
onApprove: (data) => {
console.log('Unicorn bought, yay !')
},
// The user closed the window
onCancel: () => {
console.log('The user canceled the payment');
},
onError: (err) => {
console.log('Something went wrong', err);
}
}).render('#paypal-button-container');
Section intitulée paypal-button-custom-credit-card-inputsPayPal button + custom credit card inputs
It did not fit our needs since the PayPal default buttons do have the credit card option. So we won’t walk you through that one, but know that this option also exists. 😉 If you’re interested, the documentation walks you through the steps.
Section intitulée paypal-in-another-window-with-a-formPayPal in another window with a Form
You also might be asked to have the PayPal payment window to be opened in another tab instead of a popup. The only solution we’ve seen was to use a form with target="_blank"
, but we would not recommend it since it’s way easier to modify the transaction’s amount in this case. If you want to use it anyway, be sure to specify a callback URL to verify that the amount paid corresponds to the order amount. Also, don’t forget to check the currency used to pay on PayPal : Yes the user paid 250, but 250 what? 😱
<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post" target="_blank">
<!-- Identify your business so that you can collect the payments. -->
<input type="hidden" name="business" value="busines-owner-mail@business.example.com
">
<!-- Specify a Buy Now button. -->
<input type="hidden" name="cmd" value="_xclick">
{# Provide a more specific callback URL here #}
<input type="hidden" name="return" value="http://127.0.0.1:8000/confirmation">
<!-- Specify details about the item that buyers will purchase. -->
<input type="hidden" name="item_name" value="Flying unicorn">
{# Be very careful here, take the time to verify if the paid amount corresponds to
the Order amount in the return URL #}
<input type="hidden" name="amount" value="15.85">
<input type="hidden" name="currency_code" value="EUR">
<!-- Display the payment button. -->
<button type="submit">Give me money 💸</button>
<!-- Or use the payment button provided by PayPal. -->
<input type="image" name="submit" border="0" src="https://www.paypalobjects.com/en_US/i/btn/btn_buynow_LG.gif" alt="Buy Now">
<img alt="" border="0" width="1" height="1" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" >
</form>
Section intitulée using-the-rest-apiUsing the REST API
The JS SDK is easy and fast to install, but getting the money is not the only thing happening in a transaction. You might want to save some transaction details, send data to another service, redirect the user, send a confirmation mail, and especially, you might want to double check with PayPal if the payment was accepted.
Also, whether you choose to use the SDK or a form, you can set the intent on Authorize, then confirm the payment and capture it server side using the REST API.
Section intitulée configure-your-httpclientConfigure your HttpClient
You will interact with different endpoints, each needing a different configuration. Don’t hesitate to configure your HttpClient to ease this task, so in your service, you won’t have to think about headers or auth; you’ll just have to call the Client corresponding to your needs.
framework:
http_client:
scoped_clients:
# Get access token
http_client.paypal.auth:
#note that this is a regexp
scope: '%env(PAYPAL_API_REGEXP)%/v1/oauth2/token'
auth_basic: '%env(PAYPAL_CLIENT_ID)%:%env(PAYPAL_SECRET)%'
headers:
Content-Type: 'application/x-www-form-urlencoded'
# Get Order details
http_client.paypal.order:
scope: '%env(PAYPAL_API_REGEXP)%/v2/checkout/orders/[0-9A-Z]{17}'
headers:
Content-Type: 'application/json'
# Capture payment
http_client.paypal.payment:
scope: '%env(PAYPAL_API_REGEXP)%/v2/checkout/orders/[0-9A-Z]{17}/capture'
headers:
Content-Type: 'application/json'
Prefer: 'return=representation' # To get a complete representation of payment
In the .env.dev:
PAYPAL_FORM_ACTION=https://www.sandbox.paypal.com/cgi-bin/webscr
PAYPAL_API=https://api-m.sandbox.paypal.com
PAYPAL_API_REGEXP=https://api-m\.sandbox\.paypal\.com
PAYPAL_DEBUG=true
And in the production .env:
PAYPAL_FORM_ACTION=https://www.paypal.com/cgi-bin/webscr
PAYPAL_API=https://api-m.paypal.com
PAYPAL_API_REGEXP=https://api-m\.paypal\.com
PAYPAL_DEBUG=false
Section intitulée authenticationAuthentication
First, you’ll need an access token to query on the PayPal REST API.
// https://developer.paypal.com/api/rest/authentication/
$responseBody = $this->authClient->request('POST', $this->getAuthEndpointUrl(), [
'body' => 'grant_type=client_credentials',
]);
$accessToken = $responseBody->toArray()['access_token'];
Section intitulée get-the-order-info-from-paypalGet the Order info from PayPal
In the callback, you should get an OrderId. Use this Id to get all the informations about the transaction that happened on PayPal :
// https://developer.paypal.com/docs/api/orders/v2/#orders_get
$responseBody = $this->orderClient->request('GET', $this->getGetEndpointUrl($paypalOrderId), [
'headers' => [
'Authorization' => 'Bearer ' . $this->getAccessToken(),
],
]);
$responseHttpCode = $responseBody->getStatusCode();
$paypalOrder = $responseBody->toArray();
Section intitulée check-for-fraudCheck for fraud
public function paidRightAmount(array $paypalOrder, Order $order): bool
{
// Check if the purchase currency is the right one
if ($paypalOrder['purchase_units'][0]['amount']['currency_code'] !== 'EUR') {
return false;
}
// Check if the amount paid is the right one
if ((int) $paypalOrder['purchase_units'][0]['amount']['value'] !== $order->getAmount()) {
$order->setStatus(Order::STATUS_FRAUD_SUSPECTED);
return false;
}
return true;
}
Section intitulée actually-capture-the-paymentActually capture the payment
// https://developer.paypal.com/docs/api/orders/v2/#orders_capture
$responseBody = $this->paymentClient->request('POST', $this->getCaptureEndpointUrl($paypalOrderId), [
'headers' => [
'Authorization' => 'Bearer ' . $this->getAccessToken(),
'Paypal-Request-Id' => $order->getId(),
],
]);
$responseHttpCode = $responseBody->getStatusCode();
You can find the full code of this example here.
Section intitulée make-it-rainMake it rain 💸
PayPay is not our preferred payment method, especially compared to Stripe and its amazing documentation, but it is still very popular, and we know that we will have to implement it on many websites to come. So these were a few tips we wish we could have known before; we hope they will be useful to you too !
Commentaires et discussions
Ces clients ont profité de notre expertise
Chugulu Games a souhaité que JoliCode l’accompagne, en tant qu’expert du framework Appcelerartor Titanium, dans le cadre du développement de l’application “Music Mania”. Nous avons profondément remanié la base de code existante, et aidé à garantir le bon fonctionnement de l’application sur iOS et Android, aussi bien sur smartphone que sur…
Discourse est un logiciel libre pour forum de discussions très puissant, sur lequel Mix with the Masters s’appuie pour créer et animer sa communauté. Nous avons appliqué une forte customisation du logiciel sur plusieurs aspects : thème graphique complet en accord avec la charte graphique du site ; plugin dédiés pour afficher un paywall ; implémentation…
Aujourd’hui, notre équipe travaille avec Courbet Paris en expérimentant toutes les possibilités techniques que la visualisation en 3D pourrait apporter à leurs produits. Ce travail de recherche & développement nous mène naturellement à jouer au quotidien avec des technologies comme le gyroscope, WebGL, ThreeJS…