Developing a Discussion Forum in PHP with Recursion

If you’re interested in learning how to use recursion in PHP, look no further. Welcome to the third (and last) tutorial of the series “Recursion in PHP.” In three parts, this series walks through the fundamentals of recursive functions in PHP, in addition to explaining how to define and utilize recursive methods in object-based applications.

Introduction

As you’ll probably recall, over the previous article I drew the general guidelines for defining and using recursive methods inside PHP classes. Because a method is simply a function that belongs to a particular class in an object-based development environment, creating recursive methods doesn’t differ considerably from building regular recursive functions.

Provided that you defined the appropriate logic of a recursive method, all you have to remember is to include the popular “$this->” pointer, followed by the method’s name, at the point where the method calls itself. As you can see, this process is really straightforward and easy to grasp.

After covering in detail how to define recursive method and functions, the question is: what comes next? Luckily, there’s vast terrain to explore with reference to using recursion in PHP. As I said in previous articles of this series, recursion can be used in cases where a specific tree structure or a linked list needs to be navigated, in order to display, add, delete or edit its values. It’s exactly for that reason that this last article will be focused on building an extensible discussion forum, which precisely uses a tree structure (implemented on a single MySQL database table) for displaying forum messages and adding new posts.

Of course, using the core logic of this forum as a starting point, you can experiment by adding more features to the overall application and build a more complex discussion forum. This can be integrated with your existing PHP applications, or eventually added to future ones.

Are you ready to start learning how to build a discussion forum in PHP? Right, let’s do it together.

{mospagebreak title=Getting started with the forum: defining the structure of the MySQL database table}

To start building this simple discussion forum, what I’ll do first is define the structure of the MySQL database table that will serve for storing user data, such as messages, user names, email addresses, and so on. At the same time it will be used to construct a logical tree, in such a way that it can be traversed by a recursive function (or method, to be more accurate), in order to display all this data.

Keeping in mind the database structure that I just explained above, here is the SQL code for creating the “forum” database table:

CREATE TABLE forum
(
id INT(4) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
parent_id INT(4) UNSIGNED NOT NULL,
name CHAR(50) NOT NULL,
email CHAR(50) NOT NULL,
title CHAR(50) NOT NULL,
message TEXT NOT NULL
);

As you can see, the above sample “forum” database table is comprised of six fields: id, parent_id, name, email, title and message respectively, which are quite descriptive about the type of data they will house. However, let me stop for a while on the “parent_id” field, since it’s important to know the meaning of it. Essentially, the values this field will store will be the ID of the corresponding parent node within the structure of a linked tree, so if a particular forum thread has no associated parent (placed on top of the tree), then obviously this value will be 0. In a similar fashion, the different branching threads will be generated by assigning the corresponding parent IDs to each row, as the tree structure goes deeper and deeper.

To illustrate how the above linked tree works, here is a schematic example of the “forum” database table, populated with a few records:

In the above example, the database table contains six forum threads, where the two first are main threads, that is their parent_id fields are equal to 0, then the two subsequent ones are sub threads of threads 1 and 2 (parent_id=2 and parent_id=1), and finally the last threads are also sub threads of threads 3 and 4 respectively. Quite understandable, right?

As you can see, a linked tree structure is pretty easy to be constructed by using a simple database table, and based on this tree structure, the discussion forum will work properly.

Now that you know how the “forum” database looks, it’s time to leap forward and begin coding the PHP class responsible for making the forum work as expected. To see how this will be achieved please read the next few lines.

{mospagebreak title=Processing forums threads: defining the “ThreadProcessor” class}

Basically, I plan to make the forum work by using a single PHP class, called “ThreadProcessor,” which is responsible for displaying threads and adding new posts to the pertinent forum. As you’ll see shortly, this class will aggregate an additional one, named “MySQL,” handy for providing the forum with the capacity to connect to MySQL and run SQL queries.

Having explained the basic tasks that will be performed by the “ThreadProcessor” class, here is its corresponding definition. Please have a look at it:

class ThreadProcessor{
    // declare data members
    var $db;
    var $threadCode;
    var $threadSubmitted;
    var $user;
    var $email;
    var $title;
    var $message;
    function ThreadProcessor(&$db){
        $this->db=&$db; // instance of MySQL class
        $this->threadCode=!$_GET['threadcode']?’0′:$_GET
['threadcode'];
        $this->threadSubmitted=$_POST['send'];
        $this->user=$_POST['user'];
        $this->email=$_POST['email'];
        $this->title=$_POST['title'];
        $this->message=$_POST['message'];
    }
    // display main-child threads & thread form 
    function displayThreads(){
        // add new thread to database
        if($this->threadSubmitted){
            $this->addThread();
        }
        ob_start();
        // display threads
        $this->threadCode==’0′?$this->fetchTitles():$this-
>fetchMessages();
        // display thread form
        $this->createThreadForm();
        $contents=ob_get_contents();
        ob_end_clean();
        return $contents;
    }
    // display main threads
    function fetchTitles(){
        $result=$this->db->query(“SELECT * FROM forum WHERE
parent_id=’$this->threadCode’”);
        echo ‘<ul>’;
        // loop over result set
        while($row=$result->fetchRow()){
            echo ‘<li><a href=”‘.$_SERVER['PHP_SELF'].’?
threadcode=’.$row['id'].’”>’.$row['title'].’</a> Posted by
(‘.$row['name'].’) ‘.$row['email'].’</li>’;
            $this->threadCode=$row['id'];
            // call recursively the ‘fetchTitles()’ method
            $this->fetchTitles();
        }
        echo ‘</ul>’;
    }
    // display child threads
    function fetchMessages(){
        $result=$this->db->query(“SELECT name,title,message FROM
forum WHERE id=’$this->threadCode’”);
        if($result->countRows()==0){
            echo ‘No messages were found!’;
            return;              
        }
        $row=$result->fetchRow();
        echo ‘<h2>’.$row['title'].’</h2><hr /><p>’.$row['name'].’
wrote: ‘.$row['message'].’</p><a href=”‘.$_SERVER['PHP_SELF'].’?
threadcode=0″>Back to main threads</a>’;
    }
    // display thread form
    function createThreadForm(){
        $this->threadCode=intval($_GET['threadcode']);
            echo ‘<form method=”post” action=”‘.$_SERVER
['PHP_SELF'].’?threadcode=’.$this->threadCode.’”><h2>Post your
new message</h2>’;
            echo ‘Name <input type=”text” name=”user”
size=”30″ /><br />Email <input type=”text” name=”email”
size=”30″ /><br />’;
            echo ‘Title<input type=”text” name=”title”
size=”30″ /><br />Message<br /><textarea  name=”message”
rows=”10″ cols=”30″></textarea><br /><input type=”submit”
name=”send” value=”Add thread” /></form>’;
    }
    // add new thread
    function addThread(){
        $this->db->query(“INSERT INTO forum (id,parent_id,name,email,title,message) VALUES (NULL,’$this-
>threadCode’,'$this->user’,'$this->email’,'$this->title’,'$this-
>message’)”);
        header(‘location:’.$_SERVER['PHP_SELF']);
    }
}

As shown above, the “ThreadProcessor” class exposes a few handy methods for displaying forum threads, as well as for adding new posts to the “forum” database table, something that is achieved by coding a regular online form.

As I said a few lines above, the class aggregates an instance of a “MySQL” class that comes in quite useful for connecting to MySQL and running queries against the database table. As you can see, this class is aggregated and assigned as a new property inside the respective constructor.

Now, take a look at the signature of the “displayThreads()” method:

function displayThreads(){
    // add new thread to database
    if($this->threadSubmitted){
        $this->addThread();
    }
    ob_start();
    // display threads
    $this->threadCode==’0′?$this->fetchTitles():$this-
>fetchMessages();
    // display thread form
    $this->createThreadForm();
    $contents=ob_get_contents();
    ob_end_clean();
    return $contents;
}

In essence, what this method does is determine the course of action to be taken, in accordance with the value of the “threadCode” property. First, it checks whether the form for adding new posts has been submitted, and according to this, calls the “addThread()” method, since a new message has been posted by a user. After checking this condition, the method displays either the corresponding thread titles or the contents of submitted messages. Finally, it shows the form for posting new messages, by calling the “createThreadForm()” method.

Before proceeding to review other methods of the “ThreadProcessor” class, let me first clarify one point: notice the entire visual output is echoed to an output buffer; the data is not displayed directly on the browser. I have a good reason for doing this: since the “fetchTitles()” method calls itself (yep, here’s where recursion comes in), it generates a dynamic output for each method instance, which is easily captured by the output buffer, instead of troubling things with regular variables. Additionally, the remaining methods also use this buffer to output their contents, which are returned to calling code.

Right, now that you understand the reasons for using an output buffer, the next step consists of looking at the remaining methods of the “ThreadProcessor” class, to give you a clear idea of how they fit together. Therefore, please click on the link below and keep on reading.

{mospagebreak title=Displaying the forum: looking at the “fetchTitles(),” fetchMessages()” and “createThreadForm()” methods}

As you may have guessed, the core method of the class is “fetchTitles(),” which is responsible for recursively navigating the “forum” database table and displaying the respective tree of nested threads. First, the method begins displaying all the main threads (the values for parent_id fields are equal to 0), and then goes as deep as required, in order to display the corresponding sub threads. You can learn how this is performed if you take a look at the signature of this method:

function fetchTitles(){
    $result=$this->db->query(“SELECT * FROM forum WHERE
parent_id=’$this->threadCode’”);
    echo ‘<ul>’;
    // loop over result set
    while($row=$result->fetchRow()){
        echo ‘<li><a href=”‘.$_SERVER['PHP_SELF'].’?
threadcode=’.$row['id'].’”>’.$row['title'].’</a> Posted by
(‘.$row['name'].’) ‘.$row['email'].’</li>’;
        $this->threadCode=$row['id'];
        // call recursively the ‘fetchTitles()’ method
        $this->fetchTitles();
    }
    echo ‘</ul>’;
}

In this method, you can clearly appreciate the role played by the aggregated $this->db object, which is handy when it comes to fetching all the thread titles from the database table. Undoubtedly, the most important thing to note here is the recursive implementation of the method, something extremely powerful and elegant for scanning the tree’s branch nodes, in order to display all the sub threads.

The remaining methods speak for themselves, so I won’t spend much time on them. First, the “fetchMessages()” method is called when a thread title is clicked, and not surprisingly shows the contents of that particular thread. Its source code is listed below:

function fetchMessages(){
    $result=$this->db->query(“SELECT name,title,message FROM forum WHERE id=’$this->threadCode’”);
    if($result->countRows()==0){
        echo ‘No messages were found!’;
        return;                  
    }
    $row=$result->fetchRow();
    echo ‘<h2>’.$row['title'].’</h2><hr /><p>’.$row['name'].’
wrote: ‘.$row['message'].’</p><a href=”‘.$_SERVER['PHP_SELF'].’?
threadcode=0″>Back to main threads</a>’;
}

Finally, the “createThreadForm()” method is tasked with generating, on the fly, the online form that inserts new posts on the forum. Its definition is as follows:

function createThreadForm(){
    $this->threadCode=intval($_GET['threadcode']);
    echo ‘<form method=”post” action=”‘.$_SERVER['PHP_SELF'].’?
threadcode=’.$this->threadCode.’”><h2>Post your new
message</h2>’;
    echo ‘Name <input type=”text” name=”user”
size=”30″ /><br />Email <input type=”text” name=”email”
size=”30″ /><br />’;
    echo ‘Title<input type=”text” name=”title”
size=”30″ /><br />Message<br /><textarea  name=”message”
rows=”10″ cols=”30″></textarea><br /><input type=”submit”
name=”send” value=”Add thread” /></form>’;
}

In this case, the above method displays a basic web form, which after submitting itself, inserts a new message into the forum by the “addThread()” method. The source code for this one is listed below:

function addThread(){
    $this->db->query(“INSERT INTO forum
(id,parent_id,name,email,title,message) VALUES (NULL,’$this-
>threadCode’,'$this->user’,'$this->email’,'$this->title’,'$this-
>message’)”);
    header(‘location:’.$_SERVER['PHP_SELF']);
}

Okay, I think that’s all you need to learn about how the “ThreadProcessor” class works. Now, jump into the next section, in order to see the forum in action. It’s really worthwhile.

{mospagebreak title=The discussion forum in action: putting the “ThreadProcessor” class to work}

Assuming that you understood the logic implemented by the “ThreadProcessor” class, below I developed an example that shows the discussion forum in action. For the sake of completeness, I also included the classes that connect to MySQL and process result sets. Here is the corresponding sample code:

class MySQL {
    var $conId; // connection identifier
    var $host; // MySQL host
    var $user; // MySQL username
    var $password; // MySQL password
    var $database; // MySQL database
    // constructor
    function MySQL($options=array()){
        // validate incoming parameters
        if(count($options)>0){
            foreach($options as $parameter=>$value){
                if(empty($value)){
                    trigger_error(‘Invalid parameter
‘.$parameter,E_USER_ERROR);
                }
                $this->{$parameter}=$value;
            }
            // connect to MySQL
            $this->connectDB();
        }
        else {
            trigger_error(‘No connection parameters were
provided’,E_USER_ERROR);
        }
    }
    // connect to MYSQL server and select database
    function connectDB(){
        if(!$this->conId=mysql_connect($this->host,$this-
>user,$this->password)){
            trigger_error(‘Error connecting to the
server’,E_USER_ERROR);
        }
        if(!mysql_select_db($this->database,$this->conId)){
            trigger_error(‘Error selecting
database’,E_USER_ERROR);
        }
    }
    // perform query
    function query($query){
        if(!$this->result=mysql_query($query,$this->conId)){
            trigger_error(‘Error performing query
‘.$query,E_USER_ERROR);
        }
        // return new Result object
        return new Result($this,$this->result); 
    }
}

class Result {
    var $mysql; // instance of MySQL object
    var $result; // result set
    function Result(&$mysql,$result){
        $this->mysql=&$mysql;
        $this->result=$result;
    }
    // fetch row
    function fetchRow(){
        return mysql_fetch_array($this->result,MYSQL_ASSOC);
    }
    // count rows
    function countRows(){
        if(!$rows=mysql_num_rows($this->result)){
            return false;
        }
        return $rows;
    }
    // count affected rows
    function countAffectedRows(){
        if(!$rows=mysql_affected_rows($this->mysql->conId)){
            trigger_error(‘Error counting affected
rows’,E_USER_ERROR);
        }
        return $rows;
    }
    // get ID from last inserted row
    function getInsertID(){
        if(!$id=mysql_insert_id($this->mysql->conId)){
            trigger_error(‘Error getting ID’,E_USER_ERROR);
        }
        return $id;
    }
    // seek row
    function seekRow($row=0){
        if(!mysql_data_seek($this->result,$row)){
            trigger_error(‘Error seeking data’,E_USER_ERROR);
        }
    }
    function getQueryResource(){
        return $this->result;
    }
}

// connect to MySQL
$db=&new MySQL(array(‘host’=>’host’,'user’=>’user’,'password’=>’password’,
‘database’=>’database’));
// instantiate ‘ThreadProcessor’ object
$threadProc=&new ThreadProcessor($db);
// display forum threads
echo $threadProc->displayThreads();

As shown in the above script, after including the couple of MySQL processing classes, all I needed to do to get the forum working was instantiate a “ThreadProcessor” object and call its “displayThread()” method. Wasn’t that simple?

Also, below I included an example screen shot, which illustrates the discussion forum in action, after populating the “forum” database table with a few trivial messages:

As depicted above, the “ThreadProcessor” class does a decent job traversing recursively the “forum” database table and displaying main and sub threads, as well as the respective web form for posting new messages. I hope the above image will be clear enough to demonstrate the power of recursion in PHP.

Final thoughts

In this last article of the series, I showed you how to use recursion in a practical example: the development of a “quick and dirty” discussion forum. I left as homework coding the class methods that update and delete forum threads, but I’m sure you’ll have a clear idea of how to do this, since both database operations are very simple to translate to PHP code. As usual, see you in the next PHP tutorial!

Google+ Comments

Google+ Comments