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.

Data: Card Last 4 Digits

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Unless I cancel my subscription... which I can't actually do yet - we'll add that soon - in 1 month, Stripe will renew my subscription by automatically charging the credit card I have on file. Eventually, we'll need to allow the user to update their credit card info from right here on the account page. But let's start simple: by at least reminding them which card they have on file by showing the card brand

  • like VISA - and the last 4 card numbers.

This is yet another piece of data that's already stored in Stripe, but we're going to choose to also store it in our database, so we can quickly render the info to the user.

Printing the Credit Card Details

In the User class - aka our user table - I've already added two new columns: cardBrand and cardLast4:

... lines 1 - 11
class User extends BaseUser
{
... lines 14 - 25
/**
* @ORM\Column(type="string", nullable=true)
*/
private $cardBrand;
/**
* @ORM\Column(type="string", length=4, nullable=true)
*/
private $cardLast4;
... lines 35 - 63
public function getCardBrand()
{
return $this->cardBrand;
}
public function setCardBrand($cardBrand)
{
$this->cardBrand = $cardBrand;
}
public function getCardLast4()
{
return $this->cardLast4;
}
public function setCardLast4($cardLast4)
{
$this->cardLast4 = $cardLast4;
}
}

But these are empty right now: we're not actually setting this data yet.

Before we do that, let's update the template to print these fields. Open the profile/account.html.twig template. Down by the card details, let's say if app.user.cardBrand, then print some information about the user's credit card, like app.user.cardBrand ending in app.user.cardLast4:

... 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>
... lines 16 - 31
<tr>
<th>Credit Card</th>
<td>
{% if app.user.cardBrand %}
{{ app.user.cardBrand }} ending in {{ app.user.cardLast4 }}
{% else %}
None
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
... lines 45 - 47
</div>
</div>
</div>
{% endblock %}
... lines 52 - 53

Those fields on the User object are empty now, so let's fix that!

The Card Details on the Stripe Customer

Head to the Stripe API docs and click on Customers. The card information is attached to the customer under a field called sources. Yes, sources with an s at the end because you could attach multiple cards to a customer if you wanted. But we're not: on checkout, we set just one card on the customer, and replace any existing card, if there was one.

In other words, sources will always have just one entry. That one entry will have a data key, and that will describe the card: giving us all the info you see here.

Now to the plan: use the Stripe API to populate the card information on the User table right during checkout.

Setting the Card Details

In OrderController::chargeCustomer(), we either create or retrieve the \Stripe\Customer. Assign both calls to a new $stripeCustomer variable:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
... lines 86 - 88
if (!$user->getStripeCustomerId()) {
$stripeCustomer = $stripeClient->createCustomer($user, $token);
} else {
$stripeCustomer = $stripeClient->updateCustomerCard($user, $token);
}
... lines 94 - 123
}
}
... lines 126 - 127

In StripeClient, the createCustomer() method already returns the \Stripe\Customer object, so we're good here:

... lines 1 - 8
class StripeClient
{
... lines 11 - 19
public function createCustomer(User $user, $paymentToken)
{
$customer = \Stripe\Customer::create([
'email' => $user->getEmail(),
'source' => $paymentToken,
]);
... lines 26 - 29
return $customer;
}
... lines 33 - 76
}

The updateCustomerCard() method, however, retrieves the customer... but gets lazy and doesn't return it. Fix that with return $customer:

... lines 1 - 8
class StripeClient
{
... lines 11 - 33
public function updateCustomerCard(User $user, $paymentToken)
{
$customer = \Stripe\Customer::retrieve($user->getStripeCustomerId());
$customer->source = $paymentToken;
$customer->save();
return $customer;
}
... lines 43 - 76
}

Back in OrderController, we've got the \Stripe\Customer... so we're mega dangerous! But instead of updating the fields on User right here, let's do it in SubscriptionHelper. Add a new public function updateCardDetails() method with a User object that should be updated and the \Stripe\Customer object that's associated with it:

... lines 1 - 5
use AppBundle\Entity\User;
... lines 7 - 8
class SubscriptionHelper
{
... lines 11 - 62
public function updateCardDetails(User $user, \Stripe\Customer $stripeCustomer)
{
... lines 65 - 69
}
}

Now, this is pretty easy: $cardDetails = $stripeCustomer->sources->data[0]. Then, $user->setCardBrand($cardDetails) - go cheat with the Stripe API - the fields we want are brand and last4. So, $cardDetails->brand. And $user->setCardLast4($cardDetails->last4). Save only the user to the database with the classic $this->em->persist($user) and $this->em->flush($user):

... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 62
public function updateCardDetails(User $user, \Stripe\Customer $stripeCustomer)
{
$cardDetails = $stripeCustomer->sources->data[0];
$user->setCardBrand($cardDetails->brand);
$user->setCardLast4($cardDetails->last4);
$this->em->persist($user);
$this->em->flush($user);
}
}

Finally, call that method! $this->get('subscription_helper')->updateCardDetails() and pass it $user and $stripeCustomer:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
... lines 86 - 88
if (!$user->getStripeCustomerId()) {
$stripeCustomer = $stripeClient->createCustomer($user, $token);
} else {
$stripeCustomer = $stripeClient->updateCustomerCard($user, $token);
}
// save card details
$this->get('subscription_helper')
->updateCardDetails($user, $stripeCustomer);
... lines 98 - 123
}
}
... lines 126 - 127

No matter how you checkout, we're going to make sure your card details are updated in our database!

Before we try it out and prove how awesome we are, I want to add one more thing: I want to be able to tell the user when they will be billed next.

Leave a comment!

0
Login or Register to join the conversation
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