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

Revisiting Experience Event0:00

Alright, let's get to work. Now, you'll remember a couple lessons ago in our ExperienceTest, we had this one right here. An announcement is made when experience is earned. So we said, sign a User in, award them experience, and as part of that act, we expect this event to be fired. So in effect, we expect a bullhorn to be raised, where we notify the entire system, hey, if anyone is interested, a User just earned experience. So if we give that a run, it does return green. Okay, let's play around here.

So if we give that a run, it does return green. Okay, let's play around here. I'm going to go to my routes file. I'll tuck away all of my other routes down here, but at the top, let's just do something inline temporarily. We're going to remove it in a minute. Let's listen for that user earned experience event, and we will respond. So I can say, hello here. And now, let's go to our AchievementTest right here. This is the one we're trying to get to green.

Designing Achievement Awarding0:52

And now, let's go to our achievement test right here. This is the one we're trying to get to green. Sure enough, we did hit that handler. Okay, so let's switch back. What if we take the approach of, well, we need to track down some achievement IDs to award the User. I'm going to make it really long just for clarity, and then we'll clean things up, put them in their proper files. So I need some way to determine this variable, right? Then once I have a list of IDs, we can just sync them up in the pivot table that we created.

So I need some way to determine this variable, right? Then once I have a list of IDs, we can just sync them up in the pivot table that we created. So that would be something like, we'll get the User associated with the event, get their list of achievements, and then we will sync the achievement IDs, and let's just grab that. Yeah, that's basically what we're trying to do here. So let's think about it together. When a User earns experience, it sounds like we need to grab all of the available achievements that we have, and we're going to filter through all of them. And for each one, we're basically going to stack the User up against it, and we're going to say, hello, User, I have this achievement badge.

Then to the next achievement, do you qualify for that? And we're going to go on and on for every achievement that we have. That will ultimately give us a list or a collection of IDs that ultimately we will sync up in the pivot table and associate with the given User. So here's what I'm thinking. I need some way to fetch all achievements. Now we can hard code those in the database, and we're going to do that, but I like the idea of representing those as code. That way I could have a file name that corresponds specifically to an achievement that I offer on the site.

That way I could have a file name that corresponds specifically to an achievement that I offer on the site. So I'm just going to assume, well, maybe I can grab that out of the service container somehow. But this is kind of what I want. Like, just give me all of the achievements out of the container. And then we're going to filter it down like so. For each of these achievements, we're going to say, all right, well, I only want the list of achievements that the user qualifies for. So let's say something like achievement, and again, none of this code will work right now.

of achievements that the User qualifies for. So let's say something like achievement, and again, none of this code will work right now. We haven't implemented it. We're just trying to design it the way we would want. Let's check if the User qualifies for it. So maybe the qualifier, and then we'll pass through the User. We're going to clean this up even further with shorthand in a moment. Yeah. Yeah, let's try that. Okay.

Yeah, let's try that. Okay. So that will return a Boolean. So think about it. Get a collection of Achievement classes. We're going to filter through all of those in the collection. And for each one, we're going to call this method qualifier. And that's basically the logic that determines, does this given User qualify for the current Achievement? If it returns yes, then it gets added to the list here.

achievement? If it returns yes, then it gets added to the list here. If it returns false, then it is stripped from the list here. So now that we have our achievements, let's map over that new one here, that new collection. And for each one, I just kind of want to get the model key, right? I want to get the associated ID from the table. So is this achievement an Eloquent model? No, I don't think it will be. So I need some kind of method name, like give me the associated model primary key. Yeah, something like that.

So I need some kind of method name, like give me the associated model primary key. Yeah, something like that. That code, once we're done implementing it, should give us a Illuminate\Support\Collection class that contains a list of IDs, right? And each of these IDs represent achievements that the current User qualifies for. So then we pass that array or that collection to the sync method. And if you're not familiar with sync, it's a way to, well, synchronize your associations. So in this case, we would say the current User, maybe they have an ID of 2. In the user_achievements table, they should be associated with achievement IDs of 4, 7, and 9.

In the user_achievements table, they should be associated with achievement IDs of 4, 7, and 9. And if there's any other ones in the table that do not match up with that, well, I don't know what they're doing there because the user doesn't qualify for it. So delete those. And that's why we call the method sync. We add the rows that are missing. And then if there's any other ones that aren't in this array, we will delete them from the table. I hope that all makes sense.

Creating Achievement Classes5:00

table. I hope that all makes sense. Okay. Well, I think the first step is app Achievement. Well, how do we grab that? And what is an Achievement? So here's what we'll do. I'm going to create a new file here or class within the app/achievements directory. So we're going to add an achievements folder where every class within it corresponds to some achievement that we offer on the site.

So we're going to add an achievements folder where every class within it corresponds to some achievement that we offer on the site. So this first one will be called FirstThousandPoints. Maybe you get an achievement badge after you cross 1,000 points. And then another one at 10,000 and another one at 50,000. But I'm not worrying about that too much right now. I'll deal with that on my own later. So to start, we'll just say, yeah, you get a badge after 1,000 points. And that will be within the lyricas\achievements namespace. Oh, whoops.

And that will be within the lyricas achievements namespace. Oh, whoops. Let's fix that. Okay. Let's go back to our routes file. Now, we did say we need these two methods, qualifier and modelKey. Okay. So let's just grab those. And then later we'll figure out, do we need to extract an abstract class to make it very simple?

And then later we'll figure out, do we need to extract an abstract class to make it very simple? I assume we will. So first, the qualifier determines, you can even call it checker if you want. We might even switch to that. But it's the logic that says, okay, well, does the users experience points? Is it greater than or equal to 1,000 points? If so, you qualify for this achievement badge. Okay. Next, the model key.

Okay. Next, the model key. So yeah, how are we going to do this? We need to have some association between this AchievementType class and the associated Achievement in the database, right? Remember, this isn't an Eloquent model instance we have here. It's just a plain old php object. So we need to figure out how to create that association. So let's think about this. Let's add a constructor here.

So let's think about this. Let's add a constructor here. And what I'm thinking is, if we have an instance of this class, no matter what, we need to ensure that an associated record exists within the database. And it would be nice if all I have to do is create a new instance of an Achievement class here, and automatically it gets synced with the database upon instantiation. Okay. Well, let's consider that. When we call new First1000Points, let's save it to a model property. And we'll say, all right, give me my normal Achievement model class.

When we call new first 1,000 points, let's save it to a model property. And we'll say, all right, give me my normal Achievement model class. And we'll say firstOrCreate, at least a start. We will need the name, and that will be stored on this instance. We'll need a description. And we'll need an icon, or a default. And I think that's good. Okay. So again, we're going to extract some of this to an abstract class, because I want these achievement classes to be nice and simple.

So again, we're going to extract some of this to an abstract class, because I want these achievement classes to be nice and simple. But again, we refactor toward that. We don't immediately start with it. Okay. So for this, we could say the name is, and maybe what we could do is make this optional. So you could say first 1,000 points, or if you exclude that, for simplicity, we will grab the class name, and then maybe add spaces there to do it dynamically. All right. A description will be maybe something you see when you hover over the badge or something.

All right. A description will be maybe something you see when you hover over the badge or something. We'll start with, great job. You are on your way. You know, something like that. And then finally, we would have an icon here, and we could say first 1,000.svg, and of course your designer would whip something up for you there, or you would. So let's think. When we instantiate this class immediately, we check to see in the database, well, do we have this one already?

When we instantiate this class immediately, we check to see in the database, well, do we have this one already? If not, create it, and then save it to this model property. All right. Now when we fetch the model key, I could simply say return this.model, and we could do id if you want to hard code it, or often you'll do getKey, and that just makes it a little more dynamic in case it's not the id. That's the primary key. It will be, though. All right.

It will be, though. All right. I'll let you take a look at this. So let's go back to our event listener and go over it again. We need some way to fetch all achievements out of the service container. We'll take care of that in a moment. But assuming we do have a collection of these achievement classes like this, well, we're going to filter over them, and for each one, call this qualifier method. That's our way of checking, all right, does the user qualify for this achievement? If yes, return true.

That's our way of checking, all right, does the User qualify for this achievement? If yes, return true. If not, return false. Then once we have a new collection of only the achievements the User qualifies for, we will map over them, and for each one, I'm going to get the primary key for the associated model, 4, 5, whatever it happens to be. Then once we have an array of ids, we call our sync method to dynamically build up, strip, or add the list of achievement ids that should correspond to the current User for the event. So let's go back to achievementsTest and give this another run. Ah, it fails.

So let's go back to AchievementsTest and give this another run. Ah, it fails. Class Achievements does not exist. Of course it doesn't. Right here, we're trying to resolve this out of the container, Laravel doesn't have anything in the container associated with that string, so it's thinking instead, well, is there a class named that? No. So of course, everything blows up. Okay.

Binding Achievements Container10:06

So of course, everything blows up. Okay. I'm going to, once again, whip up a AchievementsServiceProvider. And let's take a look at that. It sounds like we need a place to store all of the achievements that our site offers. So I'm going to do it right here. We'll have an array of achievements, and at the moment, we just have first thousand points. Now whenever we add a new achievement to that directory, we would append it to the list here.

Now whenever we add a new achievement to that directory, we would append it to the list here. Okay. Next, we don't need boot. We just need to throw things into the service container. So right here for register, we'll say this $app singleton will be called Achievements, and that should resolve this function one time. So how about let's collect that list of achievements in the array above and map over them. And for each one, we're going to return a new instance of that class. And let's grab that.

And for each one, we're going to return a new instance of that class. And let's grab that. All right. Are we all on the same page? All we did here is throw something into the Laravel service container. It's called Achievement. When I want to grab that out of the container, I can just say app or resolve Achievements. Now when I call that, it's going to basically give me a single instance of this for the lifetime of the session. We're going to grab this array here, collect it into a Laravel collection, and then loop

lifetime of the session. We're going to grab this array here, collect it into a Laravel collection, and then loop over it. And for each one, we're going to return a new instance of that given class. Now think about it. When I instantiate FirstThousandPoints, as part of that, we're going to make sure that an associated or companion record exists within the database. So simply by newing up the class, I know a lot of people might say, you should never have a side effect like that when newing up a class. And there's merit to that.

have a side effect like that when newing up a class. And there's merit to that. But I think in this case, it's not too bad. Either way, if we need to, we'll tweak it slightly so that we don't do it there. Either way, when we resolve Achievements out of the service container, we're going to get a collection of Achievement instances, as well as syncing to the Achievements table. Okay. So now, at this point, we have a collection of instances just like this. We filter over them, we check if the user qualifies for it. Once we have a new array, we map over it and get a collection of IDs.

We filter over them, we check if the user qualifies for it. Once we have a new array, we map over it and get a collection of IDs. Alright, so let's go ahead and register this in my config/app.php file right around here. And we'll just do it there. And give it a run. Hmm, name. This is a phpStorm thing I've noticed before. This is going to be a mass assignment exception. But for whatever reason, it doesn't parse it out correctly. But you can run it from the terminal and see that exact thing.

But for whatever reason, it doesn't parse it out correctly. But you can run it from the terminal and see that exact thing. So the issue is, right here, when we try to create a new Achievement, we're trying to mass assign the attributes. But Eloquent protects us by default. In general, I'm not a huge fan of that, honestly. It's good for beginners, where you don't quite know the security concerns. But if you do, I often will just disable that. Or even put that on a parent model that all of your Eloquent models extend from. And that way you don't have to worry about it.

Or even put that on a parent model that all of your Eloquent models extend from. And that way you don't have to worry about it. Anyways, if we give it another run, now it's passing. So we did a lot there, right? So let's just go over it one more time, very slowly. Let's think. The Achievement badge is unlocked once the User's experience points pass 1,000. So we've created the world by saying, all right, well, if I have a User, and I award the User this number of points, well, when I try to fetch the collection of Achievements for that User, they should now have one Achievement.

the user this number of points, well, when I try to fetch the collection of Achievements for that user, they should now have one Achievement. And again, I can get rid of all that right down there. We give that a run, and it does return green. So how did we do that? All through here. We registered an event listener for when a user earns experience. When they do, we fetch a collection of all the Achievement instances. We have one right here, first 1,000 points. Once we have that collection, we filter it down.

We have one right here, first 1,000 points. Once we have that collection, we filter it down. We've already talked about this. We check for each one, does the User qualify for that Achievement? And once we have a new array, a subset, we will filter through all of those, grab the primary key, and sync it up in the database. So if you'd like to see that in action, take a look at this. I prefer not to show you my whole database structure, but here we have the UserAchievements pivot table. So the very first thing, let's go to php artisan tinker.

On our User model, down here at the bottom where we reference our Achievements, I'm going to say, with timestamps, we're going to make sure those get added on. All right. So let's think how we're doing now. Go back to our routes/web.php file. This is all the code it took. Let's go through the process of adding another one here. So we'll add a class, maybe LaracastMastery. We have that one as well. So that would be LaracastAchievements.

Abstracting Achievement Base15:19

We have that one as well. So that would be LaracastAchievements. And let's see what we would have to do here. Well, we already have one. We always need to add a name, a description, an icon. Actually, we don't always have to, but we can have defaults there. But in general, we would offer those. But I really hate the idea of having to reproduce this and this. So I think we could create an abstract parent class that we inherit from. Let's do this.

So I think we could create an abstract parent class that we inherit from. Let's do this. Within here, I'm going to create, once again, a new php class called AchievementType. And it will be abstract. OK. Now, all of these will extend from it. So in your documentation, we would say, well, when you have a new achievement, make sure it extends the AchievementType class. Next, let's move some of this to the parent class. So in PHPStorm, I can say pool members up.

Next, let's move some of this to the parent class. So in PHPStorm, I can say pool members up. I want the constructor on that, the modelKey, and the modelProperty. And we'll do that. OK. Now we have an extra usage there. We'll clean that up. And this is what we end up with. So every Achievement class, you add your basic fields for the Achievement and then a checker or a qualifier method.

So every Achievement class, you add your basic fields for the achievement and then a checker or a qualifier method. Now all of that extra logic is stored on the abstract class. Great. So think about it. Now, if I switch back to our new Achievement class, we will extend AchievementType. And we'll just add our fields here. Lericast master. The description, you are finished with your learning, Padawan. Although you'll never be finished.

The description, you are finished with your learning, Padawan. Although you'll never be finished. And then, sorry to break it to you, I don't think that ever stops. And then MasteryBadge.svg. Finally, we need our qualifier. So what determines if the User has reached this point? And again, I'm keeping it really simple looking at points. But in real life, you would maybe check the number of lessons they've completed, something like that. And then you would check to see, all right, does that match the number of total lessons

like that. And then you would check to see, all right, does that match the number of total lessons on the site? And if so, they've done everything. You know, you get the basic idea. I'm just keeping it really simple here. And we'll once again just say, well, maybe at 10,000 points, even though that's not quite right. So now, we've optimized things pretty well. We've added a new Achievement class.

So now, we've optimized things pretty well. We've added a new Achievement class. We've extracted anything tedious that we don't want to reproduce over and over into a parent class. Now, just go to your ServiceProvider, register it, and you're good to go. Now, when you fetch the collection of achievements, in fact, let's just do it together. Give it a run. And we'll now have a collection of exactly two achievements. First 1,000 points we have, and then Laracast Mastery. So at this point, if we do achievementIds to award the User, we should add exactly one.

First 1,000 points we have, and then Laracast Mastery. So at this point, if we do achievement IDs to award the user, we should add exactly one because they qualify for that first 1,000 points achievement, but not for the mastery one. So we get exactly one item. But if you want to test this, achievements test right here, let's award them enough points to qualify for both 10,000 points. Give it a run. Now, they have two. So it all works, which means if you want, let's do another test.

Testing and Refining Logic18:34

Now, they have two. So it all works, which means if you want, let's do another test. So yeah, you have to decide, are these specific achievements something that should be represented as a test? So do you want to codify that when a User hits 10,000 points, they get a badge? Maybe you want to do that, or maybe you want that to be very dynamic, and that you can generate on the fly, in which case it wouldn't make quite as much sense to have tests for each of those. You would instead just confirm that the basic process of how an achievement is awarded does in fact work.

You would instead just confirm that the basic process of how an achievement is awarded does in fact work. But I'm going to put all of it in code. So we'll say an achievement badge is unlocked once a user's experiencePoints pass 10,000. We're going to say that's a thing now. All right. So let's run this. This should return green. Let's first remove the dd. Where was it?

And we're shortening it, but in real life, you would earn 100 points, and then another 100 points, and then eventually, you would pass 10,000. So at that point, you should have a total of two achievements. Give that a run. That's failing, but we know it shouldn't. Usually in these cases, it's almost always related to previously in the test, you called a relationship and it's now cached on the object, but you actually need to get a fresh one. So let's say right here, let's just make sure we update $user. Get a fresh instance.

So let's say right here, let's just make sure we update User. Get a fresh instance. And that way, everything will be eco-loaded from scratch. So we give that a run, and now it is returning green. So a lot going on here. Take some time. Try to wrap your brain around it. But the end result, I think, is fairly elegant. When I need a new Achievement, I create a class. I give it a name, a description, an icon.

When I need a new achievement, I create a class. I give it a name, a description, an icon. I store the qualifier directly on that class, and then we simply listen for when experience is earned in the system. I'm doing it right here, but later, we'll move it. In fact, in the next episode, we'll move it. But yeah, somewhere, we register an event listener. We grab a collection of achievements. We filter it down. That's the final thing I want to do, by the way.

We filter it down. That's the final thing I want to do, by the way. We can use some shorthand here, a Collection shorthand here. So here, we filter it down, and for each achievement, we call the qualifier method. What you can also do is this little number here. Filter it down. For each achievement, call a qualifier method on that, and pass the eventUser. Now we have something like this, and we can do it one step further. Map over that, and for each achievement, call modelKey. So once again, we can just do shorthand there.

Map over that, and for each achievement, call model key. So once again, we can just do shorthand there. And if I give it a run, we're still going to get green. Now if you're not familiar with this approach, and your brain just blew up, and you're thinking, how on earth does that work? This is on the Collection object. It's called higherOrderMessaging, so you can take a look at it here. Just search for higherOrderMessages. Anyhow, now that cleans things up considerably. If you really want to get crazy, you can just inline it all, and we still get green.

Anyhow, now that cleans things up considerably. If you really want to get crazy, you can just inline it all, and we still get green. Okay, so in the next episode, we're going to figure out where an EventListener class like this should live in our system.

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