Creating a Message Board - Getting Some Action

Let's start by creating a new class named ForumController. Within this class, we will create methods for creating, retrieving, updating, and deleting forums in our database. When creating controller classes, each public method we create is an action. Our ForumController class is simply a grouping of actions that manipulate records in the forums table. Make sense? Good. Let's start coding.

Create a ForumController class like the one below and save it to the controllers/ directory of your application. Name the file ForumController.php.

ForumController class

<?php

class ForumController
{
    
}

?>

As you can see, our ForumController class is empty. At this point it is idle and useless. It needs to do something worthwhile, but what? Let's think about the first thing we want our users to see when they visit our message board. They probably want to see a list of forums, so let's create a new method.

Our First Action

Let's create a method called getAll(), which will retrieve all forums in our database. Check it out:

ForumController's getAll() method

<?php

class ForumController
{
    function getAll()
    {
        return array(
            'forums' => Forum::getAll()
        );
    }
}

?> 

As you can see, our getAll() method returns an arary with a "forums" key set to the return value from Forum::getAll(). This value will be available to our view as the $forums variable.

info-32.png StratosDataObject::getAll()
The getAll() method can be called statically on any data object to retrieve all records in its table. For instance, if the Forum data object is mapped to the forums table, Forum::getAll() will return all records in the forums table as an array of objects. Stratos Data Objects have many other methods that we will describe in detail as we encounter them later in this tutorial.

In the code above, we simply returned the result of Forum::getAll() without checking to see if anything went wrong. While this works, it's bad practice, so we will modify our getAll() method to look like the one below:

ForumController's getAll() method

<?php

class ForumController
{
    function getAll()
    {
        $forums = Forum::getAll();

        if( PEAR::isError($forums) )
        {
            Stratos::putFlash('There was a problem retrieving the forum listing.', 'error');
            return $forums;
        }
        
        return array(
            'forums' => $forums
        );
    }
}

?>

As you can see, we have added code to check that forums were returned and that no PEAR errors were returned. If everything went as planned, we send our forum list over to the view. If something did go wrong, we use the Stratos::putFlash() method to display an error message.

info-32.png Stratos::putFlash()
Stratos::putFlash() is used to display status messages. The first parameter to this method is the message to send, and the second parameter is the type of message. The message type is usually 'success', 'error', or 'warning', but it can be anything you like.

Our First View

I have been using the word view a lot, but we haven't actually created a view yet. As you probably know, the view is where we will put all of our presentation logic. Unlike many other web frameworks, Stratos does not use a separate templating engine for presentation logic. This is because separation of business and presentation logic can be achieved without introducing the overhead and additional syntax of a templating engine. For more discussion of this topic, read the articles on templating engines posted here and here. All of our views will be defined using plain PHP.

We already have the logic in place to retrieve all of the forums from the forums table. So now we will create a view containing HTML to present the forums to the user. Create a PHP script named ForumController.getAll.php and save it to the views/ directory of your application. Views in Stratos are named using the following convention:

ClassName.methodName.php

Since our method in ForumController was named getAll(), we created a script in the views directory with the filename ForumController.getAll.php, and Stratos hooked up the action to the view automatically. But what if you wanted to give your view a different name that does not follow this convention? Sure, you can do that too. We will talk about the Stratos::setView() method in a later section.

For now, enter the following code into your view script:

views/ForumController.getAll.php

<div id="forum_nav">
    <div id="forum_nav_options">
        <a href="<?= Stratos::makeUrl('Forum/add') ?>">Add a Forum</a>
    </div>
    Forums
</div>

<?php Stratos::printFlashes(); ?>

<?php if( $forums ) { ?>

<table id="forum_list">
    <tr>
        <th>Forum</th>
        <th>Topics</th>
        <th>Posts</th>
        <th>Last Post</th>
    </tr>
    
    <?php foreach( $forums as $i => $forum ) { ?>
    <tr class="<?= $i % 2 == 1 ? 'forum_even' : 'forum_odd' ?>">
        <td class="left">
            <div class="forum_name">
                <a href="<?= Stratos::makeUrl('Post/getTopicsInForum',
                    array('forum_id' => $forum->forum_id)) ?>">
                    <?= e($forum->name) ?></a>
            </div>
            <div class="forum_description">
                <?= e($forum->description) ?>
            </div>
            <div>
               [ <a href="<?= Stratos::makeUrl('Forum/update',
                   array('forum_id' => $forum->forum_id)) ?>">update</a> 
               | <a href="<?= Stratos::makeUrl('Forum/remove',
                   array('forum_id' => $forum->forum_id)) ?>">remove</a> ]
            </div>
        </td>
    </tr>
    <?php } ?>
    
</table>

<?php } else { ?>

<div class="no_forums">
    No forums have been created!
</div>

<?php } ?>

In our view above, we made use of several new methods. The first of which is the Stratos::makeUrl() method, which is used to link to other actions in your application.

info-32.png Stratos::makeUrl() and u()
This method is used to create clean URL's that link to a specific action in your application. Stratos::makeUrl() accepts two arguments, only one of which is required. The first argument is the name of the action you want to link to. For instance, if you wanted to create a link back to the forum listings page, your link would look like:

 <a href="<?= Stratos::makeUrl('Forum/getAll') ?>">View All Forums</a>.

The second parameter (which is optional) is an array of URL arguments that you would like to pass. The keys of the array are the name of the URL variables, and the values they map to are obviously the values of the URL variables. For instance, when I want to access a specific forum, I will use a link that looks like:

 <a href="<?= Stratos::makeUrl('Forum/get', array('forum_id' => $forum->forum_id)) ?>">View This Forum</a>

When this code is processed, the URL will look something like:

 <a href="http://www.litebb.com/index.php/Forum/get/forum_id/8">View This Forum</a>

u() is an alias for Stratos::makeUrl().

The second Stratos method that we leveraged was Stratos::printFlashes(), which is used to print any "flash" messages that we set in our action. We placed the call to Stratos::printFlashes() where we want these message to be displayed.

info-32.png Stratos::printFlashes()
This method will display any flash messages that were set in the action. In the getAll() method of our ForumController class, we used Stratos::putFlash() to set a flash message if a database error occurred:

 <?php Stratos::putFlash('There was a problem retrieving the forum listing', 'error'); ?>

Flash messages that were set in the action are passed to the view, and they are printed wherever a call to Stratos::printFlashes() is located in the view. If no flash messages were set, then nothing is displayed. Stratos::printFlashes() method is frequently used to display error and success messages.

We also used the function e(), which is used to escape output.

info-32.png e()
This function escapes output before sending it to the client's browser. This keeps special characters from messing up your HTML and, more importantly, helps to protect you from cross-site scripting attacks. You should pretty much always escape dynamic output.

Overall Layout View

Ah, the overall layout. Can you guess what this view is for? That's right. It's to define the overall layout of your application. It's basically your header and footer all in one. Save the following script as overall-layout.php in your views/ directory:

views/overall-layout.php

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
    <head>
        <title>litebb</title>
        <meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
        <link rel="stylesheet" type="text/css" href="<?= Stratos::makeUrl('./views/styles/style.css') ?>" />
    </head>
    <body>
        <div id="page">
            <div id="page_header">
                <div id="page_title">litebb</div>
                <div id="page_nav">
                    <a href="<?= Stratos::makeUrl('./index.php') ?>">Home</a>
                    | <a href="<?= Stratos::makeUrl('User/getAll') ?>">Users</a>
                    
                    <?php
                    $user = StratosAuth::getUser();
                    if ( StratosAuth::isAuthenticated() ) { ?>
                        | <a href="<?= Stratos::makeUrl('User/get', array(
                            'user_id' => $user->user_id)) ?>">
                            Profile</a>
                        | <a href="<?= Stratos::makeUrl('StratosAuth/logout') ?>">
                            Logout</a>
                    <?php } else { ?>
                        | <a href="<?= Stratos::makeUrl('User/add') ?>">
                            Register</a>
                        | <a href="<?= Stratos::makeUrl('StratosAuth/login') ?>">
                            Login</a>
                    <?php } ?>
                </div>
            </div>
            
            <div id="page_content">
                <?php Stratos::executeView() ?>
            </div>
        </div>
    </body>
</html>

As you can see, we have defined how the entire page will look within a single view script. We used Stratos::makeUrl() to link to the various actions in our application. In the content area of the view, we used the Stratos::executeView() method to specify where we would like action specific views to be inserted into the page.

info-32.png Stratos::executeView()
This method is used to insert the HTML of a view into the current view. You can think of it as a sort of HTML include. If Stratos::executeView() is called without any arguments, as we did above, it will insert the current action's view. If the name of a view script is passed as an argument, it will execute that view. For instance, if we had a div in our page, and we wanted to show some additional HTML that was in a script called "my-view.php", we could use:

 <div>
     <?php Stratos::executeView('my-view.php'); ?>
 </div>

Note that the file extension is required when calling Stratos::executeView(), but it is left out when calling Stratos::makeUrl(). Also, executeView() automatically looks in the views/ directory of your application for the view script.

Now that we have completed creating a controller, an action, a view, and an overall layout for our application, let's see what our page looks like. Go to your toolkit for your application, and click "General" under the "Configure" menu. Here we will specify the default action for the application, so that the toolkit doesn't appear when we visit our page. Select Forum/getAll for the default action, and click submit. Now when you revisit the page, you should see the forum listing:

The Forum Listing Page

There is a problem here. Our users can't post because there are no forums yet. Let's go through the process of creating an "add forum" page for our administrator.

Adding a Forum

Let's create a method for adding a new forum to the database. In our arguments list, we will set the default for a variable named $submitted to false. In our view, we will create an input with "submitted" as the name attribute. When we first visit our add forum form, $submitted will default to false and our add forum form will be displayed. Only when the form has been submitted will the $submitted variable be set.

If the form has been submitted, we will get a new instance of the Forum data object, and set its properties equal to the form inputs with the same name by calling the autofill() method. Once this is done, we will pass our $forum object to a _validate() method to check that our inputs are valid (we will create this method shortly). If everything looks good, we can then call the add() method on our data object to add the forum record to the database.

We capture the return value of $forum->add() in a variable called $added. We do this in order to check that there were no errors returned from the database. If $added is true and no PEAR errors occurred, then we know that our add was successful, and we can send a success message back to the view and redirect the user to an appropriate page. In our case, we will redirect the user back to the forum listing if a new forum was added successfully. At this point, the user will see that the new forum was indeed created and a success message will be displayed. If any errors occurred, then we will not redirect to the forum. We will display an error message and stay on the add page.

Add the following add() method to your ForumController class. Note that we have included the php tags and class definition here for code highlighting purposes, but you will not paste them into your class, of course:

ForumController's add() method

<?php

class ForumController
{

    function add( $submitted = false )
    {
        $forum = new Forum();
        
        if( $submitted )
        {
            $forum->autofill();
            
            if( $this->_validate($forum) )
            {
                $added = $forum->add();
            
                if( $added && !PEAR::isError($added) )
                {
                    Stratos::putFlash('The forum was added successfully',
                        'success');
                    Stratos::redirect('Forum/getAll');
                }
            }
        }
        
        return array(
            'forum' => $forum
        );
    }

}

?>

In this action, we used two new methods. The autofill() method was used to fill the properties of our data objects with the values submitted by the user.

info-32.png StratosDataObject::autofill()
The autofill() method is used to automatically fill the properties of a data object. When using data objects, the values of columns in database tables are set by setting the properties of the data object that maps to that table. For instance, to set the "name" and "description" columns of a record that is being inserted into the forums table, we would use the following code:

 <?php
 
 $forum = new Forum();
 $forum->name = 'Ancient Greek Philosophy';
 $forum->description = 'Discuss the works of Plato and Aristotle';
 $forum->add();
 
 ?>

This code creates a new Forum object (which maps to a row in our table), sets its properties, and inserts it into the database. If we want to automatically set the forum's "name" and "description" properties equal to the values of input fields named "name" and "description", respectively, then we can simply call the autofill() method. After using autofill(), you should always validate properties of the data object before calling the add() method to actually insert the record into the database.

The other new method we used was Stratos::redirect().

info-32.png Stratos::redirect()
This method is used to redirect the client's browser to another action.

Now we need a view for this action -- the actual form for adding a new forum. Notice that we fill our forum object with the data that was input by the user. If any validation errors occurred, then we simply send our data object back to the view without adding anything to the database. This allows us to easily maintain the state of the form when the user last submitted it.

views/ForumController.add.php

<div id="forum_nav">
    <div id="forum_nav_options">
    </div>
    <a href="<?= Stratos::makeUrl('Forum/getAll') ?>">
        Back to Forums</a> 
</div>

<form action="" method="post">
    <fieldset>
        <legend>Add/Edit Forum</legend>
            
            <div>
                <?php Stratos::printFlashes() ?>
            </div>
            
            <div>
                <label for="name">Forum Name</label>
                <input id="name" name="name" type="text"
                    value="<?= $forum ? e($forum->name) : '' ?>" />
            </div>
            
            <div>
                <label for="description">Description</label>
                <textarea id="description" name="description" cols="" rows=""><?= $forum
                    ? e($forum->description) : '' ?></textarea>
            </div>
            
            <div>
                <input id="id" name="id" type="hidden"
                    value="<?= $forum ? e($forum->forum_id) : '' ?>" />
            </div>
            
            <div class="buttons">
                <input type="submit" id="submit" name="submitted" value="Submit" class="button" />
                <input type="button" id="cancel" name="cancel" value="Cancel" class="button" 
                    onclick="window.location.href='<?= Stratos::makeUrl('Forum/getAll') ?>'" />
            </div>
    </fieldset>
</form>

The view script should be self explanatory. The only thing we have done new here is use the ternary operator when displaying the value of our form inputs. If a forum object has been passed back to the view, we can display the appropriate properties in the input fields of our form. In our action, we pass the populated forum object back to the view when any input validation errors occur, so that the user can see what they entered and try again. Speaking of input validation, let's create the _validate() method that we call before inserting the record into the database.

Input Validation

We don't want to just insert just anything into our forums table. We want to make sure that the administrator has at least entered values for name and description, which are required fields. So let's create a method to validate user input. We will call our method _validate() and it will accept a forum object as an argument. We prefix the method name with an underscore "_" to indicate that this is a private method. Doing so also prevents this method from being accessed directly from the web.

In our _validate() method, we will first initialize a variable $valid to true. Then we will check that the name and description attributes are set on the forum data object. If either is not set, we will set a flash message to be displayed in the view, and set $valid to false. The complete _validate() method is shown below:

ForumController's _validate() method

<?php

class ForumController
{

    function _validate( $forum )
    {
        $valid = true;
            
        if( !$forum->name )
        {
            Stratos::putFlash('Please enter a forum name', 'error');
            $valid = false;
        }
        
        if( !$forum->description )
        {
            Stratos::putFlash('Please enter a forum description', 'error');
            $valid = false;
        }
            
        return $valid;
    }

}

?>

If you wish, you may add additional validation logic to this class to fit your needs. For now, we are keeping it simple.

Updating a Forum's Details

Once an administrator adds a new forum, he may change his mind. He may want to change the name or description of the forum, or he may want to delete the forum all together. So let's provide our adminsitrator with the ability to update a forum's attributes.

First, we will create a new method in our ForumController? class called update(). It will have a $submitted argument that we will default to false, just as the add() method did. Since we are updating an existing forum, our update method will also accept a $forum_id argument, so that it will know which forum to update. This forum_id will be sent in from a hidden field named forum_id which we will be in our view.

Since the form for updating forum information is identical to the form for adding a forum, we will re-use the view we used in our add action. This can be accomplished by calling the Stratos::setView() method. Stratos will automatically use a view with the same name as our action unless we call the setView() method to specific a different view. So let's add a call to Stratos::setView() at the end of our update() method, and pass the name of the add forum view as an argument.

ForumController's update() method

<?php

class ForumController
{

    function update( $forum_id, $submitted = false )
    {
        Stratos::setView('ForumController.add.php');
    }
    
}

?>

Now that we have told Stratos which view to use for our action, let's start coding in some business logic. The first thing we will do in our update() method is make sure that there is actually a forum in the database that corresponds to the forum_id that was sent in. To do this, we will first cast $forum_id to an integer to assure that it contains numeric data. Then, we will call the getById() method on the Forum data object to obtain a reference to the forum record that we would like to update. If a forum is not returned by Forum::getById(), or if there was a database error, we will display an error message and redirect the user to the forum listing page.

Otherwise, we will continue on our quest to update the forum record. If the administrator has submitted the form, then we will autofill() the $forum object with the newly submitted values, and once again call our _validate() method to make sure that the new inputs are valid. If the inputs are indeed valid, then we will attempt to update the forum record, check for errors, and send appropriate messages back to the view. When this is complete, your update() method should look similar to the one below:

ForumController's update() method

<?php

class ForumController
{

    function update( $forum_id, $submitted = false )
    {
        $forum_id = (int) $forum_id;
        
        $forum = Forum::getById($forum_id);
        
        // if the forum was not found or if any errors occur, we will redirect
        // back to the forum listing
        if ( !$forum || PEAR::isError($forum) )
        {
            Stratos::putFlash('The forum you selected could not be found.',
                'error');
            Stratos::redirect('Forum/getAll');
        }
        
        if ( $submitted )
        {
            $forum->autofill();
            
            if( $this->_validate($forum) )
            {
                $updated = $forum->update();
                
                if( $updated && !PEAR::isError($updated) )
                {
                    Stratos::putFlash('The forum was updated successfully.',
                        'success');
                    Stratos::redirect('Forum/getAll');
                }
            }
        }
        
        return array(
            'forum' => $forum
        );
        
        Stratos::setView('ForumController.add.php');
    }
    
}

?>

Removing a Forum

Our administrator now has the ability to add and update forums, but we have not yet provided him with a way to remove forums from the database. Let's create the remove() method. The first several lines of our remove() method contain similar code, which you can refactor if you wish. We first check to see that the forum that the administrator is trying to remove actually exists. If it does not, we display an error message and redirect him back to the forum listing page.

Depending on the underlying database system you are using, you may be able to define foreign key constraints that will automatically delete all posts in a forum as soon as the forum they point to is deleted. In our code, we will not assume that the user is using a database system that allows for the creation of constraints.

We use the Post object's query() method to get a StratosDataQuery? object that will allow us to select all posts that have a specific forum_id. Then we call the remove() method on the query object to remove all of these posts. After deleting the posts, we then delete the forum that the posts were contained in.

ForumController's remove() method

<?php

class ForumController
{

    function remove( $forum_id, $submitted = false )
    {
        $forum_id = (int) $forum_id;
        
        $forum = Forum::getById($forum_id);
        
        // if the forum was not found or if any errors occur, we will redirect
        // back to the forum listing
        if ( !$forum || PEAR::isError($forum) )
        {
            Stratos::putFlash('The forum you selected could not be found.',
                'error');
            Stratos::redirect('Forum/getAll');
        }
        
        if ( $submitted )
        {
            $post_query = Post::query();
            $post_query->where('forum_id', '=', $forum->forum_id);
            $posts_removed = $post_query->remove();
            
            if ( $posts_removed && !PEAR::isError($posts_removed) )
            {
                $forum_removed = $forum->remove();
                
                if ( $forum_removed && !PEAR::isError($forum_removed) )
                {
                    Stratos::putFlash('The forum was removed successfully.',
                        'success');
                    Stratos::redirect('Forum/getAll');
                }
            }
            
            Stratos::putFlash('There was a problem removing the forum.',
                'error');
        }
        
        return array(
            'forum' => $forum
        );
    }
    
}

?>

In our view, we will create a div containing a set of buttons that allows the administrator to confirm that the forum should be deleted. If the deletion is confirmed ($submitted is set), then we will attempt to remove the forum and all posts in the forum from the database.

views/ForumController.remove.php

<div class="confirm_box">
    <form id="confirm_delete" name="confirm_delete" action="" method="post">
        <fieldset>
            <legend>Confirm Deletion</legend>
            <div class="confirm_message">
                Are you sure you want to remove this forum?
            </div>
            
            <div class="confirm_buttons">
                <input type="submit" id="submitted" name="submitted" value="Remove It" class="button"/>
                <input type="button" id="cancel" name="cancel" value="Cancel" class="button"
                    onclick="window.location.href='<?= Stratos::makeUrl('Forum/getAll') ?>'"/>
            </div>
        </fieldset>
    </div>
</div>

We've done it! You should now have a complete ForumController class, along with the views necessary to update, add, remove, and display forums in your database. Your completed ForumController class should look like the one below:

controllers/ForumController.php

<?php

class ForumController
{
    function getAll()
    {
        $forums = Forum::getAll();

        if( PEAR::isError($forums) )
        {
            Stratos::putFlash('There was a problem retrieving the forum listing.', 'error');
            return $forums;
        }
        
        return array(
            'forums' => $forums
        );
    }
    
    function add( $submitted = false )
    {
        $forum = new Forum();
        
        if( $submitted )
        {
            $forum->autofill();
            
            if( $this->_validate($forum) )
            {
                $added = $forum->add();
            
                if( $added && !PEAR::isError($added) )
                {
                    Stratos::putFlash('The forum was added successfully',
                        'success');
                    Stratos::redirect('Forum/getAll');
                }
            }
        }
        
        return array(
            'forum' => $forum
        );
    }
    
    function update( $forum_id, $submitted = false )
    {
        $forum_id = (int) $forum_id;
        
        $forum = Forum::getById($forum_id);
        
        // if the forum was not found or if any errors occur, we will redirect
        // back to the forum listing
        if ( !$forum || PEAR::isError($forum) )
        {
            Stratos::putFlash('The forum you selected could not be found.',
                'error');
            Stratos::redirect('Forum/getAll');
        }
        
        if ( $submitted )
        {
            $forum->autofill();
            
            if( $this->_validate($forum) )
            {
                $updated = $forum->update();
                
                if( $updated && !PEAR::isError($updated) )
                {
                    Stratos::putFlash('The forum was updated successfully.',
                        'success');
                    Stratos::redirect('Forum/getAll');
                }
            }
        }
        
        return array(
            'forum' => $forum
        );
        
        Stratos::setView('ForumController.add.php');
    }
    
    function remove( $forum_id, $submitted = false )
    {
        $forum_id = (int) $forum_id;
        
        $forum = Forum::getById($forum_id);
        
        // if the forum was not found or if any errors occur, we will redirect
        // back to the forum listing
        if ( !$forum || PEAR::isError($forum) )
        {
            Stratos::putFlash('The forum you selected could not be found.',
                'error');
            Stratos::redirect('Forum/getAll');
        }
        
        if ( $submitted )
        {
            $post_query = Post::query();
            $post_query->where('forum_id', '=', $forum->forum_id);
            $posts_removed = $post_query->remove();
            
            if ( $posts_removed && !PEAR::isError($posts_removed) )
            {
                $forum_removed = $forum->remove();
                
                if ( $forum_removed && !PEAR::isError($forum_removed) )
                {
                    Stratos::putFlash('The forum was removed successfully.',
                        'success');
                    Stratos::redirect('Forum/getAll');
                }
            }
            
            Stratos::putFlash('There was a problem removing the forum.',
                'error');
        }
        
        return array(
            'forum' => $forum
        );
    }
    
    function _validate( $forum )
    {
        $valid = true;
            
        if( !$forum->name )
        {
            Stratos::putFlash('Please enter a forum name', 'error');
            $valid = false;
        }
        
        if( !$forum->description )
        {
            Stratos::putFlash('Please enter a forum description', 'error');
            $valid = false;
        }
            
        return $valid;
    }
}

?>

You now have enough knowledge of the Stratos Framework to create simple database-driven web applications. But we aren't done yet. We have yet to create the PostController, which will allow users to post to the forums.

Attachments

Copyright © 2006-2007 Sephira Software, LLC. Be sure to check out Mashfest, a music festival site that we are working on.