13
Php

Laravel – Nested Relationship Revised

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.

If you didn’t read the previous article then you don’t need to read that to understand this one but you may try that.
Our Requirements:

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:

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:

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).

Now let’s create the Comment model in the same location (“/app” folder) where both the Post model and the User model is stored.

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:

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:

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:

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:

screen-shot-2016-09-15-at-5-00-13-am

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.

I owe you a little explanation:

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:

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:

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:

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:

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.

  • Testing…

  • Captain

    Thanks for this bro. Please am still having issue with view. Am having “Trying to get property of non-object (View: /home/vagrant/captain/myblog/resources/views/post/single.blade.php)”

    I dont mine, to give you access to my code.

    Thanks

    • Can you show some code?

      • Captain

        This is my view single.blade.php
        —————————————–
        @extends(‘layouts.master’)

        @section(‘content’)

        <div class=’post’>
        <h2 class=’post_title’->{{$post->title}}</h2>
        <p class=’post_body’->{{$post->body}}</p>
        </div>

        <div class=’comments’>
        <h3 class=’comment_header’>
        {{$count = $post->comments->count()}} Comments
        </h3>

        @if($count -> $post->relationLoaded(‘parentComments’))

        @include(‘post.comments’, [‘comments’=> $post->parentComments])

        @endif

        </div>
        @stop

        ————————
        comments.blade.pho
        —————–

        @foreach($comments as $comment)

        <ul>

        <li>{{$comment->body}} – By – {{$comment->owner->name}}</li>

        @if($comment->relationLoaded(‘allRepliesWithOwner’))

        @include(‘post.comments’, [‘comments’ => $comment->allRepliesWithOwner])

        @endif

        </ul>

        @endforeach

        —————–
        App/Comment.php
        ——————

        hasMany(__CLASS__, ‘parent_id’);
        }

        public function allRepliesWithOwner(){

        return $this ->replies()->with(__FUNCTION__, ‘owner’);
        }

        public function owner(){

        return $this ->belongsTo(User::class, ‘user_id’);
        }
        }

        ———-
        App/User
        ———–

        <?php

        namespace App;

        use IlluminateNotificationsNotifiable;
        use IlluminateFoundationAuthUser as Authenticatable;

        class User extends Authenticatable
        {
        use Notifiable;

        /**
        * The attributes that are mass assignable.
        *
        * @var array
        */
        protected $fillable = [
        'name', 'email', 'password',
        ];

        /**
        * The attributes that should be hidden for arrays.
        *
        * @var array
        */
        protected $hidden = [
        'password', 'remember_token',
        ];

        ———-
        App/Post
        ———–

        hasMany(Comment::class);

        }

        public function parentComments(){
        return $this ->comments()->where(‘parent_id’, 0);
        }
        }

        ————–
        Routes
        —-

        Route::get(‘/post/{id}’, function($id){
        $post = AppPost::with([
        ‘comments’,
        ‘parentComments.owner’,
        ‘parentComments.allRepliesWithOwner’
        ]);

        return view(‘post.single’)->withPost(
        $post->findOrFail($id)
        );

        });

        Please, i can give u team view access, to help me out.
        I want something like this, where user can reply to comment.

        I will be glad to hear from you.

        Thanks and regards.

        • Sorry! This is not clear enough and can’t understand where the error coming from. I need to see it clearly. If possible, please create a gist on git hub or anywhere else. Also, try to {{dd($post)}} within your single view and make sure all it exists and all the properties in the post is also available.

  • Sam Wairegi Coder

    Thanks man, you saved me a big hustle

  • Please check your code sample.
    I see some code characters have been html-escaped, i.e. > has turned into >

    Btw, want to see more blog posts 🙂

    • Thanks Bro 🙂

      • Ha! I wrote & gt ; it turned to > . Paradox!

        • lol… I don’t know what happened but fixed those characters in this article. Thanks for pointing it out tho 🙂

  • Gozbeth Stanslaus

    Can you please create the sample project for demonstration

    • Sorry! Currently so busy but maybe in future but can’t tell when. Btw, all the required settings (including migration and models) are given here. I believe it’s easy for you if even new to Laravel.

Latest Blog