PHP Lesson 2: Behind the Scenes of Threading

This is going to be a very nerdy post, because I’m going to get into some actual PHP code. I’ve been thinking a lot about efficient threading. The implementation of threading on OSNews is very complex, because it involves lots of math in order to properly construct and align tables. Furthermore, because we don’t use CSS for positioning, it’s accomplished via ‘align’ commands and TWO templates, which is really clumsy, because between flat mode, admin mode, collapsed threading mode, and expanded threaded mode, we have several templates, and since they are all independent, they tend to unintentionally vary, so you might see different things in replies and threads. My goal in writing a threaded display for firsttube.com was to avoid all of the pitfalls in that implementation and come up with something clean. Read on for the gory details.

firsttube.com commenting basically worked like this:

$comments = get_comments($STORY_ID);
foreach($comment as $key => $value) { //$value is an array of each comment's data
	// $myhtml is a previously defined string created from the HTML output template
	foreach($value as $k=>$v) {
		$mykey = '{' . $k . '}';
		$html = str_replace($mykey, $value, $myhtml);
	}
	echo $html;
}

This is a pretty standard way, I think, for retreiving comments from a database and displaying them in a template. It’s fairly cut and dry – the names of the colums in the database (or the name given after the “as” in the SQL statement) serves as a placeholder in the template such as {title}. It’s dynamically replaced for each comment, which means the templates contain no PHP whatsoever.

Threading adds some complication, because you can’t loop through all of your comments anymore. If you do that, you’ll echo out the replies the same way. So this is the implementation I used on firsttube.com, which is based on the same idea as threading on OSNews. First off, we add a column in the comments table called “parent.” The parent is the parent comment’s primary key.

$comments = get_comments($STORY_ID);
foreach($comments as $key => $value) {
	if($value['parent']) {
		$K = $value['parent'];
		$C[$K][] = $value;
	} else {
		$C[0][] = $value;
	}
}

What we’ve done here is created a new array called $C. $C is a multidimensional array that holds /all/ comments. As you can see, all comments that are replies are stored as $C[PARENT_C0MMENT_ID] and all non-replies, or “original records,” are stored in $C[0]. The zero could just as easily be “parent” or “x” or any other string or integer; anything except an existing comment ID. 0 is a good choice because it’s unlikely you’ll have a comment ID of 0.

Now, like above, instead of looping through $comments, we loop through $C[0].

foreach($C[0] as $key => $value) {
	foreach($value as $k=>$v) {
		$mykey = '{' . $k . '}';
		$html = str_replace($mykey, $value, $myhtml);
	}
	echo $html;
}

Now we’ve echo’ed out all of the original records, but no replies. In my case, I added a second template, called reply.tmpl, and it been imploded into a string called $rhtml. So at the end of the above foreach() loop, we’ll add this:

if(is_array($C[$v['commentid']])) {
	foreach($C[$v['commentid']] as $ke => $va) {
		//echo reply
	}
}

That gets us all replies to the original comment. But it doesn’t get us replies to the replies. So we have created single reply threading. But what if we want deeper replies? This is the first, most elementary soltuion is pretty basic, but doesn’t solve the entire problem. It looks like this:

if(is_array($C[$v['commentid']])) {
	foreach($C[$v['commentid']] as $ke => $va) {
		//echo reply
		if($C[$va['commentid']]) {
			foreach($C[$va['commentid']] as $key => $val) {
				//echo reply to reply
			}
		}
	}
}

Of course, this is inefficient, and again, only gets us one additional layer. As you can see, threading will only go as deeper as you write this code, which is inelegant and tough to look at. We have to turn to a function.

So let’s define one:

function mkThread($array,$depth) {
	global $rhtml,$C; //this is the string of the reply text and the comments array
	foreach($array as $k=>$v) {
		$mykey = '{' . $k . '}';
		$html = str_replace($mykey, $value, $myhtml);
	}
	if(is_array($C[$array['commentid']])) {
		$newdepth = $depth++;
		mkThread($C[$array['commentid']],$newdepth);
	}
}

Okay, let break this down. So the mkThread function pretty much does what we were doing before – it takes the array of data and does some replacement in a string of HTML which gets echo’ed out. Pretty simple. But then, it looks for replies. What’s the $depth variable? That’s how we control out display. In the template is some CSS that controls margins, and this depth is multiplied times your indention – say 35px – and then pushed into your template into two varibles: myindent (which is which is $depth*pixels), and mywidth (which is, in my case, 640-$myindent). Of course, if you start with a comment box wider than 640, your milage may vary. My CSS in the template looks like this:

<div style="width:640px;margin-right:auto;margin-left:auto;">
	<div class="reply" style="width:{mywidth}px;margin-left:{myindent}px;">
	[...]</div></div>

That’s it. That’s how threading works. It’s not so intimidating once you see how easy it is insert the comments dynamically. On top of that, the entire comments section only takes one single database query. In fact, since we’re not using tables, it renders pretty quickly. Tables, in some browsers, only render to the screen after the entire table is downloaded by the client, whereas divs typically render as they are received. The only drawback to this is that you may want to add some code to limit depth, because after awhile, the comments might get so small they become silly looking. Other than that, it’s pretty easy for a custom blogging app to support comment threading.

Tagged , , , , , , ,