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.
Canceling a Subscription
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBad news: eventually, someone will want to cancel their subscription to your amazing, awesome service. So sad. But when that happens, let's make it as smooth as possible. Remember: happy customers!
Like everything we do, cancelling a subscription has two parts. First we need to cancel it inside of Stripe and second, we need to update our database, so we know that this user no longer has a subscription.
Setting up the Cancel Button
Start by adding a cancel button to the account page. In account.html.twig
, let's move the h1
down a bit:
// ... lines 1 - 2 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
<h1> | |
My Account | |
// ... lines 10 - 15 | |
</h1> | |
// ... lines 17 - 51 | |
</div> | |
// ... lines 53 - 55 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 60 - 61 |
Next, add a form with method="POST"
and make this float right:
// ... lines 1 - 2 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
<h1> | |
My Account | |
{% if app.user.subscription %} | |
<form action="{{ path('account_subscription_cancel') }}" method="POST" class="pull-right"> | |
<button type="submit" class="btn btn-danger btn-xs">Cancel Subscription</button> | |
</form> | |
{% endif %} | |
</h1> | |
// ... lines 17 - 51 | |
</div> | |
// ... lines 53 - 55 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 60 - 61 |
We don't actually need a form, but now we can put a button inside and this will POST up to our server. I don't always do this right, but since this action will change something on the server, it's best done with a POST request. Add a few classes for styling and say "Cancel Subscription".
I still need to set the action
attribute to some URL... but we need to create that endpoint first!
Open ProfileController
. This file renders the account page, but we're also going to put code in here to handle some other things on this page, like cancelling a subscription and updating your credit card.
Create a new public function cancelSubscriptionAction()
. Give this a URL: @Route("/profile/subscription/cancel")
and a name: account_subscription_cancel
:
// ... lines 1 - 11 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 14 - 21 | |
/** | |
* @Route("/profile/subscription/cancel", name="account_subscription_cancel") | |
// ... line 24 | |
*/ | |
public function cancelSubscriptionAction() | |
{ | |
// ... lines 28 - 33 | |
} | |
} |
And, since we'll POST here, we might as well require a POST with @Method
- hit tab to autocomplete and add the use
statement - then POST
:
// ... lines 1 - 4 | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; | |
// ... lines 6 - 11 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 14 - 21 | |
/** | |
* @Route("/profile/subscription/cancel", name="account_subscription_cancel") | |
* @Method("POST") | |
*/ | |
public function cancelSubscriptionAction() | |
{ | |
// ... lines 28 - 33 | |
} | |
} |
With the endpoint setup, copy the route name and go back into the template. Update action
, with path()
then paste the route:
// ... lines 1 - 2 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
<h1> | |
// ... lines 9 - 10 | |
{% if app.user.subscription %} | |
<form action="{{ path('account_subscription_cancel') }}" method="POST" class="pull-right"> | |
// ... line 13 | |
</form> | |
{% endif %} | |
</h1> | |
// ... lines 17 - 51 | |
</div> | |
// ... lines 53 - 55 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 60 - 61 |
And we are setup!
Cancel that Subscription in Stripe
Now, back to step 1: cancel the Subscription in Stripe. Go back to Stripe's documentation and find the section about Cancelling Subscriptions - it'll look a little different than what you see here... because Stripe updated their design right after I recorded. Doh! But, all the same info is there.
Ok, this is simple: retrieve a subscription and then call cancel()
on it. Yes! So easy!
Cancelling at_period_end
Tip
Since 2018-07-27, Stripe changed the way you cancel a subscription at period end. Use this code for > the updated API:
$sub->cancel_at_period_end = true;
$sub->save();
Or not easy: because you might want to pass this an at_period_end
option set to true. Here's the story: by default, when you cancel a subscription in Stripe, it cancels it immediately. But, by passing at_period_end
set to true, you're saying:
Hey! Don't cancel their subscription now, let them finish the month and then cancel it.
This is probably what you want: after all, your customer already paid for this month, so you'll want them to keep getting the service until its over.
So let's do this! Remember: we've organized things so that all Stripe API code lives inside the StripeClient
object. Fetch that first with $stripeClient = $this->get('stripe_client')
:
// ... lines 1 - 11 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 14 - 25 | |
public function cancelSubscriptionAction() | |
{ | |
$stripeClient = $this->get('stripe_client'); | |
// ... lines 29 - 33 | |
} | |
} |
Next, open this class, find the bottom, and add a new method: public function cancelSubscription()
with one argument: the User
object whose subscription should be cancelled:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 77 | |
public function cancelSubscription(User $user) | |
{ | |
// ... lines 80 - 86 | |
} | |
} |
For the code inside - go copy and steal the code from the docs! Yes! Replace the hard-coded subscription id with $user->getSubscription()->getStripeSubscriptionId()
.
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 77 | |
public function cancelSubscription(User $user) | |
{ | |
$sub = \Stripe\Subscription::retrieve( | |
$user->getSubscription()->getStripeSubscriptionId() | |
); | |
// ... lines 83 - 86 | |
} | |
} |
Then, cancel it at period end:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 77 | |
public function cancelSubscription(User $user) | |
{ | |
$sub = \Stripe\Subscription::retrieve( | |
$user->getSubscription()->getStripeSubscriptionId() | |
); | |
$sub->cancel([ | |
'at_period_end' => true, | |
]); | |
} | |
} |
Back in ProfileController
, use this! $stripeClient->cancelSubscription()
with $this->getUser()
to get the currently-logged-in-user:
// ... lines 1 - 11 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 14 - 25 | |
public function cancelSubscriptionAction() | |
{ | |
$stripeClient = $this->get('stripe_client'); | |
$stripeClient->cancelSubscription($this->getUser()); | |
// ... lines 30 - 33 | |
} | |
} |
Then, to express how sad we are, add a heard-breaking flash message. Then, redirect back to profile_account
:
// ... lines 1 - 11 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 14 - 25 | |
public function cancelSubscriptionAction() | |
{ | |
$stripeClient = $this->get('stripe_client'); | |
$stripeClient->cancelSubscription($this->getUser()); | |
$this->addFlash('success', 'Subscription Canceled :('); | |
return $this->redirectToRoute('profile_account'); | |
} | |
} |
We've done it! But don't test it yet: we still need to do step 2: update our database to reflect the cancellation.
It's no longer possible to use the 'cancel_at_period_end' method to cancel a subscription. I'm still looking for this in the official Stripe documentation. So far, the only thing I've found is how to delete the subscription, without waiting until the end of the subscription period before truly deleting it form Stripe.