Buy

Centralize your Stripe Code

Stripe's API is really organized. Our code that talks to it is getting a little crazy, unless you like long, procedural code that you can't re-use. Please tell me that's not the case.

Let's get this organized! At the very least, we should do this because eventually we're going to need to re-use some of this logic - particularly with subscriptions.

Here's the goal of the next few minutes: move each thing we're doing in the controller into a set of nice, re-usable functions. To do that, inside AppBundle, create a new class called StripeClient:

9 lines src/AppBundle/StripeClient.php
... lines 1 - 2
namespace AppBundle;
class StripeClient
{
}

Make sure this has the AppBundle namespace. We're going to fill this with functions that work with Stripe, like createCustomer() or updateCustomerCard().

Moving createCustomer()

In the controller, the first thing we do is create a Customer:

87 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 42
if (!$user->getStripeCustomerId()) {
$customer = \Stripe\Customer::create([
'email' => $user->getEmail(),
'source' => $token
]);
$user->setStripeCustomerId($customer->id);
... lines 49 - 57
}
... lines 59 - 77
}
... lines 79 - 85
}
}

In StripeClient, add a new createCustomer() method that will accept the User object which should be associated with the customer, and the $paymentToken that was just submitted:

31 lines src/AppBundle/StripeClient.php
... lines 1 - 4
use AppBundle\Entity\User;
... lines 6 - 7
class StripeClient
{
... lines 10 - 16
public function createCustomer(User $user, $paymentToken)
{
... lines 19 - 28
}
}

Copy the logic from the controller and paste it here. Update $token to $paymentToken. Then, return the $customer at the bottom, just in case we need it:

31 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 16
public function createCustomer(User $user, $paymentToken)
{
$customer = \Stripe\Customer::create([
'email' => $user->getEmail(),
'source' => $paymentToken,
]);
$user->setStripeCustomerId($customer->id);
$this->em->persist($user);
$this->em->flush($user);
return $customer;
}
}

You'll see me do with this most functions in this class.

The only problem is with the entity manager - the code used to update the user record in the database. The way we fix this is a bit specific to Symfony. First, add a public function __construct() with an EntityManager $em argument. Set this on a new $em property:

31 lines src/AppBundle/StripeClient.php
... lines 1 - 5
use Doctrine\ORM\EntityManager;
class StripeClient
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
... lines 16 - 29
}

Down below, just say $em = $this->em:

31 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 16
public function createCustomer(User $user, $paymentToken)
{
... lines 19 - 24
$this->em->persist($user);
$this->em->flush($user);
... lines 27 - 28
}
}

Registering the Service

To use the new function in our controller, we need to register it as a service. Open up app/config/services.yml. Add a service called stripe_client, set its class key to AppBundle\StripeClient and set autowire to true:

14 lines app/config/services.yml
... lines 1 - 5
services:
... lines 7 - 10
stripe_client:
class: AppBundle\StripeClient
autowire: true

With that, Symfony will guess the constructor arguments to the object.

If you're not coding in Symfony, that's OK! Do whatever you need to in order to have a set of re-usable functions for interacting with Stripe.

In the controller, clear out all the code in the if statement, and before it, add a new variable called $stripeClient set to $this->get('stripe_client'):

80 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
$token = $request->request->get('stripeToken');
\Stripe\Stripe::setApiKey($this->getParameter('stripe_secret_key'));
$stripeClient = $this->get('stripe_client');
... lines 42 - 70
}
... lines 72 - 78
}
}

This will be an instance of that StripeClient class.

In this if, call $stripeClient->createCustomer() and pass it the $user object and the $token:

80 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 40
$stripeClient = $this->get('stripe_client');
... lines 42 - 43
if (!$user->getStripeCustomerId()) {
$stripeClient->createCustomer($user, $token);
} else {
... lines 47 - 50
}
... lines 52 - 70
}
... lines 72 - 78
}
}

Done.

Moving updateCustomerCard()

Let's keep going!

The second piece of logic is responsible for updating the card on an existing customer. In StripeClient, add a public function updateCustomerCard() with a User $user whose related Customer should be updated, and the new $paymentToken:

39 lines src/AppBundle/StripeClient.php
... lines 1 - 4
use AppBundle\Entity\User;
... lines 6 - 7
class StripeClient
{
... lines 10 - 30
public function updateCustomerCard(User $user, $paymentToken)
{
... lines 33 - 36
}
}

Copy logic from the controller and past it here. Update $token to $paymentToken:

39 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 30
public function updateCustomerCard(User $user, $paymentToken)
{
$customer = \Stripe\Customer::retrieve($user->getStripeCustomerId());
$customer->source = $paymentToken;
$customer->save();
}
}

Go copy the logic from the controller, and paste it here. Update $token to $paymentToken.

In OrderController, call this with $stripeClient->updateCustomerCard() passing it $user and $token:

77 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 40
$stripeClient = $this->get('stripe_client');
... lines 42 - 43
if (!$user->getStripeCustomerId()) {
$stripeClient->createCustomer($user, $token);
} else {
$stripeClient->updateCustomerCard($user, $token);
}
... lines 49 - 67
}
... lines 69 - 75
}
}

Now the StripeClient class is getting dangerous!

Always setting the API Key

But, there's one small problem. This will work now, but look at the setApiKey() method call that's above everything:

77 lines src/AppBundle/Controller/OrderController.php
... 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
\Stripe\Stripe::setApiKey($this->getParameter('stripe_secret_key'));
... lines 40 - 67
}
... lines 69 - 75
}
}

We must call this before we make any API calls to Stripe. So, if we tried to use the StripeClient somewhere else in our code, but we forgot to call this line, we would have big problems.

Instead, I want to guarantee that if somebody calls a method on StripeClient, setApiKey() will always be called first. To do that, copy that line, delete it and move it into StripeClient's __construct() method.

Symfony user's will know that the getParameter() method won't work here. To fix that, add a new first constructor argument called $secretKey. Then, use that:

41 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 11
public function __construct($secretKey, EntityManager $em)
{
... lines 14 - 15
\Stripe\Stripe::setApiKey($secretKey);
}
... lines 18 - 39
}

To tell Symfony to pass this, go back to services.yml and add an arguments key with one entry: %stripe_secret_key%:

15 lines app/config/services.yml
... lines 1 - 10
stripe_client:
... line 12
arguments: ['%stripe_secret_key%']
... line 14

Thanks to auto-wiring, Symfony will pass the stripe_secret_key parameter as the first argument, but then autowire the second, EntityManager argument.

The end-result is this: when our StripeClient object is created, the API key is set immediately.

Moving Invoice Logic

Ok, the hard stuff is behind us: let's move the last two pieces of logic: creating an InvoiceItem and creating an Invoice. In StripeClient, add public function createInvoiceItem() with an $amount argument, the $user to attach it to and a $description:

65 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 40
public function createInvoiceItem($amount, User $user, $description)
{
... lines 43 - 48
}
... lines 50 - 63
}

Copy that code from our controller, remove it, and paste it here. Update amount to use $amount and description to use $description. Add a return statement just in case:

65 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 40
public function createInvoiceItem($amount, User $user, $description)
{
return \Stripe\InvoiceItem::create(array(
"amount" => $amount,
"currency" => "usd",
"customer" => $user->getStripeCustomerId(),
"description" => $description
));
}
... lines 50 - 63
}

In OrderController, call this $stripeClient->createInvoiceItem() passing it $product->getPrice() * 100, $user and $product->getName():

70 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 47
foreach ($this->get('shopping_cart')->getProducts() as $product) {
$stripeClient->createInvoiceItem(
$product->getPrice() * 100,
$user,
$product->getName()
);
}
... lines 55 - 60
}
... lines 62 - 68
}
}

Perfect! For the last piece, add a new public function createInvoice() with a $user whose customer we should invoice and a $payImmediately argument that defaults to true:

65 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 50
public function createInvoice(User $user, $payImmediately = true)
{
... lines 53 - 62
}
}

Who knows, there might be some time in the future when we don't want to pay an invoice immediately.

You know the drill: copy the invoice code from the controller, remove it and paste it into StripeClient. Wrap the pay() method inside if ($payImmediately). Finally, return the $invoice:

65 lines src/AppBundle/StripeClient.php
... lines 1 - 7
class StripeClient
{
... lines 10 - 50
public function createInvoice(User $user, $payImmediately = true)
{
$invoice = \Stripe\Invoice::create(array(
"customer" => $user->getStripeCustomerId()
));
if ($payImmediately) {
// guarantee it charges *right* now
$invoice->pay();
}
return $invoice;
}
}

Call that in the controller: $stripeClient->createInvoice() passing it $user and true to pay immediately:

70 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 54
$stripeClient->createInvoice($user, true);
... lines 56 - 60
}
... lines 62 - 68
}
}

Phew! This was a giant step sideways - but not only is our code more re-usable, it just makes a lot more sense when you read it!

Double-check to make sure it works. Add something to your cart. Check-out. Yes! No error! The system still works and this StripeClient is really, really sweet.

Leave a comment!

  • 2017-05-17 Diego Aguiar

    PHP was thinking that "construct__" was a custom function of your class instead of the real constructor, and that's why you didn't have the API key defined :)

  • 2017-05-16 Danny Avery

    Wow! Thanks Diego!

  • 2017-05-16 Diego Aguiar

    Hey Danny Avery!

    Look's like you have those "underscores" inverted, try changing them at the begining "__construct()"

    Have a nice day!

  • 2017-05-16 Danny Avery

    I'm getting the following error when running a payemnt:

    No API key provided. (HINT: set your API key using "Stripe::setApiKey(<api-key>)". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions.

    StripeClient.php

    public function construct__($secretKey, EntityManager $em)
    {
    $this->em = $em;
    \Stripe\Stripe::setApiKey($secretKey);
    }

    Services.yml

    stripe_client:
    class: AppBundle\StripeClient
    autowire: true
    arguments: ['%stripe_secret_key%']

    Thanks!