Buy

Give the User a Subscription (in our Database)

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.

The subscription Table

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.

Subscription and User Entities

The new subscription table is modeled in our code with a Subscription entity class:

95 lines src/AppBundle/Entity/Subscription.php
... 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:

84 lines src/AppBundle/Entity/User.php
... 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.

Prepping the Account Page

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

49 lines app/Resources/views/profile/account.html.twig
... 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.

Updating the Database

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:

63 lines src/AppBundle/Subscription/SubscriptionHelper.php
... 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):

63 lines src/AppBundle/Subscription/SubscriptionHelper.php
... 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:

102 lines src/AppBundle/Entity/Subscription.php
... 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:

102 lines src/AppBundle/Entity/Subscription.php
... 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():

63 lines src/AppBundle/Subscription/SubscriptionHelper.php
... 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:

63 lines src/AppBundle/Subscription/SubscriptionHelper.php
... 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:

63 lines src/AppBundle/Subscription/SubscriptionHelper.php
... 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:

63 lines src/AppBundle/Subscription/SubscriptionHelper.php
... 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:

76 lines src/AppBundle/StripeClient.php
... 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:

123 lines src/AppBundle/Controller/OrderController.php
... 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.

Leave a comment!

  • 2017-03-22 Diego Aguiar

    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!

  • 2017-03-22 Blueblazer172

    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')")
  • 2017-03-21 Diego Aguiar

    I'm glad to hear you could fix it :)

    Cheers!

  • 2017-03-21 Blueblazer172

    Yeah this works now ;) thanks anyways

  • 2017-03-21 Blueblazer172

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

  • 2017-03-21 Diego Aguiar

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

  • 2017-03-21 Diego Aguiar

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

  • 2017-03-21 Blueblazer172

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

  • 2017-03-21 Blueblazer172

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

  • 2017-03-21 Victor Bocharsky

    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!

  • 2017-03-20 Blueblazer172

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

  • 2017-03-20 Victor Bocharsky

    Haha, as always, simple mistakes hard to reveal ;) I'm glad you figured this out finally!

    Cheers!

  • 2017-03-20 Blueblazer172

    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

  • 2017-03-20 Victor Bocharsky

    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!

  • 2017-03-20 Blueblazer172

    remove the last ) from the url and you should see it :) xD
    yeah i'm absolutely sure

  • 2017-03-20 Victor Bocharsky

    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!

  • 2017-03-20 Blueblazer172

    so it seems that the variables are not passed to the template anyways :/

  • 2017-03-20 Blueblazer172

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

  • 2017-03-17 weaverryan

    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!

  • 2017-03-15 Blueblazer172

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

  • 2017-03-15 Victor Bocharsky

    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!

  • 2017-03-15 Blueblazer172

    Hi Victor Bocharsky ,

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

  • 2017-03-15 Victor Bocharsky

    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!

  • 2017-03-14 Blueblazer172

    hey,
    i want get the current account balance, createdAt and email from the stripeapi(https://stripe.com/docs/api....

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


    /**
    * @Route("/profile", name="show_customer_detail")
    */
    public function showCustomerDetailAction()
    {
    $stripeCustomer = $this->get('stripe_client')
    ->findCustomer($this->getUser());
    $stripeCustomerBalance = $stripeCustomer->account_balance;
    $stripeCustomerCreated = $stripeCustomer->created;
    $stripeCustomerEmail = $stripeCustomer->email;

    return $this->render('profile/account.html.twig', [
    'balance' => $stripeCustomerBalance,
    'created' => $stripeCustomerCreated,
    'email' => $stripeCustomerEmail
    ]);
    }

    and in my account.html.twig i have:

    <tr>
    <th>Account Balance</th>
    <td>
    {#{{ balance }}#}
    22€
    </td>
    </tr>
    <tr>
    <th>Account Email</th>
    <td>
    {#{{ email }}#}
    test@web.de
    </td>
    </tr>
    <tr>
    <th>Account Created</th>
    <td>
    {#{{ created|date('F jS Y') }}#}
    March 14th 2017
    </td>
    </tr>

    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?