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.

Creating the Subscription in Stripe

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Open up the Stripe docs and go down the page until you find subscriptions. There's a nice little "Getting Started" section but the detailed guide is the place to go if you've got serious questions.

But let's start with the basics! Step 1... done! Step 2: subscribing your customers. Apparently all we need to do is set the plan on the Customer and save! Cool!

The Players: Subscription, Customer, Invoice

But actually, there's more going on behind the scenes. In reality, this will create a new object in Stripe: a Subscription. And actually, we're going to subscribe a user with slightly different code than this.

Keep reading below, the docs describe the lifecycle of a Subscription. For now, there's one really important thing to notice: when you create a Subscription, Stripe automatically creates an Invoice and charges that invoice immediately.

Open up the Stripe API docs so we can look at all the important objects so far. From part 1 of the tutorial, when someone buys individual products, we do a few things: we create or fetch a Customer, we create an InvoiceItem for each product and finally we create an Invoice and pay it. When you create an Invoice, Stripe automatically adds all unpaid invoice items to it.

With a Subscription, there are two new players: Plans and Subscriptions. Click "Subscriptions" and go down to "Create a Subscription". Ah, so simple: a Subscription is between a Customer and a specific Plan. This is the code we will use.

Coding up the Stripe Subscription

Back on our site, after we fill out the checkout form, the whole thing submits to OrderController::checkoutAction(). And this passes the submitted Stripe token to chargeCustomer():

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 48
public function checkoutAction(Request $request)
{
... lines 51 - 53
if ($request->isMethod('POST')) {
... lines 55 - 56
try {
$this->chargeCustomer($token);
} catch (\Stripe\Error\Card $e) {
$error = 'There was a problem charging your card: '.$e->getMessage();
}
... lines 62 - 68
}
... lines 70 - 77
}
... lines 79 - 105
}
... lines 107 - 108

Ah that's where the magic happens: it creates or gets the Customer, adds InvoiceItems and creates the Invoice:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
$stripeClient = $this->get('stripe_client');
/** @var User $user */
$user = $this->getUser();
if (!$user->getStripeCustomerId()) {
$stripeClient->createCustomer($user, $token);
} else {
$stripeClient->updateCustomerCard($user, $token);
}
$cart = $this->get('shopping_cart');
foreach ($cart->getProducts() as $product) {
$stripeClient->createInvoiceItem(
$product->getPrice() * 100,
$user,
$product->getName()
);
}
$stripeClient->createInvoice($user, true);
}
}
... lines 107 - 108

Beautiful.

All we need to do is create a Subscription - via Stripe's API - if they have a plan in their cart. Before we create the Invoice, add if $cart->getSubscriptionPlan():

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
... lines 86 - 104
if ($cart->getSubscriptionPlan()) {
... lines 106 - 113
}
}
}
... lines 117 - 118

Next, open StripeClient: we've designed this class to hold all Stripe API setup and interactions. Add a new method: createSubscription() and give it a User argument and a SubscriptionPlan argument that the User wants to subscribe to:

... lines 1 - 4
use AppBundle\Entity\User;
use AppBundle\Subscription\SubscriptionPlan;
... line 7
class StripeClient
{
... lines 11 - 65
public function createSubscription(User $user, SubscriptionPlan $plan)
{
... lines 68 - 73
}
}

Now, go back to the Stripe API docs, steal the code that creates a Subscription, and paste it here. Set that to a new $subscription variable. For the customer, use $user->getStripeCustomerId() to get the id for this user. For the plan, just $plan->getPlanId(). Return the $subscription at the bottom:

... lines 1 - 8
class StripeClient
{
... lines 11 - 65
public function createSubscription(User $user, SubscriptionPlan $plan)
{
$subscription = \Stripe\Subscription::create(array(
'customer' => $user->getStripeCustomerId(),
'plan' => $plan->getPlanId()
));
return $subscription;
}
}

To use this in the controller, use the $stripeClient variable we setup earlier: $stripeClient->createSubscription() and pass it the current $user variable and then $cart->getSubscriptionPlan():

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
... lines 86 - 104
if ($cart->getSubscriptionPlan()) {
// a subscription creates an invoice
$stripeClient->createSubscription(
$user,
$cart->getSubscriptionPlan()
);
... lines 111 - 113
}
}
}
... lines 117 - 118

And that's all you need to create a subscription!

Don't Invoice Twice!

And there are no gotchas at all... oh except for this big one. Remember: when you create a Subscription, Stripe automatically creates an Invoice. And when you create an Invoice, Stripe automatically attaches all existing InvoiceItems that haven't been paid yet to that Invoice.

So, if the user has a Subscription, then an Invoice will be created when we call createSubscription(). And that invoice will contain any InvoiceItems for individual products that are also in the cart. If you try to create another invoice below, it'll be empty... and you'll actually get an error.

What we actually want to do is move createInvoice() into the else so that if there is a subscription plan, it will create the invoice, else, we will create it manually:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
... lines 86 - 104
if ($cart->getSubscriptionPlan()) {
// a subscription creates an invoice
$stripeClient->createSubscription(
$user,
$cart->getSubscriptionPlan()
);
} else {
// charge the invoice!
$stripeClient->createInvoice($user, true);
}
}
}
... lines 117 - 118

Yep, the user can buy a subscription and some extra, amazing products all at the same time.

Try out the Whole Flow

Try the whole thing out: add some sheep shears to the cart so we have a product and a subscription. Fill in our fake credit card information, hit check out, and ... Cool! No errors.

But the real proof is in the dashboard. Click "Payments". Perfect! Here it is, for $124. But look closer at it, and click to view the Customer.

When we checked out, it created the customer, associated the card with it, and created an active subscription. And this was all done in this one invoice. It contains the subscription plus the one-time product purchase. In other words, this kicks butt. In one month, Stripe will automatically invoice the customer again, charge their card, and keep the subscription active.

Now that our subscription is active in Stripe, we also need to update our database. We need to record that this user is actively subscribed to this plan.

Leave a comment!

13
Login or Register to join the conversation
Artur T. Avatar
Artur T. Avatar Artur T. | posted 3 years ago

Hi All, Where addSubscription() methods in project ???

Reply

Hey Artur

If you open up the file src/AppBundle/Subscription/SubscriptionHelper.php you will find the method you are looking for

Cheers!

Reply
Kiuega Avatar
Kiuega Avatar Kiuega | posted 3 years ago | edited

Hey! When we create a plan that contains a trial period and a user subscribes to this plan, why is the trial period management not automatic?

I feel like we have to do the processing ourselves, that is, when creating the subscription, we need to check if the plan has a trial period, and if so, do this processing in creation:


$subscription = \Stripe\Subscription::create([
    'customer' => 'cus_4fdAW5ftNQow1a',
    'items' => [['plan' => 'plan_CBb6IXqvTLXp3f']],
    'trial_end' => 1582205079,
]);

Is this normal?

Reply

Hey Kiuega !

I don't have experience using trial periods, but I think there are 2 ways:

A) The way you're doing it: pass trial_end when creating the subscription
B) Pass trial_period_days when creating the plan - https://stripe.com/docs/api/plans/object#plan_object-trial_period_days - and then pass a trial_from_plan=true flag when creating the subscription - https://stripe.com/docs/api/subscriptions/create#create_subscription-trial_from_plan - I think that might be what you're looking for :)

Cheers!

1 Reply
Kiuega Avatar
Kiuega Avatar Kiuega | weaverryan | posted 3 years ago | edited

Yes precisely, I manage to create a plan with a trial period, so by passing it trial_period_days when creating the plan.
So, I should make a condition to verify that the plan does have an attribute trial_period_days, and if yes, then pass a trial_from_plan=true when creating the subscription ? Or can I pass it anyway and Stripe would go check to see if the plan has a trial period, and in which case would apply it?

<b>EDIT</b>: Ok it seems that we can leave the option at true and Stripe will do the rest even if the plan has not been tested.

On the other hand I saw that in the case where there is a trial period, we still have an invoice that is created, of $ 0.

An invoice will also be created if I change the subscription formula, but always for a formula with a trial period.

I'm not sure where I should modify the code to prevent the creation of an invoice if we are in a trial period. In the controller directly or in the service?

On the other hand, I imagine that if I have two plans with a trial period each, the client can switch from one plan to another indefinitely without ever paying.
Because of this, I imagine that I should make changes to the "<b>changePlan ()</b>" function? For example, cancel the trial period, or calculate the number of days remaining compared to the current plan?

PS: Google translation is a great tool. I hope you can understand me

Reply

Hey Kiuega!

Sorry for the slow reply - I had a short holiday! :D

> I'm not sure where I should modify the code to prevent the creation of an invoice if we are in a trial period. In the controller directly or in the service?

To be clear: you want to *avoid* the invoice being created if there is a trial period? Any reason why? In general, if you can "do things the Stripe way", you'll make your life easier. If Stripe wants to (apparently) create an invoice for a trial period, I would allow it to do this - unless it's specifically a problem. To avoid it, you would probably need to manage the "trial" functionality entirely in your code. Basically, to "start a trial", you would manage that in your database and not notify Stripe at all. But then you also need to write code to do something when the trial ends... which means you're reimplementing logic that Stripe should handle for you.

> On the other hand, I imagine that if I have two plans with a trial period each, the client can switch from one plan to another indefinitely without ever paying.

Hmm, that's probably true :). It depends on your business logic. The easiest thing to do is to *not* allow changing plans while in a trial. If you DO need to allow it, then I would do exactly what you suggested: calculate the number of days "remaining" in the trial, and set the new trial length to that amount.

Let me know if that helps! Your implementation has a lot of interesting details :).

Cheers!

Reply
Kiuega Avatar

No problem ;)
In fact I noticed that there was still an invoice creation for the customer even if the final amount is $ 0. I don't find it good, but hey, if it's better.

So for the trial period, are you saying that the best thing is not to use the functionality provided by Stripe at this level? Because with Stripe, it starts the subscription, and when the trial period ends, it only starts billing, which is pretty good. In addition, it seems that it is possible with a webhook to verify that the end of a trial period is coming, which would allow me to send an email to the user to know if he wants to continue, in which case he will have to enter your bank card. Otherwise, Stripe will try to charge, but since there is no card, there will be an error, and in the end, the subscription will simply be canceled.

Why manage this by myself? Is there an advantage? And most importantly, if I do that, it means that I will have to implement an event to check if a trial period ends in the next 3 days, and I don't really know how to set it up.

Finally, would it be possible to contact you in private, I would like to show you the progress of the component that I create, to have your opinion

Reply

Hey Kiuega!

> So for the trial period, are you saying that the best thing is not to use the functionality provided by Stripe at this level?

No, I only meant that you should not use Stripe for this functionality IF you *absolutely* needed to avoid that $0 invoice during trial periods. I think you SHOULD use Stripe for this, and that you should be "ok" with the $0 invoice :).

> Finally, would it be possible to contact you in private, I would like to show you the progress of the component that I create, to have your opinion

You can message me at ryan@symfonycasts.com :). I can't promise I will have a lot of time to look into it - but I'd be happy to see what you have!

Cheers!

Reply
Kiuega Avatar

Okay perfect! I send you an email ! Do not worry I will not bother you, I would just like the opinion of an expert regarding the application that I am developing (from what we did in training of course): )

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

hey guys :)

i get this error when checking out:

Can't combine currencies on a single customer. This
customer has had a subscription, coupon, or invoice item with currency
usd

500 Internal Server Error - InvalidRequest

I've created the plans for Euro but the tutorial is with usd right?

so i have to change the plan's currency to usd ?

Reply
Default user avatar

okay i changed the plans to usd and now it works :) yey
but may you explain the error?
what if someone from germany wants to buy with a german credid card? is there still then an error? or how is it handled?
Thanks :)

Reply

Hey Blueblazer172!

Yea, it looks like each "Customer" is locked into a single currency in the system - i.e. if you create a subscription once in USD, you won't be able to bill them later in Euro. But, it's probably not a problem in theory - just make sure that your Stripe account's default currency (which I *think* is determined but your linked bank account - but I'm not 100$% sure) and your subscription plans are all in the same currency (e.g. USD or EUR). Basically, say consistent within your account and you're fine. If you choose USD, for example, and a customer uses a card whose currency is EUR, Stripe handles all the currency conversion for you. Ultimately, you'll see all the totals in your account under whatever currency you are using. Here's some more details about how that currency conversion is handled: https://support.stripe.com/...

In our case (KnpU), our currency is in USD. Many of our customers have cards in EUR, but that's basically invisible to us - the conversion happens automatically/externally and everything looks like USD in our account.

I hope that helps! If you *did* want to use EUR and are getting this error, just reset your "test" system's data so that all existing customers are deleted. Then, starting using EUR exclusively going forward.

Cheers!

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

changing to EUR was very easy thanks Ryan :)
i removed the dummy data and now everything works :)

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.8
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "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", // v1.2.1
        "phpunit/phpunit": "^5.5", // 5.7.20
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.4
        "symfony/phpunit-bridge": "^3.0", // v3.3.0
        "hautelook/alice-bundle": "^1.3", // v1.4.1
        "doctrine/data-fixtures": "^1.2" // v1.2.2
    }
}
userVoice