Add Vote Button UI0:00
Welcome back. Okay, so let's dive back in. We're working on the voting feature, but if you voted on it, we just see plus one. You can't click it to toggle it, it was really just to prove that it works. And further, if you're not logged in, I can't even see whether these links have been voted on, which isn't great. So let's tackle that, and then we'll do a bit of refactoring. So here's the list view, where we add the button. So each of these list items, just as a refresher, represents one of these. So anyways, yeah, why don't we do this? We know it's going to be a form ultimately, and we're going to post somewhere, but we'll talk about that in a bit. And generally, anytime you have a form in Laravel, you want to make sure you add a csrf field. Okay, so now we're going to add a button here. We'll give it a class of btn by default. And then I'm just going to echo out the linkVotesCount. So let's try this one out and see.
Highlight User Voted0:45
Okay, so now we're going to add a button here. We'll give it a class of btn by default. And then I'm just going to echo out the linkVotes count. So let's try this one out and see where we are. I'm going to give it a refresh. And yeah, we need to add some CSS, but nonetheless, we can see the total number of votes that each link has received. Okay, but right now, I can't tell if I, John Doe, have voted on any of these. So maybe we can use just simple classes here. Classes here, something like this. If you're signed in, and the authenticated user voted for this link, then maybe we'll add like a success button class. Otherwise, we'll stick with a default button. All right, makes sense. So if you voted on this, then we want special styling. Otherwise, the default styling is fine. So now we can see, yeah, this is maybe a little bit more clear. John Doe has voted on these two, but not this one. Okay, so let's move on. Where,
Design Voting Routes1:40
Otherwise, the default styling is fine. So now we can see, yeah, this is maybe a little bit more clear. JohnDoe has voted on these two, but not this one. Okay, so let's move on. Where, when you click on this button, is it going to post to? And this is kind of the hard part. You know, sometimes people are always focused on new architectures or new tools. But from my experiences, when you're building apps, so much of the time is just spent on this stuff. Spent on how are you going to prepare your URIs? And what method on your controller will this go? So for example, you could do this in a number of ways. You could say communityLinks, and then maybe a specific link, and then votes. Yeah, you could do like a sub-resource. But then is the controller going to be CommunityLinksController? And if so, what is the method? Is it going to be votes? Or we know it's going to toggle the vote. So is it toggleVote?
is the controller going to be CommunityLinksController? And if so, what is the method? Is it going to be votes? Or we know it's going to toggleVote. So is it toggleVote? You know what, from my experiences, I find that the more you get away from the restful methods on your controller, like index, create, show, store, the more you get away from that, the more it might be the case that you're going down the wrong path. So it's not always the rule. But in general, I like to instead think the fact that you're adding this differently named method, that's not one of the core restful method names, it's probably an indication that votes should be promoted to its own top level resource like this. Then you have a dedicated VotesController, and you've established a new, how would you put it, a new thing in your system that has value. So in our case, we're building like a community links votes application. So votes sort of gets elevated to a
Create Votes Controller3:11
established a new, how would you put it, a new thing in your system that has value. So in our case, we're building like a community links votes application. So votes sort of gets elevated to a first class citizen. So now, maybe if you post to votes, yeah, maybe something like that. Or we could do votes link, if you post to that, we're going to find the link, create a new record in the pivot table and associate it with the authenticated user. And I think that's good. Okay, so let's set this up votes link. There you go. And then I will create it. So php artisan make:controller VotesController. And now if we go back, VotesController, we add a store method. And now here's a cool thing. We've already learned about implicit route binding or route model binding. So that means it's going to find the link with this ID automatically. So I can do this. Kind of cool. And let me reformat. So at this point, yes, we could reference the relationship.
Many-to-Many Votes Relation4:08
binding. So that means it's going to find the link with this ID automatically. So I can do this. Kind of cool. And let me reformat. So at this point, yes, we could reference the relationship like this links votes create. But one thing we have to remember is that we need to toggle it. So we could say Auth user votes. And in fact, right now, we have a hasMany relationship. But really, if you think about it, we have a pivot table. And that represents a many to many relationship. So a User can vote on many links, and one link can be associated with many users. So if we wanted, we could do this, we could update our relationship type, maybe like this votes. So this would be User votes. So that's going to give me all of the links that the user has voted on. Right? So that relationship is a belongsToMany CommunityLink class. And now, the convention for the pivot table, like you may know,
that the User has voted on. Right? So that relationship is a belongsToMany CommunityLink class. And now, the convention for the pivot table, like you may know, is alphabetical order, and then the singular name of each table. So I think it would be what do we have here, community, community_link, user, that might be what it's called. But in our case, we used community_links_votes as our pivot table name. So we have to be explicit about that. Okay, so now, we have a many to many relationship with the timestamps included. Which means, if we want to keep vote for what you can do that, but just note that you might have to do an unvote method. And this is where you may find that very quickly, your User model can get kind of bloated. And yes, you can extract some of these methods to like a concern or trait. That's good in a lot of cases. In other cases, maybe just get
very quickly, your User model can get kind of bloated. And yes, you can extract some of these methods to like a concern or trait. That's good in a lot of cases. In other cases, maybe just get these methods off of the User model, and then reference a different model. So it's always kind of a give and take. So you need to figure out what's most appropriate, and how important votes are to the User, given the project, there's no wrong or right, it just depends on what's most important, and how much you want to rely on this API. So vote for like, how much do you really want that to be the API? And if so, make it work. Otherwise, see if you can do something else. Okay, so in this case, if you did want to keep the method, your new relationship would be let's see this votes, and I want to attach a new record to the pivot table. And we'll give it the link. You may not know this, often you'll do five or something like that. And that means create a new
this votes, and I want to attach a new record to the pivot table. And we'll give it the link. You may not know this, often you'll do five or something like that. And that means create a new record in the votes pivot table, named community_links_votes, where the user_id is this current user. And then the community_link_id is five, right? But if you give it a model, well, behind the scenes, it's just going to detect Oh, you gave us an Eloquent model. So let's just call the getKey method to figure out what the ID should be. And in fact, if we if we hunt that down, it's right here. Kind of cool. Always good to look in the source. Okay, so next, if you wanted to unvote, yeah, you would basically have to do the the opposite here, but use detach. Or actually, here's a little tip you may not know about, dependent upon how you've set up your pivot table. If you didn't set up a primary key
but use detach. Or actually, here's a little tip you may not know about, dependent upon how you've set up your pivot table. If you didn't set up a primary key that is composed of, in this case, both user_id and community_link. So if those two together represent the primary key, well, any attempt to create a new row that has those same two columns is going to throw an exception. But if you didn't do that, you can end up in a situation where this attach method, well, you can end up where the User is associated with the same record multiple times in the table. And sometimes you get this where like the User clicks on the button really fast on the form. And that ends up sending multiple requests for this, where you attach this link to the User over and over, and then everything gets out of sync. So like I said, you can either fix that by changing your primary key to be dual, to be combined. Or you could do
this link to the User over and over, and then everything gets out of sync. So like I said, you can either fix that by changing your primary key to be dual, to be combined. Or you could do sync, and then you provide the link. So you'd have to be explicit here. And then set the detach value to false. So when you call sync, what it's going to do is it basically says, okay, for that table, the only record I want to have associated with the User is this one. So if there's any other link ID in that table associated with the User, delete those, and I'm just going to add this one. We're going to sync them up. But if you set to false, what that's going to do is it's not going to delete anything. So yes, it will add the record. But if it already exists, it's not going to add it again. And if any other links exist, well, because we set this to false, it's not going to delete anything. So that's a safe way to ensure that you never attach the same record twice. Like I said,
Implement Vote Toggle9:03
again. And if any other links exist, well, because we set this to false, it's not going to delete anything. So that's a safe way to ensure that you never attach the same record twice. Like I said, if you don't have any other protection in place. Good little trick to know. Now though, here's another thing I want to show you. If I go to my VotesController, yeah, if we come back, we could say auth user, vote for this link, and it's going to work. And then we don't have an inject request, so I can just redirect back. So let's try that. Back in Chrome, click on this, it adds the new record, and it redirects back. However, if I click on it again, it'll redirect back. But notice that because we use sync, yeah, you're not going to end up with multiple records. So when I, for example, click this, what I really want to do is undo my vote. But we don't have that set up. And really, what I would love for pivot tables is if I could do something like auth user, vote, toggle for the.
click this, what I really want to do is undo my vote. But we don't have that set up. And really, what I would love for pivot tables is if I could do something like auth user, vote, toggle for the link. And I might even submit a pull request for this, if Taylor likes the idea of it. So basically, what a toggle method would do, it's a companion to attach and detach. What toggle would do is say, well, if this record exists on the pivot table, the user wants to delete it. If it does not exist, they want to add it. And you'll find yourself doing this exact logic all the time, whether it's like with favoriting articles, or videos, or content, or completing, I even have that concept at Lerikast, or voting like this, where you often want to remove a vote or add a vote. And a toggle method would be a cool way to do that. But you can't do that right now. So you would have to do something that equates to this. So like, let's, let's catch this. You could say, if user voted
method would be a cool way to do that. But you can't do that right now. So you would have to do something that equates to this. So like, let's, let's catch this. You could say, if $user voted for this link, yeah, you get where I'm going, then we want to say $user unvote for the link. Otherwise, they haven't voted for it. So they do want to vote for it. So yeah, you end up with something like this. That would work. Another option is to store the method. So like toggleVoteFor would be equal to, and we're basically going to use the ternary here. unvoteFor voteFor, and then you would say $user toggleVoteFor the link, and then get rid of that. Yeah, I mean, you can clean it up however you want, but it's still still kind of gross. Another option, of course, is to defer to a toggleVoteFor method. But once again, you would add yet another vote related method. So you can see how quickly the stuff can get kind of bloated.
Another option, of course, is to defer to a toggleVote method. But once again, you would add yet another vote related method. So you can see how quickly the stuff can get kind of bloated. Now, what you might do as an alternative is keep it really, really simple. Can we just reference the CommunityLinkVote model directly, and then do something like this? Give me the first one from the database that matches these columns, where, you know, basically like this. And the community_link_id is equal to the link that was passed in. Yeah, so you're going to give me the Yeah, so you're going to give me the first vote in the database that matches these, or if you didn't find one, just new up a new instance of that model. Then, and we'll clean this up in two steps, but then you could say if the vote exists in the database, that means somewhere in the past they had already voted on this record, and they're
Then, and we'll clean this up in two steps, but then you could say if the vote exists in the database, that means somewhere in the past they had already voted on this record, and they're clicking it again, which means they want to unvote for it. So at that point, you could just delete the record from the table. Otherwise, persist it. Or you could even create a toggle method directly on that model, and then defer there for this sort of logic. So you would do something like this. toggle, paste that in, and then update these references to this. And then maybe even this. Return that result, and then get rid of the else statement. Yeah, does that make sense? Let's go through it. So with this approach, we find the first communityLinkVote. So find the first record in the pivot table, or create a brand new instance. And then I'm going to call toggle. Now the toggle method says, well, if it exists in the database, delete it, because we're toggling.
first record in the pivot table, or create a brand new instance. And then I'm going to call toggle. Now the toggle method says, well, if it exists in the database, delete it, because we're toggling it. Otherwise, persist it. And we can return that as well. And yeah, that would be another way to go about it. And what's nice about this is, yeah, maybe it's not as expressive as userVoteFor, there's no denying that. But yeah, it's all a balancing act. So how much do you want to bloat your User model to add some of this functionality? If we're very important key components in your system, I think it's good. But if you're not careful, yeah, the User is the first class to become a God object. And anyone who's maintained a project for a year or two years, you absolutely have experienced this. So for a simple voting function, we already have four different methods. Now imagine that being multiplied by 10 for all the 10 different
you absolutely have experienced this. So for a simple voting function, we already have four different methods. Now imagine that being multiplied by 10 for all the 10 different things in your system that a user can do. Anyway, something to think about. So if we wanted to go this route, we could get rid of these two methods entirely, and just stick with these two. And in fact, you could even remove these as well. So like if you wanted this to be stored on the Link model, then you could say link voted for by and then you would accept the User, you know, you kind of defer the logic over there if you want, or keep it here. In this case, it's fine. The app is dedicated to community links and votes. So it makes sense to have a couple of vote related methods on the User, in my opinion. Okay, so I think we should try this out. Right now we have voted on all three of these. So why don't we toggle it and now it gets removed. And also the class
methods on the User, in my opinion. Okay, so I think we should try this out. Right now we have voted on all three of these. So why don't we toggle it and now it gets removed. And also the class gets removed as well. Let's remove all of them. There we go. This one right here. Yeah, that was probably voted on by somebody else. So let's vote for each one again. And we're not using Ajax yet, but we'll do that. Nonetheless, everything's working exactly the way we would expect. And notice that it's really very, very simple. And in fact, if you even decided that you didn't like this here, you could still put it on the User model if you thought it improved your API. So you could do toggleVote for the community link. You know, you can structure this however you want. And once again, just reference that model there. You know what, actually a quick note on that. Sometimes relationships with Eloquent can get kind of tricky, especially when you get into
you want. And once again, just reference that model there. You know what, actually a quick note on that. Sometimes relationships with Eloquent can get kind of tricky, especially when you get into polymorphic relationships and many to many polymorphic, it gets incredibly complicated really fast. When sometimes it's easier just to do stuff like this or even reference your DB facade. So just be careful of that. Ask yourself, is there a way I can write this where in six months I can come back and totally know what's going to happen versus having to decipher what morphMany versus morphedByMany versus for, you know, it gets very, very tricky, like I said. So when you can always keep this stuff pretty simple. But yeah, if you just wanted to move this logic out of your controller, because it could be referenced in multiple places or multiple controllers, or from a command or a job. Yeah, this would be an option as well. So now this would become
Protect Votes with Auth16:06
controller, because it could be referenced in multiple places or multiple controllers, or from a command or a Job. Yeah, this would be an option as well. So now this would become offUserToggleVote for link. And this ends up being pretty darn clean, I think. So refresh, it's all going to work just the way it did before. Let's add one back on and you get the idea. Now, the very, very last thing for this episode, right now, if you're not signed in, well, you can still see the buttons, which might be okay. But if I click on it, it's going to fail, right? Because our VotesController right here, well, we didn't have any protection in place. And we immediately tried to reference the authenticatedUser. So instead, why don't we, within our constructor, declare that any action here is bound to the auth middleware. So you have to be signed in in order to hit these. Otherwise, we're going to link you to the login page. So like, for example,
