If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
So far, we only offer monthly plans. But sheep love commitment, so they've been asking for yearly options. Well, great news! After all that upgrade stuff we just handled, this is going to be easy.
First, in Stripe's dashboard, we need to create two new plans. Call the first "Farmer Brent yearly" and for the total... how about 99 X 10: so $990, per year.
Then, add the New Zealander yearly, set to 1990, billed yearly.
Cool! I'm not going to update our checkout to allow these plans initially, because, honestly, that's super easy: just create some new links to add these plans to your cart, and you're done.
Nope, we'll skip straight to the hard stuff: allowing the user to change between monthly and yearly plans.
First, we need to add these plans to our system. Open the SubscriptionPlan
class.
To distinguish between monthly and yearly plans, add a new property called duration
:
this will be a string, either monthly
or yearly
. At the top, I love constants,
so create: const DURATION_MONTHLY = 'monthly'
and const DURATION_YEARLY = 'yearly'
:
... lines 1 - 4 | |
class SubscriptionPlan | |
{ | |
const DURATION_MONTHLY = 'monthly'; | |
const DURATION_YEARLY = 'yearly'; | |
... lines 9 - 44 | |
} |
Next, add a $duration
argument to the constructor, but default it to monthly.
Set the property below:
... lines 1 - 4 | |
class SubscriptionPlan | |
{ | |
... lines 7 - 15 | |
private $duration; | |
public function __construct($planId, $name, $price, $duration = self::DURATION_MONTHLY) | |
{ | |
... lines 20 - 22 | |
$this->duration = $duration; | |
} | |
... lines 25 - 44 | |
} |
Finally, I'll use the "Code"->"Generate" menu, or Command
+N
on a Mac, select "Getters"
and then choose duration
. That gives me a nice getDuration()
method:
... lines 1 - 4 | |
class SubscriptionPlan | |
{ | |
... lines 7 - 40 | |
public function getDuration() | |
{ | |
return $this->duration; | |
} | |
} |
In SubscriptionHelper
, we create and preload all of our plans. Copy the two monthly
plans, paste them, update their keys to have yearly
and add the last argument for
the yearly duration:
... lines 1 - 8 | |
class SubscriptionHelper | |
{ | |
... lines 11 - 15 | |
public function __construct(EntityManager $em) | |
{ | |
... lines 18 - 31 | |
$this->plans[] = new SubscriptionPlan( | |
'farmer_brent_yearly', | |
'Farmer Brent', | |
990, | |
SubscriptionPlan::DURATION_YEARLY | |
); | |
$this->plans[] = new SubscriptionPlan( | |
'new_zealander_yearly', | |
'New Zealander', | |
1990, | |
SubscriptionPlan::DURATION_YEARLY | |
); | |
} | |
... lines 46 - 131 | |
} |
Now, these are at least valid plans inside the system.
Here's the goal: on the account page, next to the "Next Billing at" text, I want to add a link that says "bill yearly" or "bill monthly". When you click this, it should follow the same workflow we just built for upgrading a plan: it should show the cost, then make the change.
In ProfileController::accountAction()
, add yet another variable here called
$otherDurationPlan
:
... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
... lines 17 - 19 | |
public function accountAction() | |
{ | |
$currentPlan = null; | |
$otherPlan = null; | |
$otherDurationPlan = null; | |
... lines 25 - 42 | |
} | |
... lines 44 - 168 | |
} |
This will eventually be the SubscriptionPlan
object for the other duration
of the current plan. So if I have the monthly Farmer Brent, this will be set
to the yearly Farmer Brent plan.
To find that plan, open SubscriptionHelper
and add a new function called
findPlanForOtherDuration()
with a $currentPlanId
argument:
... lines 1 - 8 | |
class SubscriptionHelper | |
{ | |
... lines 11 - 74 | |
public function findPlanForOtherDuration($currentPlanId) | |
{ | |
if (strpos($currentPlanId, 'monthly') !== false) { | |
$newPlanId = str_replace('monthly', 'yearly', $currentPlanId); | |
} else { | |
$newPlanId = str_replace('yearly', 'monthly', $currentPlanId); | |
} | |
return $this->findPlan($newPlanId); | |
} | |
... lines 85 - 131 | |
} |
I'll paste in some silly code here. This relies on our naming conventions to switch between monthly and yearly plans.
Back in the controller, copy the $otherPlan
line, paste it, then update the variable
to $otherDurationPlan
and the method to findPlanForOtherDuration()
:
... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
... lines 17 - 19 | |
public function accountAction() | |
{ | |
... lines 22 - 23 | |
$otherDurationPlan = null; | |
if ($this->getUser()->hasActiveSubscription()) { | |
... lines 26 - 31 | |
$otherDurationPlan = $this->get('subscription_helper') | |
->findPlanForOtherDuration($currentPlan->getPlanId()); | |
} | |
... lines 35 - 42 | |
} | |
... lines 44 - 168 | |
} |
Pass that into the template as a new variable:
... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
... lines 17 - 19 | |
public function accountAction() | |
{ | |
... lines 22 - 23 | |
$otherDurationPlan = null; | |
if ($this->getUser()->hasActiveSubscription()) { | |
... lines 26 - 31 | |
$otherDurationPlan = $this->get('subscription_helper') | |
->findPlanForOtherDuration($currentPlan->getPlanId()); | |
} | |
return $this->render('profile/account.html.twig', [ | |
... lines 37 - 40 | |
'otherDurationPlan' => $otherDurationPlan, | |
]); | |
} | |
... lines 44 - 168 | |
} |
Cool!
In account.html.twig
, scroll down to the Upgrade Plan button. Copy that whole
thing. Then, keep scrolling to the "Next Billing at" section. If the user has a
subscription, paste the upgrade button:
... lines 1 - 67 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
... lines 73 - 88 | |
<table class="table"> | |
<tbody> | |
... lines 91 - 116 | |
<tr> | |
<th>Next Billing at:</th> | |
<td> | |
{% if app.user.hasActiveNonCancelledSubscription %} | |
{{ app.user.subscription.billingPeriodEndsAt|date('F jS') }} | |
<button class="btn btn-xs btn-link pull-right js-change-plan-button" | |
data-preview-url="{{ path('account_preview_plan_change', {'planId': otherDurationPlan.planId}) }}" | |
data-plan-name="{{ otherDurationPlan.name }} {{ otherDurationPlan.duration }}" | |
data-change-url="{{ path('account_execute_plan_change', {'planId': otherDurationPlan.planId}) }}" | |
> | |
Bill {{ otherDurationPlan.duration }} | |
</button> | |
... lines 130 - 131 | |
{% endif %} | |
</td> | |
</tr> | |
... lines 135 - 148 | |
</tbody> | |
</table> | |
</div> | |
... lines 152 - 160 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 165 - 166 |
And since this process will be the same as upgrading, we can re-use this exactly.
Just change otherPlan
to otherDurationPlan
... in all 4 places. Update the text
to "Bill" and then otherDurationPlan.duration
. So, this will say something like
"Bill yearly".
Before we try this, go back into ProfileController
and find previewPlanChangeAction()
.
The truth is, changing a plan from monthly to yearly should be identical to upgrading
a plan. But, it's not quite the same. To help us debug an issue we're about to
see, dump the $stripeInvoice
variable:
... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
... lines 17 - 122 | |
public function previewPlanChangeAction($planId) | |
{ | |
... lines 125 - 127 | |
$stripeInvoice = $this->get('stripe_client') | |
->getUpcomingInvoiceForChangedSubscription( | |
$this->getUser(), | |
$plan | |
); | |
dump($stripeInvoice); | |
... lines 134 - 141 | |
} | |
... lines 143 - 168 | |
} |
And now that I've told you it won't work, let's try it out! Refresh the account page. Then click the new "Bill yearly" link. Ok:
You will be charged $792.05 immediately
Wait, that doesn't seem right. The yearly plan is $990 per year. Then, if you subtract approximately $99 from that as a credit, it should be something closer to $891. Something is not quite right.
Hey Blueblazer172,
Yeah, I see, that will be a totally no good! So you need to track whether the user has active subscription or no on your website. I think a simple User::$subscriptionStatus field will be enough to tracking that. And you could consider some predefined statuses like: active, active, active_pending_cancellation, canceled, etc. depends on your subscription logic. But also you probably need to store more information in the DB like Stripe customer ID, Stripe subscription ID, Stripe subscription period end, etc.
Cheers!
// 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
}
}
Hey,
my last question is about how to deny buying a subscriptoion again if there is already one. I think that would be very annoying if the user tests buying twice and it works and he still has one subscription.
you get what i mean? :P