Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

Pro Error Handling

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $9.00

A lot of failures are stopped right here: instead of passing us back a token, Stripe tells us something is wrong and we tell the user immediately.

But guess what? We are not handling all cases where things can go wrong. Go back to the Stripe documentation and click the link called "Testing". This page is full of helpful information about how to test your Stripe setup. One of the most interesting parts is this cool table of fake credit card numbers that you can use in the Test environment. They include cards that will work, but also cards that will fail, for various reasons.

Ah, this one is particularly important: this number will look valid, but will be declined when we try to charge it.

Let's try this out. Use: 4000 0000 0000 0002. Give it a valid expiration and then hit enter. It's submitting, but woh! A huge 500 error:

Your card was declined.

This is a problem: on production, this would be a big error screen with no information. Instead, we need to be able to tell our user what went wrong: we need to be able to say "Hey Buddy! You card was declined".

Stripe Error Handling 101

Let's talk about how Stripe handles errors. First, on the error page, if I hover over this little word, it tells me that a Stripe\Error\Card exception object was thrown. Whenever you make an API request to Stripe, it will either be successful, or Stripe will throw an exception.

On Stripe's API documentation, near the top, they have a section that talks about Errors.

There are a few important things. First, Stripe uses different status codes to give you some information about what went wrong. That's cool, but these types are more important. When you make an API request to Stripe and it fails, Stripe will send back a JSON response with a type key. That type will be one of these values. This goes a long way to telling you what went wrong.

So, how can we read the type inside our code?

Open up the vendor/stripe directory to look at the SDK code. Hey, check this out: the library has a custom Exception class for each of the possible type values. For example, if type is card_error, the library throws a Card exception, which is what we're getting right now.

But if Stripe was rate limiting us because we made way too many requests, Stripe would throw a RateLimit exception. This means that we can use a try-catch block to handle only specific error types.

Isolating the Checkout Code

The one error we need to handle is card_error - because this happens when a card is declined.

To do that, let's move all of this processing logic into its own private function in this class. That'll make things cleaner.

To do this, I'll use a PhpStorm shortcut: select the code, hit Control+T (or go to the "Refactor"->"Refactor This" menu) and select "Method". Create a new method called chargeCustomer(). Hit refactor:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 38
$this->chargeCustomer($token);
... lines 40 - 44
}
... lines 46 - 52
}
/**
* @param $token
*/
private function chargeCustomer($token)
{
$stripeClient = $this->get('stripe_client');
/** @var User $user */
$user = $this->getUser();
if (!$user->getStripeCustomerId()) {
$stripeClient->createCustomer($user, $token);
} else {
$stripeClient->updateCustomerCard($user, $token);
}
foreach ($this->get('shopping_cart')->getProducts() as $product) {
$stripeClient->createInvoiceItem(
$product->getPrice() * 100,
$user,
$product->getName()
);
}
$stripeClient->createInvoice($user, true);
}
}
... lines 79 - 80

You don't need PhpStorm to do that: it just moved my code down into this private function and called that function from the original spot.

Handling the Card Exception

OK, back to business: we know that when a card is declined, something in that code will throw a Stripe\Error\Card exception. I'm adding a little documentation just to indicate this:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 62
/**
* @param $token
* @throws \Stripe\Error\Card
*/
private function chargeCustomer($token)
{
... lines 69 - 85
}
}
... lines 88 - 89

Back in checkoutAction(), add a new $error = false variable before the if, because at this point, no error has occurred:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
$error = false;
if ($request->isMethod('POST')) {
... lines 38 - 51
}
... lines 53 - 60
}
... lines 62 - 86
}
... lines 88 - 89

Next, surround the chargeCustomer() call in a try-catch: try chargeCustomer() and then catch just a \Stripe\Error\Card exception:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
$error = false;
if ($request->isMethod('POST')) {
... lines 38 - 39
try {
$this->chargeCustomer($token);
} catch (\Stripe\Error\Card $e) {
... line 43
}
... lines 45 - 51
}
... lines 53 - 60
}
... lines 62 - 86
}
... lines 88 - 89

If we get here, there was a problem charging the card. Update $error to some nice message, like: "There was a problem charging your card.". Then add $e->getMessage():

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
$error = false;
if ($request->isMethod('POST')) {
... lines 38 - 39
try {
$this->chargeCustomer($token);
} catch (\Stripe\Error\Card $e) {
$error = 'There was a problem charging your card: '.$e->getMessage();
}
... lines 45 - 51
}
... lines 53 - 60
}
... lines 62 - 86
}
... lines 88 - 89

That's will be the message that Stripe's sending back like, "Your card was declined," or, "Your card cannot be used for this type of transaction."

Now, if there is an error, we don't want to empty the cart, we don't want to add the nice message and we don't want to redirect. So, if (!$error), then it's safe to do those things:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 35
$error = false;
if ($request->isMethod('POST')) {
... lines 38 - 39
try {
$this->chargeCustomer($token);
} catch (\Stripe\Error\Card $e) {
$error = 'There was a problem charging your card: '.$e->getMessage();
}
if (!$error) {
$this->get('shopping_cart')->emptyCart();
$this->addFlash('success', 'Order Complete! Yay!');
return $this->redirectToRoute('homepage');
}
}
... lines 53 - 60
}
... lines 62 - 86
}
... lines 88 - 89

If there is an error, our code will continue down and it will re-render the checkout template, which is exactly what we want! Pass in the $error variable so we can show it to the user:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 53
return $this->render('order/checkout.html.twig', array(
... lines 55 - 57
'error' => $error,
));
}
... lines 62 - 86
}
... lines 88 - 89

Then, in then template, specifically the _cardForm template, render error inside of our error div:

<form action="" method="POST" class="js-checkout-form checkout-form">
... lines 2 - 53
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 text-center">
<div class="alert alert-danger js-checkout-error {{ error ? '' : 'hidden' }}">{{ error }}</div>
</div>
</div>
... lines 59 - 66
</form>

If there is no error, no problem! It won't render anything. If there is an error, then we we need to not render the hidden class. Use an inline if statement to say:

If error, then don't render any class, else render the hidden class

A little tricky, but that should do it.

Ok, let's try it again. Refresh the page. Put in the fake credit card: the number 4, a thousand zeroes, and a 2 at the end. Finish it up and submit.

There's the error! Setup, complete.

Ok, guys, you have a killer checkout system via Stripe. In part 2 of this course, we're going to talk about where things get a little bit more difficult: like subscriptions and discounts. This includes handling web hooks, one of the scariest and toughest parts of subscriptions.

But, don't stop - go make a great product and sell it.

All right, guys, seeya next time.

Leave a comment!

11
Login or Register to join the conversation
Peter-K Avatar
Peter-K Avatar Peter-K | posted 4 years ago

For whatever reason I thought you have to always give a card owner address details that is associated with the card. Basically billing address but here I cant see it therefore I dont think this would work on production.

Also my bank has some sort of guard implemented because sometimes when I do shopping online the shop will return the iframe to provide more bank details to confirm it is really me and it will confirm payment only at that stage I will submit this popup. The good example was pizzahut.co.uk it is quite random this iframe on different sites. Now I dont understand if it is bank doing this or the payment provider?

How is this different to paypal system? I have a paypal and paypal keeps my address and when I am purchasing something I need to provide address to make successfull payment but not here? How this can even work?

Reply

Hey Peter K.!

Welcome to the strange world of payment processing :).

> I thought you have to always give a card owner address details that is associated with the card

Nope! As I understand it, pretty much all information is optional (except for the card number, expiration, obviously). By including the address, you *may* make it more likely that the charges is approved, but that depends on the customer's card company. For example, on card company might not care at all about the address - and so if we send it, it will be ignored. Other card companies might decline if there is no address or, at least be more *likely* to decline. It's all just "inputs" that go into some calculation of whether or not the charge should be accepted.

> Also my bank has some sort of guard implemented because sometimes when I do shopping online the shop will return the iframe. Now I dont understand if it is bank doing this or the payment provider?

Hmm, I actually don't know the answer to this :). There may be some non-credit card payment "flow" that requires this process, that places like pizzahut.co.uk are using. For example, I know nothing about them, but Stripe talks about both IBAN and iDEAL (https://stripe.com/docs/str... which could be something like this (or I may be totally wrong). Anyways, IF Stripe, for example, supports a system like this, Stripe would be the one handling all of that. In the new way of using stripe, you're strongly encouraged to use their "Stripe elements" (basically, pre-made JavaScript & HTML) instead of creating the elements by yourself. This allows them to control a bit more and, in theory, to facilitate this entire flow without us needing to do anything. Ultimately, if everything is approved, they would send us back the same information they do now.

> How is this different to paypal system? I have a paypal and paypal keeps my address and when I am purchasing something I need to provide address to make successfull payment but not here? How this can even work?

Paypal is actually a pretty good example of a system that's similar to what you were asking about above. We DO integrate with PayPal on SymfonyCasts via Braintree. We basically just embed some JavaScript and they take care of the rest. In practice, the flow looks like this:

1) We embed some JavaScript which says who we are / the address that should be paid
2) The JavaScript causes a "Pay with PayPal" button to be displayed
3) When the user clicks this, a popup is opened up. Inside that popup the customer logs into their PayPal account, confirms all the information, and hits "Pay".
4) The PayPal popup disappears and a temporary payment "token" (exactly like with Stripe) is sent back to our JavaScript
5) We send that payment token to the server via AJAX
6) On the server, we make an API request to Braintree (so, basically PayPal) using that token and double-check that the payment was successful and all the info is correct.

Does that makes sense? If not (on any of these), let me know!

Cheers!

Reply
Peter-K Avatar

It makes sense. Everyday is a school day I really thought I have to provide billing adress to make successfull payment. Obviously I was wrong :)

Reply
Peter L. Avatar

What you describe here is more and more used these days. It is something like second auth from bank or credit company (card issuer). Your transaction is suspicious and they may ask for address, or even confirmation through SMS or bank/credit app.
It is officialy called 3D secure. Link to Stripe and 3D secure is this, if not working, just google it.
https://stripe.com/docs/pay...
And as per that link, this is mandatory for payments in Europe and handled automatically by Stripe.

Reply

Hey Peter L.!

Thanks for posting that - it's really relevant and important info, and something that this screencast does not cover. But yea, you increasingly see a multi-step process where your bank will ask for more info, which is actually pretty cool :). Stripe (obviously, because you linked to it) supports this.

Cheers!

Reply

Well, not wrong - but it’s always more complicated ;). Just last night I was on a site that DOES collect address info. I entered a wrong address on accident and payment was denied. Who knows, if they hasn’t forced me to enter my address at all, it might have been successful! Interesting stuff either way :).

Cheers!

Reply
Cesar Avatar

Hello Guys!

I finished to develop a checkout page using Stripe as you have showed in this tutorial and it's working perfectly. However, because it's my first time with Stripe, I have two questions:
1. Do you recommend to do something else in AWS EC2 to improve security? I have provisioned my EC2 using Ansible and the configuration you have showed in your tutorial.
2. Do you recommend to install recaptcha validation in the form? I have seen some bundles in KNP Bundle but I am not sure if its a good idea.

Again, I hope you can help me. I would appreciate any tip you can give me.

Cesar

Reply

Hey Cesar,

Congrats with completing the course!

1. Hm, first of all, you need to configure SSL certificate, and enforce your users using HTTPS only. If they came to HTTP page - redirect them to the same page but with HTTPS. It will increase your security a lot and your users will trust you more :) And do not store any sensitive user data on your server, like credit card credentials, etc. This should be stored directly in Stripe. As you probably noticed, we do not store any CC credentials in our app during the course.

2. Captcha? Hm, for what? If you're talking about payment form - it's not needed, because to make a payment your users need to enter a valid CC credentials, spam robots probably do not have them :)

Cheers!

Reply
Cesar Avatar

Thanks Victor. I did it as you say. Have a nice weekend!

Reply
Default user avatar
Default user avatar Shairyar Baig | posted 5 years ago | edited

I am having a wierd issue on handling exceptions

While using the test card '4000 0000 0000 0002' i see the exception of
<br />\Stripe\Error\Card<br />

I handled it using the try catch and that worked out fine.


            try {
                $this->chargeCustomer($user, $token);
            } catch (\Stripe\Error\Card $e) {
                $error = 1;
                $this->addFlash('error', 'There was a problem charging the card. ' . $e->getMessage() . ' Please try again or use another card.');
            }

Then i made another test transaction using the card '4100 0000 0000 0019' and this time the same exception was thrown but it was not caught. Any idea why that is?

Reply

Yo Shairyar Baig!

Hmm, I would check your code again - both of these cards should throw that \Stripe\Error\Card... and by the rules of PHP, it *should* fall into your catch. The only difference should be the "code on each exception (card_declined versus processing_error). So, double-check things. If you still are getting weird behavior, give me all the info you have and I'll check into it further! In other words, this *should* work as you've coded... but if it isn't, then something very weird is happening :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9, <7.4",
        "symfony/symfony": "3.1.*", // v3.1.10
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
        "stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
        "stripe/stripe-php": "^3.15", // v3.23.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "twig/twig": "^1.24.1", // v1.35.2
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.2
        "hautelook/alice-bundle": "^1.3", // v1.3.1
        "doctrine/data-fixtures": "^1.2" // v1.2.1
    }
}
userVoice