gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
Congrats on creating the subscription in Stripe! But now, the real work starts. Sure, Stripe knows everything about the Customer and the Subscription. But there are always going to be a few things that we need to keep in our database, like whether or not a user has an active subscription, and to which plan.
We're already doing this in one spot. The user table - which is modeled by this
User
class - has a stripeCustomerId
field. Stripe holds all the customer data,
but we keep track of the customer id.
We need to do the same thing for the Stripe Subscription. It also has an id, so if we can associate that with the User, we'll be able to look up that User's Subscription info.
There are a few good ways to store this, but I chose to create a brand new subscription
table. I'll open up a new tab in my terminal and use mysql to login to the database.
fos_user
is the user table and here's the new table I added: subscription
.
There are a few important things. First, the subscription
table has a relationship
back to the user table via a user_id
foreign key column. Second, the subscription
table stores more than just the Stripe subscription id, it will also hold the
planId
so we can instantly know which plan a user has. It also holds a few other
things that will help us manage cancellations.
So our mission is clear: when a user buys a subscription, we need to create a new row in this table, associate it with the user, and set some data on it. This will ultimately let us quickly determine if a user has an active subscription and to which plan.
The new subscription table is modeled in our code with a Subscription
entity class:
... lines 1 - 4 | |
use Doctrine\ORM\Mapping as ORM; | |
/** | |
* @ORM\Entity | |
* @ORM\Table(name="subscription") | |
*/ | |
class Subscription | |
{ | |
/** | |
* @ORM\Id | |
* @ORM\GeneratedValue(strategy="AUTO") | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
/** | |
* @ORM\OneToOne(targetEntity="User", inversedBy="subscription") | |
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE") | |
*/ | |
private $user; | |
/** | |
* @ORM\Column(type="string") | |
*/ | |
private $stripeSubscriptionId; | |
/** | |
* @ORM\Column(type="string") | |
*/ | |
private $stripePlanId; | |
/** | |
* @ORM\Column(type="datetime", nullable=true) | |
*/ | |
private $endsAt; | |
/** | |
* @ORM\Column(type="datetime", nullable=true) | |
*/ | |
private $billingPeriodEndsAt; | |
... lines 45 - 93 | |
} |
It has properties for all the columns you just saw. And in the User
class, for
convenience, I added a $subscription
property shortcut:
... lines 1 - 11 | |
class User extends BaseUser | |
{ | |
... lines 14 - 35 | |
/** | |
* @ORM\OneToOne(targetEntity="Subscription", mappedBy="user") | |
*/ | |
private $subscription; | |
... lines 40 - 55 | |
/** | |
* @return Subscription | |
*/ | |
public function getSubscription() | |
{ | |
return $this->subscription; | |
} | |
... lines 63 - 82 | |
} |
With this, if you have a User
object and call getSubscription()
on it, you'll
get the Subscription
object that's associated with this User
, if there is one.
And that's cool because we'll be able to fill in this fancy account page I created!
All this info: yep, it's just hardcoded right now. Open up the template for this page
at app/Resources/views/profile/account.html.twig
. Instead of "None", add an if
statement: if app.user
- that's the currently-logged-in user app.user.subscription
,
then we know they have a Subscription. Add a label that says "Active". If they don't
have a subscription, say "None":
... lines 1 - 2 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
... lines 6 - 11 | |
<div class="row"> | |
<div class="col-xs-6"> | |
<table class="table"> | |
<tbody> | |
<tr> | |
<th>Subscription</th> | |
<td> | |
{% if app.user.subscription %} | |
<span class="label label-success">Active</span> | |
{% else %} | |
<span class="label label-default">None</span> | |
{% endif %} | |
</td> | |
</tr> | |
... lines 26 - 37 | |
</tbody> | |
</table> | |
</div> | |
... lines 41 - 43 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 48 - 49 |
If you refresh now... it says None. We actually do have a Subscription in Stripe from a moment ago, but our database doesn't know about it. That's what we need to fix.
Since our goal is to update the database during checkout, go back to OrderController
and find the chargeCustomer()
method that holds all the magic.
But instead of putting the code to update the database right here, let's add it
to SubscriptionHelper
: this class will do all the work related to subscriptions.
Add a new method at the bottom called public function addSubscriptionToUser()
with two arguments: the \Stripe\Subscription
object that was just created and the
User
that the Subscription should belong to:
... lines 1 - 4 | |
use AppBundle\Entity\Subscription; | |
use AppBundle\Entity\User; | |
... lines 7 - 8 | |
class SubscriptionHelper | |
{ | |
... lines 11 - 45 | |
public function addSubscriptionToUser(\Stripe\Subscription $stripeSubscription, User $user) | |
{ | |
$subscription = $user->getSubscription(); | |
... lines 49 - 60 | |
} | |
} |
Inside, start with $subscription = $user->getSubscription()
. So, the user may already
have a row in the subscription
table from a previous, expired subscription. If
they do, we'll just update that row instead of creating a second row. Every User
will have a maximum of one related row in the subscription
table. It keeps things
simple.
But if they don't have a previous subscription, let's create one:
$subscription = new Subscription()
. Then, $subscription->setUser($user)
:
... lines 1 - 8 | |
class SubscriptionHelper | |
{ | |
... lines 11 - 45 | |
public function addSubscriptionToUser(\Stripe\Subscription $stripeSubscription, User $user) | |
{ | |
$subscription = $user->getSubscription(); | |
if (!$subscription) { | |
$subscription = new Subscription(); | |
$subscription->setUser($user); | |
} | |
... lines 53 - 60 | |
} | |
} |
Our other todo is to update the fields on the Subscription
object:
$stripeSubscriptionId
and $stripePlanId
. To keep things clean, open Subscription
and add a new method at the bottom: public function activateSubscription()
with
two arguments: the $stripePlanId
and $stripeSubscriptionId
:
... lines 1 - 10 | |
class Subscription | |
{ | |
... lines 13 - 94 | |
public function activateSubscription($stripePlanId, $stripeSubscriptionId) | |
{ | |
... lines 97 - 99 | |
} | |
} |
Set each of these onto the corresponding properties. Also add $this->endsAt = null
:
... lines 1 - 10 | |
class Subscription | |
{ | |
... lines 13 - 94 | |
public function activateSubscription($stripePlanId, $stripeSubscriptionId) | |
{ | |
$this->stripePlanId = $stripePlanId; | |
$this->stripeSubscriptionId = $stripeSubscriptionId; | |
$this->endsAt = null; | |
} | |
} |
We'll talk more about that later, but this field will help us know whether or not a subscription has been cancelled.
Back in SubscriptionHelper
, call $subscription->activateSubscription()
:
... lines 1 - 8 | |
class SubscriptionHelper | |
{ | |
... lines 11 - 45 | |
public function addSubscriptionToUser(\Stripe\Subscription $stripeSubscription, User $user) | |
{ | |
$subscription = $user->getSubscription(); | |
if (!$subscription) { | |
$subscription = new Subscription(); | |
$subscription->setUser($user); | |
} | |
$subscription->activateSubscription( | |
... lines 55 - 56 | |
); | |
... lines 58 - 60 | |
} | |
} |
We need to pass this the stripePlanId
and the stripeSubscriptionId
. But remember!
We have this fancy \Stripe\Subscription
object! In the API docs, you can see its
fields, like id
and plan
with its own id
sub-property.
Cool! Pass the method $stripeSubscription->plan->id
and $stripeSubscription->id
:
... lines 1 - 53 | |
$subscription->activateSubscription( | |
$stripeSubscription->plan->id, | |
$stripeSubscription->id | |
); | |
... lines 58 - 63 |
Booya!
And, time to save this to the database! Since we're using Doctrine in Symfony, we
need the EntityManager object to do this. I'll use dependency injection: add an
EntityManager
argument to the __construct()
method, and set it on a new $em
property:
... lines 1 - 6 | |
use Doctrine\ORM\EntityManager; | |
class SubscriptionHelper | |
{ | |
... lines 11 - 13 | |
private $em; | |
public function __construct(EntityManager $em) | |
{ | |
$this->em = $em; | |
... lines 19 - 30 | |
} | |
... lines 32 - 61 | |
} |
For Symfony users, this service is using auto-wiring. So because I type-hinted this
with EntityManager
, Symfony will automatically know to pass that as an argument.
Finally, at the bottom, add $this->em->persist($subscription)
and
$this->em->flush($subscription)
to save just the Subscription:
... lines 1 - 8 | |
class SubscriptionHelper | |
{ | |
... lines 11 - 45 | |
public function addSubscriptionToUser(\Stripe\Subscription $stripeSubscription, User $user) | |
{ | |
... lines 48 - 53 | |
$subscription->activateSubscription( | |
$stripeSubscription->plan->id, | |
$stripeSubscription->id | |
); | |
$this->em->persist($subscription); | |
$this->em->flush($subscription); | |
} | |
} |
With all that setup, go back to OrderController
to call this method. To do that,
we need the \Stripe\Subscription
object. Fortunately, the createSubscription
method returns this:
... 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; | |
} | |
} |
So add $stripeSubscription =
in front of that line. Then, add
$this->get('subscription_helper')->addSubscriptionToUser()
passing it $stripeSubscription
and the currently-logged-in $user
:
... 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 | |
$stripeSubscription = $stripeClient->createSubscription( | |
$user, | |
$cart->getSubscriptionPlan() | |
); | |
$this->get('subscription_helper')->addSubscriptionToUser( | |
$stripeSubscription, | |
$user | |
); | |
... lines 116 - 118 | |
} | |
} | |
} | |
... lines 122 - 123 |
Phew! That may have seemed like a lot, but ultimately, this line just makes sure that there is a subscription row in our table that's associated with this user and up-to-date with the subscription and plan IDs.
Let's go try it out. Add a new subscription to your cart, fill out the fake credit card information and hit checkout. No errors! To the account page! Yes! The subscription is active! Our database is up-to-date.
Hey Blueblazer172 ,
What do you mean when say it won't work as expected? Do you have some errors? What problem exactly do you have? Your code looks valid and fine for me :)
Cheers!
Hi victor ,
if uncomment e.g {{ balance }}. it says the variable email is undefined. The same error is for email and created. But i pass the parameter to the account.html.twig in my showCustomerDetailAction().
I think there is no connection to stripe or the Action() Method has wrong Code.
Could one cause the issue for the above error? and what do i have to change if so ?
Thanks:)
Hm, it's weird! Because I see you pass those vars to the template, so the worst that could be is that you print nothing instead of those variables if they are null. Probably you do a mistake somewhere. Could you debug what you get back from Stripe in this action? Dump it below after you set those variables:
dump($stripeCustomer, $stripeCustomerBalance, $stripeCustomerCreated, $stripeCustomerEmail); die;
Also, looks closely to this error, Do you have it exactly in "profile/account.html.twig" template?
Cheers!
still this errors:
Variable "balance" does not exist in profile\account.html.twig at line 99
Variable "email" does not exist in profile\account.html.twig at line 106
Variable "created" does not exist in profile\account.html.twig at line 113
and my dumped output is here:
https://www2.pic-upload.de/...
so it seems the api works perfectly. I never user the name="show_customer_detail" attribute from the ProfileController.
I dont know where to load it. I think that is the problem.
Can you solve it ? :)
Hey Blueblazer172!
Ok, as will all tough-to-debug errors, this must be something quite small :). As you probably know now, the error 100% doesn't have anything to do with whether or not the Stripe API calls are working. It's actually much simpler than that: this error literally means that you are simply NOT passing "balance", "email" or "created" variables into your template. It's not that these may be null or something, it literally means that you are failing to pass them to your template - the variables are undefined.
And that's the mystery :). Because you are passing the variables into your template from your controller - we can see it very plainly:
return $this->render('profile/account.html.twig', [
'balance' => $stripeCustomerBalance,
'created' => $stripeCustomerCreated,
'email' => $stripeCustomerEmail
]);
This definitely means that you (should) have "balance", "created" and "email" variables in your template. So, for now, like Victor, I'm a bit stumped! So here's the help I need to help track this down:
1) Comment out the variables in account.html.twig once again so that the page works. Also, remove the dump() calls you have in your controller. Then, anywhere in account.html.twig, add this code:
{{ dump() }}
That's it. What does this look like? This will dump all variables we have access to in our template, and might help hint us as to what's going wrong.
2) Post all of your code :). I mean, your entire controller file and your entire template. Also, can you take a full screenshot of your entire page - I want to see what the web debug toolbar looks like at the bottom.
Thanks!
sorry for my late reply:) i've been busy at the weekend xD
so here is the dummped data: http://imgur.com/FZANocY
and my code is here for the
ProfileController.php: http://pastebin.com/rhy5SD0X
and account.html.twig: http://pastebin.com/5pgLH7eL
Thanks again Ryan and Victor for any help:)
Hey Blueblazer!
I don't know if you already know this but you can use {{ dump(variable) }} in your twig templates, so you can see what you are really passing to them :)
Hey Blueblazer172 ,
Hm, your PasteBin links doesn't work anymore :/
It's weird, looks like you're talking about 2 different templates: you pass variables into one, but do "dump()" in another one. Are you sure you wrote "dump()" in template which is located exactly in "app/Resources/views/profile/account.html.twig" path?
Cheers!
Ok, I think I see a problem...
Actually, the problem number one: I bet you're on "/profile" page but you should be on the "/profile/customer" - see your route definitions, for both routes you're using the same "profile/account.html.twig" template. So go to the "/profile/customer" page to see email, balance and created at date.
The problem number too: you can share this templates with 2 actions, but then you should pass all those variables in both actions. Or you can add some extra checks in your template to ensure that variables are defined, i.e. use if statements with "is defined" Twig test in them. Or you just need to use different template for your new showCustomerDetailAction() since "profile/account.html.twig" one is already used in accountAction().
Cheers!
i've rendered it in the accountAction and passed the variables and now it is working. so stupid that i've not seen it before xD
Haha, as always, simple mistakes hard to reveal ;) I'm glad you figured this out finally!
Cheers!
okay now i have again errors :P this won't stop xD
when i register as a new customer i cannot access the profile page, because there is no stripe user with that email. now my question is how can i create the stripe user on registration? i've looked at the docs from the api but i didnt know how to handle it :/
Hey Blueblazer172 ,
Probably you don't need to create a new Stripe customer for new users - you have to create it only when they do purchases. Otherwise you will have a lot of empty Stripe customers who probably could never buy anything on your site. So just to do some extra checks and display hardcoded values for new users, I mean just print "0" for their balance, render User::getEmail()/User::getCreatedAt() to show their current email and created at date.
But if you still want to create a new Stripe customer for every new user - then you have to hook to the registration process. If you use FOSUserBundle - they dispatch an event when a new user is registered. If you write custom registration - then you just need to add a few line of code into it.
Cheers!
thanks :)
currently i send the users a flash message when they want to access the account page without having purchased anything.
i'll try your hint :)
hey,
how can i assign a role to a user e.g if he bought the new zelander he should get the role ROLE_NEW_ZEALANDER?
should i do this after successfully purchasing the subscription? i think yes. But how ?
Thanks :)
Hey Blueblazer!
Are you using FOSUserBundle for managing your Users ? In that case you could use
$user->addRole("ROLE_NEW_ZEALANDER");
just dont forget to persist it
But, why you need to add a new role to the user ? He will have access to secret sections or something ?
Have a nice day :)
Yes I am using FOS. ;)
Okay thanks a lot ;)
Yeah I want to let the users only with New Zealander to access some pages. That is easy ;)
I forgot to persist it to the db and so I had some errors 🙈 But now it is working ;)
is this the right way ? :P
//set user role on registration
$this->em = $this->get('doctrine.orm.entity_manager');
$role = $user->addRole("ROLE_REGISTERED_MEMBER");
$this->em->persist($role);
$this->em->flush();
this too? :P
@Security("is_granted('ROLE_REGISTERED_MEMBER') and has_role('ROLE_PRO_USER') or has_role('ROLE_LEGEND_USER')")
Hey Blueblazer!
There is a better way for accesing to Entity Manager, like this:
$this->getDoctrine()->getManager()```
Your annotation looks fine, but I'm not sure if you have to wrap between braces "(...)" the part after that AND, like this;
@Security("is_granted('ROLE_REGISTERED_MEMBER') and (has_role('ROLE_PRO_USER') or has_role('ROLE_LEGEND_USER'))")`
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,
i want get the current account balance, createdAt and email from the stripeapi(https://stripe.com/docs/api#customer_object-account_balance).
I've wrote a own Action in my ProfileControle to try this, but it won't work as expected.
here the code what i've tried so far from my showCustomerDetailAction():
and in my account.html.twig i have:
whitch looks like this: http://imgur.com/a/Wrce6
now i actually want it dynamically and not hardcoded. but i don't know where to start.
Is there an easier way to do it, not like i did?