If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
The coupon form will submit its code
field right here:
... lines 1 - 9 | |
use Symfony\Component\HttpFoundation\Request; | |
class OrderController extends BaseController | |
{ | |
... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
... lines 86 - 97 | |
} | |
... lines 99 - 144 | |
} | |
... lines 146 - 147 |
To fetch that POST parameter, add the Request
object as an argument. Then add
$code = $request->request->get('code')
:
... lines 1 - 9 | |
use Symfony\Component\HttpFoundation\Request; | |
class OrderController extends BaseController | |
{ | |
... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
$code = $request->request->get('code'); | |
... lines 87 - 97 | |
} | |
... lines 99 - 144 | |
} | |
... lines 146 - 147 |
And in case some curious user submits while the form is empty, send back a validation
message with $this->addFlash('error', 'Missing coupon code')
. Redirect to the checkout
page:
... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
$code = $request->request->get('code'); | |
if (!$code) { | |
$this->addFlash('error', 'Missing coupon code!'); | |
return $this->redirectToRoute('order_checkout'); | |
} | |
... lines 93 - 97 | |
} | |
... lines 99 - 144 | |
} | |
... lines 146 - 147 |
Great! At this point, all we need to do is talk to Stripe and ask them:
Hey Stripe! Is this a valid coupon code in your system?. Oh, and if it is, ow much is it for?
Since Coupon
is just an object in Stripe's API, we can fetch it like anything
else. Booya!
As usual, add the API call in StripeClient
. At the bottom, create a new public
function called findCoupon()
with a $code
argument:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 183 | |
/** | |
* @param $code | |
* @return \Stripe\Coupon | |
*/ | |
public function findCoupon($code) | |
{ | |
... line 190 | |
} | |
} |
Then, return \Stripe\Coupon::retrieve()
and pass it the $code
string, which
is the Coupon's primary key in Stripe's API:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 187 | |
public function findCoupon($code) | |
{ | |
return \Stripe\Coupon::retrieve($code); | |
} | |
} |
Back in OrderController
, add $stripeCoupon = $this->get('stripe_client')
and
then call ->findCoupon($code)
:
... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
$code = $request->request->get('code'); | |
if (!$code) { | |
$this->addFlash('error', 'Missing coupon code!'); | |
return $this->redirectToRoute('order_checkout'); | |
} | |
$stripeCoupon = $this->get('stripe_client') | |
->findCoupon($code); | |
dump($stripeCoupon);die; | |
} | |
... lines 99 - 144 | |
} | |
... lines 146 - 147 |
If the code is invalid, Stripe will throw an exception. We'll handle that in a few
minutes. But just for now, let's dump($stripeCoupon)
and die
to see what it
looks like.
Ok, refresh, hit "I have a coupon code," fill in our CHEAP_SHEEP
code, and submit!
There it is! In the _values
section where the data hides, the coupon has an id
,
it shows the amount_off
in cents and has a few other things, like duration
, in
case you want to create coupons that are recurring and need to tell the user that
this will be applied multiple times.
Now that we know the coupon is legit, we should add it to our cart. I've already
prepped the cart to be able to store coupons. Just use
$this->get('shopping_cart')
and then call ->setCouponCode()
, passing it the
$code
string and the amount off, in dollars: so $stripeCoupon->amount_off/100
:
... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
... lines 86 - 93 | |
$stripeCoupon = $this->get('stripe_client') | |
->findCoupon($code); | |
$this->get('shopping_cart') | |
->setCouponCode($code, $stripeCoupon->amount_off / 100); | |
... lines 99 - 102 | |
} | |
... lines 104 - 149 | |
} | |
... lines 151 - 152 |
The cart will remember - via the session - that the user has this coupon.
We're just about done: add a sweet flash message - "Coupon applied!" - and then redirect back to the checkout page:
... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
... lines 86 - 96 | |
$this->get('shopping_cart') | |
->setCouponCode($code, $stripeCoupon->amount_off / 100); | |
$this->addFlash('success', 'Coupon applied!'); | |
return $this->redirectToRoute('order_checkout'); | |
} | |
... lines 104 - 149 | |
} | |
... lines 151 - 152 |
Refresh and re-POST the form! Coupon applied! Except... I don't see any difference: the total is still $99.
Here's why: it's specific to our ShoppingCart
object. In checkout.html.twig
,
we print cart.total
:
... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
<table class="table table-bordered"> | |
... lines 29 - 49 | |
<tfoot> | |
<tr> | |
<th class="col-xs-6 info">Total</th> | |
<td class="col-xs-3 info checkout-total">${{ cart.total }}</td> | |
</tr> | |
</tfoot> | |
</table> | |
... lines 57 - 75 | |
</div> | |
... lines 77 - 79 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
I designed the ShoppingCart
class so that the getTotal()
method adds up all
of the product prices plus the subscription total:
... lines 1 - 9 | |
class ShoppingCart | |
{ | |
... lines 12 - 83 | |
public function getTotal() | |
{ | |
$total = 0; | |
foreach ($this->getProducts() as $product) { | |
$total += $product->getPrice(); | |
} | |
if ($this->getSubscriptionPlan()) { | |
$price = $this->getSubscriptionPlan() | |
->getPrice(); | |
$total += $price; | |
} | |
return $total; | |
} | |
... lines 100 - 153 | |
} |
But, this method doesn't subtract the coupon discount. I did this to keep things clean: total is really more like a "sub-total".
But no worries, the method below this - getTotalWithDiscount()
- subtracts the
coupon code:
... lines 1 - 9 | |
class ShoppingCart | |
{ | |
... lines 12 - 100 | |
public function getTotalWithDiscount() | |
{ | |
return max($this->getTotal() - $this->getCouponCodeValue(), 0); | |
} | |
... lines 105 - 153 | |
} |
So back in the template, use cart.totalWithDiscount
:
... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
<table class="table table-bordered"> | |
... lines 29 - 56 | |
<tfoot> | |
<tr> | |
<th class="col-xs-6 info">Total</th> | |
<td class="col-xs-3 info checkout-total">${{ cart.totalWithDiscount }}</td> | |
</tr> | |
</tfoot> | |
</table> | |
... lines 64 - 82 | |
</div> | |
... lines 84 - 86 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Ah, now it shows $49.
But, it'll be even clearer if we display the discount in the table. At the bottom
of that table, add a new if statement: if cart.couponCode
and an endif
. Then, copy
the subscription block from above, paste it here, and change the first variable to
cart.couponCode
and the second to cart.couponCodeValue
without the / month
,
unless you want to make all your coupons recurring. Oh, and add "Coupon" in front
of the code:
... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
<table class="table table-bordered"> | |
... lines 29 - 34 | |
<tbody> | |
... lines 36 - 49 | |
{% if cart.couponCode %} | |
<tr> | |
<th class="col-xs-6 checkout-product-name">Coupon {{ cart.couponCode }}</th> | |
<td class="col-xs-3">${{ cart.couponCodeValue }}</td> | |
</tr> | |
{% endif %} | |
</tbody> | |
<tfoot> | |
<tr> | |
<th class="col-xs-6 info">Total</th> | |
<td class="col-xs-3 info checkout-total">${{ cart.totalWithDiscount }}</td> | |
</tr> | |
</tfoot> | |
</table> | |
... lines 64 - 82 | |
</div> | |
... lines 84 - 86 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
This time, the whole page makes sense! $99 - $50 = $49. It's a miracle!
Now for the easy step: apply the coupon to the user's order at checkout... ya know, so that they actually save $50.
Hey Cameron,
Basically, you got this right! Stripe coupon is something more low-level, if you create a Coupon entity in your project - it's more like a wrapper around that low-level thing that brings you more flexibility, you can store the coupons in your DB, list them, view orders with attached coupons, easier apply some custom validation logic to coupons, etc. And all that you can do without executing any API to the Stripe server, so it's also a good performance boost on your app side. But that's not a requirement, so you can skip this abstraction and create coupons directly in Stripe if you want, but that might be less flexible and convenient. But it's totally up to you and depends on your specific project.
Cheers!
Hello, if I manage my coupons in database, and I also want to update them at the same time as they are on Stripe, what way do you recommend?
Should I use a "coupon.updated" webhook and check if the number of uses has decreased and if it has expired, then compare it to that stored in the database and make the modification?
Hey Kiuega!
Hmmm. We store coupon data locally also here on SymfonyCasts. We did it this way:
A) When you create a new "coupon" in our system (via an admin area) we only store that coupon data locally. We do NOT notify stripe about it at all.
B) On checkout, if a "coupon" is applied to the cart, *right* before we create the new Order in Stripe, we generate a random coupon code for the correct value in Stripe and attach it to the Order.
Because of this, we do all the management & validation of the coupons. For example, if a coupon can be used only 1 time, we store that info in *our* database and then do that check ourselves before allowing the coupon to be applied on the checkout page. It gives us more control, but it means we're managing a lot more stuff and aren't leveraging the coupon functionality that Stripe is giving us.
So... the best path depends on your goals. The easiest thing to do is let Stripe handle everything and make API calls to it. So the question would be: *why/8 do you want to have a local storage of coupons?
Cheers!
Okay thanks for your info! So my answer to
*why/8 do you want to have a local storage of coupons?
is just : "Why not ?"
Imagine that we manage several sites. It is true that going through Stripe directly to manage everything is easy.
But just for "aesthetic" reasons, being able to manage this directly from our site's admin interface would be just as pleasant!
For my part, I created a "Coupon" entity, and a "CouponService" service. And I set up an interface really similar to that of Stripe and I store everything in a database, but parallel to that, I also store on Stripe API.
Now the point that I have to develop is that of "updating the coupon in my database when it is used or expired". I imagine that using a webhook will be more profitable for me
Hey Kiuega
If the point of doing that is to exercise your coding skills, then that's ok. If not, then, it's an overkill (unless you have a specific reason to do so) because you will have now to keep in sync both systems, yours and Stripe.
Cheers!
In fact the main goal of this work is as follows: If in the future I have several sites online and which each have their plans, their products, their coupons, it is easier to manage it directly from the site in question .
Because otherwise, let's say I use the same Stripe account for all of my sites. I will have products, plans, coupons, but I will not find myself there, I will no longer know which one is for which site.
Finally, in my opinion, an advantage to manage also in database is the fact that if I have several sites again, and that I have a webhook which is sent, it will be sent to all my sites for which I will have configured the webhook. And in this way, in backend I will only have to intercept the webhook in the WebhookController.php and only establish a treatment if it is indeed an object belonging to my site, and not to another. You see ?
EDIT : And last thing of why I also want to have a full panel on each site rather than using Stripe directly: A case where I create an application for a company. They will therefore have their own Stripe account. But who is going to create the subscription plans, add products, and coupons? Maybe not the same person, because that would mean that they would all have access to the company's Stripe account. If they are assigned a role on the created application, then they will not have access to all the commands
Ohh, so you DO have a real reason to do the admin panel. In this case, it makes perfect sense to me :+1
Hey Kiuega!
That all makes sense :). Here's my advice then: rely as much as you can on webhooks. For example, here is one "pretend" way you could do it, just to highlight what I'm talking about. You could, in theory, rely 100% on webhooks to update your local database. So, when a user creates a new coupon code in your system, you DON'T immediately save it to your local database. Instead, you only make the API request to Stripe to create the coupon code. Then, when the webhook for that creation "fires", you receive it, and add that record to your local database. The same thing would happen when the user edits a coupon code: you wouldn't save the data locally on submit - you would just send the API request and then allow your webhook system to update the local database. This is the simplest and cleanest way to get this all working: rely 100% on webhooks to synchronize your local database.
The reason this is only a "pretend" idea is that, when a user creates a new coupon code in your system, if you don't save it to your database immediately, then there may be a few seconds delay before the user sees it, which would be super confusing :). But, you can still use the same philosophy: on submit, first send the API request to Stripe. Stripe will return some new "Discount" object in JSON. You can pass that to the same code (more or less) that handles the "discount created" webhook so that you don't have duplicated logic. The only trick then is that, in your "discount created" webhook logic, you'll need to add code to avoid adding a new row in your database if it is already there. With this setup, you could still create a coupon directly in Stripe's interface, and it would synchronize back to your system perfectly.
One thing to watch out for is this: sometimes Stripe webhooks are SO fast that they are delivered even while the code that sent the API request is still running. This is easiest to see in an example with some fake code:
$discountData = $stripeApi->createCouponCode('YAY_FOR_DISCOUNT', 2000);
$discountEntity = new Discount('YAY_FOR_DISCOUNT', 2000);
$entityManager->persist($discountEntity);
$entityManager->flush();
In this example, you expect that things will happen in this order:
A) You send the API request to stripe to create the discount/coupon
B) You save the Discount to your local database
C) ... some time later, the webhook is received from Stripe about the discount creation.
But we've found that sometimes (C) can happen between (A) and (B). You need to write your code to allow for this. OR, you need to do (B) first (save the entity to the local database) and then (A). That's what I'd recommend. And it will work great, as long as your "discount created" webhook logic is smart enough to avoid inserting a new row in the database if it sees that the YAY_FOR_DISCOUNT
Discount is already there.
Phew! Anyways - have fun :).
Cheers!
Wow thank you very much for all this information!
Regarding the way of doing things (A), (B), (C), yes you are entirely right!
And regarding plans and products I would also have a question.
If I want to manage this in a database, then in the case where I delete a plan as well as its product, since the users who use this plan can continue to use it, it means that they are not actually deleted from the database. Stripe data right?
To reproduce this diagram, when I delete a plan or a product, I must simply say in MY database that the object goes to "inactive" status? Or can I still delete it from my database too?
I'm not sure if you would be able to delete the plan and product if there is a user related to it. If you see it from the database point of view, you will have a record in other table relating to the product/plan id that you want to delete, hence, the delete query will fail. I think your best choice is to do what you said, inactivate a plan or product, so nobody else can consume it anymore
Cheers!
is there a warning attribute for an addFlash function? i only see success and error :) would be nice if i can get it like those 3 from bootstrap :)
Hey Blueblazer172
Flash messages works as bags, whenever you add a new flash message it's stored to the name you chose, so then you can loop through them and do what you need, in your case, rendering an element with a bootstrap class "alert-warning", you can check all alert types here:
http://getbootstrap.com/com...
Have a nice day!
thanks for replying. im familliar with the bootstrap alerts :)
but what i mean is this:
$this->addFlash('error', $error);```
to something like this:
$this->addFlash('warning', $error);`
is that possible ?
Actually you can choose whatever name you want, is just for your convenience, if you are lazy (I mean wise) like me, you could name those messages with the same name as bootstrap class and loop through them like this:
{% for type, messages in app.session.flashBag.all %}
{% for message in messages %}
<div class="alert-{{ type }}">
{{ message|trans }}
</div>
{% endfor %}
{% endfor %}
Then you can add more logic if you have other kind of messages
// 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
}
}
Hey, I was wondering if there's any advantages to using the stripe coupon vs building a coupon entity in symfony? It seems that you have advantages to building a coupon system within symfony as it allows this feature to be customized (user can only use coupon once, coupon tied to specific products only, can attribute coupons to users / "sales people" for commission report generation etc). Is there any advantages to using stripes coupon system? I'm very curious to know *curious pikachu face*