تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Reviewing Quick-Delivery Code0:56

And sure, if we had two weeks to work on this, we might have been able to do it differently. But when you have one day, and it's got to be done and deployed, you have to think quickly on your feet. So let's reflect on the code I wrote that day. I haven't looked at it since. We're going to review it together, decide what we like, maybe what we don't like, and the sacrifices and the compromises you make when you have to get something done quickly. All right, so if we switch back to Chrome, we have this endpoint gift_certificates. OK, let's go to my routes file. And I'll quickly browse to this section.

Splitting Controllers by Responsibility1:24

OK, let's go to my routes file. And I'll quickly browse to this section. OK, so here's all the routes related to gift certificates. And I want you to notice immediately, I have this split up into two different controllers. This is a pattern I've used for many years right now. I originally learned it from the Rails world. It's a common convention there, and I think it's a good one. So you'll notice, if we go to GiftCertificatesController, we have actions for create, store, and show, the basic RESTful controller actions. However, if you think about it, create would be to create a new gift certificate, right?

and show, the basic RESTful controller actions. However, if you think about it, create would be to create a new gift certificate, right? store would be to persist it in the database. And show would be to show that gift certificate to the user. But now what about the situations where it needs to be redeemed? So you have that gift certificate number, and you now want to redeem it towards a new account. Well, yeah, you might put it on GiftCertificatesController, but what would be the RESTful action for that? You don't really have one.

action for that? You don't really have one. So what you end up doing is you create a method like redeem. And one method doesn't seem too bad. But then maybe that shows a page to redeem it, and when you submit that, you now have something like post redeem. And very quickly, your controllers become kind of unwieldy. They become difficult to manage. This is where people end up extracting dedicated service classes, because at this point, you have all these methods.

This is where people end up extracting dedicated service classes, because at this point, you have all these methods. It feels weird to create more protected methods to assist these. No, they would say create a service class instead that can handle that responsibility. And yeah, that's what they say. Or you get rid of this entirely, and you create a brand new controller. That way, you can return to those RESTful actions like show and create. And further, if you do need to add a protected method or two at that point to assist the controller, fine. It's no problem at all.

controller, fine. It's no problem at all. You don't have to extract a service class. That's just going to reproduce what you would have done in the controller. In the first place, I think it ends up being much cleaner. So if we go back to the routes file, you'll see, yes, we have a GiftCertificatesController to create one, persist one, and show one. But then when we redeem it, we're going to create a whole new controller called GiftCertificateRedemptionsController. And you'll notice I return to those actions again.

CertificateRedemptionsController. And you'll notice I return to those actions again. If I need to show the page to redeem a gift certificate, then I have an action called show. And once you submit that form, that will, of course, hit a store method. This keeps your controllers nice and clean. So I like this part. But let's now switch back over here, because once again, when you need to implement something in a day, you don't always have the ability to spend two weeks going over it with a fine tooth comb.

New Certificate Page Setup4:17

All right. Let's take a look. Right here, show the page to create a new GiftCertificate. This is the exact code we're using there. All right. Well, all that's doing is loading a view. We list the discount there for some calculations I have to do. And then we have a list of featured collections. And that will be down here. It's just a quick overview of the featured collections that we have there.

And that will be down here. It's just a quick overview of the featured collections that we have there. And because that's common and used throughout the site, I extract that to a dedicated query object. A query object is just a plain old class that will have a get method. And within that method, you have your Eloquent query. That's all it is. So here, I inject that. We fetch our featured collections. And then in my view, I can filter through those and render it as you can see here.

Processing Payment and Creation4:54

We fetch our featured collections. And then in my view, I can filter through those and render it as you can see here. OK. So that looks fine. I honestly wouldn't change a thing there at all. Next, a store action. Submit a new gift certificate request. So that will be right up here. When you fill out this Stripe modal, you click the button. Behind the scenes, fire off an Ajax request to Stripe that will validate the credentials.

When you fill out this Stripe modal, you click the button. Behind the scenes, fire off an Ajax request to Stripe that will validate the credentials and it will return a token to us, a unique token that we can then submit to our server. At that point, we hit this method here, store. And if we switch back, here is the endpoint. So we make a POST request to that and we'll hit the store method. OK. So let's take a look at that. Here's what I ended up with. Now, we're going to talk about this because a lot of people would say, wait a minute.

Once again, this is an example of measuring the pros and cons. Well, yes, I could store gift certificate campaigns in the database and then associate that with a unique ID that I fetch, and then I pull down the discount pricing and all of that. Or we can keep it simple and get it done. Okay, so we're going to charge the $user $250 using the cardToken we receive, and we will process the charge. Okay, now if we catch any Stripe exception, in this case, I just caught a fullException, had to be quick. In real life, I would do more of a targeted Stripe exception there.

had to be quick. In real life, I would do more of a targeted Stripe exception there. Okay, and actually, in this case, I can even see mistakes I've made, like right here. That's not correct. That would just return an instance of my Billing class, and then we will process a charge for that cost. Again, I show you mistakes like this that make their way to production, even though that last one was harmless. But I show you these to make it very clear that it's not always about writing the cleanest possible code and making the most elegant system.

But I show you these to make it very clear that it's not always about writing the cleanest possible code and making the most elegant system. Yeah, if you can do that, great, and for your own projects, great. But sometimes the job has to be done. Sometimes deadlines are a thing, and you can't get mad at management because it needs to be done. It's just a reality. It has to be done. Okay, so anyways, we charge the User, and then at this point, we create a gift certificate. Once again, I'm just referencing database attributes there.

Okay, so anyways, we charge the User, and then at this point, we create a GiftCertificate. Once again, I'm just referencing database attributes there. Maybe if I had a little more time, I would have created a dedicated static constructor so that I can isolate these attribute values to the GiftCertificate model, and you'll see to create a GiftCertificate, I decided, all right, I need to store the id of the person who purchased it. So we're going to call that purchaserId. I'm next going to record how much they gave us. I will record the id of the Stripe charge, and then I will create a giftCertificateKey.

I will record the ID of the Stripe charge, and then I will create a gift certificate key. Once we have our gift certificate, I'm going to email that to the user. So we will send a mail to the user, and specifically, I knew up a gift certificate confirmation, and that's a standard mailable class here. So we build up emails, gift certificates, and this is the email that you'll receive. Very, very simple, right? You can make this stuff as complicated as you want to, or you can get it done and get it out the door. All right, so let's switch back, and then we return a redirect.

it out the door. All right, so let's switch back, and then we return a redirect. And then finally, if you need to show it, we use route model binding to quickly find the gift certificate, and then we pass that to a view. And that's it. That's all we're doing within this class. So once again, let's take a look at this. There will be those who would say, well, you shouldn't be validating within the controller. The model should validate itself, which I've always thought is silly. This is perfectly fine to validate an HTTP request.

The model should validate itself, which I've always thought is silly. This is perfectly fine to validate an HTTP request. Next we find the User, we charge the User, we create a gift certificate, and we send a letter, basically. Now reflecting on this code, I don't think it's bad. A lot of people might tell you this is a horrible way to go because you're mixing all these different responsibilities. I can promise you a lot of people would say, well, no, you can't do this because there's too many responsibilities. This action alone is responsible for charging a User, preparing a gift certificate, and

You want to be careful when you have multiple steps that will affect your database structure. And you may have situations where if one fails, or one nulls out, or one throws an exception, well, that's going to mess up the entire process. So in this case, you can see we're charging the User. That's actually performing a charge. In this case, $250 is being charged. Now if anything goes wrong, we catch that and we immediately redirect back. But after that, we then create a gift certificate, and we then send an email. So sometimes you might want to reach for a database transaction. A database transaction is useful for saying, okay, change the database in this way, and

I'm not too worried about it. But nonetheless, for other projects, it's something to consider. If you have multiple actions that change your database, you might want to wrap all of that up within a database transaction. Anyhow, I show you this just to further demonstrate that there will be those who say you never reference Eloquent within a controller. You should instead extract that to a dedicated repository that implements an interface. And that way, you're not dependent upon the framework. You're just dependent upon an implementation that an interface specifies. It sounds great, right?

Redemption Flow and Model Method13:35

They can give it to their friend. They can use it themselves. They now want to redeem it. So when they click on that link in their email, it will take them here. A GET request to this endpoint will send us to GiftCertificateRedemptionsController. Let's see what we have here. Well, if we switch back, that's going to hit a show method. And if we take a look at that, once again, we're just fetching that certificate immediately and passing it to the redeem page. If you want to take a look at that, it's just a standard form.

and passing it to the redeem page. If you want to take a look at that, it's just a standard form. Now you can see sections like this. We have a Certificate model. Well, if it has been redeemed already by the User, then we need to give them some kind of feedback. So if we take a look at our GiftCertificate model, it's very, very simple. Very, very simple. So the only interesting method here is called redeemedBy. That allows us to say, all right, this certificate has been redeemed by the User with an ID of

So the only interesting method here is called redeemedBy. That allows us to say, all right, this certificate has been redeemed by the User with an ID of five. Now, behind the scenes, that just encapsulates the process of updating some attributes there. And that's what I was referring to earlier. Often it's useful to take this array of attributes, database specific columns, and you keep it contained within your model. And then you can instead assign a readable name. Now, anywhere outside of the model, I can just say, all right, mark the certificate as being redeemed by this User.

Now, anywhere outside of the model, I can just say, all right, mark the certificate as being redeemed by this User. And then if I ever need to dig further, I can go to the model and see, okay, well, what encompasses that action is updating this column and this column. Anyways, let's head back here. All right. So when you submit that form, you're now going to redeem your gift certificate. That will hit this store method. And here's what we have. Once again, it's fast.

All right. So let's check. When you redeem it, we do have a form that we validate. Next, we try to track down the gift certificate. Now this is another example where, yeah, maybe rather than using a where query, which I don't really mind, but yeah, if I want, I could use maybe a static helper, like byKey, and then we pass in the key there. That might be something that would clean it up a little bit. Those are all things you can review. In this case, I kept it simple.

Those are all things you can review. In this case, I kept it simple. Next, we check, okay, well, you're trying to redeem a certificate, but what if it has already been redeemed? Well, in this case, I just do a quick abort with unauthorized. Now this is only a small tweak, but what I might do instead is reach for an abortIf helper. Notice we can get a Boolean there and the code. So it just wraps the process of doing your conditional and then aborting, which means I could say, how about this, abort if the certificate is redeemed and leave a 403. You can even do a third argument if you want, such as certificate has already been redeemed.

I could say, how about this, abort if the certificate is redeemed and leave a 403. You can even do a third argument if you want, such as certificate has already been redeemed. That would be an option as well. And yeah, I think that's just a little bit cleaner. Those things make a difference, I promise. Anyways, we'll keep it as it was. Otherwise, we fetch the user. If they're active, well, in this case, when we upgrade to a lifetime, that's not really a subscription plan, that's separate. So we cancel your Stripe subscription using my API here, user, subscription, cancel immediately.

a subscription plan, that's separate. So we cancel your Stripe subscription using my API here, user, subscription, cancel immediately. Very readable. Quick note on cancel immediately. You might think, well, why not just cancel? Well, the default way for my project that cancel works is it's going to cancel your subscription at the end of your billing period, right? So if you sign up for 30 days of access and you cancel on day five, well, you shouldn't lose access, right? You should only lose access at the end of your billing period.

However, if you instead say cancel immediately, you will know what that means. And all this is, is a wrapper that calls the cancel method and passes the Boolean. Okay. Now here you can see, once again, I'm referencing those database columns directly. In general, I'm not too much of a person to beat you up over this, but when you can, I always think it's useful and very readable to isolate it on User. So rather than this right here where you can see, we're just updating the necessary values to mark the person as a forever account holder, well, wouldn't it be better if I just had a method that did that? Something like upgradeToLifetime.

a method that did that? Something like upgrade to lifetime. And then behind the scenes, that method can be responsible for it. But notice that very clearly says what we're trying to do. This implicitly describes what I'm trying to do there. So that would be a change I would make. And you know what? I think I even have a method on User that I forgot to use. So that would be an example reflecting on this code that I would change. Next, let's go over this one more time.

So that would be an example reflecting on this code that I would change. Next, let's go over this one more time. So find the certificate. If the user is active, well, we have to cancel their account because we're going to manually migrate them over to the special lifetime plan. We then mark the certificate as being redeemed by the user. And once again, that just updates some columns in the database and records that this specific user is the one who redeemed it. Okay, and then we log in the user, flash a message, and we send them home. And that's it.

Writing Feature Tests19:43

to be maintained and changed in the future. In this case, I haven't touched the code in five months. That's a good example of, okay, well, this really doesn't need that much maintenance. So I can approach things a little bit differently versus code that I know I will be iterating upon on a daily basis. Last step, we do, of course, have tests. So I would create a new feature test for my gift certificates. And let's take a look at these actions. So here, it generates a gift certificate upon payment. All right, well, if we hit the endpoint to prepare a new gift certificate, I'm going

So this is another thing that the system does. Okay, well, if I make a POST request once again, well, let's fake the mailer since I'm not actually going to fire off an email. But I will assert that a giftCertificateConfirmationEmail was in fact queued. So we give that a run and it passes. Now let's go to our controller, comment that out. If for whatever reason during the refactor later, I don't send that email, well, of course, I will be notified immediately. Okay, let's head back just a couple more. A giftCertificate can be marked as redeemed.

Okay, let's head back just a couple more. A gift certificate can be marked as redeemed. This is a very important thing. So given we have a gift certificate, well, then by default, it should be not redeemed. But then if we call this redeemedBy method, this could also, by the way, be on a GiftCertificate unit test or a model test if you prefer. But anyways, if we use the API like this, well, now redeemed should be set to true in the database. So we give that a run and it's green. Finally, one more, a gift certificate can be redeemed.

So if we try to redeem the certificate with that key, well, now when I fetch that certificate, it should be marked as redeemed. And if we fetch the user, I need to make sure that their account has been upgraded to the lifetime plan. And then we have one more. You cannot redeem an already redeemed gift certificate. And we'll skip over that. It's basically the same thing. But yeah, notice you can visit this test and figure out exactly how things should function versus kind of a more programmer way of saying that when I call this method, it should update

But yeah, notice you can visit this test and figure out exactly how things should function versus kind of a more programmer way of saying that when I call this method, it should update this property to true. That doesn't benefit me. Why do I care that such and such property is changed when I call the method? No, what I care about is what can it do? So I consider this the best of both worlds. When you have to go quickly, some people might say tests slow you down. I would say at first, they very much do. You will get to a point where writing these tests and then the associated production code

If you need to be quick as I was in this case, all the tests are still going to pass. And if later you do decide that maybe some kind of Service class is what you want or a UseCase class, it doesn't matter. Act that as you need to, rerun your tests, and if everything still works, you're going to get green without having to change a thing. It's the best of both worlds. It allows you to go quick while still offering the opportunity to refactor to something a bit more structured when and if you need to. And if at any point in that process you make a mistake, trust me, the tests are going to notify you instantly.

And if at any point in that process you make a mistake, trust me, the tests are going to notify you instantly.

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