PHP Comment System With Replies

The parent is the id, not the r_to:

function show_posts($parent, $dbconn) {
    $stmt = $dbconn->prepare('SELECT id, r_to, name, comment FROM comments WHERE r_to = ?');

    $stmt->bind_param('s', $parent);
    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result($id, $r_to, $name, $comment);

    while ($stmt->fetch()) {
        echo $name . "<br>";
        echo $comment;
        echo "<br><br>";
        // now call the function recursively to get all the child posts for this post
        show_posts($id, $dbconn);
    }
}

show_posts("0", $mysqli);

Two questions though:

  1. Why is there no timestamp on these posts? Does order not matter? Or are just going to assume that a higher ID means posted later?

  2. How many rows are we talking about here, realistically? If it’s (similar to) the number of rows shown in your database you’re far better off performance wise just selecting them all and then do the recursion in memory instead of firing a new query every time.

2 Likes

Also, I’d separate the fetching logic from the displaying logic (adhering to the Single Responsibility Priniple):

function fetch_comments($parent, $dbconn) {
    $stmt = $dbconn->prepare('SELECT id, r_to, name, comment FROM comments WHERE r_to = ?');

    $stmt->bind_param('s', $parent);
    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result($id, $r_to, $name, $comment);

    $comments = [];
    while ($stmt->fetch()) {
        $comments[] = [
            'name' => $name,
            'comment' => $comment,
            'replies' => fetch_posts($id, $dbconn),
        ];
    }

    return $comments;
}

function render_comments($comments) {
    foreach ($comments as $comment) {
        echo $comment['name'] . "<br>";
        echo $comment['comment'];
        echo "<br><br>";
        render_posts($comment['replies']);
    }
}

render_comments(
    fetch_comments('0', $mysqli)
);
1 Like

And, you know, just for the heck of it, let’s take it a bit further and use objects for comments instead of plain old arrays. Then we can let those render themselves. Create a nice Composite pattern.

So let’s a define a comment:

<?php

class Comment
{
    /**
     * @var string
     */
    public $name;

    /**
     * @var string
     */
    public $comment;

    /**
     * @var Comment[]
     */
    public $replies;

    public function __construct($name, $comment, array $replies)
    {
        $this->name = $name;
        $this->comment = $comment;
        $this->replies = $replies;
    }

    public function render()
    {
        echo $this->name . "<br>";
        echo $this->comment;
        echo "<br><br>";
        foreach ($this->replies as $reply) {
            $reply->render();
        }
    }
}

Okay, so that’s cool, let’s see how that looks with the rest of the code:

function fetch_comments($parent, $dbconn) {
    $stmt = $dbconn->prepare('SELECT id, r_to, name, comment FROM comments WHERE r_to = ?');

    $stmt->bind_param('s', $parent);
    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result($id, $r_to, $name, $comment);

    $comments = [];
    while ($stmt->fetch()) {
        $comments[] = new Comment(
            $name,
            $comment,
            fetch_comments($id, $dbconn)
        );
    }

    return $comments;
}

foreach (fetch_comments('0', $mysqli) as $comment) {
    $comment->render();
}

So the render_comments function is just a simple foreach, and the recursion has moved to the Comment class, so let’s get rid of that function and just put it inline.

One thing annoys me still, is that I have to manually loop over those posts, let’s introduce a CommentCollection to take care of that:

class Comment
{
    /**
     * @var string
     */
    public $name;

    /**
     * @var string
     */
    public $comment;

    /**
     * @var CommentCollection
     */
    public $replies;

    public function __construct($name, $comment, CommentCollection $replies)
    {
        $this->name = $name;
        $this->comment = $comment;
        $this->replies = $replies;
    }

    public function render()
    {
        echo $this->name . "<br>";
        echo $this->comment;
        echo "<br><br>";
        $this->replies->render();
    }
}

class CommentCollection
{
    /**
     * @var Commment[]
     */
    public $comments;

    public function __construct(array $comments)
    {
        $this->comments = $comments;
    }
    
    public function add(Comment $comment)
    {
        $this->comments[] = $comment;
    }
    
    public function render()
    {
        foreach ($this->comments as $comment) {
            $comment->render();
        }
    }
}

function fetch_comments($parent, $dbconn) {
    $stmt = $dbconn->prepare('SELECT id, r_to, name, comment FROM comments WHERE r_to = ?');

    $stmt->bind_param('s', $parent);
    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result($id, $r_to, $name, $comment);

    $comments = new CommentCollection();
    while ($stmt->fetch()) {
        $comments->add(
            new Comment(
                $name,
                $comment,
                fetch_comments($id, $dbconn)
            )
        );
    }

    return $comments;
}

fetch_comments('0', $mysqli)->render();

Okay, so that’s the rendering taken care of. Last thing we need now, with all these objects, is create an object for the fetching. Let’s call it a CommentRepository:

class Comment
{
    /**
     * @var string
     */
    public $name;

    /**
     * @var string
     */
    public $comment;

    /**
     * @var CommentCollection
     */
    public $replies;

    public function __construct($name, $comment, CommentCollection $replies)
    {
        $this->name = $name;
        $this->comment = $comment;
        $this->replies = $replies;
    }

    public function render()
    {
        echo $this->name . "<br>";
        echo $this->comment;
        echo "<br><br>";
        $this->replies->render();
    }
}

class CommentCollection
{
    /**
     * @var Commment[]
     */
    public $comments;

    public function __construct(array $comments)
    {
        $this->comments = $comments;
    }

    public function add(Comment $comment)
    {
        $this->comments[] = $comment;
    }

    public function render()
    {
        foreach ($this->comments as $comment) {
            $comment->render();
        }
    }
}

class CommentRepository
{
    /**
     * @var mysqli
     */
    private $dbConn;

    public function __construct(mysqli $dbConn)
    {
        $this->dbConn = $dbConn;
    }

    function findComments($parent)
    {
        $stmt = $this->dbconn->prepare('SELECT id, r_to, name, comment FROM comments WHERE r_to = ?');

        $stmt->bind_param('s', $parent);
        $stmt->execute();
        $stmt->store_result();
        $stmt->bind_result($id, $r_to, $name, $comment);

        $comments = new CommentCollection();
        while ($stmt->fetch()) {
            $comments->add(
                new Comment(
                    $name,
                    $comment,
                    $this->findComments($id)
                )
            );
        }

        return $comments;
    }

}

(new CommentRepository($mysqli))->findComments('0')->render();

And there you have it, an OOP solution to the problem :slight_smile:

It does exactly the same thing, but this is a lot more resilient to change, because not everything is tightly coupled together anymore. Want to change how comments are fetched from the database? Change the repository. Want to change how they’re rendered? Change the Comment. etc.

In an ideal world the comments would not even render themselves, that would be done by a CommentRenderer, but that might be taking things too far for this example :wink:

Hi rpkamp and thank you so much for setting things straight with the code we’ve come up with so far.
Before I move on to more advanced coding style (implementing classes and additional functions), I would like to stick with the code below, and throw in some form of indentation that shows each reply is slightly shifted off to the right below the comment that it has replied to.

I know that it must remain inside of the while loop to check for the child comments (reply comment), so I was trying to figure out how to do so. Here is what I have so far:

<?php
$conn = new mysqli('localhost', 'root', 'Jordan123', 'commentsystem2');

function show_posts($parent, $dbconn) {

    $stmt = $dbconn->prepare('SELECT id, name, comment, r_to FROM comments2 WHERE r_to = ?');
    $stmt->bind_param('s', $parent);
    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result($id, $name, $comment, $r_to);


    while ($stmt->fetch()) {
       echo '
        <div class="comments" style="position:relative; margin:auto; width:25%; border:1px solid black; margin-bottom:2px;">
          <div class="name">'.$name.'</div>
          <div class="name">'.$comment.'</div>
   
        </div>
       ';

        // now call the function recursively to get all the child posts for this post
        show_posts($id, $dbconn);
    }
}


show_posts("0", $conn);

?>

or, as shown in the image below, I’d like to have it to where I can detect the child comments (replies) as shown in the arrow of this image below:

So tell me if I’m wrong, but is it something like:

If($r_to > 0){
 <div class="comments" style="position:relative; margin:auto; width:25%; border:1px solid black; margin-bottom:2px;">
          <div class="name">'.$name.'</div>
          <div class="comment">'.$comment.'</div>
   <div class="reply-comments" style="position:relative; margin:auto; width:25%; border:1px solid black; margin-bottom:2px;">
          <div class="reply-name">'.$name.'</div>
          <div class="reply-comment">'.$comment.'</div>
   
        </div>
        </div>
}

Something like that? Thanks again rpkamp.

P.S.
Maybe it would require an additional SELECT statement?

Oh, and to answer your two questions, the table of comments will store possibly hundreds or even thousands of comments as it will be a complete comment and reply system. Now I’m just trying to figure out the indention of the reply comments as they appear under the original comment. I would need 1 indention only on all of the reply comments that correspond to each parrent comment.

There is no need for an if statement; this should work:

while ($stmt->fetch()) {
       echo '
        <div class="comments" style="position:relative; margin:auto; width:25%; border:1px solid black; margin-bottom:2px;">
          <div class="name">'.$name.'</div>
          <div class="name">'.$comment.'</div>';
          
          // now call the function recursively to get all the child posts for this post
          show_posts($id, $dbconn);
   
        echo '</div>';
    }

This will render another <div class="comments"> when there are more children, otherwise it will render nothing.

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.