Codeigniter Doctrine In this tutorial we will be looking into Doctrine Record Hooks. This will allow us to trigger certain actions in our Models.

We are going to use this feature to simplify a few things in the existing code, and even gain some performance benefits.

"CodeIgniter and Doctrine from Scratch" Series:

download_code

Record Hooks

In the Day 8 article we talked about CodeIgniter Hooks. This time we are going to look at Record Hooks with Doctrine.

Hooks are used to execute certain code when a certain event is triggered. Record Hooks are used when something happens with a Doctrine Record. For example we can use the postInsert hook to do some task every time a new record is inserted.

Here is a list of all available Record Hooks:

  • preSave
  • postSave
  • preUpdate
  • postUpdate
  • preInsert
  • postInsert
  • preDelete
  • postDelete
  • preValidate
  • postValidate

To use a one of these hooks, simply add a method with that name into the Doctrine_Record model:

class Post extends Doctrine_Record {

	public function setTableDefinition() {
		// ...
	}

	public function setUp() {
		// ...
	}

	public function postInsert() {
		// a new record was inserted
		// you can put some code here
	}

}

Let’s look at what we are going to be doing today to utilize this feature.

Forum Display Page Improvements

In the Day 9 article we built a page for displaying all Threads under a Forum.

ci_doctrine_day9_5

Let’s look at the code we used for getting the list of threads:

// from the Forum model

// ...
	public function getThreadsArray($offset, $limit) {

		$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')
			->addSelect('MAX(p.created_at) as last_post_date')
			->from('Thread t, t.Posts p')
			->where('t.forum_id = ?', $this->id)
			->groupBy('t.id')
			->orderBy('last_post_date DESC')
			->limit($limit)
			->offset($offset)
			->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;

	}
// ...

There are two main DQL queries. The first one for fetching the Threads, and the second one, inside of a loop, for fetching further details about each Thread, such as number of replies, the username etc…

There is quite a bit going on in there. With the changes we will make today, we will be able to reduce that into a single DQL call.

Adding a “First_Post” Relationship

We first had to get the id of the first post, and then get information on that post in another DQL query. If we had a direct relationship with a “First_Post” record, it would simplify things.

  • Edit: system/application/models/thread.php
<?php
class Thread extends Doctrine_Record {

	public function setTableDefinition() {
		$this->hasColumn('title', 'string', 255);
		$this->hasColumn('forum_id', 'integer', 4);
		$this->hasColumn('first_post_id', 'integer', 4);
	}

	public function setUp() {
		$this->hasOne('Forum', array(
			'local' => 'forum_id',
			'foreign' => 'id'
		));
		$this->hasMany('Post as Posts', array(
			'local' => 'id',
			'foreign' => 'thread_id'
		));

		$this->hasOne('Post as First_Post', array(
			'local' => 'first_post_id',
			'foreign' => 'id'
		));
	}

}

The highlighted lines have been added. Now each Thread will have a relationship with a Post record, which will be the first Post in that Thread.

Setting up the Hook

When a new Post is added, if it is the first Post in that Thread, it needs to be assigned to the First_Post relationship. This will be done with the following “postInsert” Record Hook.

  • Edit: system/application/models/post.php
<?php
class Post extends Doctrine_Record {

// ...
	public function postInsert() {

		// is this the first post?
		if (!$this['Thread']['First_Post']->exists()) {
			$this['Thread']['First_Post'] = $this;
			$this['Thread']->save();
		}

	}

}

Note that “postInsert” means, “after” insert. It has nothing to do with the class name “Post”.

This function gets invoked right after a new Post record was created.

First we check if a First_Post relationship exists, with the exist() function. If not, we assign “$this”, which is the current Post record, to this relationship with the corresponding Thread record.

Rebuild the Database

  • Drop all tables.

You may use this query:

SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE category, forum, post, thread, user;

Now we need to rebuild it all.

Now all the tables are rebuilt and the fixture data is reloaded.

Take a look at your thread table:

ci_doctrine_day11_1

You can see the new column. And thanks to the Record Hook, the values have been assigned already.

Simplify Things

Now we can go ahead and simplify the “getThreadsArray” method under the Forum Model.

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

// ...

	public function getThreadsArray($offset, $limit) {

		$threads = Doctrine_Query::create()
			->select('t.title')
			->addSelect('p.id, (COUNT(p.id) - 1) as num_replies')
			->addSelect('MAX(p.created_at) as last_post_date')
			->addSelect('fp.created_at, u.username')
			->from('Thread t, t.Posts p, t.First_Post fp, fp.User u')
			->where('t.forum_id = ?', $this->id)
			->groupBy('t.id')
			->orderBy('last_post_date DESC')
			->limit($limit)
			->offset($offset)
			->setHydrationMode(Doctrine::HYDRATE_ARRAY)
			->execute();

		foreach ($threads as &$thread) {

			$thread['num_replies'] = $thread['Posts'][0]['num_replies'];
			$thread['created_at'] = $thread['First_Post']['created_at'];
			$thread['username'] = $thread['First_Post']['User']['username'];
			$thread['user_id'] = $thread['First_Post']['User']['id'];

			unset($thread['Posts']);

		}

		return $threads;

	}

}

Line 13: Now we have 2 more relationships in the from() call. First_Post and the User for the First_Post.
Line 12: We select the created_at and username fields from these new relationships.
Line 25,26,27: These lines have been changed to use the new fields.
Also the DQL query inside the loop is now gone. This should have a positive impact on performance.

Note that the loop now is only being used to simplify the $threads array structure, so it’s actually optional.

Testing the Results

We haven’t really changed anything on the actual pages. So everything should look the same as before.

ci_doctrine_day11_2

Stay Tuned

There are many things you can do with Hooks and Listeners in Doctrine. Make sure to read the documentation.

I hope you enjoyed this tutorial. See you next time!

"CodeIgniter and Doctrine from Scratch" Series: