7min.

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:

Paypal buttons example

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.

Rule #2 Double Tap

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