Scaffolding Like Model0:00
Something that's pretty much universal on forums that our forum is currently missing is the ability to like content. So, for example, being able to like a Post, being able to like a Comment. It seems simple, but it's actually a fairly complex relationship. So, in this episode, let's get the models set up. We'll start with the all too familiar by now php artisan make:model Like. And let's call this model Like. We'll want a factory, a migration. Yeah, let's include a policy and a resource controller. We'll have to edit those a little later, but they'll give us a good head start. Let's open up our migration file.
Building Likes Migration0:41
We'll have to edit those a little later, but they'll give us a good head start. Let's open up our migration file. And we'll need to add the link between the User who liked the content and, of course, the like itself. So, that's a belongsTo relationship. So, on the likes table, we'll say table foreignId four. We can import the User fully qualified class name. I want to constrain it. And for clarity, I'll say cascade on delete. So, if you delete this User from the database, all of their likes will also be deleted. We also need a link between the like and whatever it is we're liking.
So, if you delete this User from the database, all of their likes will also be deleted. We also need a link between the like and whatever it is we're liking. Now, you might be tempted to reach for, say, a belongsTo relationship there. Something like table foreignId four Post. And we could say Post::class, constrained(), cascadeOnDelete(). Much the same as we've done for User. But what if you want to like a Comment? How would we tell the difference between the fact that you've liked a Post with an ID of one and the fact that you've liked a Comment with an ID of one? The answer is there is no way to tell that using this basic belongsTo relationship.
Adding Polymorphic Columns1:38
and the fact that you've liked a comment with an ID of 1? The answer is there is no way to tell that using this basic belongsTo relationship. Instead, we need to reach for what Laravel refers to as a polymorphic relationship. Let's remove line 19, and I'm going to replace it with a call to morphs. Instead of creating one column, the morphs method is going to create two columns, a type and an ID. The type will say, look, this is the particular table you should be looking in. Perhaps it's the post table or the comments table. And the ID will say, and this is the row you should find, much as any other belongsTo relationship.
And the ID will say, and this is the row you should find, much as any other belongsTo relationship. So using those two columns, Laravel is able to link a like to many different tables, hence the name polymorphic. We have to define a prefix for this morph column, and convention is that you take the name of the model. So in this case, it would be like, and then you suffix it with the word able. So likable in this case. A comment is likable. A post is likable.
A comment is likable. A post is likable. With our migration created, let's run it. php artisan migrate, and then I'm going to run php artisan model:show, and I'll pass the Like model. And it will return information about the model we've created. I'm interested in this attribute section. So here's the user_id column that links to a User. It's a big int, and it's unsigned. Then we have our polymorphic columns.
It's a bigInteger, and it's unsigned. Then we have our polymorphic columns. So likable_type, well, that's VARCHAR(255), and we have our likable_id, which is a bigInteger, unsigned. So that's what the morphs method is going to create under the hood. One thing I'd like to do here is create a unique relationship on the migration. If you think about it, a User can only like a Post once. They can only like a Comment once. So we should have a unique lookup on the user_id, likable_type, and likable_id columns.
So we should have a unique lookup on the user_id, likable_type, and likable_id columns. Let's add that to our migration. So underneath timestamps, I'll say table, unique, and we're interested in the user_id, likable_type, and likable_id columns. Once I have that in place, we can jump back to artisan, and I'll say php artisan migrate:rollback to undo creating the likes table, and then I'll rerun migrate to redo creating the likes table.
Defining Model Relationships4:00
to undo creating the likes table, and then I'll rerun php artisan migrate to redo creating the likes table. Now let's turn our attention to setting up the relationships on the various model classes. So you already know how to do a belongsTo relationship, say from a Like to a User. We say public function user belongsTo, and it needs to return the belongsTo method, passing in the fully qualified class name of the User. Nice and simple.
passing in the fully qualified class name of the User. Nice and simple. What about that polymorphic call? So again, we called it likable. All we have to do is create another method, public function, and we'll call this the same thing, likable. It's going to return a morphTo relationship, and all you have to do is return this morphTo. That's it. All set up and ready from this side of things.
That's it. All set up and ready from this side of things. What about on the Post and the Comment model? Let's start with the Post model, so public function, and we're interested in the likes that this Post has. You may think that this is a hasMany relationship, but it has to do that polymorphic lookup, so we actually want what's called a morphMany relationship. To set this up, you return this->morphMany, you pass in the fully qualified class name of the model we're linking to,
To set this up, you return this morphMany, you pass in the fully qualified class name of the model we're linking to, which is Like, and then you pass in that special name that we created, that suffix, which is likable, and Laravel will do all of the rest of the work for you. Let's copy this, and let's also paste it on the Comment model. So we'll find, yeah, just underneath the Post here, paste in the likes method. Everything else is absolutely fine.
paste in the likes method. Everything else is absolutely fine. Finally, we have our User model. So let's jump into User, and we'll find the relationships. Here we go. We'll create a new one for likes, and this is a hasMany relationship because, if you remember, we have a very standard belongsTo relationship for the User on the likes table. So return this, hasMany, and we're looking for the Like fully qualified class name.
Creating Like Factory5:57
So return this, hasMany, and we're looking for the like fully qualified class name. So there we go. That's our migration setup. That's our model relationship setup. We created a LikeFactory. Let's go ahead and fill that one out. We'll start with the basic userId belongs to relationship, which can be an instance of a UserFactory. Then we have those two columns, likeable_type and likeable_id.
which can be an instance of a UserFactory. Then we have those two columns, likeable_type and likeable_id. For now, we'll set the likeable_type to the fully qualified class name of the Post, and we'll set the likeable_id to an instance of PostFactory. Now, of course, already you can see an issue with that approach, but we'll change it in just a moment when we can see these particular instances in the database. Let's see if we can get this working. I'll come to the terminal and run php artisan tinker, and why don't we go ahead and create some likes?
I'll come to the terminal and run php artisan tinker, and why don't we go ahead and create some likes? So like factory, perhaps I'll create 10 likes, and we'll call create. There we go. So you can see at the moment it's creating a Post for each of these likes. It sets the post fully qualified class name, and it sets the likeable ID. Why don't we try going the other way around? So we'll say post equals PostFactory, and I can say that it has a certain number of likes. So like factory, maybe it has 10 likes, and I'll call the create method.
and I can say that it has a certain number of likes. So like factory, maybe it has 10 likes, and I'll call the create method. Then we'll go ahead and output those likes. Here we go. So you can see again the likeable type is being set to the Post fully qualified class name, and the likeable ID is being set to 257 in this case, which is the ID of the Post. The other scenario we have is a Comment, so let's try that. In this case, we'll say comment equals comment factory,
The other scenario we have is a Comment, so let's try that. In this case, we'll say $comment equals CommentFactory, and we want to say that, again, it has a certain number of likes. Like factory, let's create 20 this time, and then we'll call the create method. And then I can say $commentLikes to show the likes. Yeah, so you'll see this time instead of the Post fully qualified class name, it uses the Comment fully qualified class name, and it links it to the comment ID, which in this case is 3,121. Perfect.
Enforcing Morph Map8:06
and it links it to the comment ID, which in this case is 3,121. Perfect. So it would seem that our relationships are set up correctly. We're able to create likes. We're able to store them against comments and posts, and that polymorphic lookup works as expected. There is one thing I'd like to change at this stage rather than later down the line, and that is how we're storing the likeable type. By default, Laravel is going to use the fully qualified class name.
and that is how we're storing the likeable type. By default, Laravel is going to use the fully qualified class name for that type column, so App\Models\Comment or App\Models\Post. But what if down the line we rename that model? Or what if down the line we move that model to a new namespace? Well, then when Laravel performs the lookup, it's going to miss any old data in our database unless we do a full data migration, which we want to avoid if we can. So instead, we can tell Laravel to use a custom value for our polymorphic models,
So instead, we can tell Laravel to use a custom value for our polymorphic models, and in doing so, we can avoid that issue altogether. Head to the AppServiceProvider, and I'm going to come down to the boot method here, and I want to call the relation class, specifically the enforceMorphMap static method, which receives an array. Now, the keys of this array are those custom values that we want to use for the polymorphic type column,
Now, the keys of this array are those custom values that we want to use for the polymorphic type column, and the values are the related models. So let's say that the key post links to, well, the Post fully qualified class name, and then we could have another one called comment, which links to the Comment fully qualified class name. The cool thing about the enforceMorphMap method is if we forgot to set up a particular relationship in here for one of our models, Laravel will throw an exception.
is if we forgot to set up a particular relationship in here for one of our models, Laravel will throw an exception. So it stops bad data from ever being able to get into the database in the first place. Now, if we come back to php artisan tinker, and we'll create a Post with 10 likes, and then let's output those likes, you'll see that the likable type is now that string post instead of the Post fully qualified class name, and it doesn't matter if we go ahead
instead of the Post fully qualified class name, and it doesn't matter if we go ahead and rename the Post model. It will still use that word Post, whatever we've defined in our AppServiceProvider for the type column, saving us headaches down the line more than likely. Our woes are not entirely over, however, because if we create a new instance of a LikeFactory directly and then call create,
because if we create a new instance of a LikeFactory directly and then call create, it's still using the Post fully qualified class name, and that's not exactly what we'd expect because if we jump into the LikeFactory, yeah, we defined the fully qualified class name for the likable type column. Let's go ahead and fix that. Rather than hard code a value here, I can actually pass a Closure,
Rather than hard code a value here, I can actually pass a closure, and that closure is going to accept an array of values which are the values that we created in definition. So, for example, there will be a column in values called likable_id, and likable_id is going to be an instance of a PostFactory. So whatever we return inside this closure will be the value that actually ends up getting used for likable_type.
will be the value that actually ends up getting used for likableType. I'm going to extract this to a little helper function. So protected function likableType, and it will receive that array of values. Okay, so inside here we could say this->likableType passing in the values, and then we can store the particular type of that ID column inside a variable. Let's say type equals values['likable_id'].
of that ID column inside a variable. Let's say type equals values likableID. Now, factories have a cool method called modelName. So in this case we could call that method, and it will return the fully qualified class name of the related model. In the case of our example here, if we were to change nothing, it will return the Post fully qualified class name. We'll then need to instantiate this model.
it will return the Post fully qualified class name. We'll then need to instantiate this model. So new model name, in this case that would be a new empty instance of a Post, and there's a method on all models called getMorphClass. So getMorphClass will return to us whatever we've set in the AppServiceProvider. If it's an instance of a Post, it will return the string post. If it's an instance of a Comment,
it will return the string post. If it's an instance of a comment, it will return the string comment, and that is what will return inside likableType. We can actually clean this up further by making it a first class callable, which essentially means we remove the closure, and we pass ... as the parameters to our method, which will instruct php to turn this into a closure. It's syntactic sugar,
which will instruct php to turn this into a closure. It's syntactic sugar, but I think it actually looks really clean. The bottom line is that if we go back now to php artisan tinker, and we create a new instance of a LikeFactory, instead of using the Post fully qualified class name, it does what we'd expect, and it uses that little post string defined in our morph map. Due to the dynamic nature
and it uses that little post string defined in our morph map. Due to the dynamic nature of that likable type method we created, we can actually new up a like factory and link it to any model we want. So in this case, likeFactory::create is going to link not to a Post, but to a Comment, and when we do that, note that it set the likable type to the string comment,
Seeding Likes Uniquely13:12
and when we do that, note that it set the likable type to the string comment, because that's exactly what we set inside enforceMorphMap inside the AppServiceProvider. To round off this episode, let's update our database seeder so that our primary User already has some likes on various Posts. So we already have 200 Posts that we can make use of. Here's our primary User.
So we already have 200 posts that we can make use of. Here's our primary User. We might think of doing something like has, and let's set up a like. So has like factory, maybe 100 likes, and we'll recycle on the posts. Let's see what happens when we run that. php artisan migrate:refresh,
php artisan migrate --fresh, and I'll use the --seed flag. And we got an exception. Essentially the exception is that we have duplicate entries for a given Post. Remember we set up that unique constraint, a User can only like a certain model once. So in this case, they can only like post number 16 once,
So in this case, they can only like Post number 16 once, but because of how recycle works, we're trying to do that more than once. Essentially recycle is going to pull a post from this collection at random. Each time it needs to create a like, which means if it pulls the same post twice, which is highly likely, it will cause an exception.
which is highly likely, it will cause an exception. In other words, if we want to do this, we need to do a little bit of manual work rather than relying on recycle. Instead of saying factory 100, I'm just going to pass factory as is, and instead of saying recycle, I'm going to use the foreach sequence method.
and instead of saying recycle, I'm going to use the foreach sequence method. Essentially foreach sequence allows you to pass any number of arrays, and those arrays are basically state objects. So for example, inside the first array, we might say likeableId is equal to posts.first(), and then in the second array, we could say likeableId is equal to posts.last(),
and then in the second array, we could say likeable_id is equal to posts.last(), and essentially it's going to create two likes where the first like is for the first post, and the second like for my user is for the last post. So knowing that that's the case, we could do a little bit of collection magic. We could say give me the posts, and let's ask for 100 at random. So we'll use the random method.
and let's ask for 100 at random. So we'll use the random method and say give me 100 random Posts, and I want to map those posts, so it's going to give me an instance of the Post to work with, and I'll return an array which is going to be our state object where I set the likeableId to an instance of that post,
where I set the likeable ID to an instance of that post, and everything else should work. Let's find out. We'll go to our terminal. I'll type php artisan migrate:fresh --seed, and hopefully we won't get an exception this time. Oh, we did get an exception, but the exception is different. Argument two must be of type array,
but the exception is different. Argument two must be of type array, Illuminate\Support\Collection given. Essentially, for each sequence, expect a variadic number of arguments denoted by these three dots here, and I've passed a Collection instance instead. Now, you can turn a Collection instance into a variadic sequence of arguments by prefixing it with ..., like so.
into a veradic sequence of arguments by prefixing it with ..., like so. If we rerun this, I'm pretty sure it should now pass with flying colors, and we have another exception. Oh, I'm not doing very well here, am I? Call to undefined method, App\Models\Post, model name. I know why this is happening. If we jump into our like factory,
I know why this is happening. If we jump into our LikeFactory, remember we created this likeable type. We are assuming that likeableId is an instance of a factory, but here we've passed an instance of a model instead. So I guess we could say, well, is type an instance of a factory? If it is, then we'll grab type model name, but otherwise, let's just assume.
If it is, then we'll grab type model name, but otherwise, let's just assume type is already an instance of a model, in which case we can just pass the fully qualified class name directly. Okay, big breath. Let's see if it works. php artisan migrate:fresh --seed, and there we go, it passed. Thank goodness for that.
and there we go, it passed. Thank goodness for that. So hopefully, if we go to php artisan tinker, let's grab my User, which I know has an ID of 11, and then let's say user likes, and let's grab a count of those likes, there we go. We have 100 likes against my User any time that we see the database.
We have 100 likes against my User any time that we see the database. So that's a great start. How about in the next episode, we look at showing those likes on the front end.
