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.

Give the User a Subscription (in our Database)

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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:

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

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

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

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

Leave a comment!

24
Login or Register to join the conversation
Default user avatar
Default user avatar Blueblazer172 | posted 5 years ago | edited

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



/**
 * @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>
        {#<span>{{ balance }}</span>#}
        <span class="pull-right">22€</span>
    </td>
</tr>
<tr>
    <th>Account Email</th>
    <td>
        {#<span>{{ email }}</span>#}
        <span class="pull-right">test@web.de</span>
    </td>
</tr>
<tr>
    <th>Account Created</th>
    <td>
        {#<span>{{ created|date('F jS Y') }}</span>#}
        <span class="pull-right">March 14th 2017</span>
    </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?

Reply

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!

Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago | edited

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

Reply

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!

1 Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago

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

Reply

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!

1 Reply
Default user avatar
Default user avatar Blueblazer172 | weaverryan | posted 5 years ago

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

Reply
Default user avatar

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

Reply

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

Reply
Default user avatar
Default user avatar Blueblazer172 | MolloKhan | posted 5 years ago

Yeah this works now ;) thanks anyways

Reply

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!

Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago

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

Reply

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

Cheers!

1 Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago

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

Reply

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!

1 Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago

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

Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago

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

Reply

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

1 Reply
Default user avatar
Default user avatar Blueblazer172 | MolloKhan | posted 5 years ago

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

Reply

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

Cheers!

Reply
Default user avatar
Default user avatar Blueblazer172 | MolloKhan | posted 5 years ago | edited

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')")
Reply

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!

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