Build a forum with CakePHP (part 3)

In this tutorial, we will start building the core part of CapForum, which is the forum.

Download link of entire source code is provided in part 4 of this series.

Create models

We create four models representing four database tables. And four tables are tied together with following relationships:

  1. User has many Post, User has many Topic.
  2. Forum has many Post, Forum has many Topic.
  3. Topic has many Post, Topic belongs to User, Topic belongs to Forum.
  4. Post belongs to User, Topic belongs to Forum, Post belongs to Topic.

Let us create model files:

"app/Model/User.php":

<?php
App::uses('AppModel', 'Model');
 
class User extends AppModel {
 
    public $validate = array(
        'username' => array(
            'notEmpty' => array(
                'rule' => array('notEmpty')
            ),
        ),
        'password' => array(
            'notEmpty' => array(
                'rule' => array('notEmpty')
            ),
        ),
        'email' => array(
            'email' => array(
                'rule' => array('email')
            ),
        ),
    );
 
    //The Associations below have been created with all possible keys, those that are not needed can be removed
 
/**
 * hasMany associations
 *
 * @var array
 */
    public $hasMany = array(
        'Post' => array(
            'className' => 'Post',
            'foreignKey' => 'user_id',
            'dependent' => false,
            'conditions' => '',
            'fields' => '',
            'order' => '',
            'limit' => '',
            'offset' => '',
            'exclusive' => '',
            'finderQuery' => '',
            'counterQuery' => ''
        ),
        'Topic' => array(
            'className' => 'Topic',
            'foreignKey' => 'user_id',
            'dependent' => false,
            'conditions' => '',
            'fields' => '',
            'order' => '',
            'limit' => '',
            'offset' => '',
            'exclusive' => '',
            'finderQuery' => '',
            'counterQuery' => ''
        )
    );
 
}

"app/Model/Forum.php":

?

<?php
App::uses('AppModel', 'Model');
 
class Forum extends AppModel {
 
    public $validate = array(
        'name' => array(
            'notEmpty' => array(
                'rule' => array('notEmpty')
            ),
        ),
    );
 
 
    public $hasMany = array(
        'Post' => array(
            'className' => 'Post',
            'foreignKey' => 'forum_id',
            'dependent' => false,
            'conditions' => '',
            'fields' => '',
            'order' => '',
            'limit' => '',
            'offset' => '',
            'exclusive' => '',
            'finderQuery' => '',
            'counterQuery' => ''
        ),
        'Topic' => array(
            'className' => 'Topic',
            'foreignKey' => 'forum_id',
            'dependent' => false,
            'conditions' => '',
            'fields' => '',
            'order' => '',
            'limit' => '',
            'offset' => '',
            'exclusive' => '',
            'finderQuery' => '',
            'counterQuery' => ''
        )
    );
 
}

"app/Model/Topic.php":

<?php
App::uses('AppModel', 'Model');
 
class Topic extends AppModel {
 
    public $validate = array(
        'name' => array(
            'notEmpty' => array(
                'rule' => array('notEmpty')
            ),
        ),
        'content' => array(
            'notEmpty' => array(
                'rule' => array('notEmpty')
            ),
        ),
        'forum_id' => array(
            'numeric' => array(
                'rule' => array('numeric')
            ),
        ),
    );
 
 
/**
 * belongsTo associations
 *
 * @var array
 */
    public $belongsTo = array(
        'Forum' => array(
            'className' => 'Forum',
            'foreignKey' => 'forum_id',
            'conditions' => '',
            'fields' => '',
            'order' => ''
        ),
        'User' => array(
            'className' => 'User',
            'foreignKey' => 'user_id',
            'conditions' => '',
            'fields' => '',
            'order' => ''
        )
    );
 
/**
 * hasMany associations
 *
 * @var array
 */
    public $hasMany = array(
        'Post' => array(
            'className' => 'Post',
            'foreignKey' => 'topic_id',
            'dependent' => false,
            'conditions' => '',
            'fields' => '',
            'order' => '',
            'limit' => '',
            'offset' => '',
            'exclusive' => '',
            'finderQuery' => '',
            'counterQuery' => ''
        )
    );
 
}

"app/Model/Post.php":

<?php
App::uses('AppModel', 'Model');
 
class Post extends AppModel {
 
    public $validate = array(
        'topic_id' => array(
            'numeric' => array(
                'rule' => array('numeric')
            ),
        ),
        'forum_id' => array(
            'numeric' => array(
                'rule' => array('numeric')
            ),
        ),
        'content' => array(
            'notEmpty' => array(
                'rule' => array('notEmpty')
            ),
        ),
        'user_id' => array(
            'numeric' => array(
                'rule' => array('numeric')
            ),
        ),
    );
 
 
/**
 * belongsTo associations
 *
 * @var array
 */
    public $belongsTo = array(
        'Topic' => array(
            'className' => 'Topic',
            'foreignKey' => 'topic_id',
            'conditions' => '',
            'fields' => '',
            'order' => ''
        ),
        'Forum' => array(
            'className' => 'Forum',
            'foreignKey' => 'forum_id',
            'conditions' => '',
            'fields' => '',
            'order' => ''
        ),
        'User' => array(
            'className' => 'User',
            'foreignKey' => 'user_id',
            'conditions' => '',
            'fields' => '',
            'order' => ''
        )
    );
}

We will also use Containable behavior from CakePHP. Containable behavior is good for controlling model find operations. To add the behavior to all models, we can do from AppModel class.

"app/Model/AppModel.php":

<?php
App::uses('Model', 'Model');
 
class AppModel extends Model {
     
    public $actsAs = array('Containable');
     
}

Modify router

We want CapForum to show forum index page whenever user access the URL directly. This can be handled by Router.

Open file "app/Config/routes.php" and add following line to the begining of the file:

Router::connect('/', array('controller' => 'forums', 'action' => 'index'));

This tells the router to serve the forum's index page as the default page.

Create a paginator element

Before we move to create the actual forum's index page. Let us create a reusable paginator element first. The first reason we want to create an element for paginator is that, we will need this paginator in several pages, so creating an element makes the view look cleaner. Second reason is that, by default, Bootstrap's paginator does not work with the CakePHP generated paginator out of box, we need to add some custom CSS to make them work together.

Copy following to "app/View/Elements/paginator.ctp". This element is very useful, it will not only work for CapForum, but also work with any Bootstrap based paginator. Feel free to use it for your other projects.

<style>
ul.pagination .active,
ul.pagination .disabled {
  float: left;
  padding: 3px 11px 4px 11px;
  text-decoration: none;
  border: 1px solid;
}
 
ul.pagination .active {
  background-color: #428bca;
  border-color: #428bca;
}
 
ul.pagination .disabled {
   color: #999;
   cursor: not-allowed;
   background-color: #fff;
   border-color: #ddd;
}
 
ul.pagination  > li:first-child {
    border-left-width: 1px;
    -webkit-border-bottom-left-radius: 4px;
    border-bottom-left-radius: 4px;
    -webkit-border-top-left-radius: 4px;
    border-top-left-radius: 4px;
    -moz-border-radius-bottomleft: 4px;
    -moz-border-radius-topleft: 4px;
}
 
ul.pagination > li:last-child  {
    -webkit-border-top-right-radius: 4px;
    border-top-right-radius: 4px;
    -webkit-border-bottom-right-radius: 4px;
    border-bottom-right-radius: 4px;
    -moz-border-radius-topright: 4px;
    -moz-border-radius-bottomright: 4px;
}
</style>
 
<ul class="pagination pagination-sm">
    <?php
        echo $this->Paginator->prev('&larr; ' . __('previous'), array('tag' => 'li', 'escape' => false), null, array('tag' => 'li', 'class' => 'disabled', 'escape' => false));
        echo $this->Paginator->numbers(array('separator' => '', 'tag' => 'li', 'currentClass' => 'active'));
        echo $this->Paginator->next(__('next') . ' &rarr;', array('tag' => 'li', 'escape' => false), null, array('tag' => 'li', 'class' => 'disabled', 'escape' => false));
    ?>
</ul>

Build the forum index page

We have told the router to serve forum index page as the default page, however we have not actually created this page yet.

First let us create "app/Controller/ForumsController.php":

<?php
App::uses('AppController', 'Controller');
 
class ForumsController extends AppController {
 
    public $components = array('Paginator');
     
    public function beforeFilter() {
        $this->Auth->allow();
    }
     
    public function index() {
        $this->Paginator->settings['contain'] = array('Topic', 'Post'=>array('User','Topic'));
        $this->set('forums', $this->Paginator->paginate());
    }
 
}

This controller is very simple. It allows all public access, thus we use $this->Auth->allow();. We use Paginator component to find forum data for the view.

Next we need to create a view "app/View/Forums/index.ctp" for "public function index()":

<div class="row">
          <div class="col-lg-12 ">
                <table class="table table-bordered">
                    <thead>
                        <tr>
                            <th colspan=2>Forum</th>
                            <th>Topics</th>
                            <th>Posts</th>
                            <th>Activity</th>
                        </tr>
                    </thead>
                     
                    <tbody>
                        <?php foreach ($forums as $forum): ?>
                        <tr>
                            <td>&nbsp;</td>
                            <td>
                                <?php
                                echo $this->Html->link('<h4>'.$forum['Forum']['name'].'</h4>',
                                                        array('controller'=>'topics','action'=>'index',$forum['Forum']['id']),
                                                        array('escape'=>false));
                                ?>
                            </td>
                            <td><?php echo count($forum['Topic']);?></td>
                            <td><?php echo count($forum['Post']);?></td>
                            <td>
                               <?php
                               if(count($forum['Post'])>0) {
                                $post = $forum['Post'][0];
                                echo $this->Html->link($post['Topic']['name'],array('controller'=>'topics',
                                                                                            'action'=>'view',
                                                                                            $post['Topic']['id']));
                                echo '&nbsp;';
                                echo $this->Time->timeAgoInWords($post['created']);
                                echo '&nbsp;<small>by</small>&nbsp;';
                                echo '&nbsp;';
                                echo $this->Html->link($post['User']['username'],array('controller'=>'users',
                                                                                                'action'=>'profile',
                                                                                                $post['User']['id']));
                               }
                               ?>
                                 
                            </td>
                        </tr>
                        <?php endforeach;?>
                    </tbody>
                </table>
                <div class="pull-right">
                    <?php
                        echo $this->element('paginator');
                    ?>
                 </div>
          </div>
</div>

Couple of points to take note in the view file.

  • We use the paginator element created above at "echo $this->element('paginator');".
  • We use CakePHP's TimeHelper to print user friendly date string at "echo $this->Time->timeAgoInWords($post['created']);".

The end

If you have followed everything correctly. You should be able to view the forum index page as below:

CapForum forum index

Next tutorial, we are going to write the last part of this series. Which covers pages for viewing topics, adding topic, posting replies as well as some wrap-up tasks.

Hopefully this simple tutorial helped you with your development. If you like our post, please follow us on Twitter and help spread the word. We need your support to continue. If you have questions or find our mistakes in above tutorial, do leave a comment below to let us know.