Codeigniter Doctrine In this article we are going to create Templates (with CodeIgniter) to keep our Views organized, and avoid repeating the same HTML code. Also we are going to learn about Doctrine Data Hydrators to see alternative ways to structure the data returned from the database.

"CodeIgniter and Doctrine from Scratch" Series:

download_code

Templates

What and Why?

The term Template might be a little ambiguous right now. Maybe you are already thinking of Views as Templates. Which is mostly true. However, there is a problem with using separate Views for every single page in our application. There will be headers, footers and other content that will be repeated everywhere. That’s why there is a need for actual Templates for our Views.

For example, we will be creating a new View for the Forum pages. It will look mostly the same as the home page, using the same css stylesheet, same header, same user control panel, etc… There will be other Views later that will be the same way. If we ever make a change to any of these common parts, we don’t want to go through every single View. There should be Views that have the common html output that we can quickly edit. Using Templates will enable us to do just that, and maybe more.

Creating a Template

Let’s create our first Template, which is actually going to be a View itself. The idea is to keep all the common elements in one file, and take out the variable stuff.

For example, take a look at this:

ci_doctrine_day9_2

The content section is definitely going to change from page to page. Also, I would like to take out the user controls section, so it can have its own View. We’re going to replace these with some code instead.

  • Create: system/application/views/template.php
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title><?php echo $title; ?> | CI+Doctrine Message Board</title>
	<link rel="stylesheet" href="<?php echo base_url(); ?>css/style.css"
		type="text/css" media="all">
</head>
<body>

<div class="<?php echo $container_css; ?> container">

	<div class="user_controls">
		<?php $this->load->view('user_controls'); ?>
	</div>

	<h1>CI+Doctrine Message Board</h1>

	<?php $this->load->view($content_view); ?>

</div>

</body>
</html>

I have copied the contents of the Home View we created before, and replaced a few things.

Line 14: The code for the user_controls div will now be found in a View named user_controls.php.
Line 19: $content_view is the name of the View that will be loaded to fill up the content area. For example, we will use a View named forum_list for the home page. The controller will have to pass this variable.
Line 5: Now the page title can be passed as a $title variable.
Line 11: We can apply a different css class for the container div on every page.

As you can see, a View can load other Views. That’s a big help in creating Templates. Now, all Controllers can just load this Template View, and they can pass the name for a content View that will be loaded in the middle of the page.

Now let’s create the user_controls View.

  • Create: system/application/views/user_controls.php
<?php if ($user = Current_User::user()): ?>
	Hello, <em><?php echo $user->username; ?></em> <br/>
	<?php echo anchor('logout', 'Logout'); ?>
<?php else: ?>
	<?php echo anchor('login','Login'); ?> |
	<?php echo anchor('signup', 'Register'); ?>
<?php endif; ?>

Basically it’s the same code as before, but now it is in its own View file.

Now let’s create the forum_list View.

  • Create: system/application/views/forum_list.php
<?php foreach($categories as $category): ?>
<div class="category">

	<h2><?php echo $category->title; ?></h2>

	<?php foreach($category->Forums as $forum): ?>
	<div class="forum">

		<h3>
			<?php echo anchor('forums/display/'.$forum->id, $forum->title) ?>
			(<?php echo $forum->Threads[0]->num_threads; ?> threads)
		</h3>

		<div class="description">
			<?php echo $forum->description; ?>
		</div>

	</div>
	<?php endforeach; ?>

</div>
<?php endforeach; ?>

This code is also taken from the middle section of the old Home View. (I only made a small change to the link at line 10.)

Now we can get rid of the Home View file.

  • Delete: system/application/views/home.php

Updating the Home Controller

This new Template needs to know what content to load, what the title of the page is, etc… So we need to update the Home Controller.

  • Edit: system/application/controllers/home.php
<?php
class Home extends Controller {

	public function index() {

		$vars['categories'] = Doctrine_Query::create()
			->select('c.title, f.title, f.description')
			->addSelect('t.id, COUNT(t.id) as num_threads')
			->from('Category c, c.Forums f')
			->leftJoin('f.Threads t')
			->groupBy('f.id')
			->execute();

		$vars['title'] = 'Home';
		$vars['content_view'] = 'forum_list';
		$vars['container_css'] = 'forums';

		$this->load->view('template', $vars);
	}	

}

The changes are only the highlighted lines. We are now passing the page title, name of the content View, and a CSS class name for styling purposes. Then we load the View named ‘template’ instead of ‘home’ (since we deleted that one).

See the Results

The page should look exactly the same as before:

ci_doctrine_day9_1

Later in this article we are going to use this Template to build the Forum page, that lists Threads in a Forum.

Data Hydrators (Doctrine)

Our Doctrine Models extend the Doctrine_Record class. We have been using instances of this class for representing and persisting the database records. Also, we have been using instances of the Doctrine_Collection class when dealing with multiple records (e.g. fetching multiple rows with DQL).

With Data Hydrators, we can work with the data in other structure formats. For example, with DQL we can have it return the results of a SELECT query as an Array instead of Doctrine_Collection.

HYDRATE_SCALAR

This returns an array similar to what you would get from doing raw SQL queries.

$category = Doctrine_Query::create()
	->select('c.*, f.*')
	->from('Category c, c.Forums f')
	->where('c.id = ?', 1)
	->setHydrationMode(Doctrine::HYDRATE_SCALAR)
	->execute();

print_r($category);
/* output:
Array
(
    [0] => Array
        (
            [c_id] => 1
            [c_title] => The CodeIgniter Lounge
            [f_id] => 1
            [f_title] => Introduce Yourself!
            [f_description] => Use this forum to introduce yourself to the CodeIgniter community, or to announce your new CI powered site.

            [f_category_id] => 1
        )

    [1] => Array
        (
            [c_id] => 1
            [c_title] => The CodeIgniter Lounge
            [f_id] => 2
            [f_title] => The Lounge
            [f_description] => CodeIgniter's social forum where you can discuss anything not related to development. No topics off limits... but be civil.

            [f_category_id] => 1
        )

)
*/

We fetched the Category with id 1, and it’s associated Forums. There were 2 Forums under that Category, so the result contains 2 rows of data.

If I run the same raw query from phpMyAdmin, it looks like this:

ci_doctrine_day9_4

Note that the Category title is repeated in both rows, because of the nature of JOIN queries. That is also the case in the Array that was returned by DQL.

HYDRATE_ARRAY

This also returns an Array. But this time, it will have a nice multi-tier structure based on the Model relationships.

$category = Doctrine_Query::create()
	->select('c.*, f.*')
	->from('Category c, c.Forums f')
	->where('c.id = ?', 1)
	->setHydrationMode(Doctrine::HYDRATE_ARRAY)
	->execute();

print_r($category);
/* output:
Array
(
    [0] => Array
        (
            [id] => 1
            [title] => The CodeIgniter Lounge
            [Forums] => Array
                (
                    [0] => Array
                        (
                            [id] => 1
                            [title] => Introduce Yourself!
                            [description] => Use this forum to introduce yourself to the CodeIgniter community, or to announce your new CI powered site.

                            [category_id] => 1
                        )

                    [1] => Array
                        (
                            [id] => 2
                            [title] => The Lounge
                            [description] => CodeIgniter's social forum where you can discuss anything not related to development. No topics off limits... but be civil.

                            [category_id] => 1
                        )

                )

        )

)
*/

Now we have a multidimensional Array. As you can see, this time the Category info is not repeated. And Forums is a sub-array under the Category. Also the keys of the array retain their original names, like ‘title’, instead of ‘c_title’.

Other Hydrators

By default Doctrine is using HYDRATE_RECORD, that causes objects to be returned. And there are a few other Hydrators you can read about here: http://www.doctrine-project.org/documentation/manual/1_2/en/data-hydrators

Why?

Hydrators can have certain performance benefits. Creating an array is faster than creating an object that has lots of overhead. And arrays take less memory too.

Also there are things you can do with arrays that you cannot do with a Doctrine object. If you prefer to handle the data as an array, Hydration is the way to go.

We are going to be using HYDRATE_ARRAY to fetch a list of Threads in the following sections, to demonstrate the usage.

CSS Changes

Before we move on, I want to make some CSS changes, so the new pages we build will be styled.

  • Edit: css/style.css
body {
	font-family: "Trebuchet MS",Arial;
	font-size: 14px;
	background-color: #212426;
	color: #B9AA81;
}
a			{color: #FFF;}
a:hover		{color: #B9AA81;}
input, textarea, select {
	font-family:inherit; font-size:inherit;	font-weight:inherit;
}
.container	{width: 720px; margin: auto;}
/* FORUMS -----------------------------------------*/
.forums.container h2 {
	font-size: 16px;
	color: #000;
	padding: 5px 10px 5px 10px;
	margin: 0px;
	background-color: #BBB;
	-moz-border-radius-topleft: 6px;
	-moz-border-radius-topright: 6px;
	-webkit-border-top-left-radius: 6px;
	-webkit-border-top-right-radius: 6px;
}
.forums.container h3			{font-size: 15px; margin: 0px;}
.forums.container .category 	{margin-bottom: 40px;}
.forums.container .forum 		{border-bottom: 1px solid #666;	padding: 10px;}
.forums.container .forum .description	{font-size: 14px;}
/* FORUM -----------------------------------------*/
.forum.container h2 {
	font-size: 16px;
	color: #000;
	padding: 5px 10px 5px 10px;
	margin: 0px;
	background-color: #BBB;
	-moz-border-radius-topleft: 6px;
	-moz-border-radius-topright: 6px;
	-webkit-border-top-left-radius: 6px;
	-webkit-border-top-right-radius: 6px;
}
.forum.container h3			{font-size: 15px; margin: 0px 0px 5px 0px;}
.forum.container .thread	{border-bottom: 1px solid #666;	padding: 10px;}
/* SIGNUP FORM ------------------------------------*/
#signup_form	{margin: auto; width: 360px; font-size: 16px;}
#signup_form .heading {
	text-align: center;	font-size: 22px; font-weight: bold;	color: #B9AA81;
}
#signup_form form {
	background-color: #B9AA81;
	padding: 10px;
	-moz-border-radius: 8px;
	-webkit-border-radius: 8px;
}
#signup_form form label {font-weight: bold;	color: #11151E;}
#signup_form form input[type=text],input[type=password] {
	width: 316px;
	font-weight: bold;
	padding: 8px;
	border: 1px solid #FFF;
	-moz-border-radius: 4px;
	-webkit-border-radius: 4px;
}
#signup_form form input[type=submit] {
	display: block;
	margin: auto;
	width: 200px;
	font-size: 18px;
	background-color: #FFF;
	border: 1px solid #BBB;
}
#signup_form form input[type=submit]:hover {border-color: #000;}
#signup_form .error {font-size: 13px; color: #690C07; font-style: italic; }
/* USER CONTROL BOX -------------------------------*/
.user_controls {float: right; text-align: right;}

Forum Pages

Now, let’s use what we have, to build the Forum Pages. These pages will list all the Threads, in a given Forum.

Currently, on the Home page we have links like this:

ci_doctrine_day9_3

We created these links, because we expect to have a Controller named ‘Forums’ that has a function named ‘Display’.

There is also an ID number at the end of the URL. That is passed to the Controller function as a parameter.

This Controller is supposed to load a page that lists all the Threads in that particular Forum.

Forums Controller

The skeleton structure first:

  • Create: system/application/controllers/forums.php
<?php
class Forums extends Controller {

	public function display($id) {

	}

}

It looks like any other Controller. But this time we are expecting a parameter being passed to the display() method. The string that comes after /forums/display/ in the URL automatically gets passed as the first parameter to that method.

For example:


http://localhost/ci_doctrine/forums/display/1

Will call display(1) in the Forums Class. We are going to use that parameter as the Forum ID. Then we need to display the Forum page.

The Controller needs to obtain the following pieces of data, and pass them on to the View:

  • The title of the Forum.
  • Each Thread under the Forum.
  • For each Thread, we need Thread title, create date, author name and number of replies.

Please note that the create date of the Thread is not part of the Thread Model (or table), because of the way we designed the Models. The first Post inside of the Thread carries a create date already, so we did not want to duplicate that data again. Same thing goes for the author (or user_id) of the Thread.

Now let’s add some code to make this happen:

<?php
class Forums extends Controller {

	public function display($id) {

		$forum = Doctrine::getTable('Forum')->find($id);

		$vars['title'] = $forum['title'];
		$vars['threads'] = $forum->getThreadsArray();
		$vars['content_view'] = 'forum';
		$vars['container_css'] = 'forum';

		$this->load->view('template', $vars);

	}

}

First we get an instance of the Forum object with the id $id. Then we create the array of variables that will be passed to the template View.

At line 9 you can see that I am attempting to call a method named getThreadsArray(). This does not exist yet, so let’s build it.

The idea is to create a method that can return all associated Threads in an array structure. But we also want each Thread to have information such as: number of replies, date (of first post), author of (first post).

  • Edit: system/application/models/forum.php
<?php
class Forum extends Doctrine_Record {

// ...

	public function getThreadsArray() {

		$threads = Doctrine_Query::create()
			->select('t.title')
			->addSelect('p.id, (COUNT(p.id) - 1) as num_replies')
			->addSelect('MIN(p.id) as first_post_id')
			->from('Thread t, t.Posts p')
			->where('t.forum_id = ?', $this->id)
			->groupBy('t.id')
			->setHydrationMode(Doctrine::HYDRATE_ARRAY)
			->execute();

		foreach ($threads as &$thread) {

			$post = Doctrine_Query::create()
				->select('p.created_at, u.username')
				->from('Post p, p.User u')
				->where('p.id = ?', $thread['Posts'][0]['first_post_id'])
				->setHydrationMode(Doctrine::HYDRATE_ARRAY)
				->fetchOne();

			$thread['num_replies'] = $thread['Posts'][0]['num_replies'];
			$thread['created_at'] = $post['created_at'];
			$thread['username'] = $post['User']['username'];
			$thread['user_id'] = $post['User']['id'];
			unset($thread['Posts']);

		}

		return $threads;

	}

In the first DQL query:

Look at the from() call first. We are joining 2 Models (Thread, Post) because we need information from both.

Now look at the select() and addSelect() calls to see what we are getting:
- We select the title for every Thread.
- Then we select the number of Posts under each Thread by using COUNT(p.id), and subtract 1 to get number of replies.
- Finally we are also getting the ID for the first Post in that Thread using MIN(p.id). That will be used later in the second DQL query.

where(‘t.forum_id = ?’, $this->id) indicates which Forum id we are looking up.

groupBy(‘t.id’) groups the results by the Thread id, so the Post count works properly.

Then, we loop through the Threads to get some more data into each Thread:

In the first query we fetched the ID of the first post on each Thread and stored it as first_post_id. So this DQL query now gets data for that Post and also the username of its author by joining to the related User Model.

The rest of the code, I just arranged all the values in $thread so it has a nice structure when returned.

Just to give you a better picture, if you were to print_r $threads , it would look this this:

Array
(
    [0] => Array
        (
            [id] => 1
            [title] => Hi there!
            [num_replies] => 1
            [first_post_id] => 1
            [created_at] => 2009-12-13 13:00:09
            [username] => TestUser
            [user_id] => 2
        )

    [1] => Array
        (
            [id] => 2
            [title] => Greetings to all
            [num_replies] => 0
            [first_post_id] => 3
            [created_at] => 2009-12-13 13:00:09
            [username] => Foobar
            [user_id] => 3
        )

)

I did unset($thread['Posts']) in the code just to keep the resulting array cleaner.

Note that we used HYDRATE_ARRAY, like we talked about earlier. This allowed us to do 2 things that we couldn’t have done with Doctrine_Collection objects:
- When we foreach() the Threads, we are able to get each element by reference (&$thread), and modify them.
- We are able to create new array elements and assign values to them, like this:
$thread['username'] = $post['User']['username'];.

Now, we need to create the View that will be for the content section of this page.

Forum View

In the Controller we passed this: $vars['content_view'] = ‘forum’;
So the Template will look for a View named ‘forum’.

  • Create: system/application/views/forum.php
<h2><?php echo $title ?></h2>

<?php foreach($threads as $thread): ?>
<div class="thread">

	<h3>
		<?php echo anchor('threads/display/'.$thread['id'], $thread['title']) ?>
		(<?php echo $thread['num_replies']; ?> replies)
	</h3>

	<div>
		Author: <em><?php echo anchor('profile/display/'.$thread['user_id'],
								$thread['username']); ?></em>,
		Posted: <em><?php echo $thread['created_at']; ?></em>
	</div>

</div>
<?php endforeach; ?>

There is nothing new here. Just putting the data into HTML.

The Result

  • Go to the Home Page and click on the first Forum.

You should see this:

ci_doctrine_day9_5

Seems to be working!

Stay Tuned

We built another page in our project and explored two more important subjects along the way. Stay tuned for the next article, there is still plenty of work to do :)

Thank you for reading.

"CodeIgniter and Doctrine from Scratch" Series: