Introduire le paiement Stripe dans vos projets PHP

TL;DR

Référentiel de code fonctionnel complet ici (avec simulation de panier) Exemple simple sur la façon d'implémenter Stripe PaymentIntent (conforme SCA) dans votre projet pour permettre le paiement.

Introduction

Si vous développez des sites Web, des SaaS, des applications Web ou à peu près n'importe quoi d'autre, vous avez probablement eu besoin de certaines fonctionnalités de paiement à un moment donné pour un projet. La plupart du temps, si vous construisez votre projet sur un CMS, vous aurez accès à des projets contributifs (tels que WooCommerce pour WordPress ou Commerce pour Drupal) qui géreront le processus de paiement pour vous et vous aurez juste besoin de fournir des clés API pour activer les paiements. Ceci est également géré pour vous si votre projet est construit sur une plateforme comme Sylius, PrestaShop ou d'autres plateformes de commerce électronique.

Mais dans certains cas, votre projet n'est pas construit sur l'une des solutions citées juste avant, vous devrez donc mettre en œuvre votre propre système de paiement. (Ou peut-être êtes-vous simplement curieux de savoir comment faire) Il existe de nombreuses solutions de paiement disponibles sur le marché (PayPal, Apple Pay, Six Payment, ...) et certains projets tentent d'agréger ces passerelles de paiement, c'est le cas de Omnipay.

Afin de rester simple, nous nous concentrerons aujourd'hui uniquement sur une solution de paiement qui est Stripe qui vous donne la possibilité d'accepter les paiements par carte sur votre projet.

Implémentation de Stripe

Tout d'abord, veuillez vous inscrire sur Stripe pour accéder à vos clés secrètes et publiques et les rendre accessibles à votre application.

Veuillez NE PAS enregistrer les cartes de vos clients dans votre application/base de données, envoyez les données de carte DIRECTEMENT à Stripe, NE JAMAIS enregistrer les informations d'identification des cartes !

Front

Vous devrez créer un formulaire pour vos utilisateurs qui contiendra toutes les informations que vous souhaitez ainsi que les informations de carte de l'utilisateur.

<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 représente votre clé publique et STRIPE_CURRENCY_SYMBOL représente le symbole dans lequel vous facturerez le client. (pour l'exemple, supposons que c'est en euros, le symbole est donc ) De plus, $price contient le montant qui sera facturé, cela devrait être compréhensible par vos clients. (quelque chose comme 12,99)

L'élément principal ici est #card-element qui sera géré par Stripe.js pour afficher de belles entrées pour que vos utilisateurs puissent remplir les informations de la carte. Pour que cela fonctionne, incluez le script Stripe dans votre balise head.

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

Ensuite, vous devrez également écrire du Javascript pour monter les comportements de carte Stripe sur votre formulaire, créer un fichier script.js et le charger à la fin de votre balise body. Voici le début de votre fichier : (uniquement du javascript vanilla, adaptez-le à vos besoins / à votre framework)

var stripe = null;
var form = document.getElementById('payment-form');
var formBtn = document.querySelector('button');
var formBtnText = document.querySelector('#button-text');
var formSpinner = document.querySelector('#spinner');
var cardErrorContainer = document.getElementById('erreurs de carte');
var errorContainer = document.querySelector('.sr-field-error');
var resultContainer = document.querySelector('.sr-result');
var preContainer = document.querySelector('pre');

document.addEventListener('DOMContentLoaded', function () {
    var stripePublicKey = form.dataset.stripePublicKey;
    stripe = Stripe(stripePublicKey);
    var card = stripe.elements().create('card', {
        // This hide zipcode field. If you enable it, you can increase card acceptance
        // and also reduce card fraud. But sometime your users don't like to fill it
        // Please adapt to your need
        hidePostalCode: true,
        style: {
            base: {
                color: '#32325d',
                fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
                fontSmoothing: 'antialiased',
                fontSize: '16px',
                '::placeholder': {
                    color: '#aab7c4',
                },
            },
            invalid: {
                color: '#fa755a',
                iconColor: '#fa755a',
            },
        },
    });

// Lier la carte Stripe au DOM
card.mount('#card-element');

// Afficher les erreurs lors de la saisie des informations de la carte
card.addEventListener('change', function (e) {
if (e.error) {
cardErrorContainer.classList.add('show');
cardErrorContainer.textContent = e.error.message;
} else {
cardErrorContainer.classList.remove('show');
cardErrorContainer.textContent = '';
}
});

// Lancer le paiement lors de la soumission
form.addEventListener('submit', function (event) {
event.preventDefault();
pay(stripe, card);
});
});

Il s'agit essentiellement d'initialisation, de montage des comportements de carte Stripe, d'écoute des informations de carte et d'écoute lors de la soumission du formulaire. La partie importante est l'appel à la fonction pay(), elle démarrera le processus de paiement lorsque vous soumettrez le formulaire. Voici la fonction pay :

// Collecter les détails de la carte et payer la commande
var pay = function (stripe, card) {
changeLoadingState(true);

stripe
.createPaymentMethod('card', card)
.then(function (result) {
if (result.error) {
showError(result.error.message);
throw result.error.message;
} else {
return fetch('/pay.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentMethodId: result.paymentMethod.id,
}),
});
}
})
.then(function (result) {
return result.json();
})
.then(function (paymentData) {
// La carte nécessite une validation en 2 étapes / 3D secure
if (paymentData.requiresAction) {
handleAction(paymentData.clientSecret);
}
// Une erreur a été déclenchée par serveur, nous l'affichons
else if (paymentData.error) {
showError(paymentData.error);
}
// Tout s'est parfaitement déroulé, paiement effectué !
else {
orderComplete(paymentData.clientSecret);
}
})
.catch(function (error) {
console.log(error);
});
};

Il gère l'ensemble du processus de paiement mais appelle également différentes fonctions pour y parvenir ; - changeLoadingState() gère le comportement du spinner

  • showError() affiche l'erreur si nécessaire
  • handleAction() gère les actions supplémentaires si nécessaire (qui seront dictées par la banque de la carte client)
  • orderComplete() appelé lorsque le paiement est réussi et terminé
// Afficher un spinner lors de la soumission du paiement
var changeLoadingState = function (isLoading) {
if (isLoading) {
formBtn.disabled = true;
formSpinner.classList.remove('hidden');
formBtnText.classList.add('hidden');
} else {
formBtn.disabled = false;
formSpinner.classList.add('hidden');
formBtnText.classList.remove('hidden');
}
};

Il bascule le spinner pour informer l'utilisateur du processus en attente.

// Afficher un message d'erreur
var showError = function (errorMsgText) {
changeLoadingState(false);

errorContainer.textContent = errorMsgText;

setTimeout(function () {
errorContainer.textContent = '';
}, 4000);
};

Informe l'utilisateur avec le message d'erreur donné.

// Affiche un message de réussite lorsque le paiement est terminé
var orderComplete = function (clientSecret) {
stripe
.retrievePaymentIntent(clientSecret)
.then(function (result) {
var paymentIntent = result.paymentIntent;
var paymentIntentJson = JSON.stringify(paymentIntent, null, 2);

form.classList.add('hidden');
preContainer.textContent = paymentIntentJson;
resultContainer.classList.remove('hidden');

setTimeout(function () {
resultContainer.classList.add('expand');
}, 200);

changeLoadingState(false);
});
};

Le paiement est terminé, adaptez-le à vos besoins.

// Demande d'authentification (utilisé par certaines cartes qui nécessitent des paiements sécurisés 3D / une validation supplémentaire)
var handleAction = function (clientSecret) {
stripe
.handleCardAction(clientSecret)
.then(function (data) {
if (data.error) {
showError('Votre carte n'a pas été authentifiée, veuillez réessayer');
} else if (data.paymentIntent.status === 'requires_confirmation') {
fetch('/pay.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentIntentId: data.paymentIntent.id,
}),
})
.then(function (result) {
return result.json();
})
.then(function (response) {
if (response.error) {
showError(response.error);
} else {
orderComplete(clientSecret);
}
});
}
});
};

Dans certains cas, la banque de la carte de l'utilisateur nécessitera une étape supplémentaire afin de valider la transaction, cette fonction s'en charge (stripe.handleCardAction(clientSecret)) elle est représentée par une fenêtre contextuelle d'ouverture de Stripe qui demande à l'utilisateur de confirmer le paiement ou de confirmer sa validation en 2 étapes afin de valider l'ensemble de la transaction. Cette fonction va atteindre le même point de terminaison (/pay.php) de notre serveur pour resoumettre le paiement mais cette fois elle donnera le paymentIntentId qui a été construit lors de la première étape de notre processus de paiement (quand nous avons appelé la fonction pay()).

C'est tout, le seul Javascript dont nous aurons besoin pour effectuer des paiements !

Retour

Nous devons maintenant gérer les requêtes adressées à nos points de terminaison, mais d'abord, n'oubliez pas d'installer le Stripe PHP SDK.

$ composer require stripe/stripe-php

(A l'heure actuelle, j'utilise la dernière version v7.27.0 du SDK)

Et pour mieux nous organiser et rester simple nous allons créer quelques fichiers :

  • public/pay.php endpoint pour recevoir les demandes de paiement
  • src/StripeHelper.php Classe d'aide pour gérer les différentes étapes de notre processus de paiement
  • src/BodyException.php Exception levée lorsque la requête $body n'est pas formatée correctement

Voici donc l'endpoint pay.php :

<?php
# public/pay.php

/**
* Fichier principal, il gère le processus de paiement
*/

use App\BodyException;
use App\StripeHelper;
use Stripe\Stripe;

require __DIR__ . '/../vendor/autoload.php';

// Renverra du JSON
header('Content-Type: application/json');

// Accessible uniquement par la méthode POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);

echo json_encode([
'error' => 'Invalid request.',
]);

exit;
}

// Commencer le processus de paiement
try {
// Définir la clé Stripe
Stripe::setApiKey(STRIPE_SECRET_KEY);

// Votre implémentation de panier, cela peut être tout ce que vous voulez
$cart = \App\ImaginaryContainer::get('app.cart');

// Calculez le montant total de l'intention de paiement, encore une fois, implémentez le vôtre ici
$amount = StripeHelper::calculateAmountFromCart($cart);

// Construisez un objet corps à partir de la requête, nous l'utiliserons pour créer notre intention de paiement
$body = StripeHelper::buildBodyFromRequest();

// Construisez l'intention de paiement
$intent = StripeHelper::createPaymentIntent($body, $amount);

// Paiement effectué, mettez votre logique ici
if (StripeHelper::isPaymentIntentCompleted($intent)) {
// Enregistrez le paiement dans la base de données, redirigez l'utilisateur, ...
}

// Construisez la réponse
$response = StripeHelper::generateResponse($intent);

echo json_encode($response);
}
// Cela peut se produire si la requête n'a pas été correctement formatée
catch (BodyException $e) {
http_response_code(400);

echo json_encode([
'error' => $e->getMessage(),
]);

exit;
}
// Détectez toute autre erreur, juste au cas où
catch (Exception $e) {
echo json_encode([
'error' => $e->getMessage(),
]);
}

Nous avons décomposé le processus de paiement en étapes plus petites contenues dans notre classe StripeHelper (pour les besoins du tutoriel, nous ne nous soucions pas des principes SOLID) pour des raisons de simplicité, afin que nous puissions clairement lire comment le processus est exécuté via notre point de terminaison.

  1. Nous pouvons d'abord voir que notre point de terminaison ne renverra que du JSON
  2. Nous autorisons uniquement les requêtes POST
  3. Nous définissons notre clé secrète Stripe
  4. Nous récupérons notre panier, cela peut être n'importe quoi, un tableau, un objet ou tout ce que vous voulez, c'est votre propre implémentation. Stripe ne s'en soucie pas, Stripe n'a besoin que d'un montant total qui sera lié au PaymentIntent pour savoir combien sera facturé au client
  5. Nous extrayons le montant total de notre panier, encore une fois, cette méthode devra être remplacée pour fonctionner avec votre propre implémentation de votre panier. *Remarquez également que le montant total DOIT être en centimes*
  6. Nous obtenons nos informations de corps à partir de la requête (requête effectuée par notre script.js qui fournit les données d'intention de paiement dans son corps)
  7. Nous construisons notre objet PaymentIntent à partir du corps et du montant
  8. Si l'intention de paiement est entièrement réalisée, c'est un succès ! Nous pouvons faire de la logique métier à partir d'ici (enregistrer le paiement, rediriger l'utilisateur vers la page de fin de commande, préparer le transporteur, ...)
  9. Nous renvoyons une réponse au client

Remarquez également que nous avons enveloppé l'ensemble du processus dans un bloc try/catch, en effet, pendant le processus, de nombreux problèmes peuvent survenir (le corps de la requête n'était pas formaté correctement, l'API Stripe n'a pas répondu à nos appels, ...) nous allons donc détecter toutes ces erreurs pour éviter encore plus de problèmes.

Je ne couvrirai pas le sujet du panier car ce n'est pas le but de cet article, mais vous pouvez trouver une simulation d'un panier dans l'exemple de démonstration.

Voyez comment les étapes ont été divisées en petits morceaux :

# StripeHelper::buildBodyFromRequest()

/**
* Construire le corps qui sera utilisé par l'intention de paiement Stripe à partir de la requête
*
* @return object
*
* @throws \App\BodyException
*/
public static function buildBodyFromRequest(): object
{
$input = file_get_contents('php://input');
$body = json_decode($input);

// Vérifiez si le corps a été correctement décodé.
if (json_last_error() !== JSON_ERROR_NONE) {
throw new BodyException('Invalid request.');
}

return $body;
}

A partir de là, nous obtenons un objet stdClass qui contient soit la clé paymentMethodId (de la fonction pay() dans script.js) soit la clé paymentIntentId (de la fonction handleAction() dans script.js). Bien sûr, il pourrait contenir bien plus si vous donniez des clés supplémentaires au corps de votre requête.

# 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'],
]);
// Après la création, si le statut de PaymentIntent est réussi, exécutez la commande.

return $intent;
}
// Confirmez PaymentIntent pour finaliser le paiement après avoir traité une action requise sur le client.
else if (!empty($body->paymentIntentId)) {
$intent = PaymentIntent::retrieve($body->paymentIntentId);
$intent->confirm();
// Après confirmation, si le statut de PaymentIntent est réussi, exécuter la commande.

return $intent;
}
}
// Cela peut se produire si la demande échoue
catch (ApiErrorException $e) {
throw new Exception("Une erreur s'est produite lors du traitement du paiement. Veuillez réessayer plus tard");
}

// Si $body a été modifié, alors peut-être que les arguments souhaités ont été supprimés
throw new Exception("Une erreur s'est produite lors du traitement du paiement. Veuillez réessayer plus tard");
}

Nous essayons ici de construire notre objet PaymentIntent, dans la première condition (if (!empty($body->paymentMethodId))) il va créer un tout nouvel objet PaymentIntent (qui serait initié par la fonction pay() depuis le front) auquel sera attribué un identifiant unique.

Dans le cas où la banque de la carte client n'a pas besoin de validation supplémentaire, tout va bien, le paiement sera réussi dès la première demande ! Mais dans certains cas, la banque demandera à l'utilisateur de confirmer le paiement (validation en 2 étapes) ou de le sécuriser (3D secure). Ces validations font partie du protocole SCA.

Ainsi, dans le deuxième scénario, notre processus indiquera au front qu'il a besoin d'actions supplémentaires de la part du client pour terminer le processus de paiement. Il passera par la fonction handleAction() sur notre front puis resoumettra la demande de paiement mais cette fois avec le paymentIntentId qui a été créé dans notre première étape (puisque c'est cette intention de paiement que nous voulions confirmer).

Cette deuxième requête ira dans notre deuxième condition (else if (!empty($body->paymentIntentId))) pour être récupérée par Stripe et confirmée.

Si pour une raison quelconque ce processus échoue, nous lancerons des exceptions pour empêcher le flux de paiement de continuer.

# 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",
];
}
}

Générer la réponse renvoyée au client. Notez que nous spécifions requiresAction si le statut de l'intention de paiement indique qu'une action supplémentaire est nécessaire pour être terminée. C'est cette clé qui déclenche notre fonction pay() pour appeler la fonction handleAction().

# StripeHelper::isPaymentIntentCompleted()

/**
* L'intention de paiement donnée est-elle terminée ?
*
* @param PaymentIntent $intent
*
* @return bool
*/
public static function isPaymentIntentCompleted(PaymentIntent $intent): bool
{
return $intent->status === 'succeeded';
}

Renvoie simplement si l'intention de paiement est terminée.

Solution

Eh bien, je pense que ce tutoriel est suffisamment simple pour démarrer avec l'API Stripe et créer votre propre implémentation personnalisée à partir de celle-ci. Je n'ai utilisé aucun framework front-end ou back-end, il est donc plus facile pour tout le monde de comprendre le PHP / JS simple.

N'oubliez pas non plus que vous pouvez faire beaucoup de choses avec Stripe ; paiements récurrents, sauvegarde des clients, envoi de factures, ...

Vous pouvez trouver l'exemple officiel de Stripe ici](https://github.com/stripe-samples/accept-a-card-payment/tree/master/without-webhooks/server/php) à partir duquel j'ai construit cette solution. (Veuillez noter que l'exemple original ne fonctionne pas si vous l'installez, vous devrez le modifier un peu pour le faire fonctionner)

Voici tout le référentiel de code fonctionnel pour l'exemple de projet

Apprenez-en plus sur Stripe Payments et sa documentation ici

N'hésitez pas à me faire part de vos commentaires ou à me corriger si vous voyez des erreurs

Merci de votre lecture !

Read more

Créons ensemble

Naviguer

Connecter

Bureau

1%