If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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()
.
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 | |
} | |
} |
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.
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!
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.
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.
Hey Danny Avery!
Look's like you have those "underscores" inverted, try changing them at the begining "__construct()"
Have a nice day!
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 :)
// 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
}
}
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!