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.

Centralize your Stripe Code

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

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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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'):

... 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:

... 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:

... 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:

... 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:

... 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, but look at the setApiKey() method call that's above everything:

... 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:

... 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%:

... 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:

... 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:

... 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():

... 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:

... 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:

... 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:

... 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 it's good, 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. Annnd check-out using our fake information. Yes! No error! The system still works and this StripeClient class is looking really, really sweet.

Leave a comment!

4
Login or Register to join the conversation
Default user avatar
Default user avatar Danny Avery | posted 5 years ago

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!

Reply

Hey Danny Avery!

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

Have a nice day!

Reply
Default user avatar

Wow! Thanks Diego!

Reply

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 :)

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