{{$post->title}}
{{$post->body}}
Back in 2014, I’d posted an article about Model Self Relationship
using the title Laravel – Model Relationship To Itself, where I tried to explain how one can make relationship using Eloquent ORM in Laravel to make relationship with a model to load the related models where the related model is the parent model itself. Sounds confusing? Let’s discuss about a real world example.
So, imagine that, you’ve a blog and blog has many articles/posts where readers can leave comments for posts and you want to allow the readers to reply on comments. So, when you’ll load a post to show it on your post page (post.single.blade.php)
you would load the post with related comments with all the replies of each comment. To produce such a system we need a few models such as Post, Comment and User and we have to define relationship between these models and also we have to design the database accordingly.
Hence, this article is kind of a follow up post of my previous article Laravel – Model Relationship To Itself, here I’m trying to reproduce the system with code with a slight modification. Why I’m doing this, writing the same article using a different title because I’ve got some requests from some readers of my blog to post the code including the presentation logic and I did it on my previous post but everyone didn’t get the idea so mainly, I’m going to post the implementation of code here as a separate article so it won’t get messy. So let’s begin.
We need to create at least three tables: users
, posts
and comments
. We only need to setup migrations for the posts
table and comments
table and we’ll leave users table because by default, Laravel
comes with a users table which will serve the purpose here but you can modify the table according to your need. So let’s create the migration for posts
table at first:
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreatePostsTable extends Migration { public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id')->unsigned(); $table->string('title'); $table->string('body'); $table->timestamps(); }); } public function down() { Schema::drop('posts'); } }
This is a very basic example of posts table to store the posts but you may add more fields to extend it’s functionality. The “user_id” field is a required field here so we can make relationship with users table to identify the posts of a particular user. Now create the migration class for comments
table:
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateCommentsTable extends Migration { public function up() { Schema::create('comments', function (Blueprint $table) { $table->increments('id'); $table->integer('post_id')->unsigned(); $table->integer('user_id')->unsigned(); $table->integer('parent_id')->unsigned()->default(0); $table->text('body'); $table->timestamps(); }); } public function down() { Schema::drop('comments'); } }
The comments
table will store the comments of posts written by user. In this case, the “post_id” is a foreign key which will allow us to identify the post, the comment belongs to and the “user_id” is another foreign key which will tell us who made the comment. Also, notice the field “parent_id”, this will contain the id
of a particular comment in the comments
table if the comment is a reply of the comment. So, these fields are required as foreign keys. By default, the “comment_id” field will contain “0” unless it’s a reply to another comment.
Now, let’s create the models. Remember that, the User
is also required and Laravel
comes with the App\User
model out of the box so we don’t need to create that model but we may need to add some methods in that. So, let’s create the Post
model in “/app” (The default namespace is “App” for models).
namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { public function comments() { return $this->hasMany(Comment::class); } public function parentComments() { return $this->comments()->where('parent_id', 0); } }
Now let’s create the Comment
model in the same location (“/app” folder) where both the Post
model and the User
model is stored.
namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model { public function replies() { return $this->hasMany(__CLASS__, 'parent_id'); } public function allRepliesWithOwner() { return $this->replies()->with(__FUNCTION__, 'owner'); } public function owner() { return $this->belongsTo(User::class, 'user_id'); } }
Okay! We’ve just created our models and declared the relationship methods so now we can try this out from anywhere, I’ll use an anonymous function within a route declaration for this example. So let’s declare a route to test it from a browser. To fetch a single post from the database by “posts.id” we may declare a route like one given below:
Route::get('/posts/{id}', function ($id) { $post = App\Post::with([ 'comments', 'parentComments.owner', 'parentComments.allRepliesWithOwner' ]); return view('post.single')->withPost( $post->findOrFail($id) ); });
So, we need a template to generate the html response for the browser so let’s create a template inside “resources/views/post” folder and name it “single.blade.php”. The code for the template is given below and I’m not using any base layout file, just using a hardcoded html template for this example but you may use one layout and wrap the content within the “section” directive. So, the code may look something like the following:
@extends('layouts.master') @section('content'){{$post->title}}
{{$post->body}}
We also need to create a partial template (comments.blade.php) to show the comment list so let’s create it inside “resources/views/post” folder and paste the following code inside that file:
@foreach($comments as $comment)
Now, if you did everything right and navigate to following URI: http://localhost:8000/posts/1
from your browser (Make sure the local development server is running, I’m using PHP
‘s builtin development server) then, you may look something similar to this:
Well, I’m seeing this in my browser because I’ve also seeded my database with some fake data (users, posts, comments) so make sure you have some data to test it out.
The Post
model and Comment
model. The Post
contains two relationship methods, while the comments
method will load the comments of a post, including the child ones as a single collection (without any nesting) and the parentComments
method will load only the parent comments (with “parent_id” of 0).
In the Comment
model, we’ve three methods, the replies
method will load all the replies (direct children) of a particular Comment
, the allRepliesWithOwner
method will load all the replies just like replies
method by calling the replies
method but it’ll also load the owner
of the comment and the __FUNCTION__
within the with
method will recursively call the same method (__FUNCTION__ will return the calling function name) again and again. So, the replies
method is actually making the relationship with the Comment
model itself because __CLASS__
pre-defined constant will return the class name which is “Comment” so actually we are writing something like this:
public function replies() { return $this->hasMany("Comment", 'parent_id'); }
This method is making the actual relationship using the “parent_id” field of a comment. The owner
method is making the relationship with the user
model which will give us the user who wrote the comment. So, that’s it.
Now, let’s talk a little bit about the used code to fetch the Post and all the related replies and owner:
$post = App\Post::with([ 'comments', // comments() method to call count() method to get the count o all comments including replies 'parentComments.owner', // parentComments()->owner() method to load the owner of a parent comment only 'parentComments.allRepliesWithOwner' // load all the replies including their replies recursively and owner of each ]);
So, without the “parentComments.owner” we won’t get the “owner/User” objects of parent comments and “comments”is being used to get the count of all the comments of a Post so we can show the total comment count in the template, something like “8 Comments” in the template by calling $post->comments->count()
. Finally the “parentComments.allRepliesWithOwner” is responsible to load all the nested comments/replies recursively.
Now, let’s check the templates, at first let’s check the “single.blade.php” file:
{{$post->title}}
{{$post->body}}
{{$count = $post->comments->count()}} Comments
@if($count && $post->relationLoaded('parentComments')) @include('post.comments', ['comments' => $post->parentComments]) @endif
In the above snippet, the main important part to print out the comment list is the last “if” statement, where we are checking, if $count
variable is truthy because the $count = $post->comments->count()
will give us zero or some number if comments exists, so if we’ve some comments then include the partial comment template and when we are including the partial comment template, we are also passing the comment list into the $comments
variable using ['comments' => $post->parentComments]
so we can use the $comments
variable to iterate the comment list. Now check the partial:
@foreach($comments as $comment)
This is very strait forward, we are looping over the comments we sent from the “single.blade.php” file and if each comment $comment
in the loop has it’s own allRepliesWithOwner
as relation key loaded in the $comment
object then include the same file within it so it’ll be included recursively. The “relationLoaded” tells whether the relationship collection is already loaded or not because if we just call $comment->relationLoaded
, then the Eloquent
will call the method directly from the view, I found this helpful but if you want to allow dynamic calls to your relationship method then remove the code and check the count instead using @if($comment->allRepliesWithOwner->count())
. That’s all about it. I tried to explain it in short so please forgive me if I missed anything or made it less understandable. My intention was only to produce the code not any discussion but I tried a little in hurry. Hope you enjoyed it.
As a PHP developer with over 16 years of experience, I have witnessed the evolution of PHP security practices. While PHP continues to be one […]
FluentCRM is a brand new email marketing automation and self hosted CRM plugin for WordPress. Whether you want a centralized console for all your customer […]
In PHP 8.0 there is a new feature or I should say a new language construct (keyword) going to be introduced, which has been implemented depending […]
{{$count = $post->comments->count()}} Comments
@if($count && $post->relationLoaded('parentComments')) @include('post.comments', ['comments' => $post->parentComments]) @endif