Why add favorites0:00
One of my favorite types of features to add to a web app is to enable something that users are already doing, but it's a pain in the butt when they're doing it. And with this feature, it makes it super easy and you kind of like imagine a collective sigh of relief as they start using it. So let's take a look at one of those for Symposium. Before this video, I just copied the talks list page and I created a conferences list page and a conferences show page, just really rudimentary stuff. And what we're going to imagine doing is coming to this site when there's 50 or 100 or 300 conferences here, which is usually the case. Imagine figuring out which of these you're going to want to speak at and you're command
or 300 conferences here, which is usually the case. Imagine figuring out which of these you're going to want to speak at and you're command clicking them and you've got 13 tabs open and then you have to come back to it later because you only had five minutes over your lunch break to work on this. It can be a big pain in the butt to identify which you're really interested in, even if you don't have time to apply to them right now. Favoriting already exists in other sections of the industry like Gmail and everything. So instead of having to make up a brand new user interface or user experience, let's just lean on that one and build favorites into talks. So in order to build favoriting here, we're going to need two things.
Modeling the relationship1:02
lean on that one and build favorites into talks. So in order to build favoriting here, we're going to need two things. We're going to need to build a user interface to allow you to favorite or unfavorite a given conference. And then we're going to have to build the database relationships to back that up. So favoriting is a relationship between two things. You've got the User who's doing the favoriting and then you've got the Conference. And anytime you're trying to build a relationship like this, you want to start thinking through, is this one-to-one, one-to-many, many-to-many, many-to-many polymorphic, and it can get kind of overwhelming.
is this one-to-one, one-to-many, many-to-many, many-to-many polymorphic, and it can get kind of overwhelming. So I like to think through just asking the question, can a User favorite, you know, have many Conferences? And the answer is absolutely yes. So we at least have one here and we at least have many here. Can a Conference be favorited by multiple Users? Yes. So we've got many-to-many. Now it would be many-to-many polymorphic if you can have more than one type of thing down
So we've got many-to-many. Now it would be many-to-many polymorphic if you can have more than one type of thing down here. That's what polymorphic means, is you can do this thing to multiple types of thing. So if a User can favorite Conferences and can favorite Talks and can favorite Comments, that's polymorphic. This favorite has to be able to determine whether is it conference number one you're favoriting or talk number one you're favoriting. But if we're only doing User favoriting Conferences, we can keep it to many-to-many. Often people want to come up with a more future-facing solution and automatically are
But if we're only doing User favoriting Conferences, we can keep it to many-to-many. Often people want to come up with a more future-facing solution and automatically are going to leap into polymorphism here. And if it's favoriting, maybe that's the case. But I do want to say that polymorphism for a beginner can make things a lot more complicated. So for that reason, we're just going to stick to many-to-many right now. So as you can see here, we probably are going to need a pivot table for that. Now a Conference can have a user_id on it if it's only got one User. A User can have a conference_id on it if it's only got one Conference. But if you've got multiple on both sides, that's when you need a pivot table.
A user can have a conference ID on it if it's only got one conference. But if you've got multiple on both sides, that's when you need a pivot table. So you can go take a look at the Laravel docs, and you can look for many-to-many relationships. It's actually going to walk you through the basics of the table structure you need. You need three tables, users, which we have, roles, which is in our case, conferences, and then the pivot table in the middle. RoleUser is the alphabetical order of the related model names. So that means RoleUser right here would be ConferenceUser, and it's going to contain user_id and role_id. So that means it's going to have user_id and conference_id.
Creating favorites pivot table3:16
user ID and role IDs. So that means it's going to have user ID and conference ID. This table is an intermediate or pivot table linking the users and the roles. And then the model structure is that a User has a roles, or in our context, favorited conferences that belongs to many. And then on the other side, you can do the inverse. So let's build this out. First we're going to make our pivot table. Now we could name it role_user or in our case, conference_user, but I'm going to tell you right now, it's actually possible to pass just a single variable in and then choose
Now we could name it roleUser or in our case, conferenceUser, but I'm going to tell you right now, it's actually possible to pass just a single variable in and then choose to rename this. And often this relationship doesn't have a concept, a name, right? What is the name for the relationship between a User and its Role? roleUser. But in our context, there actually is a name for the idea of the relationship between a User and a Conference. It's not just the relationship, it's that a User favorited it. So we can actually name it the favorites table and then just pass in one little thing.
It's not just the relationship, it's that a User favorited it. So we can actually name it the favorites table and then just pass in one little thing. And instead of conference_user being its assumption, it will actually just use our given name. So create favorites table. And now that we've created the favorites table, we can go over to that migration. And remember, we need to add two things. We can keep the timestamps if we want, which will actually tell us when they favorited it. But for simplicity, we're not going to worry about it.
it. But for simplicity, we're not going to worry about it. So we're just going to say two things. Remember, we need an ID for the User and we need an ID for the Conference. And there's a couple of different ways to do that. The simplest one in my mind is say foreignId for user and then table foreignId for conference. And that's it. We now have the migration for that pivot table.
Defining Eloquent relationships5:07
And that's it. We now have the migration for that pivot table. And now we need to go to our User model and we need to give it a new relationship. And remember, the relationship is named after the thing on the end. So you could just say conferences. But again, for us, it's not that User has conferences, it's they have favoritedConferences. So returns belongsToMany, as they told us in the docs, we just say return this->belongsToMany. And you're going to say what it's belonging to. So this is going to be a Conference.
And you're going to say what it's belonging to. So this is going to be a conference. And the optional second parameter is the table. And usually, you don't need that. But if you've renamed the table, you do. If we want, we can also do in the conference level. This would be users who favorited it or something like that. belongsToMany User through the favorites table. Great. So we could write some tests here, making sure that those things work.
Building the UI toggle6:06
Great. So we could write some tests here, making sure that those things work. But in the end, those are just Eloquent relationships. And I don't think it's usually necessary to write tests to assure ourselves that Eloquent is working correctly. We could write a test to make sure that we wrote the Eloquent correctly. But we're going to know that very quickly. So what I like to do instead is actually build the user interface here. So we want to go over to our conference index page. And we want to put a little button next to each one that allows us to favorite.
So we want to go over to our conference index page. And we want to put a little button next to each one that allows us to favorite. So we go to conferences and next.play.php. And next to every single conference title, we're going to have a little thing. And we want to know is this particular conference favorited or not. And if so, we're going to give us a specific user interface item. And if not, we won't show it. So we're going to say something like, you know, we've got to figure this out. Conference is favorited. So this can be maybe a link, say, you know, star, and this can be a link, and this can
Conference is favorited. So this can be maybe a link, say, you know, star, and this can be a link, and this can be like an O or something, right? So right now, they're all going to be a star because we didn't actually give them a true user interface item. So what does it look like for us to figure out whether this conference is favorited? Well, one of the things you might be tempted to do is to make a method on the conference that checks whether it's favorited. And that does make sense, and that's probably something we should eventually build. However, when we're on a list like this, pulling that information in could be pretty expensive.
And that does make sense, and that's probably something we should eventually build. However, when we're on a list like this, pulling that information in could be pretty expensive. Whereas what we could do is the User itself can have a knowledge of which conferences it's favorited, and then we can pull that off of the User, and that's a single query. So remember, we go to User and we already added something called favoritedConferences. So if you make this call once, you're going to have it for the rest of that query. So you can say, basically, if User favoritedConferences, so we're basically saying if the User's list of favoritedConferences, we're not putting the parentheses here. And when you do that, that means it's actually going to take this relationship and it's going to look it up, and it's going to save it as a Laravel collection or an Eloquent collection.
And when you do that, that means it's actually going to take this relationship and it's going to look it up, and it's going to save it as a Laravel collection or an Eloquent collection. And it's not going to look it up every single time this runs. It's going to have it cached. And then you can say pluck ID contains conferenceID. Pulling out just the ID field, and we're making sure that this conferenceID is in it. And if so, it's going to give us the sign that suggests that it is currently favorited. Should probably run our migrations and then log in again. All right, and now we go to the conferences index page, and it correctly shows that none of these are favorited because they're not.
All right, and now we go to the conferences index page, and it correctly shows that none of these are favorited because they're not. So let's go over to TablePlus and our favorites. And we are currently logged into our GitHub user, which is user2. And there should be a conference number one. Yep, perspeciatus. So let's make a new one just to make sure that actually works correctly. userID, conferenceID one. And there we go. Shows as favorited.
Wiring routes and controller9:05
And there we go. Shows as favorited. So we've got that working correctly. You could write a test around that if you want. But what I'd rather we do is figure out what does it look like to turn this favoriting on or off. So we can come over here and we can say we want a link of some sort to turn it on and a link of some sort to turn it off. And we can build that directly in a route, or we can build it in JavaScript. Either way, the code in the controller is going to have to be the same. So let's get started directly in a route. So we're going to say something like conferences.favorite.
So let's get started directly in a route. So we're going to say something like conferences.favorite. Nope, that's going to be unfavorite. And then this next one is going to be conferences.favorite. And both of them are going to pass in the conference as a parameter. So if we go to the routes/web.php, we're going to create two endpoints there. One of them /favorite. One of them /unfavorite. These are not cruddy by design. If you are a Adam Wathen fan, you're going to see that this is not the way to do it.
These are not cruddy by design. If you are a Adam Wathen fan, you're going to see that this is not the way to do it. And we'll make it a little bit cleaner when we switch to JavaScript. So this is the favorite method. This is the unfavorite method. So let's see. Does that break anything? No. All right, so when we click over here, it's going to say that method does not exist because we didn't create it.
All right, so when we click over here, it's going to say that method does not exist because we didn't create it. So a ConferenceController can temporarily have those just so we can show how it works. All right, so over here, we can say auth $user favorited conferences attach conference_id. And we can do the same thing here. detach. Oh, and I guess we should redirect. We should do return back. All right, so if we click it, it changes. We click it again.
All right, so if we click it, it changes. We click it again. Changes back. Not bad. Not really the way we like to structure our controllers and everything in Laravel, but if you needed something to work, this works. But let's clean it up just a little bit. Let's imagine we were working with the JavaScript framework of your choice, and we wanted to make this a little bit less ugly. What I like the idea, and there's a lot of different ways to do this,
and we wanted to make this a little bit less ugly. What I like the idea, and there's a lot of different ways to do this, but what I like the idea is conferences/conference/favorite, and you can either post to it or you can delete to it. And instead of this being in the ConferenceController, we're going to make a new controller called the ConferenceFavoriteController. And we're just going to have a store and a destroy. So same functions happening, but we're expecting these to happen in JavaScript now. So that back actually won't matter. That's sort of like an ugly internal API,
So that back actually won't matter. That's sort of like an ugly internal API, so you could return a status back if your JavaScript framework needed it. That'd probably be a good idea, but we're not going to spend too much time on that right now. Whoops, I just noticed. We should be posting and deleting to this favorite route, and also this should be our new ConferenceFavoriteController. Not the ConferenceController. We should probably make sure that the ConferenceController no longer has those methods, or somebody in the future might get confused.
We should probably make sure that the ConferenceController no longer has those methods, or somebody in the future might get confused. Great, so now we're posting or deleting to the favorite endpoint, which is pointing to the store and destroy methods. And finally, we can build out the JavaScript, but because this is not a JavaScript course, I don't know if you're using Vue or React or Alpine or whatever else. I've got a little JavaScript snippet for you right here. And all this does right now is pulls up two methods. It's favoriteConference and unfavoriteConference.
And all this does right now is pulls up two methods. It's favoriteConference and unfavoriteConference. And the first one posts to conferences/1/favorite. It's not even allowing you to define which. And it passes in a content-type header, but most importantly, it passes in the CSRF token, which proves to Laravel, hey, this is a valid JavaScript request. And unfavoriteConference just does the opposite. It sends a delete request to conferences/1/favorite. So this is just proving out that you can build this out in your JavaScript however you want.
It sends a delete request to conferences/1/favorite. So this is just proving out that you can build this out in your JavaScript however you want. This is not actually how you would build it. So let's just make some fun links up top. So we'll call favoriteConference here. Then you'll return false, which keeps it from navigating away. Old school. We can say favoriteNumberOne. And then we can just modify that unfavoriteConference, return false, unfavoriteNumberOne.
And then we can just modify that unfavorite conference, return false, unfavorite numberOne. And again, you'd be building this in React or Vue or whatever you're comfortable with, but we just want to show that we can do this. So if numberOne is currently favorited, so let's unfavorite it. And then hit refresh and look at that. We can favorite it, refresh and look at that. And if you want, you can look at your network tab and watch those unfavorites, favorites, unfavorites, favorites happening. So if you were building this in your normal JavaScript framework,
and watch those unfavorites, favorites, unfavorites, favorites happening. So if you were building this in your normal JavaScript framework, you would attach one of those handlers to every single one of these, passing in the specific correct URL. You might want to use Ziggy, which is an easy way to use your Laravel named routes in JavaScript. So you can do something like route('conferences.favorite'), passing the ID just like you do in Laravel, but you're using it in JavaScript and you can generate those URLs. And then every single time that fetch comes back,
but you're using it in JavaScript and you can generate those URLs. And then every single time that fetch comes back, you know, it's an asynchronous call, so you'd say asynchronously when it comes back, then change the user interface to show the favorited star instead of the unfavorited star or vice versa, if that's what you were doing. The last thing we might want to do is to make a test. Make a test, and this will be FavoriteConferenceTest. And here, what we want to do is make sure it favorites a conference. So you want to do a POST to that particular endpoint. You, of course, want to create a conference and log in first and all that kind of stuff.
So you want to do a post to that particular endpoint. You, of course, want to create a Conference and log in first and all that kind of stuff. favorite. We'll have to make a Conference. We'll have to make a Conference. And then we'll have to make a User. All right, so we've created a Conference, we've created a User, and we're going to post to that favorite endpoint. And we want to make sure that there's actually favorites. So we're saying, create a Conference, log in as a User,
And we want to make sure that there's actually favorites. So we're saying, create a Conference, log in as a User, and post to that favorite endpoint, and make sure that this User has one favorited Conference. Oops, and of course, we want to do our only here. Okay, so that one worked. And let's make sure it actually includes that one. So this assertTrue, $user favorited conferences, pluck id, contains conferenceId.
pluck ID, contains conference ID. Remember, that's the test that we did in line. And then we can also do the same test for it unfavorites a conference. And so there's a couple different ways to create it, but let's just kind of create it ourselves. We'll have to create the User first this time. Because we have to create the favorite ourselves. So we have to write the code ourselves. It says user favorited conferences,
So we have to write the code ourselves. It says User favorited conferences, attach Conference. So now we know that should be positive. And now that we run this on the unfavorite route, there should be zero. And we don't even have to check to make sure it's not in there, because of course, it's not in a zero list. If you wanted to double check. Whoops, I forgot to change this from a Post to a delete.
If you wanted to double check. Whoops, I forgot to change this from a post to a delete. There we go. And the thing is, that can be tricky sometimes, because they have different names. This is named favorite, and this is named unfavorite. But those actually point to the same URL. The route short names don't actually define that it's post or get. They're literally just links to the same URL. So you could even type favorite in here,
They're literally just links to the same URL. So you could even type favorite in here, and you're sending a delete to that same URL, so it's still working. And there's lots of other tests that you can write, but this gives us the basics. If I send a post in the way I want, then it's going to work. And if I send a post in or a delete in the way I want, it's going to work the way I want it. So there we go.
it's going to work the way I want it. So there we go. Our users can favorite conferences. They can unfavorite conferences. We've got that method that is user->favoritedConferences. So if you want to pull a list on their profile somewhere showing all their favorited conferences, you can. You can just iterate over that list. However you want to handle it, you have access to that information to make lives easier for your users, which is the whole goal here.
However you want to handle it, you have access to that information to make lives easier for your users, which is the whole goal here. So see you next time.
