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.

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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

Leave a comment!

9
Login or Register to join the conversation
Carlos M. Avatar
Carlos M. Avatar Carlos M. | posted 3 years ago

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.

Reply
Carlos M. Avatar
Carlos M. Avatar Carlos M. | Carlos M. | posted 3 years ago | edited

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"

}
}`

Reply
Carlos M. Avatar
Carlos M. Avatar Carlos M. | Carlos M. | posted 3 years ago | edited

You can directly cancel it like this:

$stripeSubscription = \Stripe\Subscription::retrieve($currentSubscription->getStripeSubscriptionId());<br />$stripeSubscription->cancel();

Reply

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!

2 Reply
Carlos M. Avatar

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

Reply
Carlos M. Avatar

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

Reply

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!

Reply
Mohammed Avatar
Mohammed Avatar Mohammed | posted 4 years ago

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.

Reply

Hey Mohammed

Thanks for notifying us about those changes, we will add a note about that

Cheers!

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