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:
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.

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.
- Go to: http://localhost/ci_doctrine/doctrine_tools/create_tables and click the button.
- Go to: http://localhost/ci_doctrine/doctrine_tools/load_fixtures and click the button.
Now all the tables are rebuilt and the fixture data is reloaded.
Take a look at your thread table:

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.

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!

Pingback: Tweets that mention CodeIgniter and Doctrine from scratch. Day 11 – Record Hooks | PHP and Stuff -- Topsy.com
#1 by DynamiteN on February 8th, 2010
| Quote
loving this series, keep ‘em comming
only series it feels like im learning something
#2 by krzysko on February 9th, 2010
| Quote
Thanks Burak. Your tutorials are cool. thx thx thx
#3 by Maor on February 9th, 2010
| Quote
Amazing one! keep up the good job!
Hope to see a lot more coming!
#4 by Dani on February 9th, 2010
| Quote
Great tuto! I’m a fun of this series too.
I’m beginner in codeigniter… has it an automatic process to populate and set forms?
Thanks!
#5 by Burak on February 9th, 2010
| Quote
Here you can read about form helper functions:
http://codeigniter.com/user_guide/helpers/form_helper.html
And towards the end, you will see set_* functions. Those can be used for auto populating the form field values.
#6 by Ivan Rivera on February 9th, 2010
| Quote
Hello,
I get an error every time I try to load fixtures, and the problem is with the first_post_id, the error says that it doesn´t have a default value. i hope you could help me..
#7 by Burak on February 9th, 2010
| Quote
Can you copy paste your error message via http://pastebin.com/ please?
#8 by Ivan Rivera on February 10th, 2010
| Quote
Hello again. here is the error message
http://pastebin.com/m3af7587e
but something strange happens, when i try to load fixtures in my local computer with Windows, Apache, MySQL and PHP, I see the error message described before, but when i load the files in the server with Linux, Apache, MySQL and PHP everything runs perfectly.
#9 by Burak on February 10th, 2010
| Quote
Looks like you are running MySQL in strict mode.
Open my.ini in your MySQL installation folder. Find a line that looks like this:
sql-mode=”STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION”
Delete this part: STRICT_TRANS_TABLES,
Restart MySQL.
#10 by Boi Huynh on February 10th, 2010
| Quote
Hi Burak, I get an error when I click button create_table. I posted message via http://pastebin.com/. Please help fix it.
Thanks Burak.
#11 by Burak on February 10th, 2010
| Quote
I need the full url. That’s just the homepage url.
#12 by Boi Huynh on February 23rd, 2010
| Quote
Thanks Burak. I fixed that error. (error create key foreign).
#13 by Adam on February 10th, 2010
| Quote
Hello, Great series can you cover the search feature of doctrine? I can’t see a way to create a DQL query which does something similar to SQL full text search. At present I can only see a way of doing a ‘AND’ search using the Doctrine ‘->search()’ method, is there an ‘OR’ search for the ‘->search()’ method? Thanks. Looking forward to the next tutorial.
#14 by zack kitzmiller on February 10th, 2010
| Quote
Burak, still great series. I’d assume we’ll get to adding posts at some point
You should put that donate button back
#15 by Paul Chater on February 10th, 2010
| Quote
Again, Burak…
Great work. Love it. Keep em’ coming! Hopefully the next one will be released in a week?
lol.
#16 by Mohamed Mahmoud on February 11th, 2010
| Quote
Great work … Thanks for this awesome series .. Waiting your new one
#17 by Lasse on February 12th, 2010
| Quote
Hi Burak,
Im really enjoy this series. I´m pretty new into CI and MVC.
I have a question about the protected function in the usermodel to encrypt password. I creating a forgot password function. But when it save the password it dont save it encrypted. I use DQL if that make difference?
#18 by Burak on February 13th, 2010
| Quote
If you are inserting with DQL, i don’t think that function will be used. You might need to call it manually.
#19 by Zack Kitzmiller on February 15th, 2010
| Quote
Where are you placing that function, Lasse? In the User Model?
#20 by Richard on February 23rd, 2010
| Quote
this example doesn’t follow the tutorial exactly – notice tablename and field are named differently plus there is a slat field.
provided you use the field name correctly this works.
everytime password is saved a new salt is generated and the password is encrypted with that salt – salt get saved too
generate your password in current_user if lost a password
BaseEmployee is a generated model from employee table – eg user table in your case
class Employee extends BaseEmployee {
public function setUp() {
$this->hasMutator(’passwd’, ‘_encrypt_password’);
}
protected function _encrypt_password($value) {
$salt = $this->_new_salt();
$this->_set(’passwd’, md5($salt . $value));
}
protected function _new_salt() {
$salt = substr(md5(uniqid(rand(), true)), 0, 16);
$this->_set(’salt’, $salt);
return $salt;
}
}
fix the encryption as you see fit
#21 by Richard on February 23rd, 2010
| Quote
I dont want to overload this tutorial with comments as it is great – it has got me excited about the next project. think you may need this as I don’t know if the above affects the tutorial login.
I had to change the login – yes to dql
public static function login($username, $password) {
// get User object by username
$q = Doctrine_Query::create()
->select(’id,login,empname’)
->from(’Employee user’)
->where(”user.login = :login and user.passwd = MD5(CONCAT(`salt`, :password))”,array(’:login’ => $username, ‘:password’ => $password));
//notice backward quote on salt telling mysql to use the field – not sure if this is cross database
//$u = $q->fetchOne();
$u = $q->execute(); //execute returns array so reference result as array
if ($u->count() > 0) { //can also check exists?
$CI =& get_instance();
$CI->load->library(’session’);
$CI->session->set_userdata(’user_id’,$u[0]->id);
self::$user = $u[0];
return TRUE;
}
// login failed
return FALSE;
}
#22 by Lasse on February 15th, 2010
| Quote
Yes Zack, its a protected function in User model. But obviously it isn´t used when using DQL.
#23 by Philippe de Chabot on February 18th, 2010
| Quote
It’s a great serie. Can’t wait for the rest.
#24 by jant on February 19th, 2010
| Quote
Congrats Burak for this so useful series. Only one question: I’m surprised to see you’re using WordPress for this blog and it’s so clear that you are able to manage personalized CMS with these amazing and powerful tools. Would you be so kind to explain your reasons?
Sorry for my English, greetings from Andalusia and thanks again. Awaiting for more soon.
#25 by Burak on February 20th, 2010
| Quote
I’m not sure if I understand the question. Why exactly are you surprised that I am using WordPress for this blog?
#26 by jant on February 21st, 2010
| Quote
Well, maybe the word “surprised” is not the more appropriate. Just for curiosity, I was wondering what’s your reasons for use WP instead to build your own CMS with CodeIgniter + Doctrine. I don’t know if it’s because it’s a lot of unnecessary work or WP is all you need or…
Thanks for your attention.
#27 by Zack Kitzmiller on February 22nd, 2010
| Quote
I would assume it’s because there’s not reason to reinvent the wheel.
#28 by Burak on February 22nd, 2010
| Quote
Wordpress already works well enough for now. It wouldn’t justify the time I would have to invest in building something custom.
#29 by Alex on February 20th, 2010
| Quote
It’s the ONLY framework tutorial series that REALLY help to LEARN. Thank you!
#30 by seekbob on February 21st, 2010
| Quote
I found your blog from nettuts….
before, I really don’t know about doctrine, but this is very helpfull plugin for codeigniter…
thanks for this tutorial series… very deep and really nice!
#31 by Jonathan on February 27th, 2010
| Quote
This has been *the* best tutorial I’ve ever come across on ci and doctrine, and in fact has taught me doctrine when nothing else Ive come across has helped me put the pieces together.
I’m begging you to continue – I’d love to continue to learn more. Seriously, you should do a paid tutorial, or run some classes on webcast – I for one would be happy to pay to attend.
#32 by Jonathan on February 27th, 2010
| Quote
For the sake of losing my learning codebase, I updated to the downloaded version here, dropped all the tables in the db, and ran doctrine tools’ create_tables and load_fixtures/true
With this clean install, I’m getting a 404 error (it’s showing up in the access log, but not in the error log somehow – there’s just nothing) on /threads/display/1
/forums/display/1 works like a charm. I’m kind of confused – did I miss something? I’m not getting any error messages so it’s requiring more brain power than I think I have yet.
Has anyone else run into this before?
#33 by Jonathan on February 28th, 2010
| Quote
Another retarded question. Let’s say I wanted to extend your singleton pattern to make it a totally login protected application (redirect only to /login). How would you go about doing that?
Hours of fiddling later, no idea why the 404 is happening. But this is the “right way” to be using CI. Props.
#34 by Richard on March 3rd, 2010
| Quote
I use a template (which uses the controller class) then each class uses that page instead of the controller class – I guess this could be done another way
in the template use
/* override only on pages that need to be public */
public function is_logged_in () {
if ( Current_User::user())
return TRUE;
else
redirect(’login’);
}
in the login page and any other page that doesn’t require login override that function with something like this
public function is_logged_in () {
if ( Current_User::user() )
redirect(’/');
else
return false;
}
I’m sure someone will show us both a better way but it works while the app is being developed.
#35 by Hannes on February 28th, 2010
| Quote
Thanks for this awesome series ^^
Do you plan some kind of “AdminPanel” ? That would be very nice and helpful if you want to do something else with the gained knowledge.