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.

Building the Custom Checkout Form

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

Earlier, we were rushing to get the site up and the sheep shopping. That's why we used Stripe's pre-built embedded form. And this is completely fine if you like it. But I want to build a custom form that looks like native on our site.

To do that, go back to the Stripe docs. Instead of embedded form, click "Custom Form". Using a custom form is very similar: we still send the credit card information to Stripe, and Stripe will still give us back a token. The difference is that we are responsible for building the HTML form.

Setting up the Stripe JavaScript

To help communicate with Stripe, we need some JavaScript. Copy the first JavaScript code and then find the checkout.html.twig template. At the top, override {% block javascripts %} and then call the {{ parent() }} function. Paste the script tag below:

... lines 1 - 3
{% block javascripts %}
{{ parent() }}
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
... lines 8 - 11
{% endblock %}
... lines 13 - 52

This is just the Twig way of adding some new JavaScript to our page. The base layout also has a javascripts block and jQuery is already included:

... line 1
<html>
... lines 3 - 14
<body>
... lines 16 - 73
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"></script>
... lines 78 - 79
{% endblock %}
</body>
</html>

Next, we need to tell the JavaScript about our publishable key. Copy that code from the docs and add it in the block:

... lines 1 - 3
{% block javascripts %}
{{ parent() }}
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">
Stripe.setPublishableKey('{{ stripe_public_key }}');
</script>
{% endblock %}
... lines 13 - 52

We already know from our original code that we have a variable called stripe_public_key. Inside of the JavaScript quotes, print stripe_public_key:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
Stripe.setPublishableKey('{{ stripe_public_key }}');
</script>
{% endblock %}
... lines 13 - 52

Awesome!

Rendering the HTML Form

With that done, it's time to build the form itself. And surprise! I already built us a basic HTML form. Delete the old, embedded form code. Replace it with {{ include('order/_cardForm.html.twig') }}:

... lines 1 - 13
{% block body %}
<div class="nav-space-checkout">
<div class="container">
<div class="row">
... lines 18 - 44
<div class="col-xs-12 col-sm-6">
{{ include('order/_cardForm.html.twig') }}
</div>
</div>
</div>
</div>
{% endblock %}

This will read this other template file I prepared: _cardForm.html.twig:

<form action="" method="POST" class="js-checkout-form checkout-form">
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-user"></i>
</span>
<input data-stripe="name" class="form-control" type="text" autocomplete="off" id="card-name" required placeholder="Card Holder Name"/>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-credit-card"></i>
</span>
<input data-stripe="number" type="text" autocomplete="off" class="form-control js-cc-number" id="card-number" required placeholder="Card Number"/>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-4 col-sm-3 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-calendar-o"></i>
</span>
<input data-stripe="exp" type="text" size="4" autocomplete="off" class="form-control js-cc-exp" id="card-expiration" required="required" placeholder="mm/yy"/>
</div>
</div>
<div class="col-xs-4 col-sm-3 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-lock"></i>
</span>
<input data-stripe="cvc" type="text" size="4" autocomplete="off" class="form-control js-cc-cvc" id="card-cvc" required="required" placeholder="CVC"/>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-sm-3 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-map-marker"></i>
</span>
<input type="text" autocomplete="off" class="form-control" id="card-zip" placeholder="Zip"/>
</div>
</div>
</div>
<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 hidden"></div>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 text-center">
<button type="submit" class="js-submit-button btn btn-lg btn-danger">
Checkout
</button>
</div>
</div>
</form>

And as you can see, this is a normal HTML form. Its method is POST and its action is still empty so that it will submit right back to the same URL and controller. Then, there's just a bunch of fields that are rendered to look good with Bootstrap.

Let's see how awesome my design skills are: go back and refresh. Hey, it looks pretty good! Probably because someone styled this for me.

Do NOT Submit Card Data to your Server

There are a few really important things about this form. Most importantly, notice that the input fields have no name attribute. This is crucial. Eventually, we will submit this form, but we do not want to submit these fields because we do not want credit card information passing through our server. Because these fields do not have a name attribute, they are not submitted.

So instead of name, Stripe asks you to use a data-stripe attribute. This tells Stripe which data this field holds. Since this is the cardholder name, we have data-stripe="name". Then below, data-stripe="number", data-stripe="exp" and so-on.

But I'm not choosing these values at random. Inside Stripe's documentation, it tells you which data-stripe value to use for each piece. If you follow the rules, Stripe's JavaScript will do all the work of collecting this data and sending it to Stripe.

OK, let's hook up that JavaScript logic next.

Leave a comment!

10
Login or Register to join the conversation

Hi
Thanks for you awesome tutorial.

1 Reply

Hello!
Do yo know where I can find the documentation about the custom form on Stripe documentation website?
Thx!

Reply

Hey be_tnt

I found these two pages
https://stripe.com/en-mx/pa...
https://stripe.com/docs/pay...

I hope it helps.Cheers!

Reply

Hello,

I am trying to follow the tutoriel but with Symfony 4 and new checkout Stripe form... So I have a question please.

Is create a Stripe session is mandatory ?
https://stripe.com/docs/payments/checkout/server#integrate

`// Set your secret key: remember to change this to your live secret key in production
// See your keys here: https://dashboard.stripe.com/account/apikeys
\Stripe\Stripe::setApiKey("sk_test_Ag");

$session = \Stripe\Checkout\Session::create([ 'payment_method_types' => ['card'],
'line_items' => [[

'name' => 'T-shirt',
'description' => 'Comfortable cotton t-shirt',
'images' => ['https://example.com/t-shirt.png'],
'amount' => 500,
'currency' => 'usd',
'quantity' => 1,

]],
'success_url' => 'https://example.com/success&#039;,
'cancel_url' => 'https://example.com/cancel&#039;,
]);`

Thank you.

Reply

Hey Camille,

I suppose so, it's required in case you're going to use this Checkout feature from Stripe: https://stripe.com/docs/pay...

Cheers!

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

what happens if it is the existing customer that we are charging but their card on stripe has been expired? i am assuming stripe will send back an error asking to update their card? then we will ask the customer to provide updated card information send that to stripe to charge? will that change the customer id?

Reply

Hey Shairyar!

It depends :). We've built the site so that we *always* ask the user for a new credit card. But, you *could* update your app so that you don't require an existing user to provide a credit card. In this case, if their card fails (e.g. because its expired), you'll actually get the same error we talk about here: https://knpuniversity.com/s...

Also, though we haven't published it yet, we go even a little bit *further* with error handling in part 2: https://knpuniversity.com/s...

Let me know if this clarifies!

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

Ya it done, thanks.

Reply
Default user avatar
Default user avatar Blueblazer172 | posted 5 years ago

the official docs at https://stripe.com/docs/cus... say that it is no longer safe...

"This method of using Stripe.js to collect card information using a custom payment form has been deprecated. When creating a payment form to collect card information, use Stripe Elements—our pre-built UI components. Check out our Elements migration guide to learn how to migrate your checkout flow to Elements."

what can i do ? :P

Reply

Hey Blueblazer172!

Ah, you're right! This is the first I've heard of this deprecation :/. So, first, I'm chatting with Stripe to find out more about this. My guess is that they simply think that their new system is a bit less error-prone - I don't think there's any security problem with the old system (except, perhaps, that it's a bit easier to make a security mistake, like give your fields name attributes, which would cause them to be submitted to your server). I'll let you know if (hopefully when) I hear back from them!

Until then, my best guess is: using a custom checkout form is *fine* still. And Stripe has a great history of keeping old versions of the API available for a LONG time... years. That being said, I'll definitely look into the new system, and see if we need to update the tutorial. It looks fairly straightforward: a new stripe.js v3 file, and some extra JavaScript in place of physically creating the fields. But, that's just on first glance.

If I were you, I would checkout the new Elements system, and see if you can figure it out. Ultimately, it will give you back a token, just like the old system... then everything else is the same. But, if it's tricky, I think you're fine with the old system.

Thanks for pointing this out! Cheers!

1 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