Better Wordpress threaded comments
Posted on Wednesday the 8th of August, 2007 at 1:13 am in DevHere is something slightly different, I’m trying to get into the habit of writing out my notes and detailed changes in INAP 3.0 for my own use, so I thought they may help others in the process. The following walk through details the process I took to create a method to thread comments for Wordpress. Most of the project specific code has been removed to keep the code from being cluttered.
The threading structure for the plugin needed to be able to handle an unlimited number of children in an order, and the ability to “flatten” the children after a certain user-specified depth (shown in the code examples as $max_depth). The threading structure needed to be rewritten because in short, the old version (seen here) worked by individually querying the database for each comment to look for children, utilized globals to pass information from one section to another, used a complex method of if/elses to determine what functions should do, and was very slow.
My first attempt for a solution was to harness the built in power of WordPress, so I attempted to replace this with the WordPress “Walker” class (my code to utilize this can be seen here), but I stopped while testing as I determined that the WordPress walker class could not easily handle multiple levels of comments to the extent I needed it to, was not able to “flatten” comments, and used over 100 lines of code for the main class alone. I also plan to release this plugin for Habari also, so avoiding the temptation to utilize classes like this in the core this will help to keep it platform independent.
I rewrote the old code to use a single query, utilize some class elements, static variables rather than globals, and a logical structure that is easily readable, and the result can be seen here.
There are a few issues that can still be improved, but it is far superior to the original. Rather than passing a large number of IDs and variables we now just call a new template object which will remember it the arguments passed to it, so we can call as many new ones of these as we need too without needing to introduce new variables to “hide” the content in globals.
The $depth structure is also far more logical. Previously, it used a complex pattern that used negative numbers to stop counting up; however, now we determine if the level should go up or down by whether or not we have child comments — if a comment has child comments then we know the $depth is going to increase, but if it doesn’t, we know the $depth is going to keep decreasing until a new child comment is found. Both the new code and the old code had the main thread function called inside the template of the comment itself, but in the old method the depth was passed back to the function, but in the new $depth calculator this is not needed as the static variable is automatically changed based on the child comments.
With this new method the processing times have decreased by up to a third; on a single test post with 53 first-level parents and 121 total comments up to 8 levels deep( but threading to 5 levels before flattening), the new method takes between 1.01 and 1.11 seconds while the old methods takes 1.16 to 3.51 seconds. (As a side note, this test post is a “usable” test with comment lengths ranging from 3 to 500 words, formatted using default WordPress functions and a total page size of 112KB of HTML, so even though the functions seem to take a long time, it is reasonable for the content.) These times for the threading, formatting (not shown in the code examples) and outputting, but the only thing that changed between the two times is the threading functions, so by switching to the new method we decrease the output time by up to a third.
So what now? I like the simplicity of the Walker code, but it takes almost as many lines of code to initialize the walker as it does to completely thread and output the comments my way, so I’ll be sticking with it, but now that I have the main method I’m going to continue tweaking the code.
Notes:
All code not directly relevant to the post has been removed, so don’t worry about little things like where $max_depth was set.
All code posted here is released under the Creative Commons license, and the attribution requirement is a “Hat-tip” link back to either the main site page or this page.
The Old Code
class comments {function thread($comment_parent_id=0,$depth=0, $continue=0) {global $post, $wpdb, $comments, $max_depth;$parent = "AND comment_parent='$comment_parent_id'";if(($comment_parent_id && !is_numeric($comment_parent_id)) || $max_depth == false){$parent = $comment_parent_id = '';}if(!$comment_parent_id){$order = $awpall['comment_order'];}$comments[$comment_parent_id+1] = $wpdb->get_results("SELECT * FROM $wpdb->comments WHERE comment_post_ID = '$post->ID' AND comment_approved = '1' $parent ORDER BY comment_date $order");if(count($comments[$comment_parent_id+1]) >0){if($depth == $max_depth){$depth = -2;}elseif($depth <$max_depth && $depth > -1 ){$depth++;}else{$depth = -1;}comments::template($comment_parent_id+1,$depth);}}function template($comment_parent_id,$depth=0){global $awpall, $comments,$comment,$post,$id;if($comment_parent_id){$the_comments = $comments[$comment_parent_id];}else{$the_comments = $comments;}if(count($the_comments) >0){if($depth != -1){echo '<ol>';}foreach ($the_comments as $comment){echo "\n\n".'<li>';//Echo actual comment hereif($depth <0){echo '</li>';}AWP_comments::thread($comment->comment_ID,$depth);if($depth > -1){echo '</li>'."\n\n";}$cn++;}}if($depth != -1){echo '</ol>';}}}- Use the following to copy and paste the code.
The New Code
class threader{function template($comments,$depth){global $max_depth,$comment, $post,$id;if(count($comments) >0){if($depth < $max_depth){echo '<ol>';}foreach ($comments as $comment){echo "\n\n".'<li>';//Echo actual comment hereif($awpall['comment_threaded']==1){if($depth <= $max_depth){echo '</li>';}comments::thread($comment->comment_ID);}echo '</li>';$cn++;}if($depth < $max_depth){echo '</ol>';}}}}class comments{function comment_array(){global $post, $wpdb,$awpall;$allcomments = $wpdb->get_results("SELECT * FROM $wpdb->comments WHERE comment_post_ID = '$post->ID' AND comment_approved = '1' ORDER BY comment_date $order");foreach($allcomments as $acomment){$the_comments[$acomment->comment_parent][] = $acomment;}return $the_comments;}function thread($comment_parent_id=0) {global $id, $comment,$comments,$awpall;static $the_comments;static $depth =-1;static $crawl_up;if(!$the_comments){$the_comments = comments::comment_array();}if(is_array($the_comments[$comment_parent_id])){//Resync the array under the global comments array//Format $comments[array]->object$comments = $the_comments[$comment_parent_id];if($depth < $awpall['comment_threaded_depth'])$depth++;$temp = new AWP_threader;AWP_threader::template($comments, $depth);}}else{if($depth >0)$depth--;}}}- Use the following to copy and paste the code.
The Rejected (Unfinished) Walker Code
<?phpclass comments{function thread($comment_parent_id=0,) {global $id,$wpdb, $comment,$comments;$comments = $wpdb->get_results("SELECT * FROM $wpdb->comments WHERE comment_post_ID = '$id' AND comment_approved = '1'");echo '<ol>';echo comments::walk_comment_tree($comments, 15, $max_depth);echo '</ol>';}function walk_comment_tree(){$walker = new Walker_Comments;$args = func_get_args();return call_user_func_array(array(&$walker, 'walk'), $args);}}class Walker_Comments extends Walker {var $tree_type = 'comments';var $db_fields = array ('parent' => 'comment_parent', 'id' => 'comment_ID'); //TODO: decouple thisfunction start_lvl($output, $depth) {global $awpall;$indent = str_repeat(" ", $depth);$output .= $indent.'<ol>';return $output;}function end_lvl($output, $depth) {global $awpall;$indent = str_repeat(" ", $depth);$output .= $indent.'</ol>';return $output;}function start_el($output, $the_comment, $depth, $args) {global $comment, $awpall;if ( $depth )$indent = str_repeat(" ", $depth);extract($args);$comment = $the_comment;ob_start();echo "\n\n".'<li>';include('templates/comment.php');$output .= ob_get_contents();ob_end_clean();return $output;}function end_el($output, $comment, $depth) {global $awpall;$output .= '</li>'."\n\n";return $output;}}?>- Use the following to copy and paste the code.


Good site - you’re a pretty good writer.
Reply to BobExcellent stuff!
Reply to M