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!

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
    }
}