Integrate Stripe payment into your PHP projects

June 17, 2024

/

14 minutes

TL;DR

Complete functional code repository here (with cart simulation) Simple example on how to implement Stripe PaymentIntent (SCA compliant) in your project to enable payment.

Introduction

If you're developing websites, SaaS, web applications, or pretty much anything else, you've probably needed certain payment features at some point for a project. Most of the time, if you're building your project on a CMS, you will have access to contributory projects (such as WooCommerce for WordPress or Commerce for Drupal) that will handle the payment process for you, and you'll just need to provide API keys to enable payments. This is also handled for you if your project is built on a platform like Sylius, PrestaShop, or other e-commerce platforms.

But in some cases, your project is not built on one of the solutions mentioned just before, so you will need to implement your own payment system. (Or maybe you're just curious about how to do it) There are many payment solutions available on the market (PayPal, Apple Pay, Six Payment, ...) and some projects try to aggregate these payment gateways, like Omnipay.

To keep things simple, we will focus today only on a payment solution which is Stripe that gives you the ability to accept card payments on your project.

Implementing Stripe

First of all, please sign up on Stripe to access your secret and public keys and make them accessible to your application.

Please DO NOT store your customers' cards in your application/database, send card data DIRECTLY to Stripe, NEVER store card credential information!

Front

You will need to create a form for your users that will contain all the information you wish as well as the user's card information.

<form id="payment-form" data-stripe-public-key="<?= STRIPE_PUBLIC_KEY ?>">
<div class="sr-combo-inputs-row">
<div class="sr-input sr-card-element" id="card-element"></div>
<div id="card-errors"></div>
</div>

<div class="sr-field-error" id="card-errors" role="alert"></div>

<button id="submit">
<div class="spinner hidden" id="spinner"></div>

<span id="button-text">
Payer

<span id="order-amount"><?= $price ?> <?= STRIPE_CURRENCY_SYMBOL ?></span>
</span>
</button>
</form>

STRIPE_PUBLIC_KEY represents your public key and STRIPE_CURRENCY_SYMBOL represents the symbol in which you will charge the customer. (for the example, let's assume it's in euros, so the symbol is ) Additionally, $price contains the amount that will be charged, this should be understandable by your customers. (something like 12.99)

The main element here is #card-element which will be managed by Stripe.js to display nice inputs for your users to fill in the card information. For this to work, include the Stripe script in your head tag.

<head>
<!-- ... -->
<script src='https://js.stripe.com/v3/'></script>
<!-- ... -->
</head>

Then, you will also need to write Javascript to mount the Stripe card behaviors on your form, create a script.js file and load it at the end of your body tag. Here is the beginning of your file : (only vanilla javascript, adapt it to your needs / your framework)



This is essentially about initialization, setting up Stripe card behaviors, listening for card information, and listening during form submission. The important part is the call to the pay() function, it will start the payment process when you submit the form. Here is the pay function:


It manages the entire payment process but also calls different functions to achieve this; - changeLoadingState() manages the behavior of the spinner

  • showError() displays the error if necessary

  • handleAction() manages additional actions if necessary (which will be dictated by the client's card bank)

  • orderComplete() called when the payment is successful and complete


It toggles the spinner to inform the user of the pending process.


Informs the user with the given error message.


The payment is completed, adapt it to your needs.


In some cases, the user's card bank will require an additional step to validate the transaction, this function handles it (stripe.handleCardAction(clientSecret)) it is represented by a Stripe pop-up window that asks the user to confirm the payment or to confirm their validation in 2 steps to validate the entire transaction. This function will reach the same endpoint (/pay.php) of our server to resubmit the payment but this time it will provide the paymentIntentId that was built during the first step of our payment process (when we called the pay() function).

That's it, the only Javascript we'll need to make payments!

Return

We now need to handle requests addressed to our endpoints, but first, don't forget to install the Stripe PHP SDK.

(Currently, I am using the latest version v7.27.0 of the SDK)

And to better organize ourselves and keep it simple we will create a few files:

  • public/pay.php endpoint to receive payment requests

  • src/StripeHelper.php Helper class to manage the different steps of our payment process

  • src/BodyException.php Exception thrown when the request $body is not formatted correctly

So here is the endpoint pay.php:


We have broken down the payment process into smaller steps contained in our StripeHelper class (for the needs of the tutorial, we do not care about SOLID principles) for simplicity, so that we can clearly read how the process is executed through our endpoint.

  1. We can first see that our endpoint will only return JSON

  2. We only allow POST requests

  3. We set our Stripe secret key

  4. We retrieve our cart, it can be anything, an array, an object, or whatever you want, it’s your own implementation. Stripe doesn’t care, Stripe only needs a total amount that will be linked to the PaymentIntent to know how much will be charged to the customer

  5. We extract the total amount from our cart, again, this method will need to be replaced to work with your own implementation of your cart. Also note that the total amount MUST be in cents

  6. We get our body information from the request (request made by our script.js that provides the payment intent data in its body)

  7. We build our PaymentIntent object from the body and the amount

  8. If the payment intent is fully realized, it's a success! We can do business logic from here (record the payment, redirect the user to the order completion page, prepare the carrier, ...)

  9. We return a response to the client

Also note that we have wrapped the entire process in a try/catch block, indeed, during the process, many issues can arise (the body of the request was not formatted correctly, the Stripe API did not respond to our calls, ...) so we will detect all these errors to prevent even more problems.

I will not cover the subject of the cart as it is not the purpose of this article, but you can find a mockup of a cart in the demo example.

See how the steps have been broken down into small pieces:


From there, we obtain a stdClass object that contains either the key paymentMethodId (from the pay() function in script.js) or the key paymentIntentId (from the handleAction() function in script.js). Of course, it could contain much more if you gave additional keys to the body of your request.

# StripeHelper::createPaymentIntent()

/**
* Essayez de créer un objet PaymentIntent
*
* @param object $body
* @param int $amount
*
* @return PaymentIntent
*
* @throws Exception levée si l'intention de paiement n'a pas pu être créée
*/
public static function createPaymentIntent(object $body, int $amount): PaymentIntent
{
try {
// Créez un nouveau PaymentIntent avec un ID PaymentMethod du client.
if (!empty($body->paymentMethodId)) {
$intent = PaymentIntent::create([
'amount' => $amount,
'currency' => STRIPE_CURRENCY,
'payment_method' => $body->paymentMethodId,
'confirmation_method' => 'manual',
'confirm' => true,
// Ceci n'est utilisé que pour le test d'intégration, vous pouvez les tester ici : https://stripe.com/docs/payments/accept-a-payment#web-test-integration
// 'metadata' => ['integration_check' => 'accept_a_payment']

Here we try to build our PaymentIntent object, in the first condition (if (!empty($body->paymentMethodId))) it will create a brand new PaymentIntent object (which would be initiated by the pay() function from the front) to which a unique identifier will be assigned.

In the case where the client card bank does not need additional validation, that’s fine, the payment will be successful from the first request! But in some cases, the bank will ask the user to confirm the payment (2-step validation) or to secure it (3D secure). These validations are part of the SCA protocol.

Thus, in the second scenario, our process will indicate to the front that it needs additional actions from the client to complete the payment process. It will go through the handleAction() function on our front and then resubmit the payment request but this time with the paymentIntentId that was created in our first step (since that is the payment intent we wanted to confirm).

This second request will go into our second condition (else if (!empty($body->paymentIntentId))) to be retrieved by Stripe and confirmed.

If for any reason this process fails, we will throw exceptions to prevent the payment flow from continuing.

# StripeHelper::generateResponse()

/**
* Générer un tableau de réponses correct en fonction du statut de l'intention de paiement
*
* @param \Stripe\PaymentIntent $intent
*
* @return array
*/
public static function generateResponse(PaymentIntent $intent): array
{
switch ($intent->status) {
// La carte nécessite une authentification
case PaymentIntent::STATUS_REQUIRES_ACTION:
case 'requires_source_action':
return [
'requiresAction' => true,
'paymentIntentId' => $intent->id,
'clientSecret' => $intent->client_secret,
];

// La carte n'a pas été correctement authentifiée, suggérez un nouveau mode de paiement
case PaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD:
case 'requires_source':
return [
'error' => "Votre carte a été refusée, veuillez fournir un nouveau mode de paiement",
];

// Le paiement est terminé, l'authentification n'est pas requise
// Pour annuler lepaiement après la capture, vous devrez émettre un remboursement (https://stripe.com/docs/api/refunds)
case PaymentIntent::STATUS_SUCCEEDED:
return [
'clientSecret' => $intent->client_secret,
];

case PaymentIntent::STATUS_CANCELED:
return [
'error' => "Le paiement a été annulé, veuillez réessayer plus tard"
];

// Si un autre cas inattendu se produit, nous renvoyons une erreur
default:
return [
'error' => "Une erreur s'est produite, veuillez réessayer plus tard",
]

Generate the response returned to the client. Note that we specify requiresAction if the status of the payment intent indicates that additional action is needed to be completed. This is the key that triggers our pay() function to call the handleAction() function.


Returns simply if the payment intent is completed.

Solution

Well, I think this tutorial is simple enough to get started with the Stripe API and create your own custom implementation from it. I haven’t used any front-end or back-end framework, so it’s easier for everyone to understand plain PHP / JS.

Also remember that you can do a lot with Stripe; recurring payments, customer storage, sending invoices, ...

You can find the official Stripe example here](https://github.com/stripe-samples/accept-a-card-payment/tree/master/without-webhooks/server/php) from which I built this solution. (Please note that the original example does not work if you install it, you will need to modify it a bit to make it work)

Here is the entire functional code repository for the example project

Learn more about Stripe Payments and its documentation here

Feel free to give me your feedback or correct me if you see any mistakes

Thank you for reading!

Share

Share

Share

English

Stay connected with us by signing up for our newsletter!

© Riven 2025

English

Stay connected with us by signing up for our newsletter!

© Riven 2025

English

Stay connected with us by signing up for our newsletter!

© Riven 2025