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.
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!
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!
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.
Using the 'up to date' method in this guide provokes this error:
`{
"error": {
"code": "parameter_unknown",
"doc_url": "https://stripe.com/docs/error-codes/parameter-unknown",
"message": "Received unknown parameter: at_period_end",
"param": "at_period_end",
"type": "invalid_request_error"
}
}`
You can directly cancel it like this:
$stripeSubscription = \Stripe\Subscription::retrieve($currentSubscription->getStripeSubscriptionId());<br />$stripeSubscription->cancel();
Hey Carlos M.!
Yep, this part has changed since the tutorial was created. It looks like you found the answer :). The cancel_at_period_end
now is a key you pass when you update a subscription - you can see an example in this comment: https://symfonycasts.com/screencast/stripe-level2/reactivate-subscription#comment-4765234516
Let me know if that helps! There are various changes to the Stripe API since this tutorial - we're working on updating the tutorial or deprecating it fully (even though many things still work). If you hit any other problems, let us know (and check the comments - sometimes there are some details posted there!).
Cheers!
Again, I wouldn't deprecate the full tutorial, just update it. I understand the video, or parts of it, will have to be done again. However the quality of the tutorial is very high and just needs an update.
I've been able to follow it and migrate from my failing implementation of Stripe with Payum bundle, which doesn't easily allow working with subscriptions. The whole migration has been done in a day and a half. And just 2 errors in the tutorial. Simply update it :)
Yes! Updating the Subscription with the 'cancel_at_period_end' field set to true worked great! Now Stripe shows it'll cancel the subscription at the end of the period :)
Woohoo! Thanks for confirming that!
> The whole migration has been done in a day and a half. And just 2 errors in the tutorial
Ok, that is AWESOME. Payment changes should always take longer than that ;). Thanks again.
Cheers!
Changes since API version 2018-05-21:
You can no longer set at_period_end in the subscription DELETE endpoints. The DELETE endpoint is reserved for immediate canceling going forward. Use cancel_at_period_end on the subscription update endpoints instead.
// 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
}
}
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.