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!

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
        "twig/twig": "^1.24.1" // v1.35.2
    },
    "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
    }
}