user7031
user7031

Reputation: 445

Nested comments using iteration in PHP

I'm currently working on a comment system using PHP,and I'm using 'parent ID' solution as to connect one reply to another.The problem is I haven't figured out how to cast these 'parent ID' connected data stored in mysql to a PHP array and render them out.I've been searching for iteration solution but nothing found out.My database structure is as follow: Parent_id 0 means top level comment.

comment_id content parent_id
1          xxx     0
2          xxx     0
3          xxx     1
4          xxx     3
5          xxx     4
6          xxx     3
...        ...     ...

Here is what I have done,I fetched all the comments out in an array,and the array looks like this:

$comment_list = array(0=>array('comment_id'=>1,'content'=>'xxx','parent_id'=>0),
                      0=>array('comment_id'=>2,'content'=>'xxx','parent_id'=>0),
                      0=>array('comment_id'=>3,'content'=>'xxx','parent_id'=>1),
                      0=>array('comment_id'=>4,'content'=>'xxx','parent_id'=>3),
                      ...

)

I need to attach comment with parent_id 1 to comment with comment_id 1,and so on,depth should be unlimited,and working for hours still can't find a way to iterate properly,can someone give me some pointers on how to do this?I know a solution but it makes new request to the database upon every iteration,so I prefer to do it using PHP array once for all,thank you !

Upvotes: 2

Views: 503

Answers (1)

Jared Clemence
Jared Clemence

Reputation: 1065

When faced with a complex structure like this, it is sometimes better to create an object oriented solution, and then use the objects to create the array that you require.

For example, based on your above, I might define the following class:

class Comment{
    protected $id;
    protected $children;
    protected $content;

    public function __construct( $id, $content ){
        $this->id = $id;
        $this->content = $content;
        $this->children = array();
    }
    public function addChild( $child ){
        $this->children[] = $child;
    }
}

Now, we use this object to transfer your database into the working memory as follows:

$workingMemory = array(); //a place to store our objects
$unprocessedRows = array(); //a place to store unprocessed records

// here, add some code to fill $unproccessedRows with your database records

do{
  $row = $unprocessedRows; //transfer unprocessed rows to a working array
  $unprocessedRows = array(); //clear unprocessed rows to receive any rows that we need to process out of order.
  foreach( $row as $record ){
    $id = $record[0]; //assign your database value for comment id here.
    $content = $record[1]; //assign your database value for content here.
    $parentId = $record[2]; //assign your database value for parent id here

    $comment = new Comment( $id, $content );

    //for this example, we will refer to unlinked comments as 
    //having a parentId === null.
    if( $parentId === null ){
         //this is just a comment and does not need to be linked to anything, add it to working memory indexed by it's id.
         $workingMemory[ $id ] = $comment;
    }else if( isset( $workingMemory[ $parentId ] ) ){
         //if we are in this code block, then we processed the parent earlier.
         $parentComment = $workingMemory[ $parentId ];
         $parentComment->addChild( $comment );
         $workingMemory[ $id] = $comment;
    }else{
         //if we are in this code block, the parent has not yet been processed. Store the row for processing again later.
         $unprocessedRows[] = $record;
    }

   }
  }while( count( $unprocessedRows ) > 0 );

Once all the unprocessedRows are complete, you now have a representation of your comments entirely stored in the variable $workingMemory, and each cell of this array is a Comment object that has an $id, a $content, and links to all children $comments.

We can now iterate through this array and make whatever data arrays or tables we want. We must remember, that the way that we stored arrays, we have direct access to any comment directly from the $workingMemory array.

If I were using this to generate HTML for a website, I would loop through the workingMemory array and process only the parent comments. Each process would then iterate through the children. By starting with the parents and not the children, we would guarantee that we are not processing the same comment twice.

I would alter my Comment class to make this easier:

class Comment{
    protected $id;
    protected $children;
    protected $content;
    protected $isRoot;

    public function __construct( $id, $content ){
        $this->id = $id;
        $this->content = $content;
        $this->children = array();
        $this->isRoot = true;
    }
    public function addChild( $child ){
        $child->isRoot = false;
        $this->children[] = $child;
    }
    public function getChildren(){ return $this->children; }
    public function getId(){ return $this->id; }
    public function getContent(){ return $this->content; }
}

After this change, I can create my HTML as follows:

function outputCommentToHTML( $aComment, $commentLevel = 0 ){
    //I am using commentLevel here to set a special class, which I would use to indent the sub comments.
    echo "<span class'comment {$commentLevel}' id='".($aComment->getId())."'>".($aComment->getContent())."</span>";
    $children = $aComment->getChildren();
    foreach( $children as $child ){
         outputCommentToHTML( $child, $commentLevel + 1 );
    }
}
foreach( $workingMemory as $aComment ){
    if( $aComment->isRoot === true ){
         outputCommentToHTML( $aComment );
    }
}

This would convert database columns into the format you require. For example, if we had the following data:

comment_id content parent_id
 1          xxx     0
 2          xxx     0
 3          xxx     1
 4          xxx     3
 5          xxx     4
 6          xxx     3
 ...        ...     ...

It would output in HTML:

  Comment_1
      Comment_3
           Comment_4
                Comment_5
           Comment_6
  Comment_2

This is done recursively in the function, which processes Comment_1 fully before moving to Comment 2. It also processes Comment_3 fully before moving to Comment 2, which is how Comments 4, 5 and 6 all get output before Comment 2.

The above example will work for you, but if it were my personal project, I would not mix linear and Object oriented code, so I would create a code factory to convert Comments into HTML. A factory make data strings from source objects. You can create an Object that acts as a factory for HTML, and another factory that acts as a generator of SQL, and by layer objects with solutions like this, you can create an entirely Object Oriented solution, which is easier to understand to the average reader and sometimes even to non-coders to produce something like this:

//these definition files get hidden and tucked away for future use
//you use include, include_once, require, or require_once to load them
class CommentFactory{
      /**** other Code *****/

      public function createCommentArrayFromDatabaseRecords( $records ){
                  /*** add the data conversion here that we discussed above ****/
                  return $workingMemory;
      }
}
class HTMLFactory{
       public function makeCommentTableFromCommentArray( $array ){
            $htmlString = "";
            foreach( $array as $comment ){
                 if( $comment->isRoot ){
                        $htmlString .= $this->getHTMLStringForComment( $comment );
                 }
            }
            return $htmlString;
       }
       private function getHTMLStringForComment( $comment, $level=0 ){
            /*** turn your comment and all it's children into HTML here (recursively) ****/
            return $html;
       }
}

Done properly, it can clean up your active code file so that it reads almost like a list of instructions like this:

//let database be a mysqli or other database connection
//let the query function be whatever method works for your database
// of choice.
//let the $fetch_comment_sql variable hold your SQL string to fetch the
//   comments
$records = $database->query( $fetch_comment_sql )
$comFactory = new CommentFactory();
$commentArray = $comFactory->createCommentArrayFromDatabaseRecords( $records );
$htmlFactory = new HTMLFactory();
$htmlResult = $htmlFactory->makeCommentTableFromCommentArray( $commentArray );
echo $htmlResult;

Upvotes: 3

Related Questions