Redirect Dashboard to Games0:00
Well, obviously, if you're going to join a game, we need to see a list of games here in the dashboard. Now, in web.php, we already have a dashboard route defined, and if you just do a general search after installing Breeze for dashboard, you'll see it's used everywhere. Now, I don't want to take time up at the moment refactoring all of those use cases, so what I'm actually going to do, I think, is create a redirect from dashboard over to games. So that's going to be the games index, and let's give this a name of dashboard as well so that it can be picked up across any other controllers or middleware or whatever it might be in the application.
so that it can be picked up across any other controllers or middleware or whatever it might be in the application. Alright, once we've got that out of the way, I can remove this. We'll come down to the root resource here, and we're going to add index as one of the methods. Then we can jump into GameController, and here in index, let's go ahead and accept the $request, and then inside here, we can return Inertia, and I'm looking for that dashboard component. Let's just check that works. So we have a link at the top here.
Load Joinable Games List1:08
Let's just check that works. So we have a link at the top here. You can see at the bottom left, it's going to /dashboard. If I click it, I am actually taken to /games, which is exactly what we'd like, and it is loading the correct Inertia component. Let's go ahead and actually pass some games into the dashboard. So we'll have a key called games. We can say game. We want to make sure that we're only passing games that have no playerTwo currently, and I want people to join older games before they start picking up new games so that you don't
We want to make sure that we're only passing games that have no playerTwo currently, and I want people to join older games before they start picking up new games so that you don't have the situation where you're just sat waiting in the lobby for hours and hours. So we'll use oldest for that, and then let's simplePaginate, perhaps by 100, just so we're not loading thousands and thousands of games for no necessary reason. Okay, so with that defined, we can jump into our dashboard component, and let's go ahead and create that as a prop. So const props = defineProps(). Here is games, and I like to have my script down at the bottom, so I'm going to do that. Nice.
Here is games, and I like to have my script down at the bottom, so I'm going to do that. Nice. And then we could probably loop over that list. For now, let's just make sure it's actually being passed down okay. So I'll say games.data, and then we'll jump to the front end, refresh. Yeah, we have an empty array because no games exist. Let's fix that. We can go to the terminal, php artisan tinker, and we can say game factory. Let's create ourselves 100 games, call create. There we go.
Render and Style Lobby2:34
Let's create ourselves 100 games, call create. There we go. Cancel out of that, and hopefully now, yeah, we have all of this data with the games that are available. Let's spend a couple of minutes making it look a little prettier than a JSON blob. As with all these things, it usually starts with an unordered list followed by a list item. We can loop over using v-for here, and we're looking for game in games.data. Let's add a key. That would be the game.id property.
Let's add a key. That would be the game.id property. And then just inside here, well, we want a link to be able to actually join the game, right? So now, I'll just create a simple link, join game, and we'll see what that actually looks like. Well, that's not a bad start. Let's carry on. So on the unordered list, we can use divide-y, and maybe on the list items themselves, we could do px-2, py-1.5 perhaps.
So on the unordered list, we can use divide-y, and maybe on the list items themselves, we could do px-2, py-1.5 perhaps. How does that look? Yeah, again, not bad. I think just having joinGame doesn't really give us enough information, so how about we also send down player1's name so that we can display that so that you know who you're actually going to be joining. So in order to do that, we need to go back to our GameController, and we have $game where null. Let's load the player1 relationship along with that.
null. Let's load the player1 relationship along with that. All right, let's go back to the dashboard, and I think now we should be able to perhaps add a span here, and we'd say game.player1.name. Let's see if that loads. Yep, here we go. That's looking great, and then perhaps we could spread things with flex, justify-between, items-center to make sure it's all aligned correctly. Great, and then let's make that button look a little more like a button. So we'll add a class, and perhaps we'll say that when you hover, we'll give it a background.
Great, and then let's make that button look a little more like a button. So we'll add a class, and perhaps we'll say that when you hover, we'll give it a background of gray-200, and we could use transition colors to make that a little bit of a nicer look. Okay, gray-200 is way too much. Let's do gray-50. Yeah, that's better. I'm going to give that some padding as well, so maybe p-2. There we go, and then let's make that rounded as well. Nice. Okay, subtle, but enough to let us know that that is definitely a button that we can click.
Nice. Okay, subtle, but enough to let us know that that is definitely a button that we can click. Okay, last thing, I'm just going to add a little bit of spacing at the top here, mt-6, and I think with that, we're kind of done with the lobby list. So now we need to make that link actually do something, and you may think, well, would that not just be a <a href=""> that would point to the games.show route? Well, that kind of would work if we come back and refresh, and we seem to have an issue. Let's take a look. game parameter is required. Of course, we need to pass in the game itself.
Add Join Game Endpoint5:28
Game parameter is required. Of course, we need to pass in the game itself. There we go. Now it's not complaining. So this works in that if I click join game, I am taken to the correct game, but it doesn't actually assign me as playerTwo. So I think what we need to do is create a new endpoint. The join game button will actually post to that endpoint, assign us as playerTwo, and then redirect us back to the game show screen. I think I'm going to go ahead and create a new join endpoint inside our GameController.
then redirect us back to the game show screen. I think I'm going to go ahead and create a new join endpoint inside our GameController. If you want to stay cruddy by design, you'd probably create a new controller at this point, JoinGameController or GameJoinController or something along those lines. But it's literally one method. So for me, I think it's absolutely fine nested here using the word join inside the GameController. Fight me in the comments. Anyhow, public function, I'm going to call it join. Let's accept the $request. We're also going to accept the $game that you're going to join.
Let's accept the request. We're also going to accept the game that you're going to join. And it would be quite basic. We would essentially say that given that game, we want to update it and say that the playerTwoId should become whoever the authenticated user is. And then finally, we'll return to the root, games.show, passing in the game. All right. Let's jump into web.php and wire that up. So Route::post. And that would be forward slash games.
So root post. And that would be forward slash games. We'd want the actual game that we're interested in, forward slash join. And then that would point to GameController class looking for that join endpoint. And we'll give it a name of games.join. I think we can now wire that up inside view. So we just need to replace games.show here with games.join. We'll want to add a method. So method equals post. One thing that I neglected to do in the previous episode is set this to a button class.
So method equals post. One thing that I neglected to do in the previous episode is set this to a button class. So by default, link is going to be an anchor tag. But any time you're making a post request, in other words, you're making a form submission, you should actually indicate that it should appear in HTML as a button, which will help with accessibility. So hopefully now if we come back to our list and I click join game, yeah, I'm still taking two games, in this case, /3. The question is, have I been added as player two? So I think if we go perhaps to our GameController, find the show endpoint, and I'm going to pass
The question is, have I been added as player two? So I think if we go perhaps to our GameController, find the show endpoint, and I'm going to pass our game down. And then let's jump into games/show. And we'll just dump out the page.props. So $page.props. And let's see what we've got. Here we go. game, playerOneIdea is four and playerTwoIdea is one. We have successfully been assigned to this game.
Game, playerOneIdea is four and playerTwoIdea is one. We have successfully been assigned to this game. Awesome. There is one thing I'd like to check. I'm pretty sure this is going to be an issue. So I'm going to create a Game. And if we come back to the dashboard, here is my User. Can I join my own game? Yes, I can. You can see playerOneIdea is set to one and playerTwoIdea is set to one.
Prevent Invalid Joins8:35
Yes, I can. You can see playerOneIdea is set to 1 and playerTwoIdea is set to 1. That's not great. No, you should not be able to join your own game. So let's go ahead and set up a policy method to prevent you from being able to do that. We already have a GamePolicy. So perhaps under create here, I'll add another function called join. It's going to accept, obviously, the User and the Game that we're interested in joining. And it returns a Boolean. So what is it we actually want to check for here?
And it returns a Boolean. So what is it we actually want to check for here? Well, you shouldn't be able to join your own game. So that's an easy one. gamePlayerOneIdea is not equal to userIdea. And whilst we're here, we should also prevent you from being able to join a game that's already in progress. I know that technically that doesn't show on the dashboard, but that wouldn't stop you manually editing the URL. So let's say gamePlayerOneIdea is not equal to userIdea. And gamePlayerTwoIdea, here we are, is equal to null. You've got to have no playerTwo to be able to join a game.
And gamePlayerTwo idea, here we are, is equal to null. You've got to have no playerTwo to be able to join a game. We can now add this to our GameController. So we'll find our little join that we added here. And we can use the Gate facade here. So Gate, make sure we import the right one. We're looking to authorize you to be able to join the given game. Let's test that in the front end. So if I try to access Aubrey's game, yeah, I'm allowed to do that. Aubrey is a different User.
So if I try to access Aubrey's game, yeah, I'm allowed to do that. Aubrey is a different User. But if I come down to, oh, where's mine gone? Has that completely disappeared? Of course, I joined it, didn't I? Let's create another one. So createGame, head back to the dashboard. Here's my game at the bottom. If I try to join, I get a 403. This action is unauthorized.
If I try to join, I get a 403. This action is unauthorized. Of course, really, we shouldn't actually even see our own games in this list. So we could update the controller to make sure that never happens. Let's spend a second just cleaning this up by putting each method on its own line. And then we'll also say where playerOneId is not equal to the request user id. Okay, let's test that out. So we'll come back and refresh. And yeah, now we don't see our own games inside the index. Anyway, I think we need to be careful at this point.
And yeah, now we don't see our own games inside the index. Anyway, I think we need to be careful at this point because this is not like standard CRUD. You know, in like a standard CRUD application that you build with Laravel, things rarely change. It's rare that something is deleted. It's rare that something is updated whilst the user is browsing the index. So you don't have to worry about something having disappeared or no longer being available. But when it comes to, say, a game lobby,
or no longer being available. But when it comes to, say, a game lobby, these games are being picked up and joined all the time. So something that was available to join just a second ago might no longer be valid. And I want to make sure that as the user clicks to join different games, they're not just hitting 403 errors and getting frustrated. Now, we could do this by just polling the page and then refreshing. But seeing as we're going to be using real-time events and WebSockets later down the line, we could make a start with them now and get them integrated.
Add Realtime Lobby Updates11:52
and WebSockets later down the line, we could make a start with them now and get them integrated. We'll need to get broadcasting installed. So let's start with php artisan install:broadcasting. We will install Laravel Reverb. It's by far the simplest way to get WebSockets set up in a Laravel application. And then it's also going to ask us once that installation is complete if we want to compile the node dependencies. Hit yes to that as well. That's going to set up Laravel Echo,
Hit yes to that as well. That's going to set up Laravel Echo, which will make real-time events much easier to work with in JavaScript. Once installation is complete, why don't we go ahead and create our first event, php artisan make:event, and we're going to call the event GameJoined. So the hope is that any time a user clicks to join a game, we fire this event, we broadcast it in real-time through WebSockets, and that will allow anyone in the lobby to filter their events down to just the ones that are actually joinable.
and that will allow anyone in the lobby to filter their events down to just the ones that are actually joinable. So first thing we'll need to do is implement shouldBroadcast on this class. Let's also make sure that we accept the game. I want public here, not private. So public, we can say it's read-only, game, game. Very nice. And we want to broadcast on a private channel, which why don't we call it lobby, seeing as that's essentially what it is.
which why don't we call it lobby, seeing as that's essentially what it is. Very good. Okay, so that's that set up. Let's go to our GameController, and inside our join endpoint, we'll want to make sure we fire this event. So after we've done the update, perhaps, we'll say gameJoined. We want to dispatch, and we'll pass in the game to the constructor that's actually been updated. Let's now turn to the front end.
to the constructor that's actually been updated. Let's now turn to the front end. Down here in our script, we can use Echo, and if you're wondering where Echo has come from, when we ran php artisan install:broadcasting, it updated the bootstrap file with this import statement and also created this echo.js file, which is just set up exactly how you'd expect out of the box. Laravel is good like that. So we want to join a private channel called lobby.
Laravel is good like that. So we want to join a private channel called lobby. That's exactly what we set up in gameJoined here, and once we've joined that private channel, we want to listen for a new event. The event is whatever the name of the event is in php. In this case, that would be gameJoined, and whenever an event comes through from that particular listener, we want to perform the following action. The action would essentially just be to filter down the games from props
we want to perform the following action. The action would essentially just be to filter down the games from props and remove the joined game from that list. So in order to perform that filter, we're going to have to transform this from a prop into a ref. Let's say const games = ref, which by default when the component loads would be props.games.data. Nice, and we could update the HTML here to just be games at that point. Now, I know that this breaks pagination, but I'm not really interested in pagination.
Now, I know that this breaks pagination, but I'm not really interested in pagination. The only reason I'm paginating by 100 is so that we don't crash the server if there happen to be tens of thousands of games. So essentially, it's a non-issue. Games will automatically be loaded in in oldest order, and newer games will just start to drift to the top as time goes on. All right, so with our games now defined, anytime an event comes through, I want to update that ref.
All right, so with our games now defined, anytime an event comes through, I want to update that ref. Games.value equals, and we'll grab the existing value, and then we could use the array_filter method, right? So filter receives a callback that's going to accept the current game, and we want to say, I'm only interested in games that aren't the game in this event. So gameId is not equal to event.game.id. Okay, I'm pretty sure that that would work. The last thing we'll need to do is go into channels.php.
Okay, I'm pretty sure that that would work. The last thing we'll need to do is go into channels.php. This is in your root directory. Again, it's created when you install broadcasting, and this is where we define that lobby channel. So we're going to say Broadcast::channel. We're looking for lobby. It's going to accept a closure that receives the given $user, the user that's trying to access the lobby, and all we have to do is say whether they can access it or not.
the User that's trying to access the lobby, and all we have to do is say whether they can access it or not. Now, any authenticated User is allowed to access the lobby, so I'm just going to return true. All right, last couple of steps before we test this out. We need to make sure that reverb is started. So php artisan reverb:start, simple enough. We also need to make sure that our queue is started because our gameJoined event is going to be placed on the queue ready for broadcasting.
because our game joined event is going to be placed on the queue ready for broadcasting. So let's create a new tab, and we can say php artisan queue:listen, and I actually want to listen, I think. If we listen to the queue, then anytime we change our code, it will automatically restart, meaning we don't have to keep stopping and starting the queue manually. Okay, let's see if this works.
and starting the queue manually. Okay, let's see if this works. I'm going to need to log in as a different User alongside my existing login. In order to facilitate that, why don't we jump into our database seeder, and along with creating this User here, I'll create a second User. I'll call this User Luke, and we can say luke@example.com, and why not?
I'll call this user Luke, and we can say luke@example.com, and why not? While we do that, we'll also create 10 more User instances, and we'll say that each of these users has a Game. That'll give us a good start anytime we refresh our database. So hasGame factory, and I'm pretty sure we didn't actually define that relationship. No, we didn't, so we'll have to add that. public function games() to hasMany relationship.
No, we didn't, so we'll have to add that. public function games to hasMany relationship. return this, hasMany, and we're looking for the Game class, and the column is playerOneId. Those games belong to you. We'll now run php artisan migrate --fresh --seed to make sure we're set up. I should imagine we now need to log back in here. Yep, and you can see we have some games there.
I should imagine we now need to log back in here. Yep, and you can see we have some games there. We can also now log in in the other browser window. So we said luke at example.com with a password of password. Oh, I get a 419 page expired. Let's try that again. There we go. Okay, so we can see both dashboards at the same time here. The question is, do these events work?
Okay, so we can see both dashboards at the same time here. The question is, do these events work? So I'm gonna join the game on the first screen. You see that? In the second browser window, it automatically removed the item from the list. Let's see that in action again. We'll go back to the dashboard, and let's do it the other way around. I'm going to remove Caitlin by joining the game,
and let's do it the other way around. I'm going to remove Caitlin by joining the game, and you can see immediately, Caitlin disappears from the list here. This is perfect. There is one slight issue I can think of here, which is that as items are automatically removed from this list, you might actually eventually end up with no items at all, even though things may still exist in the database.
Auto-Refresh Low Items18:49
with no items at all, even though things may still exist in the database. It's just that the page has not been refreshed. In order to account for that, I think it would be good if we had some sort of refresh mechanism in Inertia when the items in this list get below a certain threshold. After updating the game value, why don't we say something like, look, if the games.value.length is less than five items,
why don't we say something like, look, if the games.value.length is less than five items, we're going to ask Inertia to go and refresh the page. So we'll grab the Inertia router to do that, and we're looking for the reload event. We need to make sure we've actually imported this. So Inertia router.reload, and we can pass the only prop here because the only thing we're actually interested in loading are the games.
because the only thing we're actually interested in loading are the games. We want a fresh instance of games. And then on success, obviously the prop will have been updated, but not our updated ref here. So let's accept closure, and we want to say games.value is equal to props.games.data. We should now hopefully be able to test this out.
to props.games.data. We should now hopefully be able to test this out. So each of these has five items in the list. I'm going to jump into our terminal and I'll php artisan tinker in order to create a bunch of additional games. Game, factory, let's create 10 more games and call create. And then we'll go ahead and hit join game. And you can see on the opposite screen,
And then we'll go ahead and hit join game. And you can see on the opposite screen, obviously the threshold went below five items. So it requested a full refresh from Inertia. And that's why we're now seeing all these additional items in the list. So that gives us a pretty nice lobby integration where we've already integrated reverb, echo, and real-time events. But that's just the start.
Shift Focus to Board20:41
reverb, echo, and real-time events. But that's just the start. Now that we have our lobby and we're able to join two players together in a game of tic-tac-toe, let's turn our attention to the board.
