Deployment seeding problem0:00
Okay, here's the next thing we need to worry about. What happens when we deploy this new feature? Well, at the moment, it would appear that every single user has no achievements. And that's because the logic that will synchronize all of the achievements in this pivot table we have here, that only takes place once you earn new experience. So when I deploy this, everyone will have zero achievements until they earn some kind of experience. They watch a lesson, they leave a reply, something like that. At that point, we will do the test, and we will filter through all of the achievements to check if the user deserves it.
Write seeding command test0:32
At that point, we will do the test, and we will filter through all of the achievements to check if the $user deserves it. So it sounds like we need a seeder, an initial seeder that will build up this table for us. That's what we're tackling in this lesson. All right, I'm going to go to my AchievementsTest. I could have a dedicated test class for the console command we're going to make, and I might do that. But until then, let's just keep it simple right here to be quick. We'll say achievements can be seeded for all users as a console command. OK, so let's write it out like we always do.
We'll say achievements can be seeded for all users as a console command. OK, so let's write it out like we always do. So given we have two users, and those users qualify for one achievement, well, when we seed the achievements through the console command, so when we trigger the console command, then those two users should have one achievement each in the pivot table. All right, that's basically what's going on in my head right now. So very quickly, let's whip up a couple of users. I'll grab that, and let's do two of them. All right, next, those users qualify for one achievement. So we're going to give them experience.
All right, next, those Users qualify for one achievement. So we're going to give them experience. But I don't want to call this method that will fire an event. I guess I could fake the event, but I'm just going to do a manual database query, something like this. We'll get the first User, and then we'll say, get their experience relationship, and I'm just going to call update directly. So I'm going to bypass a method that will do this behind the scenes and then fire an event, because I don't want the event. So I'll set their points to at least 1,000.
event, because I don't want the event. So I'll set their points to at least 1,000. And we've decided at 1,000 points, you do earn your first achievement, at least for this series. I don't care if it's going to be that way in real life. So we're going to do that for the first User and the second User. Now I have two Users with 1,001 points. Okay. When we see the achievements through the console command. So with Laravel, when you're writing a PHP unit test, you can call this method artisan.
When we see the achievements through the console command. So with Laravel, when you're writing a phpunit test, you can call this method artisan. So for example, if I want to call an artisan command like laracasts, and how about syncUserAchievements? That would be the equivalent to doing this. Okay. So when I call that command, well, once again, we should fetch all users in the database. We'll chunk them into groups of maybe 100. That way we don't run out of memory. And for each user, we'll sync up their achievements.
That way we don't run out of memory. And for each User, we'll sync up their achievements. Okay. So at that point, I should be able to say, well, let's grab, and we already have a test up here that we wrote earlier. All right. So the first User should have exactly one achievement now, and the second User should have one achievement. And if we want, if we want our little condition, we'll say, well, before we run the artisan command, let's just be clear that both of them have zero achievements.
Generate artisan command3:22
And if we want, if we want our little condition, we'll say, well, before we run the php artisan command, let's just be clear that both of them have zero achievements. Now you don't have to do this, but I often do. It's good reality check. This is what it was before, and then we do this thing, and this is how it should change. So anyways, if we give this a run, of course it's going to fail. It's trying to find this command, but it doesn't exist. So that's our next step. php artisan make, and there's a Laravel generator called make:command to quickly whip up an artisan command.
php artisan make, and there's a Laravel generator called make command to quickly whip up an artisan command. We'll call it syncUserAchievements. All right. Now if we switch to that new class, here is the command name that I'll paste in, and let's add the namespace. So now immediately if I give this test a run, you'll see that the error has changed. The command exists now, but what we thought would happen didn't happen, and of course not. Not a single thing here.
Implement chunked sync4:14
not. Not a single thing here. So let's build this up. We'll say here, syncAllUserAchievements in the database. All right. So how can we do this? Well, really quick together. Get all users in the DB, chunk them to save on memory, and then for each user, and I basically want to reproduce what we have in that AwardAchievements listener, and this is a little awkward because we're expecting an event class.
want to reproduce what we have in that AwardAchievements listener, and this is a little awkward because we're expecting an event class. So I could new up this class and then pass through an event, but it's such a short bit of code. We could even move it into a method on User. We could do something like user->syncAchievements(). That would be maybe the dry way to do it. I think I'm going to hold off on that just one more time because I'm not sure I'm ever going to call that method again. I'm not that worried.
going to call that method again. I'm not that worried. Why did it take up space on User if I'm only going to call it in one other place? Maybe I'm not going to reach for that just yet though. So I'm just going to copy this and then we'll move it into our console command, something like that. Okay. Let's begin. Get all users in the DB. So I can call user, and there's this method called chunk.
Get all Users in the DB. So I can call User, and there's this method called chunk. So we could say chunk into sets of 100, and that means get 100 from the database, do something with it, free up the memory, then get the next 100 in the database, do something with them, then free up the memory. It's really good when you're dealing with huge data sets, like grabbing all of your users. All right. So for each collection of 100 users, we've got that, filter through it, and then sync the achievements.
So for each collection of 100 Users, we've got that, filter through it, and then sync the achievements. All right. Users for each one will have a single User. And then I'm going to grab this here, and we've got to make this look good. I'm going to comment that, and that will be userAchievementSync, something like that. And we have to provide the User, not the event User. Okay. Does that make sense? For each 100 Users in the database, loop over that collection, and then for each User, we
Does that make sense? For each 100 User in the database, loop over that collection, and then for each user, we will synchronize all of their achievements in the pivot table, and we're just going to do exactly what we did before, where we grab all achievements in our system, we're going to filter them down to only the ones that the current user qualifies for, and then for each one, we're going to return a model key that we will then throw into the pivot table. So let's give our test a run. Hmm. It's still failing. See, I would have expected that to pass, to be honest.
Fix cached relationship6:45
It's still failing. See, I would have expected that to pass, to be honest. Oh, you know what I bet it is? I bet it's because of this. Run it. Green. Yeah. Once again, this is just going to be a caching issue. So let's get a fresh instance. So basically what happened is we cached the achievements relationship.
So let's get a fresh instance. So basically what happened is we cached the achievements relationship. So after we made a change, we were still reading from the eager loaded achievements. So I just want to say, no, I want to start from scratch after that. And I think we'll get green, and we do. Great. So now we know this functionality works. I'll prove it to you. I have a bunch of users in my system right now. So let's say php artisan laravel:sync-user-achievements, and there's a bunch of users here, so I'm
I have a bunch of Users in my system right now. So let's say php artisan larica:sync-user-achievements, and there's a bunch of users here, so I'm going to Control C to get out of that, but we should provide some feedback, which we'll do in a minute. But anyways, if I come back to SQL Pro and I give it a refresh, you can see that we have begun seeding this table. So once this feature is ready and deployed, this is something I would do immediately. All right, great. So now the only remaining step is I want to provide a little bit of feedback. So why don't we say something like we'll do an info line, seeding users, and we kind of
Add progress output7:44
So now the only remaining step is I want to provide a little bit of feedback. So why don't we say something like we'll do an info line, seeding Users, and we kind of want something like this. But of course, we want it dynamic. So if we give it a run, yeah, we kind of want something like that. OK. So let's see. We can accept the index, so that would be for each iteration here, 0, 1, 2. So we could do really quickly, I could say from would be index, but we want it to be zero-based.
So we could do really quickly, I could say from index would be index, but we want it to be zero-based. So I could say index minus 1, and we're basically going to say, in this case, on the first iteration, 0 times 100. And of course, that will come to 0, but on the next iteration, it would be 1 times 100 is 100. The next iteration would be 2 times 100, right? So that's where we're starting. Here's where we're going. So compensate, and then add 100 items for where we are going to.
like that. Give it a run, and there we go. Great. So finally, once we're done here, I could say this info finished. And I think that should do it. OK. So if we want, we could just grab this. If we want a quick refactor, and maybe something like this, reportProgress, and we just need the index there. All right.
the index there. All right. Paste that in. Give that a run. This is the sort of thing I don't usually write a test for, so we'll do it manually. And then here, if you want to just skip that, we could say index times 100, and that should still work. Yep. All right. And that should do it.
All right. And that should do it. So we have our seeder, or our synchronizer, in place for as soon as we deploy this new feature.
