در حال بارگذاری ...

Feature goal overview0:00

Okay, so here's what I'm working on today. I need to add a section to the Laracast codebase for me to generate one-off coupons. Up until this point, I've been doing it directly within Stripe.com's dashboard. But yeah, I need to streamline it so I can add it from my website. It will email the User the coupon, it will save it to my database, and it will submit an API request to Stripe to actually create the coupon on their end. So you can see that I've already created a CouponsController and set up the Vue. So we're mostly going to be focusing on when you submit that form. Here is where we need to do the things like I said. Create the coupon in our system, send an API request to Stripe to generate the coupon on their end,

Admin access test0:31

Here is where we need to do the things like I said. Create the coupon in our system, send an API request to Stripe to generate the coupon on their end, and then email the user. Now you will also see that I have a test class for this feature, GenerateCoupons. So let's get started. To begin, I'd like to say, well, only administrators can generate coupons. So, of course, you can't do it. A guest can't do it. So we will say, only admins can generate coupons. Okay, so we'll say, if I submit a POST request to admin/coupons, which I've already created a route for, and by the way, that route will send us to this store method.

Okay, so we'll say, if I submit a POST request to admin/coupons, which I've already created a route for, and by the way, that route will send us to this store method. Anyways, if you're not signed in and you're not an administrator, then I assert that you will be redirected to the homepage. Okay, so let's copy this and filter my test suite down to that class alone. It fails. So we got a 200, but it's not a redirect. All right, so let's go over here, and at the very top, and let's set up a new constructor function here where I will apply my admin middleware that I already have set up for Laracasts. So let's run that again, and we get green. So now that's working.

Persist coupon locally1:40

that I already have set up for Laracasts. So let's run that again, and we get green. So now that's working. Next up, how about it records a new coupon in the database. So now I'm going to say I have this little helpful signIn method that will sign me in as the administrator. So basically, given I'm signed in, well, when I submit a POST request to admin/coupons, and I send through all the necessary data for a coupon, for example, I need to give through the code, we'll just call it couponCode, the description, and we'll just say fooDescription, then the percentageDiscount that it applies, we'll just set that to 50, and then the recipient for the coupon.

then the percentage discount that it applies, we'll just set that to 50, and then the recipient for the coupon. These coupons will be single use, which means you can use it one time, and nobody else can ever use it again. So we'll set that to foo@example.com. We could also use model factories, but I'm not sure it'll be necessary in this case. All right, so given we're signed in as the administrator, when we submit a POST request to generate a new coupon, well, at the very least, I assert that it will be saved to the database. So assertDatabaseHas within the coupons table a record where the code is coupon_code.

well, at the very least, I assert that it will be saved to the database. So assert database has within the coupons table a record where the code is coupon code. We could also save this as an array and then assert that all of that data has been saved. This is good enough, though. So let's go ahead and run it, but before we do that, I do want to make sure that I import the database transactions trait so that any database manipulation we perform, that will all be rolled back at the end. Okay, so let's run it, and it does fail. So failed asserting that a row in the table coupons matches these attributes. Of course, right? We haven't done that yet.

So failed asserting that a row in the table coupons matches these attributes. Of course, right? We haven't done that yet. So down here, we could say, let's get rid of all of that. We're going to say coupon, and let me import that at the top, and we'll say coupon::create, and then pass through the data. So the code will be whatever you gave me within the request. The description, same thing here. And then the percentage discount will be request->percentage_discount. So now at this point, though, we can see that it just all maps over perfectly. So in these situations, what you can do instead is just say request and then pass an array here.

So now at this point, though, we can see that it just all maps over perfectly. So in these situations, what you can do instead is just say request and then pass an array here. So we want the code, the description, and the percentageDiscount. All right, so let's go ahead and run that now, and we do get green. So that means it is being saved to the database. So great. So now our test specifies that only an Administrator can generate a coupon, and as part of that generation, it does record it within our database. What else should it do? Well, one thing it should do is, yes, create the coupon, but we actually have to create the coupon in two different locations.

Abstract billing gateway4:23

Well, one thing it should do is, yes, create the coupon, but we actually have to create the coupon in two different locations. So yes, I want a local record of the coupon, but I also need to generate one on Stripe so that they have a record of the coupon. So now at this point, you have a couple options. You could do things like this where you just call StripeCouponClass directly, and funnily enough, it kind of takes the exact same shape here. And I think this is honestly fine. What some people might push you a little bit on is the fact that you're now hard-coding a reference to Stripe within your controller. So if you later decide that you're going to switch it out with Braintree,

What some people might push you a little bit on is the fact that you're now hard-coding a reference to Stripe within your controller. So if you later decide that you're going to switch it out with Braintree, well, you're going to have to go through your entire project and find all these references to Stripe and swap them out. So if you've ever heard recommendations and a principle to flip your dependencies, generally this is what it's referring to. So rather than hard-coding a reference to Stripe, you might use kind of a more abstract term like your billing gateway. So maybe here you could say gateway->createCoupon(). And what's nice about this is, yes, it could be a Stripe coupon instance, but it could also be a Braintree representation,

And what's nice about this is, yes, it could be a Stripe coupon instance, but it could also be a Braintree representation, or it could be a different service altogether five years from now. So then what you do is in your Stripe-specific gateway class, only there will you find code that interacts with the various Stripe classes, and those classes will not exist anywhere else in your application. Okay, so I actually have a gateway class. I'll go ahead and grab that. Generally, I don't like the name gateway at all. You could call it payments, anything that would be appropriate for you.

Mock gateway call6:54

It generates the coupon on the billing service end. That's kind of weird, isn't it? We just want to say, as part of this, it's also going to generate the coupon with Stripe or with Braintree. So it sounds like we're going to have to duplicate this, and generally I'm okay with a little bit of duplication in my tests. I find that, yes, you're repeating yourself, but also this isn't production code, and sometimes that repetition can make it really easy to understand how to interact with the API. But if we find this too much, we're going to extract it to a dedicated createCoupon method. So given we're signed in and we submit this post request, well, we also expect it to call this createCoupon method.

So given we're signed in and we submit this post request, well, we also expect it to call this createCoupon method. And the reality is I don't want to trigger the logic on this method. I just want to make an assertion that we called this method. So as part of this test, I expect you to tell the gateway to create the coupon, and that should be enough. So it sounds like we're going to have to mock it, right? So we could say, mockery::mock, our Gateway class. And remember, this is an interface. So you'll see like in my AppServiceProvider right here, we're binding my StripeGateway to the interface here.

So you'll see like in my AppServiceProvider right here, we're binding my Stripe gateway to the interface here. And that way, if we ever request this interface here, Laravel knows, oh, well, I currently have the Stripe representation of that gateway in my container, so that's what I'm going to return to the user. All right, I hope that makes sense. So let's go ahead and mock it. So we'll call this our gateway, and we'll say gateway should receive a call to createCoupon exactly once. Okay, so let's grab our expectation code, push it up here. We could also use a spy, which I'll show you later.

Okay, so let's grab our expectation code, push it up here. We could also use a spy, which I'll show you later. So given we're signed in, well, when we submit a POST request here, we do expect that this createCoupon method on our gateway should, in fact, be called. So let's go ahead and run this single test. And we get an error, unexpected error on line 40. And whoops, there we go. All right, one more time. It fails. So mockery not found.

It fails. So mockery not found. I need to make sure I reference that globally. And again, method createCoupon from our gateway should be called exactly one time, but it was not called at all. Ah, and I know what the problem is. So can you figure out what the issue is? Well, yes, we mocked this gateway class, but then when we submitted our POST request, it asked for a gateway instance, and currently what's bound to it is my Stripe gateway instance, so that's what got injected.

it asked for a gateway instance, and currently what's bound to it is my StripeGateway instance, so that's what got injected. So the mock never got introduced into the setup here. So what we need to do in these cases is this. We could say for Laravel's container, the current instance you have of Laracast'sBillingGateway, I actually want you to associate that with our current gateway mock. So now when we request this, Laravel's going to look in its container and return our mock here, not the actual StripeGateway. So let's run it again, and it still fails. Let's see.

So let's run it again, and it still fails. Let's see. Too few arguments to function createCoupon. Zero were passed, but exactly one was expected. Okay, so that's because within our Production class, the createCoupon on my gateway does in fact expect our local coupon, so that's what it grabs its data from. So this is what the interface is expecting. So our mock, of course, failed. Okay, so let's give that another run, and it does return green.

So our mock, of course, failed. Okay, so let's give that another run, and it does return green. So that means for a sanity check, if we never make that call, it's going to fail, and this is exactly what we want. We now have an assertion that as part of this call, we are going to ask our gateway to createCoupon. And then remember, you'll also have a Stripe gateway test that physically ensures that when createCoupon is called, it submits an API call to Stripe's server and does in fact create the coupon on their end. So this is exactly what we want.

and does in fact create the coupon on their end. So this is exactly what we want. Now for a little bit of refactoring, though, here's one thing you could do. Notice when I described this, I said given we're signed in, and then we skipped over this, and we said when we submit a POST request, then we expect this createCoupon method to be called. If you want, you could always use a spy here. So you could do this, and then bring this down to the bottom and say shouldHaveReceived, like so. So if we run that again, it passes,

and say should have received, like so. So if we run that again, it passes, but once again, if we never call it at all, it fails. So that's good. And sometimes, yeah, I agree, that can make it a bit more readable. Finally, we could do this. We could say app instance. Let's just clean this up. And one thing I like to do is I will assign variables inline, like so. So the current instance of gateway in the container,

And one thing I like to do is I will assign variables inline, like so. So the current instance of gateway in the container, I actually want that to be equal to my spy. So I'll save that to a variable so that I can perform my expectation or my confirmation, and we'll still get green. Okay. But now if we go back and run all of the tests, well, notice that's going very slowly, and we get coupon already exists on Stripe send. So it actually tried to submit that API request to Stripe

and we get coupon already exists on Stripe send. So it actually tried to submit that API request to Stripe because in this very first test here, well, we didn't mock the Stripe gateway. We only did it here. So up here, it did get the Stripe gateway instance, and it did try to make that API call. So, of course, we could just copy everything over. But, yeah, at this point, I think we can extract a dedicated method here. How about addCoupon or something like that?

But, yeah, at this point, I think we can extract a dedicated method here. How about addCoupon or something like that? So when we call this method, yes, it's going to sign the User in. It's going to update the gateway instance in the container, and it's going to submit a POST request. So now if we scroll up, this can then become this addCoupon. But we still need to perform this confirmation that createCoupon was called. So maybe we could always just assign that, something like that. And then up here, I could say this gateway. Okay, let's see if that works.

And then up here, I could say this gateway. Okay, let's see if that works. Yep, we get green. So that means way up here, we could do the exact same thing. So when we try to add a coupon, we expect the database to have the record. Okay, let's run the entire suite now. And we get green. But, yeah, we dried up the code just a little bit. So what else do we have to do? Well, it is true that we asserted that a coupon is created.

Send coupon email13:29

So what else do we have to do? Well, it is true that we asserted that a Coupon is created. We asserted that we ask our gateway to create the coupon on their end. But we also need to send the user an email. So we can do it in Laravel 5.4. Add a new test. It sends the user an email with their coupon code. Now, I've actually already created a mailable for this. So you can see I have a PersonalCoupon class. And if we scroll down, it accepts the coupon.

So you can see I have a PersonalCoupon class. And if we scroll down, it accepts the coupon. And then it loads this markdown view. PersonalCoupon.blade. And here we go. Here's the email that will be sent. And it does have a link to where they can use it. All right, so I just want to make sure that that particular email is sent. So, within our test, we could say, well, yes, if we try to add a coupon,

So, within our test, we could say, well, yes, if we try to add a coupon, we can say Mail. I assert that the PersonalCoupon email was in fact sent. So let's go ahead and import that at the very top. There it is. And if we scroll back down, the only remaining thing we have to do is register a fake. So if we take a look at the fake method, notice that it swaps out the underlying instance for this facade.

So if we take a look at the fake method, notice that it swaps out the underlying instance for this facade with a MailFake class. And you can review that here. So you'll see that this class now has a bunch of various assertions that you can use, like assertSent. So we don't want to send an actual email. But when we submit a request to add a coupon, our assertion will be that the PersonalCoupon was in fact sent. So let's go ahead and run it.

our assertion will be that the PersonalCoupon was in fact sent. So let's go ahead and run it. And it fails. We expected this mailable, but it never occurred. All right, let's come back to my Controller, and we'll say Mail to the recipient. So you'll remember that we passed that through in the request. So we did that right there. So it's going to go to the recipient, and we will send a new PersonalCoupon email.

So it's going to go to the recipient, and we will send a new PersonalCoupon email and pass that through. So let's import that. And I think that looks good to me. Let's run it again. And we get green. So it worked. So now at this point, I would do just little bits of cleanup here. I'm going to inject the Mail at the top.

So now at this point, I would do just little bits of cleanup here. I'm going to inject the mail at the top. And then, of course, I would return a redirect back. I might even include a flash message. And I have this useful little flash function I use in my projects. Coupon generated and sent. Yeah, something like this. So notice how simple this is. And I would say this is perfectly fine. It's very, very clear.

If you ask why, they never quite give you an answer, but you're just told you're not supposed to do that. Instead, your domain should fire an event, and you should have a MailListener class that picks up on that event and sends the email there. People can do whatever they want, right? And there's use cases for all of these things. But as always, I would push for as simple as possible. Now, that does not mean that there isn't the ability to refactor here. So, for example, right now, all of our tests are passing.

Refactor into model methods16:44

Now, that does not mean that there isn't the ability to refactor here. So, for example, right now, all of our tests are passing. So maybe you would take a few moments to think, well, what are we really trying to do here? We're kind of generating a coupon. We even have that within our feature test, generateCoupon, but there was no reference to the word generate here. So maybe we could do something like this. Okay, well, if we wanted to do that, let's say coupon generate. Let's switch over to the Coupon class, and then at the bottom.

Okay, well, if we wanted to do that, let's say coupon generate. Let's switch over to the Coupon class, and then at the bottom. And you can ignore this step up here. But anyways, we're going to have a static function called generate where we accept the attributes. And then to start, we're just going to say static create and return that. All right, so let's bring everything back. Our first part of the refactor, everything's still working. But next, maybe when we generate the coupon there,

Our first part of the refactor, everything's still working. But next, maybe when we generate the coupon there, we can make this call to our Gateway class. Okay, well, what do we want to do here? How do we want to get the gateway? One option would be to just do something like this, fetch that out of the container, and then call createCoupon like so. And then we would need to say coupon, and then return the coupon at the bottom. Yeah, let's see if we can get that to work.

So let's try that option. Does it still work? It does. So you're free to do whatever you want there. But if we've decided that the process of generating a coupon means, yes, you'll create it on the local end, but it always needs to create one on Stripe's end where it actually counts and where it can be applied by them. Well, in those cases, I do like to wrap that up within a method so that I can never end up in a situation where the coupon exists locally,

Well, in those cases, I do like to wrap that up within a method so that I can never end up in a situation where the coupon exists locally, but it doesn't exist with Stripe or Braintree, whatever we happen to be using. Okay, so we can get rid of that. And now this is another, I would say, confrontational one. Would it be okay to put this mail call within here? Like maybe we say couponSend. And then we have that, and there is where we would send it. And let me import this at the top and then return the coupon. Okay, so now we say when we generate a coupon, it needs to be persisted to the database.

And let me import this at the top and then return the coupon. Okay, so now we say when we generate a coupon, it needs to be persisted to the database. We should create the coupon on Stripe's end. And then finally, we will physically send the coupon. But we do need the recipient. And yes, we could still use request. You can use request anywhere you want. For whatever reason, that feels a little odd to me. I generally don't like to use the request class or function anywhere outside of my controller or a class that I would consider to be sitting alongside the controller.

I generally don't like to use the request class or function anywhere outside of my controller or a class that I would consider to be sitting alongside the controller. So I do want to make sure that we send that through. Coupon, generate, and you know what? We could pass it through as a third argument, but maybe it's perfectly fine to call send here. And now I can push that process of fetching the recipient. I can bring that back to the top like that. So if that's the case, we could return the coupon here. We'll say to, like so. And how are we doing?

We'll say to, like so. And how are we doing? Green. So this is very much a successful refactor. And notice I no longer need that coupon variable, so that can be deleted as well. And let's see if we can do anything else. A little longer, so we could say attributes and the gateway. And then push that up. How's that look? Is that good? Yeah. So entirely up to you.

Is that good? Yeah. So entirely up to you. I think this looks pretty good to me though. Coupon, we're going to generate a new one and then send it to the recipient. That speaks perfectly in my mind. And now if you ever need to dig down to how a coupon is generated, you can see the specific logic right here. Now in closing, I would say the Mail class within an Eloquent model, this is like a hotly debated thing. I can even say for myself, even a couple years ago,

So the fact that I can say coupon send to this given email address, that reads perfectly to me. And when it comes to using the Mail class here, great. I have no issues with it whatsoever. But if you disagree, leave a comment below the video and we'll talk about it. But yeah, everything is passing. So let's call it a wrap. Thanks for coming along.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟