If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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
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.
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!
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.
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.
"Houston: no signs of life"
Start the conversation!
// 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
}
}